storage

package
v0.2.125 Latest Latest
Warning

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

Go to latest
Published: May 12, 2026 License: Apache-2.0 Imports: 7 Imported by: 1

Documentation

Overview

Package storage provides interfaces and utilities for OAuth token, client, and flow persistence.

The storage package defines the core storage interfaces used throughout the mcp-oauth library:

  • TokenStore: Manages OAuth access and refresh tokens
  • ClientStore: Manages registered OAuth clients
  • FlowStore: Manages OAuth authorization flow state and codes

This package also provides shared types and utility functions used by storage implementations, including token encryption/decryption helpers for sensitive token fields.

Implementations are provided in subpackages:

  • storage/memory: In-memory storage for development and testing
  • storage/mock: Mock storage for unit testing
  • storage/valkey: Valkey/Redis-compatible distributed storage for production

Package storage defines interfaces for persisting OAuth tokens, clients, and authorization flows. It supports various backend implementations including in-memory, Redis, and databases.

Index

Constants

View Source
const (
	BackendMemory = "memory"
	BackendValkey = "valkey"
)

Backend identifiers for the in-tree storage implementations. These are the canonical strings emitted as the `storage.backend` OTEL span attribute and are exported so consumers can dispatch on a configured backend name without re-declaring the same literals (e.g. cmd-line flag parsing, factory funcs).

View Source
const (
	// ErrMsgProviderStateRequired is the error message when provider state is missing.
	ErrMsgProviderStateRequired = "provider state is required"

	// ErrMsgInvalidAuthorizationState is the error message for invalid authorization state.
	ErrMsgInvalidAuthorizationState = "invalid authorization state"

	// ErrMsgRefreshTokenNotFoundOrUsed is the error message for refresh token not found or already used.
	ErrMsgRefreshTokenNotFoundOrUsed = "refresh token not found or already used"
)

Common error message strings used across storage implementations. These ensure consistent error messages and reduce code duplication.

View Source
const DummyBcryptHash = "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"

DummyBcryptHash is a pre-computed bcrypt hash used for timing attack mitigation. This hash (bcrypt hash of "test") ensures we always perform a bcrypt comparison even if a client doesn't exist, preventing timing-based client enumeration attacks. Note: Using a constant dummy hash is intentional - the timing attack mitigation comes from always performing the bcrypt comparison, not from the hash value.

Variables

View Source
var (
	// ErrTokenNotFound indicates the token does not exist in storage.
	// In refresh token scenarios, this may indicate the token was already rotated,
	// potentially signaling a reuse attempt if family metadata exists.
	ErrTokenNotFound = errors.New("token not found")

	// ErrTokenExpired indicates the token exists but has expired.
	// Expired tokens should be rejected without triggering reuse detection.
	ErrTokenExpired = errors.New("token expired")

	// ErrClientNotFound indicates the client does not exist in storage.
	ErrClientNotFound = errors.New("client not found")

	// ErrAuthorizationCodeNotFound indicates the authorization code does not exist.
	ErrAuthorizationCodeNotFound = errors.New("authorization code not found")

	// ErrAuthorizationCodeUsed indicates the authorization code has already been used.
	// This is a security event that should trigger token revocation per OAuth 2.1.
	ErrAuthorizationCodeUsed = errors.New("authorization code already used")

	// ErrAuthorizationStateNotFound indicates the authorization state does not exist.
	ErrAuthorizationStateNotFound = errors.New("authorization state not found")

	// ErrUserInfoNotFound indicates the user info does not exist in storage.
	ErrUserInfoNotFound = errors.New("user info not found")

	// ErrRefreshTokenFamilyNotFound indicates the refresh token family does not exist.
	// This is normal for tokens created before family tracking was enabled.
	ErrRefreshTokenFamilyNotFound = errors.New("refresh token family not found")

	// ErrRefreshTokenFamilyRevoked indicates the family exists but every
	// member is revoked. Distinct from ErrRefreshTokenFamilyNotFound so
	// callers can produce a better error message — "session was revoked"
	// vs "no such session". The two states converge to NotFound after the
	// revoked-family retention period elapses and the entries are wiped.
	ErrRefreshTokenFamilyRevoked = errors.New("refresh token family is revoked")
)

Storage error types for distinguishing between different failure modes. These allow callers to differentiate between "not found" errors (which may indicate reuse in refresh token scenarios) and transient errors (which should not trigger security responses).

View Source
var KnownExtraFields = []string{
	"id_token",
	"scope",
	"expires_in",
}

KnownExtraFields lists the OIDC extra fields that must be preserved through encryption. These fields are stored in oauth2.Token's private 'raw' field and are critical for downstream OIDC authentication (e.g., id_token for Kubernetes API auth).

SECURITY: This allowlist approach ensures unknown extra fields are dropped, preventing potential injection of malicious data. Only explicitly listed fields are preserved.

EXTENSIBILITY: Some OAuth/OIDC providers may include additional fields in token responses. If your provider returns custom extra fields that need to be preserved:

  1. Add the field name to this list
  2. If the field contains sensitive data (PII, secrets), also add it to SensitiveExtraFields
  3. Test that the field survives the encrypt/decrypt roundtrip

Common fields from various providers that may need to be added:

  • "token_type": Usually "Bearer", already in oauth2.Token.TokenType
  • "refresh_expires_in": Keycloak-specific refresh token lifetime
  • "session_state": Keycloak session identifier
  • "not-before-policy": Keycloak token validity timestamp
View Source
var SensitiveExtraFields = []string{
	"id_token",
}

SensitiveExtraFields lists extra fields that contain sensitive data and should be encrypted at rest. These fields contain PII or authentication credentials.

SECURITY: The id_token is a signed JWT containing user identity claims (email, name, subject). While it cannot be used for impersonation (it's signed by the IdP and typically short-lived), it contains PII that should be protected at rest.

When adding new fields to KnownExtraFields, evaluate whether they contain:

  • PII (email, name, address, phone)
  • Authentication credentials or secrets
  • Session identifiers that could enable session hijacking

If yes, add the field here to ensure it's encrypted at rest.

Functions

func DecryptExtraFields added in v0.2.15

func DecryptExtraFields(extra map[string]interface{}, encryptor *security.Encryptor) (map[string]interface{}, error)

DecryptExtraFields decrypts sensitive fields in the extra map. Returns a new map with decrypted values for sensitive fields. Non-sensitive fields are copied as-is. If encryptor is nil or disabled, returns the original map unchanged.

func EncryptExtraFields added in v0.2.15

func EncryptExtraFields(extra map[string]interface{}, encryptor *security.Encryptor) (map[string]interface{}, error)

EncryptExtraFields encrypts sensitive fields in the extra map. Returns a new map with encrypted values for sensitive fields. Non-sensitive fields are copied as-is. If encryptor is nil or disabled, returns the original map unchanged.

func ExtractTokenExtra added in v0.2.15

func ExtractTokenExtra(token *oauth2.Token) map[string]interface{}

ExtractTokenExtra extracts known extra fields from an oauth2.Token. The oauth2.Token.Extra() method is the only way to access the private raw field. We extract known OIDC fields that need to be preserved through encryption.

Returns nil if the token is nil or has no known extra fields.

func IsCodeReuseError added in v0.1.29

func IsCodeReuseError(err error) bool

IsCodeReuseError checks if an error indicates authorization code reuse.

func IsExpiredError added in v0.1.29

func IsExpiredError(err error) bool

IsExpiredError checks if an error indicates an expired token or code.

func IsNotFoundError added in v0.1.29

func IsNotFoundError(err error) bool

IsNotFoundError checks if an error indicates a "not found" condition. This is useful for distinguishing between missing resources (which may indicate security issues like token reuse) and transient errors (which should not).

Types

type ActiveRefreshTokenByFamilyStore added in v0.2.122

type ActiveRefreshTokenByFamilyStore interface {
	// GetActiveRefreshTokenByFamily returns the most recent (highest
	// generation) non-revoked refresh token for the family along with
	// the owning client ID.
	//
	// Returns ErrRefreshTokenFamilyNotFound when no entry for the family
	// exists in storage at all (never created, or aged out of revoked-
	// family retention). Returns ErrRefreshTokenFamilyRevoked when the
	// family exists but every member is revoked — a distinct state so
	// callers can surface "session was revoked" vs "no such session".
	GetActiveRefreshTokenByFamily(ctx context.Context, familyID string) (refreshToken, clientID string, err error)
}

ActiveRefreshTokenByFamilyStore is an optional extension that exposes the most recent (highest-generation) non-revoked refresh token for a family, looked up by family ID. Required by Server.RefreshSession, which otherwise has no way to refresh a session given only the family ID — the public refresh-token-grant path needs the refresh token itself, which the caller does not have on the in-process refresh path.

The method returns both the refresh token and the owning client ID in one call so RefreshSession does not need a second lookup to get the client ID for OAuth 2.1 refresh-token client binding validation.

Memory and Valkey both implement this interface. Backends that don't will cause Server.RefreshSession to return an error at call time.

type AuthorizationCode

type AuthorizationCode struct {
	Code        string
	ClientID    string
	RedirectURI string
	Scope       string
	// Resource is the target resource server identifier (RFC 8707)
	// This is carried from the authorization request to bind the token to a specific audience
	Resource string
	// Audience is the intended token audience (resource server identifier)
	// This is used for token validation to prevent token theft across resource servers
	Audience            string
	CodeChallenge       string
	CodeChallengeMethod string
	UserID              string
	ProviderToken       *oauth2.Token // Now uses oauth2.Token directly
	CreatedAt           time.Time
	ExpiresAt           time.Time
	Used                bool
}

AuthorizationCode represents an issued authorization code

type AuthorizationState

type AuthorizationState struct {
	// StateID is used for internal tracking (may be server-generated if client didn't provide state)
	StateID string
	// OriginalClientState is the state parameter the client provided (empty if client didn't provide one)
	// This is returned to the client in the callback redirect
	OriginalClientState string
	ClientID            string
	RedirectURI         string
	Scope               string
	// Resource is the target resource server identifier (RFC 8707)
	// This binds the authorization to a specific resource server
	Resource string
	// Client-to-Server PKCE challenge from MCP client
	CodeChallenge string
	// Client-to-Server PKCE method from MCP client
	CodeChallengeMethod string
	// State parameter sent to provider (different from StateID)
	ProviderState string
	// Server-to-Provider PKCE verifier (OAuth 2.1)
	ProviderCodeVerifier string
	CreatedAt            time.Time
	ExpiresAt            time.Time
}

AuthorizationState represents the state of an ongoing authorization flow

type Client

type Client struct {
	ClientID                string
	ClientSecretHash        string // bcrypt hash
	ClientType              string // "public" or "confidential"
	RedirectURIs            []string
	TokenEndpointAuthMethod string
	GrantTypes              []string
	ResponseTypes           []string
	ClientName              string
	Scopes                  []string
	CreatedAt               time.Time
}

Client represents a registered OAuth client

type ClientStore

type ClientStore interface {
	// SaveClient saves a registered client
	SaveClient(ctx context.Context, client *Client) error

	// GetClient retrieves a client by ID
	GetClient(ctx context.Context, clientID string) (*Client, error)

	// ValidateClientSecret validates a client's secret
	ValidateClientSecret(ctx context.Context, clientID, clientSecret string) error

	// ListClients lists all registered clients (for admin purposes)
	ListClients(ctx context.Context) ([]*Client, error)

	// CheckIPLimit checks if an IP has reached the client registration limit
	CheckIPLimit(ctx context.Context, ip string, maxClientsPerIP int) error
}

ClientStore defines the interface for managing OAuth client registrations. All methods accept context.Context for tracing and cancellation.

type Combined added in v0.2.108

type Combined interface {
	TokenStore
	ClientStore
	FlowStore
}

Combined is the convenience union of the three storage interfaces that a single backend typically implements together: TokenStore, ClientStore, and FlowStore. Both the in-tree memory and valkey backends satisfy it.

It exists so consumers no longer have to pass the same *Store three times to [server.NewWithCombined] (the old [server.New] signature took TokenStore, ClientStore, and FlowStore as separate arguments for backends that split persistence across backing stores). Consumers that still split per-interface can keep using the split constructor.

The optional interfaces (RefreshTokenFamilyStore, TokenRevocationStore, the TokenMetadata* interfaces) are intentionally NOT embedded here — Combined is the minimum every server needs, and backends opt into the extras by implementing them. The server uses runtime type assertions (via the Set* hooks and gated SessionCreationHandler wiring) to detect the optional ones.

type FlowStore

type FlowStore interface {
	// SaveAuthorizationState saves the state of an ongoing authorization flow
	SaveAuthorizationState(ctx context.Context, state *AuthorizationState) error

	// GetAuthorizationState retrieves an authorization state by client state (StateID).
	// Use this method when validating client-initiated requests where the client
	// provides their original state parameter for CSRF protection.
	GetAuthorizationState(ctx context.Context, stateID string) (*AuthorizationState, error)

	// GetAuthorizationStateByProviderState retrieves an authorization state by provider state.
	// Use this method during provider callback validation when the OAuth provider
	// (Google, GitHub, etc.) returns with the server-generated state parameter.
	GetAuthorizationStateByProviderState(ctx context.Context, providerState string) (*AuthorizationState, error)

	// DeleteAuthorizationState removes an authorization state
	DeleteAuthorizationState(ctx context.Context, stateID string) error

	// SaveAuthorizationCode saves an issued authorization code
	SaveAuthorizationCode(ctx context.Context, code *AuthorizationCode) error

	// GetAuthorizationCode retrieves an authorization code
	GetAuthorizationCode(ctx context.Context, code string) (*AuthorizationCode, error)

	// AtomicCheckAndMarkAuthCodeUsed atomically checks if a code is unused and marks it as used.
	// This prevents race conditions in authorization code reuse detection.
	// Returns the auth code if successful, or an error if:
	// - Code not found
	// - Code expired
	// - Code already used (reuse detected)
	// SECURITY: This operation MUST be atomic to prevent concurrent code exchange attacks.
	AtomicCheckAndMarkAuthCodeUsed(ctx context.Context, code string) (*AuthorizationCode, error)

	// DeleteAuthorizationCode removes an authorization code
	DeleteAuthorizationCode(ctx context.Context, code string) error
}

FlowStore defines the interface for managing OAuth authorization flows.

Understanding StateID vs ProviderState

OAuth authorization flows use TWO distinct state parameters for different purposes:

1. StateID (Client State):

  • Generated by the client application
  • Sent by the client in the /authorize request
  • Used for CSRF protection on the client side
  • Returned to the client in the redirect URI after successful authorization
  • Use GetAuthorizationState(stateID) when validating client-initiated requests

2. ProviderState (Server State):

  • Generated by THIS OAuth server
  • Sent to the OAuth provider (Google, GitHub, etc.) during the redirect
  • Used for CSRF protection on the server side
  • Returned by the provider in their callback
  • Use GetAuthorizationStateByProviderState(providerState) when validating provider callbacks

Example Flow:

  1. Client calls /authorize with state="client_csrf_token_123"
  2. Server stores both: StateID="client_csrf_token_123", ProviderState="server_csrf_token_xyz"
  3. Server redirects to Google with state="server_csrf_token_xyz"
  4. Google redirects back with state="server_csrf_token_xyz"
  5. Server validates using GetAuthorizationStateByProviderState("server_csrf_token_xyz")
  6. Server redirects to client with state="client_csrf_token_123"
  7. Client validates using their original state="client_csrf_token_123"

This two-state system provides defense-in-depth against CSRF attacks at both layers. All methods accept context.Context for tracing and cancellation.

type RefreshTokenFamilyByIDStore added in v0.2.120

type RefreshTokenFamilyByIDStore interface {
	// GetRefreshTokenFamilyByID returns the family metadata for the given
	// family ID. Returns ErrRefreshTokenFamilyNotFound when the family is
	// unknown (also for revoked families that have aged out of retention).
	GetRefreshTokenFamilyByID(ctx context.Context, familyID string) (*RefreshTokenFamilyMetadata, error)
}

RefreshTokenFamilyByIDStore is an optional extension to RefreshTokenFamilyStore that exposes a lookup by family ID. JWT-mode access tokens carry family_id in their claims, but they do NOT carry the refresh token, so the existing GetRefreshTokenFamily(refreshToken) API cannot be reached from a JWT validation hot path. This lookup closes that gap.

Implementations must be safe to call concurrently with the rest of the storage interface. Memory and Valkey both implement it.

All methods accept context.Context for tracing and cancellation.

type RefreshTokenFamilyMetadata

type RefreshTokenFamilyMetadata struct {
	FamilyID   string
	UserID     string
	ClientID   string
	Generation int
	IssuedAt   time.Time
	Revoked    bool
	RevokedAt  time.Time // When this family was revoked (for forensics and cleanup)
}

RefreshTokenFamilyMetadata contains metadata about a token family

type RefreshTokenFamilyStore

type RefreshTokenFamilyStore interface {
	// SaveRefreshTokenWithFamily saves a refresh token with family tracking
	SaveRefreshTokenWithFamily(ctx context.Context, refreshToken, userID, clientID, familyID string, generation int, expiresAt time.Time) error

	// GetRefreshTokenFamily retrieves family metadata for a refresh token
	GetRefreshTokenFamily(ctx context.Context, refreshToken string) (*RefreshTokenFamilyMetadata, error)

	// RevokeRefreshTokenFamily revokes all tokens in a family
	RevokeRefreshTokenFamily(ctx context.Context, familyID string) error
}

RefreshTokenFamilyStore tracks a family of refresh tokens for reuse detection (OAuth 2.1). This is optional - only implemented by stores that support reuse detection. All methods accept context.Context for tracing and cancellation.

type RevokedTokenStore added in v0.2.120

type RevokedTokenStore interface {
	// RevokeJTI marks the given JTI as revoked until the supplied
	// expiration. Implementations MUST auto-expire the entry at expiresAt
	// so the denylist stays bounded; an entry persisting past exp would
	// only consume storage with no security value.
	RevokeJTI(ctx context.Context, jti string, expiresAt time.Time) error

	// IsJTIRevoked returns true if the JTI was previously revoked and the
	// entry has not yet expired. Implementations MUST be safe to call from
	// the validation hot path (no per-call allocation outside the lookup).
	IsJTIRevoked(ctx context.Context, jti string) (bool, error)
}

RevokedTokenStore tracks revoked self-issued JWT access tokens by jti.

Self-issued JWTs (server.AccessTokenFormatJWT mode) are stateless: the bearer carries every claim a resource server needs to validate it locally. RFC 7009 token revocation must still take effect, however, so the issuing server keeps a denylist of revoked jti values with their original exp timestamps. Validation consults the denylist and rejects matches; entries auto-expire at exp so the list stays bounded.

The interface is optional: implementations are expected to mirror the shape of TokenStore (memory + Valkey). Servers operating in opaque mode do not require it — opaque revocation continues to use TokenStore deletion. Servers operating in JWT mode without a RevokedTokenStore log a warning at startup and accept revocation calls but do not enforce them on subsequent validations.

All methods accept context.Context for tracing and cancellation.

type TokenMetadata added in v0.1.34

type TokenMetadata struct {
	UserID    string    // User who owns this token
	ClientID  string    // Client who owns this token
	IssuedAt  time.Time // Server-set on write; ignored on input
	TokenType string    // "access" or "refresh"
	Audience  string    // RFC 8707: Intended resource server identifier (for audience validation)
	Scopes    []string  // MCP 2025-11-25: Scopes granted to this token (for scope validation)
	FamilyID  string    // Refresh token family ID for session tracking
}

TokenMetadata tracks ownership, audience (RFC 8707), and scope (MCP 2025-11-25) information for an issued token. The same struct is used for input to TokenMetadataStore.SaveTokenMetadata and for output from TokenMetadataGetter.GetTokenMetadata.

IssuedAt is server-set on write — the field is ignored on input and populated by the backend from the wall clock at write time. Read it after a GetTokenMetadata call; do not trust it on a caller-built instance.

type TokenMetadataGetter added in v0.1.41

type TokenMetadataGetter interface {
	GetTokenMetadata(tokenID string) (*TokenMetadata, error)
}

TokenMetadataGetter provides read access to token metadata. This is used for scope validation and token introspection.

type TokenMetadataStore added in v0.1.41

type TokenMetadataStore interface {
	// SaveTokenMetadata persists metadata for the given tokenID.
	// metadata.IssuedAt is ignored on input; implementations populate it
	// from the wall clock at write time so the stored value reflects
	// actual write time and cannot be forged by a caller.
	SaveTokenMetadata(ctx context.Context, tokenID string, metadata TokenMetadata) error
}

TokenMetadataStore stores write-side token metadata used for audit (which user / client a token was issued to), audience binding (RFC 8707), scope validation (MCP 2025-11-25), and refresh-token-family bookkeeping (OAuth 2.1 reuse detection). The whole metadata payload travels in a single TokenMetadata struct so adding a new field is a single-place change rather than a new interface.

Revocation does not flow through this interface — opaque tokens are revoked via TokenStore.DeleteToken and self-issued JWTs via RevokedTokenStore.RevokeJTI. TokenMetadata is the read-side payload for scope validation and audit.

type TokenRevocationStore added in v0.1.9

type TokenRevocationStore interface {
	// RevokeAllTokensForUserClient revokes all tokens (access + refresh) for a specific user+client combination.
	// This is called when authorization code reuse is detected (OAuth 2.1 requirement).
	// Returns the number of tokens revoked and any error encountered.
	RevokeAllTokensForUserClient(ctx context.Context, userID, clientID string) (int, error)

	// GetTokensByUserClient retrieves all token IDs for a user+client combination (for testing/debugging).
	GetTokensByUserClient(ctx context.Context, userID, clientID string) ([]string, error)
}

TokenRevocationStore supports bulk token revocation operations (OAuth 2.1 security). This is optional - only implemented by stores that support token revocation. Used for critical security scenarios like authorization code reuse detection. All methods accept context.Context for tracing and cancellation.

type TokenStore

type TokenStore interface {
	// SaveToken saves a token for a user
	SaveToken(ctx context.Context, userID string, token *oauth2.Token) error

	// GetToken retrieves a token for a user
	GetToken(ctx context.Context, userID string) (*oauth2.Token, error)

	// DeleteToken removes a token for a user
	DeleteToken(ctx context.Context, userID string) error

	// SaveUserInfo saves user information
	SaveUserInfo(ctx context.Context, userID string, info *providers.UserInfo) error

	// GetUserInfo retrieves user information
	GetUserInfo(ctx context.Context, userID string) (*providers.UserInfo, error)

	// SaveRefreshToken saves a refresh token mapping to user ID with expiry
	SaveRefreshToken(ctx context.Context, refreshToken, userID string, expiresAt time.Time) error

	// GetRefreshTokenInfo retrieves the user ID for a refresh token
	GetRefreshTokenInfo(ctx context.Context, refreshToken string) (string, error)

	// DeleteRefreshToken removes a refresh token
	DeleteRefreshToken(ctx context.Context, refreshToken string) error

	// AtomicGetAndDeleteRefreshToken atomically retrieves and deletes a refresh token.
	// This prevents race conditions in refresh token rotation and reuse detection.
	// Returns the userID, clientID, and provider token if successful, or an error if:
	// - Token not found (may indicate already used/rotated)
	// - Token expired
	// SECURITY: This operation MUST be atomic to prevent concurrent token refresh attacks.
	// SECURITY: Returns clientID for client binding validation per OAuth 2.1 Section 6.
	AtomicGetAndDeleteRefreshToken(ctx context.Context, refreshToken string) (userID string, clientID string, providerToken *oauth2.Token, err error)
}

TokenStore defines the interface for storing and retrieving tokens. This allows using in-memory, Redis, database, or other storage backends. Now uses golang.org/x/oauth2.Token directly. All methods accept context.Context for tracing and cancellation.

Directories

Path Synopsis
Package memory provides an in-memory implementation of the OAuth storage interfaces.
Package memory provides an in-memory implementation of the OAuth storage interfaces.
Package mock provides mock implementations of storage interfaces for testing purposes.
Package mock provides mock implementations of storage interfaces for testing purposes.
Package valkey provides a Valkey storage backend for the mcp-oauth library.
Package valkey provides a Valkey storage backend for the mcp-oauth library.

Jump to

Keyboard shortcuts

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