pkcestore

package
v1.87.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 22, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

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

View Source
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

View Source
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.

View Source
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.

View Source
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.

func (*MemoryStore) Put

func (s *MemoryStore) Put(_ context.Context, state string, val *State) error

Put stores a state under the given token, sweeping expired entries opportunistically.

func (*MemoryStore) Take

func (s *MemoryStore) Take(_ context.Context, state string) (*State, error)

Take returns and deletes the matching state, or ErrStateNotFound if absent. Expired entries are GC'd before lookup so callers don't need to check TTL themselves.

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

func (s *PostgresStore) Put(ctx context.Context, state string, val *State) error

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.

func (*PostgresStore) Take

func (s *PostgresStore) Take(ctx context.Context, state string) (*State, error)

Take atomically reads-and-deletes the matching row using DELETE … RETURNING so two concurrent callbacks (a real one and a replay) race-cleanly: only one wins. Returns ErrStateNotFound for missing or expired rows.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL