oauth

package
v1.97.1 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 2026 License: Apache-2.0 Imports: 19 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 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          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 or plain).
	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"`
}

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.

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.

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.

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.

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.

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.

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.

func (*MemoryStorage) SaveRefreshToken added in v0.3.0

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

SaveRefreshToken stores a refresh token.

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 (
	// PKCEMethodPlain uses plain text (not recommended).
	PKCEMethodPlain PKCEMethod = "plain"

	// PKCEMethodS256 uses SHA-256 hashing (recommended).
	PKCEMethodS256 PKCEMethod = "S256"
)

func DefaultPKCEMethod

func DefaultPKCEMethod() PKCEMethod

DefaultPKCEMethod returns the default (and recommended) PKCE method.

type RefreshToken

type RefreshToken struct {
	ID         string         `json:"id"`
	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) 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
}

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
	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
	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
}

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.

Jump to

Keyboard shortcuts

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