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
- 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 (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 RunConfig
- 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" // 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 )
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 ¶
This section is empty.
Types ¶
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) 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 RunConfig ¶
type RunConfig struct {
// Type specifies the storage backend type. Defaults to "memory".
Type string `json:"type,omitempty" yaml:"type,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 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
// 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