valkey

package
v0.2.113 Latest Latest
Warning

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

Go to latest
Published: Apr 28, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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:

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

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

func New(cfg Config) (*Store, error)

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

func (s *Store) CheckIPLimit(ctx context.Context, ip string, maxClientsPerIP int) (err error)

CheckIPLimit checks if an IP has reached the client registration limit

func (*Store) Close

func (s *Store) Close()

Close closes the Valkey client connection.

func (*Store) DeleteAuthorizationCode

func (s *Store) DeleteAuthorizationCode(ctx context.Context, code string) (err error)

DeleteAuthorizationCode removes an authorization code

func (*Store) DeleteAuthorizationState

func (s *Store) DeleteAuthorizationState(ctx context.Context, stateID string) (err error)

DeleteAuthorizationState removes an authorization state Removes both client state and provider state entries

func (*Store) DeleteRefreshToken

func (s *Store) DeleteRefreshToken(ctx context.Context, refreshToken string) (err error)

DeleteRefreshToken removes a refresh token

func (*Store) DeleteToken

func (s *Store) DeleteToken(ctx context.Context, userID string) (err error)

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) GetClient

func (s *Store) GetClient(ctx context.Context, clientID string) (result *storage.Client, err error)

GetClient retrieves a client by ID

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) GetToken

func (s *Store) GetToken(ctx context.Context, userID string) (result *oauth2.Token, err error)

GetToken retrieves an oauth2.Token for a user and decrypts if necessary

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

func (s *Store) ListClients(ctx context.Context) (result []*storage.Client, err error)

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

func (s *Store) RevokeRefreshTokenFamily(ctx context.Context, familyID string) (err error)

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

func (s *Store) SaveClient(ctx context.Context, client *storage.Client) (err error)

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

func (s *Store) SaveToken(ctx context.Context, userID string, token *oauth2.Token) (err error)

SaveToken saves an oauth2.Token for a user with optional encryption at rest

func (*Store) SaveTokenMetadata

func (s *Store) SaveTokenMetadata(tokenID, userID, clientID, tokenType string) error

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

func (s *Store) SetEncryptor(enc *security.Encryptor)

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) SetLogger

func (s *Store) SetLogger(logger *slog.Logger)

SetLogger sets a custom logger for the store.

func (*Store) TrackClientIP

func (s *Store) TrackClientIP(ctx context.Context, ip string) (err error)

TrackClientIP increments the client count for an IP address

func (*Store) ValidateClientSecret

func (s *Store) ValidateClientSecret(ctx context.Context, clientID, clientSecret string) (err error)

ValidateClientSecret validates a client's secret using bcrypt Uses constant-time operations to prevent timing attacks

Jump to

Keyboard shortcuts

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