Documentation
¶
Overview ¶
Package handlers provides HTTP handlers for the OAuth 2.0 authorization server endpoints.
This package implements the HTTP layer for the authorization server, including:
- OIDC Discovery endpoint (/.well-known/openid-configuration)
- JWKS endpoint (/.well-known/jwks.json)
- OAuth endpoints (authorize, token, callback, register) - to be implemented
The Handler struct coordinates all handlers and provides route registration methods for integrating with standard Go HTTP servers.
Index ¶
- Constants
- type Handler
- func (h *Handler) AuthorizeHandler(w http.ResponseWriter, req *http.Request)
- func (h *Handler) CallbackHandler(w http.ResponseWriter, req *http.Request)
- func (h *Handler) JWKSHandler(w http.ResponseWriter, _ *http.Request)
- func (h *Handler) OAuthDiscoveryHandler(w http.ResponseWriter, _ *http.Request)
- func (h *Handler) OAuthRoutes(r chi.Router)
- func (h *Handler) OIDCDiscoveryHandler(w http.ResponseWriter, _ *http.Request)
- func (h *Handler) RegisterClientHandler(w http.ResponseWriter, req *http.Request)
- func (h *Handler) Routes() http.Handler
- func (h *Handler) TokenHandler(w http.ResponseWriter, req *http.Request)
- func (h *Handler) WellKnownRoutes(r chi.Router)
- type NamedUpstream
- type Option
- type UpstreamFilter
- type UserResolver
Constants ¶
const ( // DefaultJWKSCacheMaxAge is the Cache-Control max-age for the JWKS endpoint (1 hour). // This balances caching efficiency with timely key rotation propagation. DefaultJWKSCacheMaxAge = 3600 // DefaultDiscoveryCacheMaxAge is the Cache-Control max-age for the discovery endpoint (1 hour). // Aligned with Google's OIDC discovery cache policy. DefaultDiscoveryCacheMaxAge = 3600 )
Cache-Control max-age values for discovery endpoints. These are not exposed to users but extracted as constants for documentation and maintainability.
const MaxDCRBodySize = 64 * 1024
MaxDCRBodySize is the maximum allowed size for DCR request bodies (64KB). This prevents DoS attacks via extremely large payloads while being generous enough for legitimate requests with multiple redirect URIs.
It is exported to serve as the single source of truth for the auth-server body-size cap: the embedded auth server (pkg/authserver/runner) derives its own request-body limit from this constant so the two cannot drift.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler provides HTTP handlers for the OAuth authorization server endpoints.
func NewHandler ¶
func NewHandler( provider fosite.OAuth2Provider, config *server.AuthorizationServerConfig, stor storage.Storage, upstreams []NamedUpstream, opts ...Option, ) (*Handler, error)
NewHandler creates a new Handler with the given dependencies. upstreams defines the ordered sequence of upstream providers consulted during multi-upstream authorization flows (e.g., sequential token acquisition).
Returns an error if config is nil, if config's embedded *fosite.Config is nil, if upstreams is empty, or if any entry has an empty name, a nil provider, or a duplicate name. Upstream names must be unique: upstreamByName returns the first match, tokens are keyed by name, and the authorization chain is keyed by name — a duplicate would silently shadow a provider. Catching misconfiguration here is far easier to diagnose than a nil-deref panic or a shadowed provider deep inside an HTTP handler at request time.
func (*Handler) AuthorizeHandler ¶ added in v0.8.1
func (h *Handler) AuthorizeHandler(w http.ResponseWriter, req *http.Request)
AuthorizeHandler handles GET /oauth/authorize requests. It validates the client's authorization request and redirects to the upstream IDP.
func (*Handler) CallbackHandler ¶ added in v0.8.1
func (h *Handler) CallbackHandler(w http.ResponseWriter, req *http.Request)
CallbackHandler handles GET /oauth/callback requests. It exchanges the upstream authorization code and issues our own authorization code.
func (*Handler) JWKSHandler ¶
func (h *Handler) JWKSHandler(w http.ResponseWriter, _ *http.Request)
JWKSHandler handles GET /.well-known/jwks.json requests. It returns the public keys used for verifying JWTs.
func (*Handler) OAuthDiscoveryHandler ¶
func (h *Handler) OAuthDiscoveryHandler(w http.ResponseWriter, _ *http.Request)
OAuthDiscoveryHandler handles GET /.well-known/oauth-authorization-server requests. It returns the OAuth 2.0 Authorization Server Metadata per RFC 8414. This endpoint is useful for non-OIDC OAuth clients.
func (*Handler) OAuthRoutes ¶
OAuthRoutes registers OAuth endpoints (authorize, callback, token, register) on the provided router.
func (*Handler) OIDCDiscoveryHandler ¶
func (h *Handler) OIDCDiscoveryHandler(w http.ResponseWriter, _ *http.Request)
OIDCDiscoveryHandler handles GET /.well-known/openid-configuration requests. It returns the OIDC discovery document describing the authorization server capabilities. This extends the OAuth 2.0 AS Metadata (RFC 8414) with OIDC-specific fields.
func (*Handler) RegisterClientHandler ¶ added in v0.8.1
func (h *Handler) RegisterClientHandler(w http.ResponseWriter, req *http.Request)
RegisterClientHandler handles POST /oauth/register requests. It implements RFC 7591 Dynamic Client Registration for public clients with loopback redirect URIs only.
func (*Handler) TokenHandler ¶ added in v0.8.1
func (h *Handler) TokenHandler(w http.ResponseWriter, req *http.Request)
TokenHandler handles POST /oauth/token requests. It processes token requests using fosite's access request/response flow.
func (*Handler) WellKnownRoutes ¶
WellKnownRoutes registers well-known endpoints (JWKS, OAuth/OIDC discovery) on the provided router. Both discovery endpoints are registered per the MCP specification requirement to provide at least one discovery mechanism, with both supported for maximum interoperability: - /.well-known/oauth-authorization-server (RFC 8414) for OAuth-only clients - /.well-known/openid-configuration (OIDC Discovery 1.0) for OIDC clients
The wildcard variants (/.well-known/oauth-authorization-server/*) handle RFC 8414 Section 3.1 path-based issuers, where clients insert /.well-known/ before the issuer's path component (e.g., /.well-known/oauth-authorization-server/inject-test for issuer https://example.com/inject-test).
type NamedUpstream ¶ added in v0.12.5
type NamedUpstream struct {
Name string
Provider upstream.OAuth2Provider
}
NamedUpstream pairs a logical provider name with its OAuth2Provider implementation. The name is used as the storage key and must be unique within the upstream slice.
type Option ¶ added in v0.31.0
type Option func(*Handler)
Option configures optional Handler behavior at construction time.
func WithUpstreamFilter ¶ added in v0.34.0
func WithUpstreamFilter(f UpstreamFilter) Option
WithUpstreamFilter injects a filter that narrows the authorization chain to a subset of the configured upstreams once the first leg resolves. When unset, the handler walks all configured upstreams — the behavior before this option existed. See UpstreamFilter for the contract.
func WithUpstreamRefresher ¶ added in v0.31.0
func WithUpstreamRefresher(r storage.UpstreamTokenRefresher) Option
WithUpstreamRefresher injects a refresher used by nextMissingUpstream to transparently refresh expired upstream tokens while evaluating the authorization chain. When unset, an expired leg is treated as missing and the user is re-prompted — the behavior before this option existed.
type UpstreamFilter ¶ added in v0.34.0
type UpstreamFilter interface {
FilterUpstreams(
ctx context.Context,
principal auth.PrincipalInfo,
configured []string,
) ([]string, error)
}
UpstreamFilter narrows the authorization chain to a subset of the configured upstreams, keyed on the identity established by the first upstream. It is consulted exactly once per authorization, in the callback handler, after the first upstream (upstreams[0]) resolves. The first upstream is always required: it is never passed to the filter and cannot be removed by it.
FilterUpstreams receives, in order:
- ctx: the request context of the first leg's callback.
- principal: the upstream-derived identity for authorization decisions. Its PlatformUserID is the canonical ToolHive user ID (the stable internal identifier); Subject is the claim-mapped upstream subject (OIDC SubjectClaim, or "sub"); Claims carries the ID-token/userinfo claims; Name and Email are populated when the upstream provides them. It carries NO tokens — it is the credential-free auth.PrincipalInfo projection. The filter MUST treat principal (including its Claims map) as read-only.
- configured: the names of the non-first configured upstreams, in configured order.
Claims contract: values are untyped and come straight from the upstream IdP, so implementations MUST assert types defensively (e.g. comma-ok on map/slice access) — a malformed IdP response must not be able to panic the filter. Claims is nil not only for OAuth2 identityFromToken / synthetic upstreams (which resolve no structured claim set) but also on a transient OIDC claims-extraction failure, so a security-relevant filter MUST treat nil/absent claims as fail-closed. Note that the `aud` claim, when present, is the upstream IdP's client_id — not this authorization server.
It returns the subset to keep. The handler preserves configured order and ignores any returned name that is not one of the non-first configured upstreams, so the filter cannot reorder the chain or introduce unknown providers. A returned error fails the authorization with a server error — the handler never falls back to walking every upstream on error.
The "first-leg identity" guarantee holds even across a rolling upgrade of a persistent backend: a subsequent-leg pending that lacks a computed chain (e.g. one written before this feature existed) is rejected and forces a fresh authorization, rather than re-running the filter against a later leg.
type UserResolver ¶ added in v0.8.1
type UserResolver struct {
// contains filtered or unexported fields
}
UserResolver handles finding or creating users based on provider identity. It manages the mapping between upstream provider subjects and internal user IDs.
func NewUserResolver ¶ added in v0.8.1
func NewUserResolver(stor storage.UserStorage) *UserResolver
NewUserResolver creates a new UserResolver with the given storage.
func (*UserResolver) ResolveUser ¶ added in v0.8.1
func (r *UserResolver) ResolveUser( ctx context.Context, providerID string, providerSubject string, ) (*storage.User, error)
ResolveUser finds an existing user or creates a new one for the provider identity. Returns the user whose ID will be the "sub" claim in our JWTs.
The resolution process: 1. Look up existing identity by (providerID, providerSubject) 2. If found, return the linked user 3. If not found, create a new user and link the identity
func (*UserResolver) UpdateLastAuthenticated ¶ added in v0.8.1
func (r *UserResolver) UpdateLastAuthenticated( ctx context.Context, providerID string, providerSubject string, )
UpdateLastAuthenticated updates the last authentication timestamp for a provider identity. This supports OIDC max_age parameter enforcement by tracking when users last authenticated. Errors are logged but not fatal - callers should continue with authorization.