oauth

package
v1.102.0 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

Package oauth provides OAuth 2.1 server capabilities.

Index

Constants

View Source
const StateMaxAge = time.Hour

StateMaxAge is how long an in-flight authorization state stays valid. The window spans the user's whole trip through the upstream IdP login form, so it is generous relative to the auth-code TTL.

Variables

View Source
var (
	ErrStateNotFound = errors.New("authorization state not found")

	// ErrNotFound indicates a stored OAuth record (authorization code,
	// refresh token) does not exist. Storage implementations return it
	// from Consume* so the grant handlers can distinguish a genuinely
	// invalid/replayed credential from a storage outage.
	ErrNotFound = errors.New("record not found")

	// ErrStorageFailure indicates the backing store failed while
	// processing a grant. The token endpoint maps it to a 500
	// server_error response so clients retry with the same credentials
	// instead of treating the failure as a terminal invalid_grant and
	// discarding a still-valid refresh token.
	ErrStorageFailure = errors.New("temporary storage failure")
)

Errors returned by the OAuth server.

Functions

func BuildAuthorizationURL

func BuildAuthorizationURL(baseURL, clientID, redirectURI, scope, state string) string

BuildAuthorizationURL builds an authorization URL. Uses paths without /oauth prefix for Claude Desktop compatibility.

func GenerateCodeChallenge

func GenerateCodeChallenge(verifier string, method PKCEMethod) (string, error)

GenerateCodeChallenge generates a code challenge from a verifier.

func HashToken added in v1.101.0

func HashToken(token string) string

HashToken returns the hex-encoded SHA-256 digest of a token value. Opaque bearer credentials (refresh tokens, authorization codes) are stored only as digests so a database read never yields a usable credential; lookups hash the presented value and compare digests.

func ValidateCodeChallenge

func ValidateCodeChallenge(challenge string) error

ValidateCodeChallenge validates a code challenge.

func ValidateCodeVerifier

func ValidateCodeVerifier(verifier string) error

ValidateCodeVerifier validates a code verifier. Per RFC 7636, it must be 43-128 characters of [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".

func VerifyCodeChallenge

func VerifyCodeChallenge(verifier, challenge string, method PKCEMethod) (bool, error)

VerifyCodeChallenge verifies a code verifier against a challenge.

Types

type AuthorizationCode

type AuthorizationCode struct {
	ID string `json:"id"`
	// Code carries the raw value when issuing; Storage implementations
	// persist only its SHA-256 digest (HashToken), so records read back
	// from storage hold the digest, never a usable credential.
	Code          string         `json:"code"`
	ClientID      string         `json:"client_id"`
	UserID        string         `json:"user_id"`
	UserClaims    map[string]any `json:"user_claims"`
	CodeChallenge string         `json:"code_challenge"`
	RedirectURI   string         `json:"redirect_uri"`
	Scope         string         `json:"scope"`
	ExpiresAt     time.Time      `json:"expires_at"`
	Used          bool           `json:"used"`
	CreatedAt     time.Time      `json:"created_at"`
}

AuthorizationCode represents an OAuth authorization code.

func (*AuthorizationCode) IsExpired

func (c *AuthorizationCode) IsExpired() bool

IsExpired checks if the authorization code has expired.

type AuthorizationRequest

type AuthorizationRequest struct {
	ResponseType        string
	ClientID            string
	RedirectURI         string
	Scope               string
	State               string
	CodeChallenge       string
	CodeChallengeMethod string
}

AuthorizationRequest represents an authorization request.

type AuthorizationState added in v0.3.0

type AuthorizationState struct {
	// ClientID is the OAuth client's client_id (e.g., Claude Desktop).
	ClientID string

	// RedirectURI is where to send the client after authentication.
	RedirectURI string

	// State is the client's original state parameter.
	State string

	// CodeChallenge is the PKCE challenge from the client.
	CodeChallenge string

	// CodeChallengeMethod is the PKCE method (S256; the only supported method).
	CodeChallengeMethod string

	// Scope is the requested scope.
	Scope string

	// UpstreamState is the state for the upstream IdP (e.g., Keycloak).
	UpstreamState string

	// PromptNoneAttempted tracks whether we already tried prompt=none
	// for this flow. Used to prevent infinite redirect loops when the
	// upstream IdP returns login_required.
	PromptNoneAttempted bool

	// CreatedAt is when this state was created.
	CreatedAt time.Time
}

AuthorizationState holds state for linking upstream IdP callbacks to original client authorization requests.

type Client

type Client struct {
	ID           string    `json:"id"`
	ClientID     string    `json:"client_id"`
	ClientSecret string    `json:"client_secret"` // #nosec G117 -- bcrypt hashed, required for OAuth client storage
	Name         string    `json:"name"`
	RedirectURIs []string  `json:"redirect_uris"`
	GrantTypes   []string  `json:"grant_types"`
	RequirePKCE  bool      `json:"require_pkce"`
	CreatedAt    time.Time `json:"created_at"`
	Active       bool      `json:"active"`
	// DynamicallyRegistered marks clients created through the unauthenticated
	// DCR /register endpoint (RFC 7591), as opposed to pre-registered
	// config-file clients. Only DCR clients are eligible for TTL-based cleanup
	// of unused registrations.
	DynamicallyRegistered bool `json:"dynamically_registered"`
}

Client represents an OAuth 2.1 client.

func (*Client) SupportsGrantType

func (c *Client) SupportsGrantType(grantType string) bool

SupportsGrantType checks if the client supports a grant type.

func (*Client) ValidRedirectURI

func (c *Client) ValidRedirectURI(uri string) bool

ValidRedirectURI checks if a redirect URI is valid for this client. For loopback redirect URIs (127.0.0.1, [::1], localhost), matching follows RFC 8252 Section 7.3: scheme and host must match, but port and path are ignored.

type DCRConfig

type DCRConfig struct {
	// Enabled enables DCR.
	Enabled bool

	// AllowedRedirectPatterns are regex patterns for allowed redirect URIs.
	// The registration endpoint is unauthenticated, so leaving this empty
	// denies all registrations unless AllowAllRedirectURIs is set.
	AllowedRedirectPatterns []string

	// AllowAllRedirectURIs explicitly opts out of redirect URI pattern
	// matching, accepting any HTTPS (or loopback HTTP) redirect URI. An
	// attacker-controlled redirect URI is the setup for authorization-code
	// interception, so this is an explicit escape hatch rather than the
	// default for an empty pattern list.
	AllowAllRedirectURIs bool

	// DefaultGrantTypes are the default grant types for new clients.
	DefaultGrantTypes []string

	// RequirePKCE requires PKCE for all clients.
	RequirePKCE bool
}

DCRConfig configures Dynamic Client Registration.

type DCRRequest

type DCRRequest struct {
	ClientName              string   `json:"client_name"`
	RedirectURIs            []string `json:"redirect_uris"`
	GrantTypes              []string `json:"grant_types,omitempty"`
	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method,omitempty"`
}

DCRRequest is a Dynamic Client Registration request.

type DCRResponse

type DCRResponse struct {
	ClientID              string   `json:"client_id"`
	ClientSecret          string   `json:"client_secret,omitempty"` // #nosec G117 -- required by OAuth 2.0 DCR spec (RFC 7591)
	ClientName            string   `json:"client_name"`
	RedirectURIs          []string `json:"redirect_uris"`
	GrantTypes            []string `json:"grant_types"`
	ClientSecretExpiresAt int      `json:"client_secret_expires_at"` // 0 means never
}

DCRResponse is a Dynamic Client Registration response.

type DCRService

type DCRService struct {
	// contains filtered or unexported fields
}

DCRService handles Dynamic Client Registration.

func NewDCRService

func NewDCRService(storage Storage, config DCRConfig) (*DCRService, error)

NewDCRService creates a new DCR service.

func (*DCRService) Register

func (s *DCRService) Register(ctx context.Context, req DCRRequest) (*DCRResponse, error)

Register registers a new OAuth client.

type ErrorResponse

type ErrorResponse struct {
	Error            string `json:"error"`
	ErrorDescription string `json:"error_description,omitempty"`
}

ErrorResponse represents an OAuth error response.

type MemoryStateStore added in v0.3.0

type MemoryStateStore struct {
	// contains filtered or unexported fields
}

MemoryStateStore is an in-memory implementation of StateStore.

func NewMemoryStateStore added in v0.3.0

func NewMemoryStateStore() *MemoryStateStore

NewMemoryStateStore creates a new in-memory state store.

func (*MemoryStateStore) CleanupExpiredStates added in v1.96.0

func (s *MemoryStateStore) CleanupExpiredStates(_ context.Context, maxAge time.Duration) error

CleanupExpiredStates removes states older than maxAge.

func (*MemoryStateStore) DeleteState added in v1.96.0

func (s *MemoryStateStore) DeleteState(_ context.Context, key string) error

DeleteState removes an authorization state.

func (*MemoryStateStore) GetState added in v1.96.0

GetState retrieves an authorization state.

func (*MemoryStateStore) SaveState added in v1.96.0

func (s *MemoryStateStore) SaveState(_ context.Context, key string, state *AuthorizationState) error

SaveState stores an authorization state.

type MemoryStorage added in v0.3.0

type MemoryStorage struct {
	// contains filtered or unexported fields
}

MemoryStorage is an in-memory implementation of Storage. It is thread-safe and suitable for development/testing. For production, use PostgresStorage.

func NewMemoryStorage added in v0.3.0

func NewMemoryStorage() *MemoryStorage

NewMemoryStorage creates a new in-memory storage.

func (*MemoryStorage) CleanupExpiredCodes added in v0.3.0

func (m *MemoryStorage) CleanupExpiredCodes(_ context.Context) error

CleanupExpiredCodes removes expired authorization codes.

func (*MemoryStorage) CleanupExpiredTokens added in v0.3.0

func (m *MemoryStorage) CleanupExpiredTokens(_ context.Context) error

CleanupExpiredTokens removes expired refresh tokens.

func (*MemoryStorage) ConsumeAuthorizationCode added in v1.96.0

func (m *MemoryStorage) ConsumeAuthorizationCode(_ context.Context, code string) (*AuthorizationCode, error)

ConsumeAuthorizationCode atomically deletes and returns an authorization code by its raw value.

func (*MemoryStorage) ConsumeRefreshToken added in v1.96.0

func (m *MemoryStorage) ConsumeRefreshToken(_ context.Context, token string) (*RefreshToken, error)

ConsumeRefreshToken atomically deletes and returns a refresh token by its raw value.

func (*MemoryStorage) CreateClient added in v0.3.0

func (m *MemoryStorage) CreateClient(_ context.Context, client *Client) error

CreateClient stores a new client.

func (*MemoryStorage) DeleteAuthorizationCode added in v0.3.0

func (m *MemoryStorage) DeleteAuthorizationCode(_ context.Context, code string) error

DeleteAuthorizationCode deletes an authorization code by its raw value.

func (*MemoryStorage) DeleteClient added in v0.3.0

func (m *MemoryStorage) DeleteClient(_ context.Context, clientID string) error

DeleteClient deletes a client.

func (*MemoryStorage) DeleteRefreshToken added in v0.3.0

func (m *MemoryStorage) DeleteRefreshToken(_ context.Context, token string) error

DeleteRefreshToken deletes a refresh token by its raw value.

func (*MemoryStorage) DeleteRefreshTokensForClient added in v0.3.0

func (m *MemoryStorage) DeleteRefreshTokensForClient(_ context.Context, clientID string) error

DeleteRefreshTokensForClient deletes all refresh tokens for a client.

func (*MemoryStorage) GetAuthorizationCode added in v0.3.0

func (m *MemoryStorage) GetAuthorizationCode(_ context.Context, code string) (*AuthorizationCode, error)

GetAuthorizationCode retrieves an authorization code by its raw value.

func (*MemoryStorage) GetClient added in v0.3.0

func (m *MemoryStorage) GetClient(_ context.Context, clientID string) (*Client, error)

GetClient retrieves a client by ID.

func (*MemoryStorage) GetRefreshToken added in v0.3.0

func (m *MemoryStorage) GetRefreshToken(_ context.Context, token string) (*RefreshToken, error)

GetRefreshToken retrieves a refresh token by its raw value.

func (*MemoryStorage) ListClients added in v0.3.0

func (m *MemoryStorage) ListClients(_ context.Context) ([]*Client, error)

ListClients returns all clients.

func (*MemoryStorage) SaveAuthorizationCode added in v0.3.0

func (m *MemoryStorage) SaveAuthorizationCode(_ context.Context, code *AuthorizationCode) error

SaveAuthorizationCode stores an authorization code. Only the SHA-256 digest of the code value is retained; the caller's struct is not mutated, so the raw value it holds stays usable for issuance.

func (*MemoryStorage) SaveRefreshToken added in v0.3.0

func (m *MemoryStorage) SaveRefreshToken(_ context.Context, token *RefreshToken) error

SaveRefreshToken stores a refresh token. Only the SHA-256 digest of the token value is retained; the caller's struct is not mutated, so the raw value it holds stays usable for issuance.

func (*MemoryStorage) UpdateClient added in v0.3.0

func (m *MemoryStorage) UpdateClient(_ context.Context, client *Client) error

UpdateClient updates an existing client.

type PKCEMethod

type PKCEMethod string

PKCEMethod defines the code challenge method.

const (
	// PKCEMethodS256 uses SHA-256 hashing. Per OAuth 2.1 it is the only
	// supported code challenge method; the "plain" method is not accepted.
	PKCEMethodS256 PKCEMethod = "S256"
)

func DefaultPKCEMethod

func DefaultPKCEMethod() PKCEMethod

DefaultPKCEMethod returns the default (and recommended) PKCE method.

type RateLimitConfig added in v1.101.0

type RateLimitConfig struct {
	// Enabled turns limiting on/off; nil means enabled (platform default-on).
	Enabled *bool

	// TrustedProxies is the set of CIDRs whose X-Forwarded-For headers are
	// trusted for client attribution. Empty means trust none: the direct peer
	// address is used and forwarding headers are ignored.
	TrustedProxies []string

	// TokenRPM / TokenBurst limit /token per client IP.
	TokenRPM   int
	TokenBurst int

	// RegisterRPM / RegisterBurst limit /register per client IP.
	RegisterRPM   int
	RegisterBurst int
}

RateLimitConfig configures rate limiting for the unauthenticated OAuth HTTP endpoints. /token performs a bcrypt compare per attempt and /register performs a bcrypt hash plus a DB insert per request, so both are CPU (and, for /register, storage) amplification levers. Limiting is on by default (Enabled nil == enabled); the per-endpoint knobs default to the *Default* constants below.

Attribution is trusted-proxy-aware (TrustedProxies) rather than naive leftmost-X-Forwarded-For, and each endpoint additionally has a global backstop bucket (see ratelimit.HTTPLimiter) so total throughput — and therefore worst-case bcrypt work — is bounded even when per-IP attribution is defeated or collapses behind a proxy.

type RefreshToken

type RefreshToken struct {
	ID string `json:"id"`
	// Token carries the raw value when issuing; Storage implementations
	// persist only its SHA-256 digest (HashToken), so records read back
	// from storage hold the digest, never a usable credential.
	Token      string         `json:"token"`
	ClientID   string         `json:"client_id"`
	UserID     string         `json:"user_id"`
	UserClaims map[string]any `json:"user_claims"`
	Scope      string         `json:"scope"`
	ExpiresAt  time.Time      `json:"expires_at"`
	CreatedAt  time.Time      `json:"created_at"`
}

RefreshToken represents an OAuth refresh token.

func (*RefreshToken) IsExpired

func (t *RefreshToken) IsExpired() bool

IsExpired checks if the refresh token has expired.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is an OAuth 2.1 authorization server.

func NewServer

func NewServer(config ServerConfig, storage Storage) (*Server, error)

NewServer creates a new OAuth server.

func (*Server) Authorize

func (s *Server) Authorize(ctx context.Context, req AuthorizationRequest, userID string, userClaims map[string]any) (string, error)

Authorize handles the authorization endpoint.

func (*Server) Close added in v1.101.0

func (s *Server) Close() error

Close releases the server's background resources. It stops the rate-limiter cleanup goroutines; it is nil-safe and idempotent. Storage and authorization-state cleanup are owned by their respective callers.

func (*Server) Issuer added in v0.4.0

func (s *Server) Issuer() string

Issuer returns the OAuth server's issuer URL.

func (*Server) RegisterClient

func (s *Server) RegisterClient(ctx context.Context, req DCRRequest) (*DCRResponse, error)

RegisterClient handles Dynamic Client Registration.

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler for the OAuth server. Handles both standard paths (with /oauth prefix) and Claude Desktop compatible paths (without prefix).

func (*Server) SetMetrics added in v1.70.0

func (s *Server) SetMetrics(m *observability.Metrics)

SetMetrics attaches the observability recorder so token issuance and refresh outcomes are recorded. Nil-safe: the Record* calls no-op when metrics are disabled, so the token path behaves identically either way.

func (*Server) SetStateStore added in v1.96.0

func (s *Server) SetStateStore(store StateStore)

SetStateStore replaces the in-memory authorization state store. Call it during setup, before the server handles requests: in multi-replica deployments the upstream IdP callback can land on a different replica than the one that started the flow, so the state must live in a shared (database-backed) store.

func (*Server) SigningKey added in v0.4.0

func (s *Server) SigningKey() []byte

SigningKey returns the OAuth server's signing key. This is needed by the OAuth JWT authenticator to validate tokens.

func (*Server) StartCleanupRoutine

func (s *Server) StartCleanupRoutine(ctx context.Context, interval time.Duration)

StartCleanupRoutine starts a background routine to clean up expired codes, tokens, and in-flight authorization states.

func (*Server) Token

func (s *Server) Token(ctx context.Context, req TokenRequest) (*TokenResponse, error)

Token handles the token endpoint.

type ServerConfig

type ServerConfig struct {
	// Issuer is the OAuth issuer URL.
	Issuer string

	// AccessTokenTTL is the access token lifetime.
	AccessTokenTTL time.Duration

	// RefreshTokenTTL is the refresh token lifetime.
	RefreshTokenTTL time.Duration

	// AuthCodeTTL is the authorization code lifetime.
	AuthCodeTTL time.Duration

	// SigningKey is the HMAC key used to sign JWT access tokens.
	// If not provided, tokens will be opaque (not recommended for production).
	// Generate with: openssl rand -base64 32
	SigningKey []byte

	// DCR configures Dynamic Client Registration.
	DCR DCRConfig

	// Upstream configures the upstream identity provider (e.g., Keycloak).
	Upstream *UpstreamConfig

	// RateLimit configures rate limiting for the unauthenticated /token and
	// /register endpoints. Limiting is on by default (see RateLimitConfig).
	RateLimit RateLimitConfig
}

ServerConfig configures the OAuth server.

type StateStore added in v0.3.0

type StateStore interface {
	// SaveState stores an authorization state, replacing any existing
	// state under the same key.
	SaveState(ctx context.Context, key string, state *AuthorizationState) error

	// GetState retrieves an authorization state.
	GetState(ctx context.Context, key string) (*AuthorizationState, error)

	// DeleteState removes an authorization state.
	DeleteState(ctx context.Context, key string) error

	// CleanupExpiredStates removes states older than maxAge.
	CleanupExpiredStates(ctx context.Context, maxAge time.Duration) error
}

StateStore manages authorization states for the OAuth flow. In multi-replica deployments the store must be shared (database-backed): the /oauth/authorize redirect and the upstream IdP callback can land on different replicas.

type Storage

type Storage interface {
	// Client management
	CreateClient(ctx context.Context, client *Client) error
	GetClient(ctx context.Context, clientID string) (*Client, error)
	UpdateClient(ctx context.Context, client *Client) error
	DeleteClient(ctx context.Context, clientID string) error
	ListClients(ctx context.Context) ([]*Client, error)

	// Authorization code management. Implementations persist only the
	// SHA-256 digest of the code value (HashToken); the get, delete, and
	// consume methods take the raw value and hash it before comparison.
	SaveAuthorizationCode(ctx context.Context, code *AuthorizationCode) error
	GetAuthorizationCode(ctx context.Context, code string) (*AuthorizationCode, error)
	DeleteAuthorizationCode(ctx context.Context, code string) error
	// ConsumeAuthorizationCode atomically deletes the authorization code
	// and returns it, so single-use invalidation and retrieval cannot
	// diverge: a replayed or concurrently exchanged code fails because
	// the record is already gone. Returns an error when the code does
	// not exist.
	ConsumeAuthorizationCode(ctx context.Context, code string) (*AuthorizationCode, error)
	CleanupExpiredCodes(ctx context.Context) error

	// Token management. Implementations persist only the SHA-256 digest
	// of the token value (HashToken); the get, delete, and consume
	// methods take the raw value and hash it before comparison.
	SaveRefreshToken(ctx context.Context, token *RefreshToken) error
	GetRefreshToken(ctx context.Context, token string) (*RefreshToken, error)
	DeleteRefreshToken(ctx context.Context, token string) error
	// ConsumeRefreshToken atomically deletes the refresh token and
	// returns it, backing rotation: the old token is provably invalid
	// before new tokens are issued. Returns an error when the token
	// does not exist.
	ConsumeRefreshToken(ctx context.Context, token string) (*RefreshToken, error)
	DeleteRefreshTokensForClient(ctx context.Context, clientID string) error
	CleanupExpiredTokens(ctx context.Context) error
}

Storage defines the interface for OAuth data persistence.

type TokenRequest

type TokenRequest struct {
	GrantType    string
	Code         string
	RedirectURI  string
	ClientID     string
	ClientSecret string // #nosec G117 -- required by OAuth 2.1 token request (RFC 6749 §4.1.3)
	CodeVerifier string
	RefreshToken string // #nosec G117 -- required by OAuth 2.1 refresh grant (RFC 6749 §6)
	Scope        string
}

TokenRequest represents a token request.

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"access_token"` // #nosec G117 -- required by OAuth 2.1 token response (RFC 6749 §5.1)
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token,omitempty"` // #nosec G117 -- required by OAuth 2.1 token response (RFC 6749 §5.1)
	Scope        string `json:"scope,omitempty"`
}

TokenResponse represents a token response.

type UpstreamConfig added in v0.3.0

type UpstreamConfig struct {
	// Issuer is the upstream IdP issuer URL (e.g., Keycloak realm URL).
	Issuer string

	// ClientID is the MCP server's client ID in the upstream IdP.
	ClientID string

	// ClientSecret is the MCP server's client secret.
	ClientSecret string // #nosec G117 -- required for upstream IdP token exchange

	// RedirectURI is the callback URL for the upstream IdP.
	RedirectURI string

	// AuthorizationEndpoint, when set, is used verbatim as the upstream IdP's
	// authorization endpoint, bypassing OIDC discovery. Empty (default) means
	// discover it from the issuer's well-known configuration document. Provide it
	// only for IdPs whose discovery document is broken or unreachable.
	AuthorizationEndpoint string

	// TokenEndpoint, when set, is used verbatim as the upstream IdP's token
	// endpoint, bypassing OIDC discovery. Empty (default) means discover it.
	// Discovery is skipped entirely only when BOTH endpoints are set.
	TokenEndpoint string
}

UpstreamConfig configures the upstream identity provider.

Directories

Path Synopsis
Package postgres provides PostgreSQL storage for OAuth 2.1 data.
Package postgres provides PostgreSQL storage for OAuth 2.1 data.
Package signkey manages the HMAC key ring used to sign and verify HS256 JWT access tokens.
Package signkey manages the HMAC key ring used to sign and verify HS256 JWT access tokens.

Jump to

Keyboard shortcuts

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