storage

package
v0.2.75 Latest Latest
Warning

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

Go to latest
Published: Mar 6, 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 (
	// 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")
)

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 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 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 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 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 // When this token was issued
	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)
}

TokenMetadata tracks ownership and audience information for a token (RFC 8707) Used for revocation by user+client and audience validation

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(tokenID, userID, clientID, tokenType string) error
}

TokenMetadataStore is the basic interface for storing token metadata. This allows tracking which tokens belong to which user and client for revocation purposes.

type TokenMetadataStoreWithAudience added in v0.1.41

type TokenMetadataStoreWithAudience interface {
	SaveTokenMetadataWithAudience(tokenID, userID, clientID, tokenType, audience string) error
}

TokenMetadataStoreWithAudience extends TokenMetadataStore with RFC 8707 audience support. This allows binding tokens to specific resource servers to prevent token theft.

type TokenMetadataStoreWithScopesAndAudience added in v0.1.41

type TokenMetadataStoreWithScopesAndAudience interface {
	SaveTokenMetadataWithScopesAndAudience(tokenID, userID, clientID, tokenType, audience string, scopes []string) error
}

TokenMetadataStoreWithScopesAndAudience extends with MCP 2025-11-25 scope support. This allows tracking which scopes were granted to a token for scope validation.

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