Documentation
¶
Overview ¶
Package pkcestore holds in-flight PKCE state across the oauth-start → /oauth/callback round trip for the platform's outbound OAuth flows.
It is deliberately separate from pkg/admin (the HTTP layer that drives the flow): the store is auth-storage infrastructure with no dependency on the admin handler, so it lives below it in the import graph. Two implementations ship — an in-memory map (single-replica, default) and a Postgres-backed store (multi-replica safe). The admin Handler selects based on whether a database is configured.
Index ¶
Constants ¶
const TTL = 10 * time.Minute
TTL is how long an in-progress oauth-start hold-state remains valid before the operator must restart the flow. Salesforce and most providers complete the redirect in seconds; 10 minutes is a generous window that survives slow MFA prompts. Both stores and the admin oauth handlers reference this single value so the GC cutoff, the Postgres expires_at, and the user-facing TTL stay in sync.
Variables ¶
var ErrStateCollision = errors.New("pkcestore: state token collision")
ErrStateCollision is returned by Store.Put when a state token already exists in the store. State tokens are 256 bits of entropy so a genuine collision is statistically impossible — in practice this means the caller's state generator is broken or someone is replaying a state. Either way, the second oauth-start must fail loudly rather than silently overwriting the first.
var ErrStateNotFound = errors.New("pkcestore: state not found")
ErrStateNotFound is returned by Store.Take when no state row matches (or the row had already expired). Callers use errors.Is to distinguish "no such pending flow" from a transport/IO error.
var ErrStorePut = errors.New("pkcestore: put failed")
ErrStorePut wraps any DB-side failure from Put. Tests assert with errors.Is rather than coupling to a specific message.
Functions ¶
This section is empty.
Types ¶
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-process map keyed by state token. It sweeps expired entries on every put/take AND on a background ticker so an idle server still releases stranded oauth-start records.
Single-replica deployments and tests use this directly. Production multi-replica deployments use PostgresStore.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore returns a store with a background GC goroutine running on gcInterval. Callers MUST Close() it to release the goroutine — typically via t.Cleanup() in tests or a deferred close in production process teardown.
func (*MemoryStore) Close ¶
func (s *MemoryStore) Close() error
Close stops the background GC goroutine. Idempotent.
type PostgresStore ¶
type PostgresStore struct {
// contains filtered or unexported fields
}
PostgresStore persists in-flight PKCE state to the oauth_pkce_states table. Used in multi-replica deployments where oauth-start may run on a different pod than /oauth/callback.
code_verifier is encrypted at rest via the platform's connoauth.FieldEncryptor — verifiers are short-lived (≤TTL) but they are paired secrets, so a DB read of them while still in flight is roughly equivalent to leaking a short-window OAuth refresh token.
We reuse connoauth's encryptor interface rather than declaring a duplicate to keep one canonical interface for at-rest field encryption across sub-package stores.
func NewPostgresStore ¶
func NewPostgresStore(db *sql.DB, enc connoauth.FieldEncryptor) *PostgresStore
NewPostgresStore returns a Postgres-backed PKCE store that runs a background sweeper to delete expired rows. Pass nil for enc to skip at-rest encryption (dev-only).
func (*PostgresStore) Close ¶
func (s *PostgresStore) Close() error
Close stops the background sweeper. Idempotent.
func (*PostgresStore) Put ¶
Put inserts a state row. The expires_at column is computed server-side (NOW() + interval matching TTL) so the take-side filter NOW() comparison can't be defeated by Go-process / Postgres clock drift. ON CONFLICT (state) DO NOTHING rejects collisions loudly via ErrStateCollision.
type State ¶
type State struct {
// Kind is the connection kind ("mcp" or "api") so the unified
// /oauth/callback handler can dispatch the per-kind config parser
// and post-auth side effects. Empty in legacy rows pre-dating
// migration 000039 — those rows are MCP gateway flows by
// construction (only kind that used this table at the time).
Kind string
// Connection is the name of the connection the flow authenticates.
Connection string
// CodeVerifier is the PKCE verifier paired with the challenge sent
// to the provider; encrypted at rest by the Postgres store.
CodeVerifier string
// StartedBy identifies the admin who initiated the flow (for audit).
StartedBy string
// CreatedAt is when oauth-start recorded the hold; drives GC expiry.
CreatedAt time.Time
// ReturnURL is where the callback redirects the operator afterward.
ReturnURL string
// RedirectURI is the OAuth redirect_uri registered for the exchange.
RedirectURI string
}
State is the server-side hold for one pending OAuth flow. It maps the random state token to the data the callback handler needs.
Fields are exported so the admin oauth handlers can construct and read a State across the package boundary; the store implementations carry pointers to it unchanged.
type Store ¶
type Store interface {
// Put stores a state record. Implementations may evict entries that
// have outlived TTL on every Put.
Put(ctx context.Context, state string, val *State) error
// Take atomically reads-and-deletes a state record. Returns
// ErrStateNotFound when no row matches (or the row had already
// expired). Other errors indicate transport/IO failure.
Take(ctx context.Context, state string) (*State, error)
// Close releases any background goroutines or DB resources. Safe to
// call multiple times.
Close() error
}
Store holds in-flight PKCE state across the oauth-start → /oauth/callback round trip. Implementations must be safe for concurrent use.