Documentation
¶
Overview ¶
Package valkey provides a Valkey storage backend for the mcp-oauth library.
Valkey is a high-performance key-value store that is wire-compatible with Redis. This package implements all storage interfaces required by the mcp-oauth library, making it suitable for production deployments that require:
- Distributed storage for horizontal scaling
- Persistence across server restarts
- Automatic TTL-based expiration
- High availability with clustering
Implemented Interfaces ¶
The Store type implements all required storage interfaces:
- storage.TokenStore: Token management (save, get, delete tokens and user info)
- storage.ClientStore: OAuth client management (save, get, validate clients)
- storage.FlowStore: Authorization flow management (states, codes)
- storage.RefreshTokenFamilyStore: OAuth 2.1 token family tracking for reuse detection
- storage.TokenRevocationStore: Bulk token revocation for security scenarios
Key Schema ¶
All keys use a configurable prefix (default "mcp:") to avoid conflicts with other applications sharing the same Valkey instance:
{prefix}:token:{userID} -> JSON(oauth2.Token)
{prefix}:userinfo:{userID} -> JSON(UserInfo)
{prefix}:refresh:{token} -> userID (with TTL)
{prefix}:refresh:meta:{token} -> JSON(familyMetadata)
{prefix}:client:{clientID} -> JSON(Client)
{prefix}:client:ip:{ip} -> count (with TTL)
{prefix}:state:{stateID} -> JSON(AuthorizationState)
{prefix}:state:provider:{state} -> stateID (for reverse lookup)
{prefix}:code:{code} -> JSON(AuthorizationCode)
{prefix}:meta:{tokenID} -> JSON(TokenMetadata)
{prefix}:userclient:{uid}:{cid} -> SET of tokenIDs
{prefix}:family:{familyID} -> SET of refresh tokens in family
Atomic Operations ¶
OAuth 2.1 requires certain operations to be atomic to prevent security issues:
- AtomicCheckAndMarkAuthCodeUsed: Prevents authorization code replay attacks
- AtomicGetAndDeleteRefreshToken: Prevents refresh token reuse attacks
These operations use Lua scripts to ensure atomicity in Valkey, providing the same security guarantees as the in-memory implementation but with distributed storage benefits.
Configuration ¶
Basic usage:
store, err := valkey.New(valkey.Config{
Address: "localhost:6379",
KeyPrefix: "mcp:",
})
With TLS:
store, err := valkey.New(valkey.Config{
Address: "valkey.example.com:6379",
Password: os.Getenv("VALKEY_PASSWORD"),
TLS: &tls.Config{MinVersion: tls.VersionTLS12},
KeyPrefix: "mcp:",
})
Security Considerations ¶
- All tokens are stored with TTLs to prevent unbounded growth
- Lua scripts ensure atomic operations for security-critical flows
- Constant-time bcrypt comparison prevents timing attacks in client validation
- TLS support enables encrypted connections to Valkey servers
- Family metadata is retained for configurable period for security forensics
- Optional token encryption at rest via SetEncryptor() using AES-256-GCM
- Input size validation prevents DoS attacks via oversized payloads
- Generic error messages prevent information leakage
Token Encryption at Rest ¶
Sensitive oauth2.Token fields (AccessToken, RefreshToken) can be encrypted before storing in Valkey:
key, _ := security.GenerateKey() encryptor, _ := security.NewEncryptor(key) store.SetEncryptor(encryptor)
When enabled, tokens are encrypted with AES-256-GCM before storage and automatically decrypted when retrieved.
Best Practices ¶
- Always use TLS in production environments
- Set strong passwords for Valkey authentication
- Enable token encryption at rest for sensitive deployments
- Use dedicated Valkey instances or databases for OAuth storage
- Monitor key count and memory usage for potential DoS attacks
- Configure appropriate TTLs based on your security requirements
Index ¶
- Constants
- type Config
- type Store
- func (s *Store) AtomicCheckAndMarkAuthCodeUsed(ctx context.Context, code string) (authCode *storage.AuthorizationCode, err error)
- func (s *Store) AtomicGetAndDeleteRefreshToken(ctx context.Context, refreshToken string) (resUserID, resClientID string, resToken *oauth2.Token, err error)
- func (s *Store) CheckIPLimit(ctx context.Context, ip string, maxClientsPerIP int) (err error)
- func (s *Store) Close()
- func (s *Store) DeleteAuthorizationCode(ctx context.Context, code string) (err error)
- func (s *Store) DeleteAuthorizationState(ctx context.Context, stateID string) (err error)
- func (s *Store) DeleteRefreshToken(ctx context.Context, refreshToken string) (err error)
- func (s *Store) DeleteToken(ctx context.Context, userID string) (err error)
- func (s *Store) GetAuthorizationCode(ctx context.Context, code string) (result *storage.AuthorizationCode, err error)
- func (s *Store) GetAuthorizationState(ctx context.Context, stateID string) (result *storage.AuthorizationState, err error)
- func (s *Store) GetAuthorizationStateByProviderState(ctx context.Context, providerState string) (result *storage.AuthorizationState, err error)
- func (s *Store) GetClient(ctx context.Context, clientID string) (result *storage.Client, err error)
- func (s *Store) GetRefreshTokenFamily(ctx context.Context, refreshToken string) (result *storage.RefreshTokenFamilyMetadata, err error)
- func (s *Store) GetRefreshTokenInfo(ctx context.Context, refreshToken string) (userID string, err error)
- func (s *Store) GetToken(ctx context.Context, userID string) (result *oauth2.Token, err error)
- func (s *Store) GetTokenMetadata(tokenID string) (*storage.TokenMetadata, error)
- func (s *Store) GetTokensByUserClient(ctx context.Context, userID, clientID string) (tokens []string, err error)
- func (s *Store) GetUserInfo(ctx context.Context, userID string) (result *providers.UserInfo, err error)
- func (s *Store) ListClients(ctx context.Context) (result []*storage.Client, err error)
- func (s *Store) RevokeAllTokensForUserClient(ctx context.Context, userID, clientID string) (count int, err error)
- func (s *Store) RevokeRefreshTokenFamily(ctx context.Context, familyID string) (err error)
- func (s *Store) SaveAuthorizationCode(ctx context.Context, code *storage.AuthorizationCode) (err error)
- func (s *Store) SaveAuthorizationState(ctx context.Context, state *storage.AuthorizationState) (err error)
- func (s *Store) SaveClient(ctx context.Context, client *storage.Client) (err error)
- func (s *Store) SaveRefreshToken(ctx context.Context, refreshToken, userID string, expiresAt time.Time) (err error)
- func (s *Store) SaveRefreshTokenWithFamily(ctx context.Context, refreshToken, userID, clientID, familyID string, ...) (err error)
- func (s *Store) SaveToken(ctx context.Context, userID string, token *oauth2.Token) (err error)
- func (s *Store) SaveTokenMetadata(tokenID, userID, clientID, tokenType string) error
- func (s *Store) SaveTokenMetadataWithAudience(tokenID, userID, clientID, tokenType, audience string) error
- func (s *Store) SaveTokenMetadataWithFamily(tokenID, userID, clientID, tokenType, audience, familyID string, ...) error
- func (s *Store) SaveTokenMetadataWithScopesAndAudience(tokenID, userID, clientID, tokenType, audience string, scopes []string) error
- func (s *Store) SaveUserInfo(ctx context.Context, userID string, info *providers.UserInfo) (err error)
- func (s *Store) SetEncryptor(enc *security.Encryptor)
- func (s *Store) SetInstrumentation(inst *instrumentation.Instrumentation)
- func (s *Store) SetLogger(logger *slog.Logger)
- func (s *Store) TrackClientIP(ctx context.Context, ip string) (err error)
- func (s *Store) ValidateClientSecret(ctx context.Context, clientID, clientSecret string) (err error)
Constants ¶
const ( // DefaultKeyPrefix is the default prefix for all Valkey keys DefaultKeyPrefix = "mcp:" // DefaultRevokedFamilyRetentionDays is the default retention period for revoked token families DefaultRevokedFamilyRetentionDays = 90 // DefaultRefreshTokenTTL is the default TTL for provider tokens that have // a refresh token. This prevents orphaned keys from living indefinitely // when the refresh token is consumed or expires without explicit cleanup. DefaultRefreshTokenTTL = 90 * 24 * time.Hour // MaxTokenLength is the maximum allowed length for token strings (512 bytes) // This prevents DoS attacks via excessively large tokens MaxTokenLength = 512 // MaxIDLength is the maximum allowed length for identifiers (userID, clientID, familyID) MaxIDLength = 256 // MaxTokenDataSize is the maximum size of serialized token data (256KB). // This prevents memory exhaustion from large token payloads while accommodating // enterprise OIDC tokens that embed large groups claims in signed JWTs. MaxTokenDataSize = 256 * 1024 )
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Address is the Valkey server address (required), e.g., "localhost:6379"
Address string
// Password is the optional password for Valkey authentication
Password string
// DB is the optional database number (default 0)
DB int
// KeyPrefix is the prefix for all keys (default "mcp:")
KeyPrefix string
// TLS is the optional TLS configuration for encrypted connections
TLS *tls.Config
// Logger is the optional structured logger (default: slog.Default())
Logger *slog.Logger
// RevokedFamilyRetentionDays is the retention period for revoked token family metadata
// Used for security forensics and auditing. Default: 90 days
RevokedFamilyRetentionDays int
// RefreshTokenTTL is the TTL applied to provider tokens that have a refresh
// token. This should match the MCP server's refresh token lifetime so that
// Valkey automatically evicts orphaned provider tokens. Default: 90 days
RefreshTokenTTL time.Duration
}
Config holds configuration for the Valkey storage backend.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a Valkey-backed implementation of all storage interfaces. It implements TokenStore, ClientStore, FlowStore, RefreshTokenFamilyStore, and TokenRevocationStore.
func New ¶
New creates a new Valkey-backed storage instance. Returns an error if the connection cannot be established.
func (*Store) AtomicCheckAndMarkAuthCodeUsed ¶
func (s *Store) AtomicCheckAndMarkAuthCodeUsed(ctx context.Context, code string) (authCode *storage.AuthorizationCode, err 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 is already used.
SECURITY: This operation is atomic via Lua script - only ONE concurrent request can succeed.
IMPORTANT: The authCode is ONLY returned on reuse errors (Used=true) to enable detection and revocation. For other errors (not found, expired), nil is returned to prevent information leakage.
func (*Store) AtomicGetAndDeleteRefreshToken ¶
func (s *Store) AtomicGetAndDeleteRefreshToken(ctx context.Context, refreshToken string) (resUserID, resClientID string, resToken *oauth2.Token, err 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.
SECURITY: This operation is atomic via Lua script - only ONE concurrent request can succeed. SECURITY: Returns clientID for client binding validation per OAuth 2.1 Section 6.
func (*Store) CheckIPLimit ¶
CheckIPLimit checks if an IP has reached the client registration limit
func (*Store) DeleteAuthorizationCode ¶
DeleteAuthorizationCode removes an authorization code
func (*Store) DeleteAuthorizationState ¶
DeleteAuthorizationState removes an authorization state Removes both client state and provider state entries
func (*Store) DeleteRefreshToken ¶
DeleteRefreshToken removes a refresh token
func (*Store) DeleteToken ¶
DeleteToken removes a token for a user
func (*Store) GetAuthorizationCode ¶
func (s *Store) GetAuthorizationCode(ctx context.Context, code string) (result *storage.AuthorizationCode, err error)
GetAuthorizationCode retrieves an authorization code without modifying it. NOTE: For actual code exchange, use AtomicCheckAndMarkAuthCodeUsed instead to prevent race conditions.
func (*Store) GetAuthorizationState ¶
func (s *Store) GetAuthorizationState(ctx context.Context, stateID string) (result *storage.AuthorizationState, err error)
GetAuthorizationState retrieves an authorization state by client state
func (*Store) GetAuthorizationStateByProviderState ¶
func (s *Store) GetAuthorizationStateByProviderState(ctx context.Context, providerState string) (result *storage.AuthorizationState, err error)
GetAuthorizationStateByProviderState retrieves an authorization state by provider state This is used during provider callback validation (separate from client state)
func (*Store) GetRefreshTokenFamily ¶
func (s *Store) GetRefreshTokenFamily(ctx context.Context, refreshToken string) (result *storage.RefreshTokenFamilyMetadata, err error)
GetRefreshTokenFamily retrieves family metadata for a refresh token
func (*Store) GetRefreshTokenInfo ¶
func (s *Store) GetRefreshTokenInfo(ctx context.Context, refreshToken string) (userID string, err error)
GetRefreshTokenInfo retrieves the user ID for a refresh token
func (*Store) GetTokenMetadata ¶
func (s *Store) GetTokenMetadata(tokenID string) (*storage.TokenMetadata, error)
GetTokenMetadata retrieves metadata for a token (including RFC 8707 audience)
func (*Store) GetTokensByUserClient ¶
func (s *Store) GetTokensByUserClient(ctx context.Context, userID, clientID string) (tokens []string, err error)
GetTokensByUserClient retrieves all token IDs for a user+client combination. This is primarily for testing and debugging purposes.
func (*Store) GetUserInfo ¶
func (s *Store) GetUserInfo(ctx context.Context, userID string) (result *providers.UserInfo, err error)
GetUserInfo retrieves user information
func (*Store) ListClients ¶
ListClients lists all registered clients
func (*Store) RevokeAllTokensForUserClient ¶
func (s *Store) RevokeAllTokensForUserClient(ctx context.Context, userID, clientID string) (count int, err error)
RevokeAllTokensForUserClient revokes all tokens (access + refresh) for a specific user+client combination. This implements the OAuth 2.1 requirement for authorization code reuse detection. Returns the number of tokens revoked and any error encountered.
func (*Store) RevokeRefreshTokenFamily ¶
RevokeRefreshTokenFamily revokes all tokens in a family (for reuse detection) This is called when token reuse is detected (OAuth 2.1 security requirement)
func (*Store) SaveAuthorizationCode ¶
func (s *Store) SaveAuthorizationCode(ctx context.Context, code *storage.AuthorizationCode) (err error)
SaveAuthorizationCode saves an issued authorization code
func (*Store) SaveAuthorizationState ¶
func (s *Store) SaveAuthorizationState(ctx context.Context, state *storage.AuthorizationState) (err error)
SaveAuthorizationState saves the state of an ongoing authorization flow Stores by both client state (StateID) and provider state (ProviderState) for dual lookup
func (*Store) SaveClient ¶
SaveClient saves a registered client
func (*Store) SaveRefreshToken ¶
func (s *Store) SaveRefreshToken(ctx context.Context, refreshToken, userID string, expiresAt time.Time) (err error)
SaveRefreshToken saves a refresh token mapping to user ID with expiry
func (*Store) SaveRefreshTokenWithFamily ¶
func (s *Store) SaveRefreshTokenWithFamily(ctx context.Context, refreshToken, userID, clientID, familyID string, generation int, expiresAt time.Time) (err error)
SaveRefreshTokenWithFamily saves a refresh token with family tracking for reuse detection This is the OAuth 2.1 compliant version that enables token theft detection
func (*Store) SaveToken ¶
SaveToken saves an oauth2.Token for a user with optional encryption at rest
func (*Store) SaveTokenMetadata ¶
SaveTokenMetadata saves metadata for a token (for revocation tracking)
func (*Store) SaveTokenMetadataWithAudience ¶
func (s *Store) SaveTokenMetadataWithAudience(tokenID, userID, clientID, tokenType, audience string) error
SaveTokenMetadataWithAudience saves metadata for a token including RFC 8707 audience
func (*Store) SaveTokenMetadataWithFamily ¶ added in v0.2.81
func (s *Store) SaveTokenMetadataWithFamily(tokenID, userID, clientID, tokenType, audience, familyID string, scopes []string) error
SaveTokenMetadataWithFamily saves metadata for a token including audience, scopes, and refresh token family ID. The familyID links the token to a session (refresh token family) for per-session state tracking.
func (*Store) SaveTokenMetadataWithScopesAndAudience ¶
func (s *Store) SaveTokenMetadataWithScopesAndAudience(tokenID, userID, clientID, tokenType, audience string, scopes []string) error
SaveTokenMetadataWithScopesAndAudience saves metadata for a token including RFC 8707 audience and MCP 2025-11-25 scopes
func (*Store) SaveUserInfo ¶
func (s *Store) SaveUserInfo(ctx context.Context, userID string, info *providers.UserInfo) (err error)
SaveUserInfo saves user information
func (*Store) SetEncryptor ¶
SetEncryptor sets the token encryptor for encryption at rest. When set, oauth2.Token access and refresh tokens will be encrypted before storing in Valkey and decrypted when retrieved.
func (*Store) SetInstrumentation ¶ added in v0.2.49
func (s *Store) SetInstrumentation(inst *instrumentation.Instrumentation)
SetInstrumentation sets OpenTelemetry instrumentation for the store. This enables tracing and metrics for storage operations. This method should be called once during initialization before any storage operations.
func (*Store) TrackClientIP ¶
TrackClientIP increments the client count for an IP address