Documentation
¶
Overview ¶
Package storage provides storage interfaces and implementations for the OAuth authorization server. This package implements fosite's storage interfaces to persist OAuth tokens and related data.
Fosite Storage Architecture ¶
Fosite uses Interface Segregation Principle to split storage into focused interfaces. Each OAuth feature (authorization codes, access tokens, refresh tokens, PKCE) has its own storage interface. This design allows:
- Feature composition: Enable only the OAuth features you need
- Testing isolation: Mock only the interfaces relevant to your test
- Clear contracts: Each interface documents exactly what it requires
The main fosite storage interfaces we implement:
- oauth2.AuthorizeCodeStorage: Authorization code grant (RFC 6749 Section 4.1)
- oauth2.AccessTokenStorage: Access token persistence
- oauth2.RefreshTokenStorage: Refresh token persistence
- oauth2.TokenRevocationStorage: Token revocation (RFC 7009)
- pkce.PKCERequestStorage: PKCE challenge storage (RFC 7636)
- fosite.ClientManager: OAuth client lookup and JWT assertion tracking
fosite.Requester: The Central Type ¶
fosite.Requester is the core abstraction representing an OAuth request context. All token storage methods store the full Requester, not just the token value, because:
- Context preservation: Token validation requires the original request context (client, scopes, audience, session) to make authorization decisions
- Introspection support: RFC 7662 token introspection returns metadata about the token (client_id, scope, exp, etc.) which lives in the Requester
- Revocation support: Revoking by request ID requires finding all tokens from the same authorization grant, which means storing the grant context
- Session data: The embedded Session contains expiration times per token type, subject, username, and custom claims needed for token generation
A Requester contains:
- ID: Unique identifier for the authorization grant (request ID)
- Client: The OAuth client that initiated the request
- RequestedScopes/GrantedScopes: What scopes were requested and granted
- RequestedAudience/GrantedAudience: What audiences were requested and granted
- Session: Token expiration times, subject, and custom data
- Form: Original request form values (sanitized for storage)
Signature vs Request ID: Two Lookup Keys ¶
Storage methods use two different keys for different operations:
Signature (token-specific operations):
- Used by: CreateAccessTokenSession, GetAccessTokenSession, DeleteAccessTokenSession
- What it is: A cryptographic signature or hash derived from the token value
- Purpose: Look up a specific token when you have the token value
- Example flow: Client sends access token -> derive signature -> look up session
Request ID (grant-wide operations):
- Used by: RevokeAccessToken, RevokeRefreshToken, RotateRefreshToken
- What it is: The unique identifier of the original authorization grant
- Purpose: Find ALL tokens issued from the same authorization grant
- Example flow: Revoke refresh token -> find request ID -> delete all related tokens
Why two keys? RFC 7009 requires that revoking a refresh token SHOULD also revoke associated access tokens. This requires finding tokens by their common origin (request ID) rather than by their individual values. The request ID ties together:
- The authorization code (one-time use)
- All access tokens issued from that grant
- All refresh tokens issued from that grant
Our implementation stores tokens keyed by signature for O(1) token lookup, but revocation requires O(n) scan by request ID. Production implementations often maintain a reverse index (request_id -> signatures) for efficient revocation.
fosite.Session: Token Metadata Container ¶
fosite.Session is an interface for storing session data between OAuth requests. Key design points:
Why GetExpiresAt lives on Session:
- Different token types have different lifetimes (access: hours, refresh: days)
- Expiration is metadata ABOUT the token, not the token itself
- Session is the natural place for token metadata
- Usage: session.GetExpiresAt(fosite.AccessToken) vs session.GetExpiresAt(fosite.RefreshToken)
Session vs Requester:
- Session: Token-specific metadata (expiration, subject, username, claims)
- Requester: Full request context including Session, Client, scopes, etc.
- Session is embedded in Requester: requester.GetSession() returns the Session
Our session.Session type extends fosite's oauth2.JWTSession to add:
- UpstreamSessionID: Links to tokens from our upstream IDP
- JWT claims: Custom claims like "tsid" for token session lookup
fosite.Client vs fosite.Requester ¶
Client and Requester serve different roles in the OAuth lifecycle:
fosite.Client represents the registered OAuth application:
- Static data: client_id, client_secret, redirect_uris, allowed scopes/grants
- Loaded from ClientRegistry (our extension) or fosite.ClientManager
- Used to validate incoming requests against client configuration
fosite.Requester represents a specific authorization request:
- Dynamic data: specific scopes requested/granted, session, form values
- Created during authorization, stored with tokens
- Contains a reference to Client via GetClient()
The relationship:
Client (static config) <--- Requester (instance) ---> Session (token metadata)
| | |
"What can this app do?" "What did this request grant?" "When does it expire?"
When to use each:
- GetClient: Validate client_id/secret, check allowed scopes/redirects
- Requester: Issue tokens, check what was actually granted, introspect tokens
Get Methods Accept Session Parameter ¶
Methods like GetAccessTokenSession(ctx, signature, session) accept a Session parameter. This session is a "prototype" that may be used for deserialization:
- Some storage backends serialize the full Requester (including Session)
- On retrieval, they need a session instance to deserialize into
- The prototype provides the concrete type for JSON/gob deserialization
- If your storage keeps Requesters in memory, this parameter may be unused
Our in-memory implementation ignores this parameter since we store live Requester objects. Persistent backends (SQL, Redis) would use it for deserialization.
ToolHive Extensions ¶
Beyond fosite's interfaces, we add ToolHive-specific storage:
- UpstreamTokenStorage: Store tokens from upstream IDPs for proxy token swap
- PendingAuthorizationStorage: Track in-flight authorizations during IDP redirect
- ClientRegistry: Dynamic client registration (RFC 7591) via RegisterClient
These integrate with fosite's token storage to provide end-to-end OAuth proxy functionality: store upstream tokens, link them to issued tokens via session IDs, and enable transparent token swap for backend requests.
Implementation Notes ¶
Thread safety: MemoryStorage uses sync.RWMutex for all map access. Persistent backends should use appropriate transaction isolation.
Expiration: We use timedEntry wrapper to track creation and expiration times. A background goroutine periodically cleans expired entries. Production backends might use database TTL features or scheduled jobs.
Defensive copies: Store and retrieve methods make deep copies to prevent aliasing issues where callers might modify returned data.
Error mapping: Storage errors are wrapped with both our sentinel errors (ErrNotFound, ErrExpired) and fosite errors (fosite.ErrNotFound) for compatibility with fosite's error handling.
References ¶
- RFC 6749: OAuth 2.0 Authorization Framework
- RFC 7009: OAuth 2.0 Token Revocation
- RFC 7636: Proof Key for Code Exchange (PKCE)
- RFC 7591: OAuth 2.0 Dynamic Client Registration
- RFC 7662: OAuth 2.0 Token Introspection
- Fosite documentation: https://github.com/ory/fosite
Package storage provides storage interfaces and implementations for the OAuth authorization server.
Index ¶
- Constants
- Variables
- func DeriveKeyPrefix(namespace, name string) string
- type ACLUserConfig
- type ACLUserRunConfig
- type ClientRegistry
- type Config
- type MemoryStorage
- func (s *MemoryStorage) ClientAssertionJWTValid(_ context.Context, jti string) error
- func (s *MemoryStorage) Close() error
- func (s *MemoryStorage) CreateAccessTokenSession(_ context.Context, signature string, request fosite.Requester) error
- func (s *MemoryStorage) CreateAuthorizeCodeSession(_ context.Context, code string, request fosite.Requester) error
- func (s *MemoryStorage) CreatePKCERequestSession(_ context.Context, signature string, request fosite.Requester) error
- func (s *MemoryStorage) CreateProviderIdentity(_ context.Context, identity *ProviderIdentity) error
- func (s *MemoryStorage) CreateRefreshTokenSession(_ context.Context, signature string, _ string, request fosite.Requester) error
- func (s *MemoryStorage) CreateUser(_ context.Context, user *User) error
- func (s *MemoryStorage) DeleteAccessTokenSession(_ context.Context, signature string) error
- func (s *MemoryStorage) DeletePKCERequestSession(_ context.Context, signature string) error
- func (s *MemoryStorage) DeletePendingAuthorization(_ context.Context, state string) error
- func (s *MemoryStorage) DeleteRefreshTokenSession(_ context.Context, signature string) error
- func (s *MemoryStorage) DeleteUpstreamTokens(_ context.Context, sessionID string) error
- func (s *MemoryStorage) DeleteUser(_ context.Context, id string) error
- func (s *MemoryStorage) GetAccessTokenSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
- func (s *MemoryStorage) GetAuthorizeCodeSession(_ context.Context, code string, _ fosite.Session) (fosite.Requester, error)
- func (s *MemoryStorage) GetClient(_ context.Context, id string) (fosite.Client, error)
- func (s *MemoryStorage) GetPKCERequestSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
- func (s *MemoryStorage) GetProviderIdentity(_ context.Context, providerID, providerSubject string) (*ProviderIdentity, error)
- func (s *MemoryStorage) GetRefreshTokenSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
- func (s *MemoryStorage) GetUpstreamTokens(_ context.Context, sessionID string) (*UpstreamTokens, error)
- func (s *MemoryStorage) GetUser(_ context.Context, id string) (*User, error)
- func (s *MemoryStorage) GetUserProviderIdentities(_ context.Context, userID string) ([]*ProviderIdentity, error)
- func (*MemoryStorage) Health(_ context.Context) error
- func (s *MemoryStorage) InvalidateAuthorizeCodeSession(_ context.Context, code string) error
- func (s *MemoryStorage) LoadPendingAuthorization(_ context.Context, state string) (*PendingAuthorization, error)
- func (s *MemoryStorage) RegisterClient(_ context.Context, client fosite.Client) error
- func (s *MemoryStorage) RevokeAccessToken(_ context.Context, requestID string) error
- func (s *MemoryStorage) RevokeRefreshToken(_ context.Context, requestID string) error
- func (s *MemoryStorage) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, _ string) error
- func (s *MemoryStorage) RotateRefreshToken(_ context.Context, requestID string, refreshTokenSignature string) error
- func (s *MemoryStorage) SetClientAssertionJWT(_ context.Context, jti string, exp time.Time) error
- func (s *MemoryStorage) Stats() Stats
- func (s *MemoryStorage) StorePendingAuthorization(_ context.Context, state string, pending *PendingAuthorization) error
- func (s *MemoryStorage) StoreUpstreamTokens(_ context.Context, sessionID string, tokens *UpstreamTokens) error
- func (s *MemoryStorage) UpdateProviderIdentityLastUsed(_ context.Context, providerID, providerSubject string, lastUsedAt time.Time) error
- type MemoryStorageOption
- type PendingAuthorization
- type PendingAuthorizationStorage
- type ProviderIdentity
- type RedisConfig
- type RedisRunConfig
- type RedisStorage
- func (s *RedisStorage) ClientAssertionJWTValid(ctx context.Context, jti string) error
- func (s *RedisStorage) Close() error
- func (s *RedisStorage) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) error
- func (s *RedisStorage) CreateAuthorizeCodeSession(ctx context.Context, code string, request fosite.Requester) error
- func (s *RedisStorage) CreatePKCERequestSession(ctx context.Context, signature string, request fosite.Requester) error
- func (s *RedisStorage) CreateProviderIdentity(ctx context.Context, identity *ProviderIdentity) error
- func (s *RedisStorage) CreateRefreshTokenSession(ctx context.Context, signature string, _ string, request fosite.Requester) error
- func (s *RedisStorage) CreateUser(ctx context.Context, user *User) error
- func (s *RedisStorage) DeleteAccessTokenSession(ctx context.Context, signature string) error
- func (s *RedisStorage) DeletePKCERequestSession(ctx context.Context, signature string) error
- func (s *RedisStorage) DeletePendingAuthorization(ctx context.Context, state string) error
- func (s *RedisStorage) DeleteRefreshTokenSession(ctx context.Context, signature string) error
- func (s *RedisStorage) DeleteUpstreamTokens(ctx context.Context, sessionID string) error
- func (s *RedisStorage) DeleteUser(ctx context.Context, id string) error
- func (s *RedisStorage) GetAccessTokenSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
- func (s *RedisStorage) GetAuthorizeCodeSession(ctx context.Context, code string, _ fosite.Session) (fosite.Requester, error)
- func (s *RedisStorage) GetClient(ctx context.Context, id string) (fosite.Client, error)
- func (s *RedisStorage) GetPKCERequestSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
- func (s *RedisStorage) GetProviderIdentity(ctx context.Context, providerID, providerSubject string) (*ProviderIdentity, error)
- func (s *RedisStorage) GetRefreshTokenSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
- func (s *RedisStorage) GetUpstreamTokens(ctx context.Context, sessionID string) (*UpstreamTokens, error)
- func (s *RedisStorage) GetUser(ctx context.Context, id string) (*User, error)
- func (s *RedisStorage) GetUserProviderIdentities(ctx context.Context, userID string) ([]*ProviderIdentity, error)
- func (s *RedisStorage) Health(ctx context.Context) error
- func (s *RedisStorage) InvalidateAuthorizeCodeSession(ctx context.Context, code string) error
- func (s *RedisStorage) LoadPendingAuthorization(ctx context.Context, state string) (*PendingAuthorization, error)
- func (s *RedisStorage) RegisterClient(ctx context.Context, client fosite.Client) error
- func (s *RedisStorage) RevokeAccessToken(ctx context.Context, requestID string) error
- func (s *RedisStorage) RevokeRefreshToken(ctx context.Context, requestID string) error
- func (s *RedisStorage) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, _ string) error
- func (s *RedisStorage) RotateRefreshToken(ctx context.Context, requestID string, refreshTokenSignature string) error
- func (s *RedisStorage) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error
- func (s *RedisStorage) StorePendingAuthorization(ctx context.Context, state string, pending *PendingAuthorization) error
- func (s *RedisStorage) StoreUpstreamTokens(ctx context.Context, sessionID string, tokens *UpstreamTokens) error
- func (s *RedisStorage) UpdateProviderIdentityLastUsed(ctx context.Context, providerID, providerSubject string, lastUsedAt time.Time) error
- type RunConfig
- type SentinelConfig
- type SentinelRunConfig
- type Stats
- type Storage
- type Type
- type UpstreamTokenStorage
- type UpstreamTokens
- type User
- type UserStorage
Constants ¶
const ( // TypeMemory uses in-memory storage (default). TypeMemory Type = "memory" // TypeRedis uses Redis Sentinel-backed storage for distributed deployments. TypeRedis Type = "redis" // DefaultCleanupInterval is how often the background cleanup runs. DefaultCleanupInterval = 5 * time.Minute // DefaultAccessTokenTTL is the default TTL for access tokens when not extractable from session. DefaultAccessTokenTTL = 1 * time.Hour // DefaultRefreshTokenTTL is the default TTL for refresh tokens when not extractable from session. DefaultRefreshTokenTTL = 30 * 24 * time.Hour // 30 days // DefaultAuthCodeTTL is the default TTL for authorization codes (RFC 6749 recommendation). DefaultAuthCodeTTL = 10 * time.Minute // DefaultInvalidatedCodeTTL is how long invalidated codes are kept for replay detection. DefaultInvalidatedCodeTTL = 30 * time.Minute // DefaultPKCETTL is the default TTL for PKCE requests (same as auth codes). DefaultPKCETTL = 10 * time.Minute // DefaultPublicClientTTL is the TTL for dynamically registered public clients. // This prevents unbounded growth from DCR. Confidential clients don't expire. DefaultPublicClientTTL = 30 * 24 * time.Hour // 30 days )
const ( DefaultDialTimeout = 5 * time.Second DefaultReadTimeout = 3 * time.Second DefaultWriteTimeout = 3 * time.Second )
Default timeouts for Redis operations.
const ( // KeyTypeAccess is the key type for access tokens. KeyTypeAccess = "access" // KeyTypeRefresh is the key type for refresh tokens. KeyTypeRefresh = "refresh" // KeyTypeAuthCode is the key type for authorization codes. KeyTypeAuthCode = "authcode" // KeyTypePKCE is the key type for PKCE requests. KeyTypePKCE = "pkce" // KeyTypeClient is the key type for OAuth clients. KeyTypeClient = "client" // KeyTypeUser is the key type for users. KeyTypeUser = "user" // KeyTypeProvider is the key type for provider identities. KeyTypeProvider = "provider" // KeyTypeUpstream is the key type for upstream tokens. KeyTypeUpstream = "upstream" // KeyTypePending is the key type for pending authorizations. KeyTypePending = "pending" // KeyTypeInvalidated is the key type for invalidated authorization codes. KeyTypeInvalidated = "invalidated" // KeyTypeJWT is the key type for client assertion JWTs. KeyTypeJWT = "jwt" // KeyTypeReqIDAccess is the key type for request ID to access token mappings. KeyTypeReqIDAccess = "reqid:access" // KeyTypeReqIDRefresh is the key type for request ID to refresh token mappings. KeyTypeReqIDRefresh = "reqid:refresh" // KeyTypeUserUpstream is the key type for user to upstream token reverse lookups. KeyTypeUserUpstream = "user:upstream" // KeyTypeUserProviders is the key type for user to provider identity reverse lookups. KeyTypeUserProviders = "user:providers" )
Key type constants for Redis storage. These define the different types of data stored in Redis.
const DefaultPendingAuthorizationTTL = 10 * time.Minute
DefaultPendingAuthorizationTTL is the default TTL for pending authorization requests.
Variables ¶
var ( // ErrNotFound is returned when an item is not found in storage. ErrNotFound = errors.New("storage: item not found") // ErrExpired is returned when an item exists but has expired. ErrExpired = errors.New("storage: item expired") // ErrAlreadyExists is returned when attempting to create an item that already exists. ErrAlreadyExists = errors.New("storage: item already exists") // ErrInvalidBinding is returned when token binding validation fails // (e.g., subject or client ID mismatch). ErrInvalidBinding = errors.New("storage: token binding validation failed") )
Sentinel errors for storage operations. Use errors.Is() to check for these error types.
Functions ¶
func DeriveKeyPrefix ¶ added in v0.9.4
DeriveKeyPrefix creates the key prefix from server namespace and name. The format is "thv:auth:{ns:name}:" where {ns:name} is a Redis hash tag.
Note: The hash tag format {ns:name} intentionally combines namespace and name into a single tag. In Redis Cluster, only the first hash tag determines slot assignment. Using {ns}:{name} would only hash on namespace, potentially spreading a single server's keys across multiple slots. The combined format ensures all keys for a specific server (namespace+name pair) are placed in the same slot, enabling atomic multi-key operations like token revocation.
Types ¶
type ACLUserConfig ¶ added in v0.9.4
ACLUserConfig contains Redis ACL user authentication configuration.
type ACLUserRunConfig ¶ added in v0.9.4
type ACLUserRunConfig struct {
// UsernameEnvVar is the environment variable containing the Redis username.
UsernameEnvVar string `json:"usernameEnvVar" yaml:"usernameEnvVar"`
// PasswordEnvVar is the environment variable containing the Redis password.
PasswordEnvVar string `json:"passwordEnvVar" yaml:"passwordEnvVar"`
}
ACLUserRunConfig contains Redis ACL user authentication configuration. Credentials are read from environment variables for security.
type ClientRegistry ¶
type ClientRegistry interface {
// ClientManager provides client lookup (GetClient)
fosite.ClientManager
// RegisterClient registers a new OAuth client.
// This supports both static configuration and dynamic client registration (RFC 7591).
// Returns ErrAlreadyExists if a client with the same ID already exists.
RegisterClient(ctx context.Context, client fosite.Client) error
}
ClientRegistry provides client registration and lookup operations. It embeds fosite.ClientManager for client lookup (GetClient) and adds RegisterClient for dynamic client registration (RFC 7591).
type Config ¶
type Config struct {
// Type specifies the storage backend type. Defaults to memory.
Type Type
}
Config configures the storage backend.
type MemoryStorage ¶ added in v0.7.2
type MemoryStorage struct {
// contains filtered or unexported fields
}
MemoryStorage implements the Storage interface with in-memory maps. This implementation is thread-safe and suitable for development and testing. For production use, consider implementing a persistent storage backend.
Fosite Storage Design ¶
Token maps store fosite.Requester (not just token strings) because fosite needs the full authorization context for validation and introspection. The Requester contains the Client, granted scopes, Session (with expiration times), and more.
Maps are keyed by "signature" (cryptographic token identifier) for O(1) token lookup. Revocation by "request ID" requires O(n) scan; production implementations should maintain a reverse index for efficiency.
func NewMemoryStorage ¶ added in v0.7.2
func NewMemoryStorage(opts ...MemoryStorageOption) *MemoryStorage
NewMemoryStorage creates a new MemoryStorage instance with initialized maps and starts the background cleanup goroutine.
func (*MemoryStorage) ClientAssertionJWTValid ¶ added in v0.7.2
func (s *MemoryStorage) ClientAssertionJWTValid(_ context.Context, jti string) error
ClientAssertionJWTValid returns an error if the JTI is known or the DB check failed, and nil if the JTI is not known (meaning it can be used).
func (*MemoryStorage) Close ¶ added in v0.7.2
func (s *MemoryStorage) Close() error
Close stops the background cleanup goroutine and waits for it to finish. This should be called when the storage is no longer needed.
func (*MemoryStorage) CreateAccessTokenSession ¶ added in v0.7.2
func (s *MemoryStorage) CreateAccessTokenSession(_ context.Context, signature string, request fosite.Requester) error
CreateAccessTokenSession stores the access token session.
func (*MemoryStorage) CreateAuthorizeCodeSession ¶ added in v0.7.2
func (s *MemoryStorage) CreateAuthorizeCodeSession(_ context.Context, code string, request fosite.Requester) error
CreateAuthorizeCodeSession stores the authorization request for a given authorization code.
func (*MemoryStorage) CreatePKCERequestSession ¶ added in v0.7.2
func (s *MemoryStorage) CreatePKCERequestSession(_ context.Context, signature string, request fosite.Requester) error
CreatePKCERequestSession stores the PKCE request session.
func (*MemoryStorage) CreateProviderIdentity ¶ added in v0.8.0
func (s *MemoryStorage) CreateProviderIdentity(_ context.Context, identity *ProviderIdentity) error
CreateProviderIdentity links a provider identity to a user. Returns ErrAlreadyExists if this provider identity is already linked.
func (*MemoryStorage) CreateRefreshTokenSession ¶ added in v0.7.2
func (s *MemoryStorage) CreateRefreshTokenSession(_ context.Context, signature string, _ string, request fosite.Requester) error
CreateRefreshTokenSession stores the refresh token session. The accessSignature parameter is used to link the refresh token to its access token. TODO: Store the accessSignature in a refreshToAccess map to enable direct lookup during token rotation instead of O(n) scan by request ID in RotateRefreshToken.
func (*MemoryStorage) CreateUser ¶ added in v0.8.0
func (s *MemoryStorage) CreateUser(_ context.Context, user *User) error
CreateUser creates a new user account. Returns ErrAlreadyExists if a user with the same ID already exists.
func (*MemoryStorage) DeleteAccessTokenSession ¶ added in v0.7.2
func (s *MemoryStorage) DeleteAccessTokenSession(_ context.Context, signature string) error
DeleteAccessTokenSession removes the access token session.
func (*MemoryStorage) DeletePKCERequestSession ¶ added in v0.7.2
func (s *MemoryStorage) DeletePKCERequestSession(_ context.Context, signature string) error
DeletePKCERequestSession removes the PKCE request session.
func (*MemoryStorage) DeletePendingAuthorization ¶ added in v0.7.2
func (s *MemoryStorage) DeletePendingAuthorization(_ context.Context, state string) error
DeletePendingAuthorization removes a pending authorization.
func (*MemoryStorage) DeleteRefreshTokenSession ¶ added in v0.7.2
func (s *MemoryStorage) DeleteRefreshTokenSession(_ context.Context, signature string) error
DeleteRefreshTokenSession removes the refresh token session.
func (*MemoryStorage) DeleteUpstreamTokens ¶ added in v0.7.2
func (s *MemoryStorage) DeleteUpstreamTokens(_ context.Context, sessionID string) error
DeleteUpstreamTokens removes the upstream IDP tokens for a session.
func (*MemoryStorage) DeleteUser ¶ added in v0.8.0
func (s *MemoryStorage) DeleteUser(_ context.Context, id string) error
DeleteUser removes a user account and all associated provider identities. Returns ErrNotFound if the user does not exist.
func (*MemoryStorage) GetAccessTokenSession ¶ added in v0.7.2
func (s *MemoryStorage) GetAccessTokenSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
GetAccessTokenSession retrieves the access token session by its signature.
The session parameter is a prototype for deserialization in persistent backends. Our in-memory implementation ignores it since we store live Requester objects. Persistent backends (SQL, Redis) use it to know what concrete type to deserialize into.
func (*MemoryStorage) GetAuthorizeCodeSession ¶ added in v0.7.2
func (s *MemoryStorage) GetAuthorizeCodeSession(_ context.Context, code string, _ fosite.Session) (fosite.Requester, error)
GetAuthorizeCodeSession retrieves the authorization request for a given code. If the authorization code has been invalidated, it returns ErrInvalidatedAuthorizeCode along with the request (as required by fosite).
func (*MemoryStorage) GetClient ¶ added in v0.7.2
GetClient loads the client by its ID or returns an error if the client does not exist.
func (*MemoryStorage) GetPKCERequestSession ¶ added in v0.7.2
func (s *MemoryStorage) GetPKCERequestSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
GetPKCERequestSession retrieves the PKCE request session by its signature.
func (*MemoryStorage) GetProviderIdentity ¶ added in v0.8.0
func (s *MemoryStorage) GetProviderIdentity(_ context.Context, providerID, providerSubject string) (*ProviderIdentity, error)
GetProviderIdentity retrieves a provider identity by provider ID and subject. This is the primary lookup path during authentication callbacks. Returns ErrNotFound if the identity does not exist.
func (*MemoryStorage) GetRefreshTokenSession ¶ added in v0.7.2
func (s *MemoryStorage) GetRefreshTokenSession(_ context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
GetRefreshTokenSession retrieves the refresh token session by its signature.
func (*MemoryStorage) GetUpstreamTokens ¶ added in v0.7.2
func (s *MemoryStorage) GetUpstreamTokens(_ context.Context, sessionID string) (*UpstreamTokens, error)
GetUpstreamTokens retrieves the upstream IDP tokens for a session. Returns a defensive copy to prevent aliasing issues.
func (*MemoryStorage) GetUser ¶ added in v0.8.0
GetUser retrieves a user by their internal ID. Returns ErrNotFound if the user does not exist.
func (*MemoryStorage) GetUserProviderIdentities ¶ added in v0.8.0
func (s *MemoryStorage) GetUserProviderIdentities(_ context.Context, userID string) ([]*ProviderIdentity, error)
GetUserProviderIdentities returns all provider identities linked to a user. Returns an empty slice (not error) if the user exists but has no linked identities. Returns ErrNotFound if the user does not exist.
func (*MemoryStorage) Health ¶ added in v0.9.4
func (*MemoryStorage) Health(_ context.Context) error
Health is a no-op for in-memory storage since it is always available.
func (*MemoryStorage) InvalidateAuthorizeCodeSession ¶ added in v0.7.2
func (s *MemoryStorage) InvalidateAuthorizeCodeSession(_ context.Context, code string) error
InvalidateAuthorizeCodeSession marks an authorization code as used/invalid. Subsequent calls to GetAuthorizeCodeSession will return ErrInvalidatedAuthorizeCode.
func (*MemoryStorage) LoadPendingAuthorization ¶ added in v0.7.2
func (s *MemoryStorage) LoadPendingAuthorization(_ context.Context, state string) (*PendingAuthorization, error)
LoadPendingAuthorization retrieves a pending authorization by internal state. Returns a defensive copy to prevent aliasing issues.
func (*MemoryStorage) RegisterClient ¶ added in v0.7.2
RegisterClient adds or updates a client in the storage. This is useful for setting up test clients.
func (*MemoryStorage) RevokeAccessToken ¶ added in v0.7.2
func (s *MemoryStorage) RevokeAccessToken(_ context.Context, requestID string) error
RevokeAccessToken marks an access token as revoked. This method implements the oauth2.TokenRevocationStorage interface.
Note: This takes requestID, not signature. Per RFC 7009, revoking by request ID enables revoking ALL tokens from the same authorization grant. This is why we store the full Requester (with its ID) rather than just token values.
The O(n) scan by request ID is acceptable for in-memory storage. Production implementations should maintain a reverse index (request_id -> signatures).
func (*MemoryStorage) RevokeRefreshToken ¶ added in v0.7.2
func (s *MemoryStorage) RevokeRefreshToken(_ context.Context, requestID string) error
RevokeRefreshToken marks a refresh token as revoked. This method implements the oauth2.TokenRevocationStorage interface.
Like RevokeAccessToken, this takes requestID to find all refresh tokens from the same authorization grant. Per RFC 7009 Section 2.1, implementations SHOULD also revoke associated access tokens, which RotateRefreshToken handles.
func (*MemoryStorage) RevokeRefreshTokenMaybeGracePeriod ¶ added in v0.7.2
func (s *MemoryStorage) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, _ string) error
RevokeRefreshTokenMaybeGracePeriod marks a refresh token as revoked, optionally allowing a grace period during which the old token is still valid. For this implementation, we don't support grace periods and revoke immediately.
func (*MemoryStorage) RotateRefreshToken ¶ added in v0.7.2
func (s *MemoryStorage) RotateRefreshToken(_ context.Context, requestID string, refreshTokenSignature string) error
RotateRefreshToken invalidates a refresh token and all its related token data. This is called during token refresh to implement refresh token rotation.
func (*MemoryStorage) SetClientAssertionJWT ¶ added in v0.7.2
SetClientAssertionJWT marks a JTI as known for the given expiry time. Before inserting the new JTI, it will clean up any existing JTIs that have expired.
func (*MemoryStorage) Stats ¶ added in v0.7.2
func (s *MemoryStorage) Stats() Stats
Stats returns current statistics about storage contents. This is useful for testing and monitoring.
func (*MemoryStorage) StorePendingAuthorization ¶ added in v0.7.2
func (s *MemoryStorage) StorePendingAuthorization(_ context.Context, state string, pending *PendingAuthorization) error
StorePendingAuthorization stores a pending authorization request. The pending authorization is keyed by the internal state used to correlate the upstream IDP callback.
func (*MemoryStorage) StoreUpstreamTokens ¶ added in v0.7.2
func (s *MemoryStorage) StoreUpstreamTokens(_ context.Context, sessionID string, tokens *UpstreamTokens) error
StoreUpstreamTokens stores the upstream IDP tokens for a session. A defensive copy is made to prevent aliasing issues.
func (*MemoryStorage) UpdateProviderIdentityLastUsed ¶ added in v0.8.0
func (s *MemoryStorage) UpdateProviderIdentityLastUsed( _ context.Context, providerID, providerSubject string, lastUsedAt time.Time, ) error
UpdateProviderIdentityLastUsed updates the LastUsedAt timestamp for a provider identity. This should be called after each successful authentication via this identity. Returns ErrNotFound if the identity does not exist.
type MemoryStorageOption ¶ added in v0.7.2
type MemoryStorageOption func(*MemoryStorage)
MemoryStorageOption configures a MemoryStorage instance.
func WithCleanupInterval ¶ added in v0.7.2
func WithCleanupInterval(interval time.Duration) MemoryStorageOption
WithCleanupInterval sets a custom cleanup interval.
type PendingAuthorization ¶
type PendingAuthorization struct {
// ClientID is the ID of the OAuth client making the authorization request.
ClientID string
// RedirectURI is the client's callback URL where we'll redirect after authentication.
RedirectURI string
// State is the client's original state parameter for CSRF protection.
State string
// PKCEChallenge is the client's PKCE code challenge.
PKCEChallenge string
// PKCEMethod is the PKCE challenge method (must be "S256").
PKCEMethod string
// Scopes are the OAuth scopes requested by the client.
Scopes []string
// InternalState is our randomly generated state for correlating upstream callback.
InternalState string
// UpstreamPKCEVerifier is the PKCE code_verifier for the upstream IDP authorization.
// This is generated when redirecting to the upstream IDP and used when exchanging
// the authorization code. See RFC 7636.
UpstreamPKCEVerifier string
// UpstreamNonce is the OIDC nonce parameter sent to the upstream IDP for ID Token
// replay protection. This is validated against the nonce claim in the returned
// ID Token. See OIDC Core Section 3.1.2.1.
UpstreamNonce string
// CreatedAt is when the pending authorization was created.
CreatedAt time.Time
}
PendingAuthorization tracks a client's authorization request while they authenticate with the upstream IDP.
type PendingAuthorizationStorage ¶
type PendingAuthorizationStorage interface {
// StorePendingAuthorization stores a pending authorization request.
// The state is used to correlate the upstream IDP callback.
StorePendingAuthorization(ctx context.Context, state string, pending *PendingAuthorization) error
// LoadPendingAuthorization retrieves a pending authorization by internal state.
// Returns ErrNotFound if the state does not exist.
// Returns ErrExpired if the pending authorization has expired.
LoadPendingAuthorization(ctx context.Context, state string) (*PendingAuthorization, error)
// DeletePendingAuthorization removes a pending authorization.
// Returns ErrNotFound if the state does not exist.
DeletePendingAuthorization(ctx context.Context, state string) error
}
PendingAuthorizationStorage provides storage operations for pending authorization requests. These track the state of in-flight authorization requests while users authenticate with the upstream IDP, correlating the upstream callback with the original client request.
type ProviderIdentity ¶ added in v0.8.0
type ProviderIdentity struct {
// UserID is the internal user ID this identity belongs to.
UserID string
// ProviderID identifies the upstream provider (e.g., "google", "github").
ProviderID string
// ProviderSubject is the subject identifier from the upstream provider.
ProviderSubject string
// LinkedAt is when this identity was linked to the user.
LinkedAt time.Time
// LastUsedAt is when this identity was last used to authenticate.
LastUsedAt time.Time
}
ProviderIdentity links a user to an upstream identity provider. Multiple identities can be linked to a single user for account linking, enabling users to authenticate via different providers (e.g., Google and GitHub) while maintaining a single ToolHive identity.
type RedisConfig ¶ added in v0.9.4
type RedisConfig struct {
// SentinelConfig is required - Sentinel-only deployment.
SentinelConfig *SentinelConfig
// ACLUserConfig is required - ACL user authentication only.
ACLUserConfig *ACLUserConfig
// KeyPrefix for multi-tenancy: "thv:auth:{ns}:{name}:".
KeyPrefix string
// Timeouts (defaults: Dial=5s, Read=3s, Write=3s).
DialTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
}
RedisConfig holds Redis connection configuration for runtime use.
type RedisRunConfig ¶ added in v0.9.4
type RedisRunConfig struct {
// SentinelConfig contains Sentinel-specific configuration.
SentinelConfig *SentinelRunConfig `json:"sentinelConfig,omitempty" yaml:"sentinelConfig,omitempty"`
// AuthType must be "aclUser" - only ACL user authentication is supported.
AuthType string `json:"authType" yaml:"authType"`
// ACLUserConfig contains ACL user authentication configuration.
ACLUserConfig *ACLUserRunConfig `json:"aclUserConfig,omitempty" yaml:"aclUserConfig,omitempty"`
// KeyPrefix for multi-tenancy, typically "thv:auth:{ns}:{name}:".
KeyPrefix string `json:"keyPrefix" yaml:"keyPrefix"`
// DialTimeout is the timeout for establishing connections (e.g., "5s").
DialTimeout string `json:"dialTimeout,omitempty" yaml:"dialTimeout,omitempty"`
// ReadTimeout is the timeout for read operations (e.g., "3s").
ReadTimeout string `json:"readTimeout,omitempty" yaml:"readTimeout,omitempty"`
// WriteTimeout is the timeout for write operations (e.g., "3s").
WriteTimeout string `json:"writeTimeout,omitempty" yaml:"writeTimeout,omitempty"`
}
RedisRunConfig is the serializable Redis configuration for RunConfig. This is designed for Sentinel-only deployments with ACL user authentication.
type RedisStorage ¶ added in v0.9.4
type RedisStorage struct {
// contains filtered or unexported fields
}
RedisStorage implements the Storage interface with Redis Sentinel backend. It provides distributed storage for OAuth2 tokens, authorization codes, user data, and pending authorizations, enabling horizontal scaling.
func NewRedisStorage ¶ added in v0.9.4
func NewRedisStorage(ctx context.Context, cfg RedisConfig) (*RedisStorage, error)
NewRedisStorage creates Redis-backed storage with Sentinel failover support. Returns error if configuration validation fails or connection cannot be established.
func NewRedisStorageWithClient ¶ added in v0.9.4
func NewRedisStorageWithClient(client redis.UniversalClient, keyPrefix string) *RedisStorage
NewRedisStorageWithClient creates a RedisStorage with a pre-configured client. This is useful for testing with miniredis.
func (*RedisStorage) ClientAssertionJWTValid ¶ added in v0.9.4
func (s *RedisStorage) ClientAssertionJWTValid(ctx context.Context, jti string) error
ClientAssertionJWTValid returns an error if the JTI is known.
func (*RedisStorage) Close ¶ added in v0.9.4
func (s *RedisStorage) Close() error
Close closes the Redis client connection.
func (*RedisStorage) CreateAccessTokenSession ¶ added in v0.9.4
func (s *RedisStorage) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) error
CreateAccessTokenSession stores the access token session.
func (*RedisStorage) CreateAuthorizeCodeSession ¶ added in v0.9.4
func (s *RedisStorage) CreateAuthorizeCodeSession(ctx context.Context, code string, request fosite.Requester) error
CreateAuthorizeCodeSession stores the authorization request for a given authorization code.
func (*RedisStorage) CreatePKCERequestSession ¶ added in v0.9.4
func (s *RedisStorage) CreatePKCERequestSession(ctx context.Context, signature string, request fosite.Requester) error
CreatePKCERequestSession stores the PKCE request session.
func (*RedisStorage) CreateProviderIdentity ¶ added in v0.9.4
func (s *RedisStorage) CreateProviderIdentity(ctx context.Context, identity *ProviderIdentity) error
CreateProviderIdentity links a provider identity to a user.
func (*RedisStorage) CreateRefreshTokenSession ¶ added in v0.9.4
func (s *RedisStorage) CreateRefreshTokenSession( ctx context.Context, signature string, _ string, request fosite.Requester, ) error
CreateRefreshTokenSession stores the refresh token session.
func (*RedisStorage) CreateUser ¶ added in v0.9.4
func (s *RedisStorage) CreateUser(ctx context.Context, user *User) error
CreateUser creates a new user account.
func (*RedisStorage) DeleteAccessTokenSession ¶ added in v0.9.4
func (s *RedisStorage) DeleteAccessTokenSession(ctx context.Context, signature string) error
DeleteAccessTokenSession removes the access token session.
func (*RedisStorage) DeletePKCERequestSession ¶ added in v0.9.4
func (s *RedisStorage) DeletePKCERequestSession(ctx context.Context, signature string) error
DeletePKCERequestSession removes the PKCE request session.
func (*RedisStorage) DeletePendingAuthorization ¶ added in v0.9.4
func (s *RedisStorage) DeletePendingAuthorization(ctx context.Context, state string) error
DeletePendingAuthorization removes a pending authorization.
func (*RedisStorage) DeleteRefreshTokenSession ¶ added in v0.9.4
func (s *RedisStorage) DeleteRefreshTokenSession(ctx context.Context, signature string) error
DeleteRefreshTokenSession removes the refresh token session.
func (*RedisStorage) DeleteUpstreamTokens ¶ added in v0.9.4
func (s *RedisStorage) DeleteUpstreamTokens(ctx context.Context, sessionID string) error
DeleteUpstreamTokens removes the upstream IDP tokens for a session.
func (*RedisStorage) DeleteUser ¶ added in v0.9.4
func (s *RedisStorage) DeleteUser(ctx context.Context, id string) error
DeleteUser removes a user account and all associated data.
func (*RedisStorage) GetAccessTokenSession ¶ added in v0.9.4
func (s *RedisStorage) GetAccessTokenSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
GetAccessTokenSession retrieves the access token session by its signature.
func (*RedisStorage) GetAuthorizeCodeSession ¶ added in v0.9.4
func (s *RedisStorage) GetAuthorizeCodeSession(ctx context.Context, code string, _ fosite.Session) (fosite.Requester, error)
GetAuthorizeCodeSession retrieves the authorization request for a given code. Matches memory.go's pattern: get the auth code data first, then check if invalidated. InvalidateAuthorizeCodeSession extends the auth code TTL to match the invalidation marker, so the data is always available when the marker exists.
func (*RedisStorage) GetPKCERequestSession ¶ added in v0.9.4
func (s *RedisStorage) GetPKCERequestSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
GetPKCERequestSession retrieves the PKCE request session by its signature.
func (*RedisStorage) GetProviderIdentity ¶ added in v0.9.4
func (s *RedisStorage) GetProviderIdentity(ctx context.Context, providerID, providerSubject string) (*ProviderIdentity, error)
GetProviderIdentity retrieves a provider identity by provider ID and subject.
func (*RedisStorage) GetRefreshTokenSession ¶ added in v0.9.4
func (s *RedisStorage) GetRefreshTokenSession(ctx context.Context, signature string, _ fosite.Session) (fosite.Requester, error)
GetRefreshTokenSession retrieves the refresh token session by its signature.
func (*RedisStorage) GetUpstreamTokens ¶ added in v0.9.4
func (s *RedisStorage) GetUpstreamTokens(ctx context.Context, sessionID string) (*UpstreamTokens, error)
GetUpstreamTokens retrieves the upstream IDP tokens for a session. Returns a new UpstreamTokens struct deserialized from Redis, which acts as a defensive copy - callers cannot modify the stored data by mutating the return value.
func (*RedisStorage) GetUserProviderIdentities ¶ added in v0.9.4
func (s *RedisStorage) GetUserProviderIdentities(ctx context.Context, userID string) ([]*ProviderIdentity, error)
GetUserProviderIdentities returns all provider identities linked to a user.
func (*RedisStorage) Health ¶ added in v0.9.4
func (s *RedisStorage) Health(ctx context.Context) error
Health checks Redis connectivity.
func (*RedisStorage) InvalidateAuthorizeCodeSession ¶ added in v0.9.4
func (s *RedisStorage) InvalidateAuthorizeCodeSession(ctx context.Context, code string) error
InvalidateAuthorizeCodeSession marks an authorization code as used/invalid. It extends the auth code key's TTL to match the invalidation marker, ensuring GetAuthorizeCodeSession can always return the Requester alongside ErrInvalidatedAuthorizeCode as required by fosite for token revocation.
func (*RedisStorage) LoadPendingAuthorization ¶ added in v0.9.4
func (s *RedisStorage) LoadPendingAuthorization(ctx context.Context, state string) (*PendingAuthorization, error)
LoadPendingAuthorization retrieves a pending authorization by internal state.
func (*RedisStorage) RegisterClient ¶ added in v0.9.4
RegisterClient adds or updates a client in the storage.
func (*RedisStorage) RevokeAccessToken ¶ added in v0.9.4
func (s *RedisStorage) RevokeAccessToken(ctx context.Context, requestID string) error
RevokeAccessToken marks an access token as revoked by request ID.
func (*RedisStorage) RevokeRefreshToken ¶ added in v0.9.4
func (s *RedisStorage) RevokeRefreshToken(ctx context.Context, requestID string) error
RevokeRefreshToken marks a refresh token as revoked by request ID.
func (*RedisStorage) RevokeRefreshTokenMaybeGracePeriod ¶ added in v0.9.4
func (s *RedisStorage) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, _ string) error
RevokeRefreshTokenMaybeGracePeriod marks a refresh token as revoked, optionally allowing a grace period. For this implementation, we revoke immediately.
func (*RedisStorage) RotateRefreshToken ¶ added in v0.9.4
func (s *RedisStorage) RotateRefreshToken(ctx context.Context, requestID string, refreshTokenSignature string) error
RotateRefreshToken invalidates a refresh token and all its related token data. This is a no-op if the token does not exist (returns nil), matching the behavior of the in-memory implementation. All cleanup operations are best-effort (see warnOnCleanupErr); the new refresh token has already been issued by fosite, so partial cleanup is acceptable.
func (*RedisStorage) SetClientAssertionJWT ¶ added in v0.9.4
SetClientAssertionJWT marks a JTI as known for the given expiry time. If the JWT has already expired (exp is in the past), this is a no-op: there is no need to store it for replay detection since it will be rejected on expiry checks before reaching the JTI lookup.
func (*RedisStorage) StorePendingAuthorization ¶ added in v0.9.4
func (s *RedisStorage) StorePendingAuthorization(ctx context.Context, state string, pending *PendingAuthorization) error
StorePendingAuthorization stores a pending authorization request.
func (*RedisStorage) StoreUpstreamTokens ¶ added in v0.9.4
func (s *RedisStorage) StoreUpstreamTokens(ctx context.Context, sessionID string, tokens *UpstreamTokens) error
StoreUpstreamTokens stores the upstream IDP tokens for a session. Uses a Lua script to atomically read the old UserID, write new token data, and update user reverse-index sets, preventing race conditions on concurrent writes.
func (*RedisStorage) UpdateProviderIdentityLastUsed ¶ added in v0.9.4
func (s *RedisStorage) UpdateProviderIdentityLastUsed( ctx context.Context, providerID, providerSubject string, lastUsedAt time.Time, ) error
UpdateProviderIdentityLastUsed updates the LastUsedAt timestamp for a provider identity. Uses a Lua script to ensure atomicity and prevent race conditions.
type RunConfig ¶
type RunConfig struct {
// Type specifies the storage backend type. Defaults to "memory".
Type string `json:"type,omitempty" yaml:"type,omitempty"`
// RedisConfig is the Redis-specific configuration when Type is "redis".
RedisConfig *RedisRunConfig `json:"redisConfig,omitempty" yaml:"redisConfig,omitempty"`
}
RunConfig is the serializable storage configuration for RunConfig. This is used when the config needs to be passed across process boundaries (e.g., in Kubernetes operator).
type SentinelConfig ¶ added in v0.9.4
SentinelConfig contains Redis Sentinel configuration.
type SentinelRunConfig ¶ added in v0.9.4
type SentinelRunConfig struct {
// MasterName is the name of the Redis Sentinel master.
MasterName string `json:"masterName" yaml:"masterName"`
// SentinelAddrs is the list of Sentinel addresses (host:port).
SentinelAddrs []string `json:"sentinelAddrs" yaml:"sentinelAddrs"`
// DB is the Redis database number (default: 0).
DB int `json:"db,omitempty" yaml:"db,omitempty"`
}
SentinelRunConfig contains Redis Sentinel configuration.
type Stats ¶ added in v0.7.2
type Stats struct {
Clients int
AuthCodes int
AccessTokens int
RefreshTokens int
PKCERequests int
UpstreamTokens int
PendingAuthorizations int
InvalidatedCodes int
ClientAssertionJWTs int
Users int
ProviderIdentities int
}
Stats contains statistics about the storage contents.
type Storage ¶
type Storage interface {
// Embed segregated interfaces for IDP tokens, pending authorizations, client registry,
// and user management for multi-IDP support.
UpstreamTokenStorage
PendingAuthorizationStorage
ClientRegistry
UserStorage
// AuthorizeCodeStorage provides authorization code storage for the Authorization Code
// Grant (RFC 6749 Section 4.1). Authorization codes are one-time-use and short-lived.
// CreateAuthorizeCodeSession stores by code, GetAuthorizeCodeSession retrieves by code,
// InvalidateAuthorizeCodeSession marks as used (subsequent Gets return ErrInvalidatedAuthorizeCode).
oauth2.AuthorizeCodeStorage
// AccessTokenStorage provides access token session storage. Methods use "signature"
// (derived from token value) as the key for O(1) lookup when validating tokens.
// The stored fosite.Requester contains the full authorization context including
// the Session with expiration times via session.GetExpiresAt(fosite.AccessToken).
oauth2.AccessTokenStorage
// RefreshTokenStorage provides refresh token session storage. CreateRefreshTokenSession
// accepts an accessSignature to link refresh tokens to their access tokens for rotation.
// RotateRefreshToken uses requestID to invalidate both the refresh token and all
// related access tokens from the same authorization grant.
oauth2.RefreshTokenStorage
// TokenRevocationStorage provides token revocation per RFC 7009. RevokeAccessToken
// and RevokeRefreshToken take requestID (not signature) because RFC 7009 requires
// revoking a refresh token SHOULD also invalidate associated access tokens, which
// requires finding all tokens by their common grant identifier.
oauth2.TokenRevocationStorage
// PKCERequestStorage provides PKCE challenge/verifier storage (RFC 7636).
// Stores the code_challenge during authorization, validates code_verifier during
// token exchange. Keyed by the same code/signature as the authorization code.
pkce.PKCERequestStorage
// Health checks connectivity to the storage backend.
// Returns nil if the storage is healthy and reachable.
Health(ctx context.Context) error
// Close releases any resources held by the storage implementation.
// This should be called when the storage is no longer needed.
Close() error
}
Storage combines fosite storage interfaces with ToolHive-specific storage for upstream IDP tokens, pending authorization requests, and client registration. The auth server requires a Storage implementation; use NewMemoryStorage() for single-instance deployments or NewRedisStorage() for distributed deployments.
Fosite Interface Segregation ¶
Fosite splits storage into separate interfaces (AuthorizeCodeStorage, AccessTokenStorage, RefreshTokenStorage, PKCERequestStorage) following the Interface Segregation Principle. This enables:
- Feature composition: Only enable OAuth features you need
- Testing isolation: Mock specific interfaces for focused tests
- Clear contracts: Each interface documents its requirements
Key Design Patterns ¶
All token storage methods store fosite.Requester (not just token values) because token validation requires the full authorization context (client, scopes, session).
Methods use two key types:
- Signature: Cryptographic token identifier for token-specific operations
- Request ID: Grant identifier for finding all tokens from one authorization
See doc.go for comprehensive documentation of fosite's storage design.
type UpstreamTokenStorage ¶
type UpstreamTokenStorage interface {
// StoreUpstreamTokens stores the upstream IDP tokens for a session.
StoreUpstreamTokens(ctx context.Context, sessionID string, tokens *UpstreamTokens) error
// GetUpstreamTokens retrieves the upstream IDP tokens for a session.
// Returns ErrNotFound if the session does not exist.
// Returns ErrExpired if the tokens have expired.
// Returns ErrInvalidBinding if binding validation fails.
GetUpstreamTokens(ctx context.Context, sessionID string) (*UpstreamTokens, error)
// DeleteUpstreamTokens removes the upstream IDP tokens for a session.
// Returns ErrNotFound if the session does not exist.
DeleteUpstreamTokens(ctx context.Context, sessionID string) error
}
UpstreamTokenStorage provides storage for tokens obtained from upstream identity providers. The auth server exposes this interface via Server.UpstreamTokenStorage() for use by middleware that needs to retrieve upstream tokens (e.g., token swap middleware that replaces JWT auth with upstream IDP tokens for backend requests).
type UpstreamTokens ¶
type UpstreamTokens struct {
// ProviderID identifies which upstream provider issued these tokens.
// Example values: "google", "github", "okta"
ProviderID string
// Token values from the upstream IDP
AccessToken string
RefreshToken string
IDToken string
ExpiresAt time.Time
// UserID is the internal ToolHive user ID (references User.ID).
// This is NOT the upstream provider's subject - it's our stable internal identifier.
// In multi-IDP scenarios, the same UserID may have tokens from multiple providers.
UserID string
// UpstreamSubject is the "sub" claim from the upstream IDP's ID token.
// This enables validation that tokens match the expected upstream identity
// and supports audit logging of which upstream identity was used.
UpstreamSubject string
// ClientID is the OAuth client that initiated the authorization.
// Tokens should only be accessible to the same client that obtained them.
ClientID string
}
UpstreamTokens represents tokens obtained from an upstream Identity Provider. These tokens are stored with binding fields for security validation and ProviderID for multi-IDP support.
type User ¶ added in v0.8.0
type User struct {
// ID is the internal, stable user identifier (UUID format).
// This value is used as the "sub" claim in ToolHive-issued JWTs.
ID string
// CreatedAt is when the user account was created.
CreatedAt time.Time
// UpdatedAt is when the user account was last modified.
UpdatedAt time.Time
}
User represents a user account in the authorization server. A user can have multiple linked provider identities. The User.ID is used as the "sub" claim in JWTs issued by ToolHive, providing a stable identity across multiple upstream identity providers.
type UserStorage ¶ added in v0.8.0
type UserStorage interface {
// CreateUser creates a new user account.
// Returns ErrAlreadyExists if a user with the same ID already exists.
CreateUser(ctx context.Context, user *User) error
// GetUser retrieves a user by their internal ID.
// Returns ErrNotFound if the user does not exist.
GetUser(ctx context.Context, id string) (*User, error)
// DeleteUser removes a user account.
// Returns ErrNotFound if the user does not exist.
DeleteUser(ctx context.Context, id string) error
// CreateProviderIdentity links a provider identity to a user.
// For account linking scenarios, caller MUST verify user owns the target User
// (typically via active authenticated session) before linking a new provider.
// Returns ErrAlreadyExists if this provider identity is already linked.
CreateProviderIdentity(ctx context.Context, identity *ProviderIdentity) error
// GetProviderIdentity retrieves a provider identity by provider ID and subject.
// This is the primary lookup path during authentication callbacks.
// Returns ErrNotFound if the identity does not exist.
GetProviderIdentity(ctx context.Context, providerID, providerSubject string) (*ProviderIdentity, error)
// UpdateProviderIdentityLastUsed updates the LastUsedAt timestamp for a provider identity.
// This should be called after each successful authentication via this identity.
// The timestamp supports OIDC auth_time claim when clients use max_age parameter.
// Returns ErrNotFound if the identity does not exist.
UpdateProviderIdentityLastUsed(ctx context.Context, providerID, providerSubject string, lastUsedAt time.Time) error
// GetUserProviderIdentities returns all provider identities linked to a user.
// This enables queries like "when did this user last authenticate via any provider"
// which is needed for OIDC max_age enforcement.
// Returns an empty slice (not error) if the user exists but has no linked identities.
// Returns ErrNotFound if the user does not exist.
GetUserProviderIdentities(ctx context.Context, userID string) ([]*ProviderIdentity, error)
}
UserStorage provides user and provider identity management operations. This interface supports multi-IDP scenarios where a single user can authenticate via multiple upstream identity providers (e.g., Google and GitHub).
The User type represents the internal ToolHive identity. Its ID becomes the "sub" claim in issued JWTs, providing a stable identity across multiple providers.
ProviderIdentity links a user to a specific upstream provider. The (ProviderID, ProviderSubject) pair uniquely identifies an upstream identity.
Account Linking Security ¶
When implementing account linking (one User with multiple ProviderIdentities), callers MUST verify user consent before linking. See OAuth 2.0 Security BCP.
TODO(auth): When implementing double-hop auth (Company IDP -> External IDP), add the following to this interface:
- DeleteProviderIdentity(providerID, subject) - unlink specific provider
- Add Primary field to ProviderIdentity to distinguish Company IDP from linked External IDPs
- Add ConsentRecord tracking for external provider linking