Documentation
¶
Index ¶
- Constants
- func MetadataPathFor(resource string) (string, error)
- func NewMetadataHandler(meta ProtectedResourceMetadata, opts HandlerOptions) (http.Handler, error)
- func RequireBearer(opts BearerOpts) (func(http.Handler) http.Handler, error)
- func WriteExchangeError(w http.ResponseWriter, err error)
- func WriteExchangeResponse(w http.ResponseWriter, t IssuedToken) error
- type BearerOpts
- type Claims
- type ClientAuth
- type DPoPKey
- type DPoPMode
- type DPoPNonceSource
- type DPoPReplayCache
- type Emitter
- type Event
- type EventKind
- type ExchangeConfig
- type ExchangeError
- type ExchangeGrant
- type ExchangePolicy
- type ExchangeRequest
- type ExchangeResult
- type ExchangeValidator
- type ExchangeValidatorOptions
- type FuncEmitter
- type HTTPContext
- type HandlerOptions
- type IssuedToken
- type Outcome
- type ProtectedResourceMetadata
- type TokenExchanger
- type TokenLookup
- type TokenType
- type ValidateOptions
Constants ¶
const WellKnownSuffix = "/.well-known/oauth-protected-resource"
WellKnownSuffix is the RFC 9728 par.3.1 well-known URI path suffix.
Variables ¶
This section is empty.
Functions ¶
func MetadataPathFor ¶
MetadataPathFor returns the RFC 9728 par.3.1 path for a resource URI. The resource's path component (if any) is appended to WellKnownSuffix. resource must be an absolute URI.
func NewMetadataHandler ¶
func NewMetadataHandler(meta ProtectedResourceMetadata, opts HandlerOptions) (http.Handler, error)
NewMetadataHandler returns an http.Handler serving the RFC 9728 Protected Resource Metadata document. Supports GET and HEAD; validates meta before constructing the handler.
func RequireBearer ¶
RequireBearer returns net/http middleware that validates OAuth 2.0 Bearer tokens. It returns an error if opts are invalid.
func WriteExchangeError ¶
func WriteExchangeError(w http.ResponseWriter, err error)
WriteExchangeError writes a token-endpoint error JSON response. If err is an *ExchangeError its code/status are used; any other error becomes a generic 500 server_error (never leaking internals).
func WriteExchangeResponse ¶
func WriteExchangeResponse(w http.ResponseWriter, t IssuedToken) error
WriteExchangeResponse writes the RFC 8693 token-exchange JSON response.
Types ¶
type BearerOpts ¶
type BearerOpts struct {
JWKSURI string
KeysJWKS []byte // raw JWKS JSON (static keys); parsed internally
Issuer string
Audience []string
Resource string // also accepted as audience; drives resource_metadata hint in WWW-Authenticate
Realm string // WWW-Authenticate realm; defaults to Resource, else "MCP"
RequiredScopes []string
ClockSkew time.Duration
JWKSCacheTTL time.Duration // remote JWKS refresh interval; 0 = 5m default
AllowedAlgorithms []string
DPoP DPoPMode
ClaimValidator func(*Claims) error
ErrorHandler func(http.ResponseWriter, *http.Request, error)
Logger *slog.Logger
AuditEmitter Emitter
TokenLookup TokenLookup
// DPoPSigningAlgs is the asymmetric-algorithm allowlist for DPoP *proofs*
// (separate from AllowedAlgorithms, which governs access-token signatures).
// Default: ["ES256", "RS256", "EdDSA"]. "none" and symmetric "HS*" always rejected.
DPoPSigningAlgs []string
// DPoPProofMaxAge bounds how old a proof's iat may be. Default 60s.
// Future-dated proofs are tolerated up to ClockSkew.
DPoPProofMaxAge time.Duration
// DPoPReplay is the jti replay cache. Default: an in-memory TTL cache.
// Supply a distributed implementation (see examples/dpop-redis) for
// multi-instance deployments.
DPoPReplay DPoPReplayCache
// DPoPNonce, when non-nil, enables the server-nonce challenge.
// Default nil (no nonce). NewDPoPNonceSource(secret) returns a stateless
// HMAC source that is multi-instance-safe with no shared store.
DPoPNonce DPoPNonceSource
// TrustForwardedHeaders derives htu from X-Forwarded-Proto/-Host instead of
// the request's own scheme+host. Default false. Enable only behind a trusted
// proxy that sets these headers (otherwise they are client-spoofable).
TrustForwardedHeaders bool
}
BearerOpts configures RequireBearer
type Claims ¶
type Claims struct {
Subject string
Issuer string
Audience []string
Expiry time.Time
IssuedAt time.Time
NotBefore time.Time
Scope []string // parsed from the space-delimited "scope" claim
// contains filtered or unexported fields
}
Claims holds the verified claims from an access token. Standard registered claims are fields; use Decode to access non-registered claims.
func ClaimsFromContext ¶
ClaimsFromContext returns the claims stored by RequireBearer, or false if the request has no validated token.
type ClientAuth ¶
type ClientAuth interface {
// contains filtered or unexported methods
}
ClientAuth attaches client credentials to an outgoing token request. It is implemented by ClientSecretAuth and PrivateKeyJWTAuth; mTLS needs no implementation (configure it on ExchangeConfig.HTTPClient's transport).
func ClientSecretAuth ¶
func ClientSecretAuth(clientID, secret string, post bool) ClientAuth
ClientSecretAuth authenticates with a client secret. post=false uses HTTP Basic (client_secret_basic); post=true uses form fields (client_secret_post).
func PrivateKeyJWTAuth ¶
func PrivateKeyJWTAuth(clientID string, key []byte, alg string) (ClientAuth, error)
PrivateKeyJWTAuth authenticates with a signed JWT assertion (RFC 7523 / private_key_jwt). key is a PEM- or JWK-encoded asymmetric private key; alg is its signature algorithm (e.g. "ES256", "RS256", "EdDSA"). Symmetric algs and "none" are rejected.
type DPoPKey ¶
type DPoPKey struct {
// contains filtered or unexported fields
}
DPoPKey is a client-held asymmetric key pair used to mint DPoP proofs when exchanging tokens (RFC 9449 par.5). Construct it with NewDPoPKey. jwx is hidden: the caller supplies key bytes and never touches a jwx type.
invariant: all fields are set together on success; private==nil iff zero value (see isZero).
func NewDPoPKey ¶
NewDPoPKey wraps a PEM- or JWK-encoded asymmetric private key for DPoP proof signing. Symmetric keys and unparseable input are rejected.
type DPoPMode ¶
type DPoPMode int
DPoPMode controls DPoP enforcement (RFC 9449)
Regardless of mode, a sender-constrained access token (one carrying cnf.jkt) is never accepted as a plain Bearer token. Presenting one as Bearer always yields 401. The modes differ only in how they treat un-bound tokens:
- DPoPOff: DPoP proofs are not processed; un-bound Bearer tokens are accepted. The DPoP scheme is unrecognized.
- DPoPOptional: un-bound Bearer tokens are still accepted (migration mode); bound tokens require a verified DPoP proof.
- DPoPRequired: all plain Bearer is rejected; every token must be presented as DPoP with a verified, cnf.jkt-bound proof.
type DPoPNonceSource ¶
type DPoPNonceSource interface {
Current(ctx context.Context, r *http.Request) string
Valid(ctx context.Context, r *http.Request, nonce string) bool
}
DPoPNonceSource issues and validates server-chosen DPoP nonces (RFC 9449 par.8). Current returns the nonce to advertise in a DPoP-Nonce response header; Valid reports whether a nonce presented in a proof is acceptable. Implementations MUST be safe for concurrent use.
func NewDPoPNonceSource ¶
func NewDPoPNonceSource(secret []byte) DPoPNonceSource
NewDPoPNonceSource returns a stateless HMAC-based DPoPNonceSource keyed by secret. The nonce is an HMAC over a rotating time window, so it requires no shared storage and is valid across every RS instance that shares secret. The current window and the immediately preceding one are accepted, tolerating clock skew and rotation at the boundary.
type DPoPReplayCache ¶
type DPoPReplayCache interface {
Seen(ctx context.Context, jti string, exp time.Time) (bool, error)
}
DPoPReplayCache rejects replayed DPoP proofs by their jti. Implementations MUST be safe for concurrent use and MUST perform an atomic check-and-record: Seen returns (true, nil) if jti was already recorded and unexpired (a replay); otherwise it records jti with the given expiry and returns (false, nil). A non-nil error signals a backend failure; the middleware fails closed (rejects the request) in that case.
func InMemoryReplayCache ¶
func InMemoryReplayCache() DPoPReplayCache
InMemoryReplayCache returns the default jti replay cache: a concurrency-safe TTL map. Single-instance only - for multi-instance deployments supply a distributed implementation (see examples/dpop-redis).
type Emitter ¶
Emitter receives audit events. Implementations must be non-blocking and safe for concurrent use; Emit is called on the hot request path.
func LogEmitter ¶
LogEmitter returns an Emitter that writes each event to logger at Info level. A nil logger uses slog.Default().
type Event ¶
type Event struct {
Timestamp time.Time `json:"timestamp"`
Kind EventKind `json:"kind"`
Outcome Outcome `json:"outcome"`
Subject string `json:"subject,omitempty"`
Issuer string `json:"issuer,omitempty"`
Audience []string `json:"audience,omitempty"`
Scope []string `json:"scope,omitempty"`
Resource string `json:"resource,omitempty"`
Reason string `json:"reason,omitempty"`
Error string `json:"error,omitempty"`
HTTP HTTPContext `json:"http,omitempty"`
Extra map[string]any `json:"extra,omitempty"`
}
Event is an audit record for a token-validation outcome.
type EventKind ¶
type EventKind string
EventKind identifies the kind of audit event. Consumers MUST tolerate unknown values (treat as informational); new kinds may be added in minor versions.
const ( EventTokenValidated EventKind = "token_validated" EventTokenRejected EventKind = "token_rejected" EventTokenExchanged EventKind = "token_exchanged" // RFC 8693 EventDPoPVerified EventKind = "dpop_verified" EventDPoPRejected EventKind = "dpop_rejected" EventMetadataServed EventKind = "metadata_served" // RFC 9728 EventJTIReplay EventKind = "jti_replay" )
type ExchangeConfig ¶
type ExchangeConfig struct {
TokenEndpoint string // explicit AS token endpoint; OR
Issuer string // RFC 8414 discovery to resolve the token endpoint
ClientAuth ClientAuth // required; how the client authenticates
// HTTPClient is an optional HTTP client for mTLS, timeouts, and proxy
// configuration. The default client does NOT follow redirects from the token
// endpoint: credentials (subject_token, client_secret, client_assertion) must
// not be replayed to a redirected host. A custom
// client SHOULD set CheckRedirect to return http.ErrUseLastResponse to
// preserve this security property.
HTTPClient *http.Client
DPoPKey DPoPKey // optional; zero value ⇒ no DPoP
Audit Emitter // optional; default no-op
}
ExchangeConfig configures a TokenExchanger.
type ExchangeError ¶
type ExchangeError struct {
Code string // e.g. invalid_request, invalid_grant, invalid_target
Description string
URI string
HTTPStatus int // status observed (client) or to write (server); 0 ⇒ derive from Code
}
ExchangeError is an RFC 6749 par.5.2 / RFC 8693 token-endpoint error. The client returns it from Exchange on a non-2xx response; the server side renders it via WriteExchangeError.
func (*ExchangeError) Error ¶
func (e *ExchangeError) Error() string
type ExchangeGrant ¶
type ExchangeGrant struct {
Subject *Claims
Actor *Claims // nil ⇒ impersonation
IsDelegation bool
RequestedAudience []string
RequestedScope []string
RequestedResource []string
RequestedTokenType TokenType
Act map[string]any // nested delegation chain to embed; nil for impersonation
Confirmation string // cnf.jkt to embed if binding the new token
}
ExchangeGrant is the verified, authorized result of an inbound token-exchange request. The consumer signs a token from it (aoa does not sign).
type ExchangePolicy ¶
type ExchangePolicy interface {
Authorize(ctx context.Context, g *ExchangeGrant) error
}
ExchangePolicy approves or narrows the requested downscope. Returning an error rejects the exchange; return an *ExchangeError to control the code (default invalid_target).
type ExchangeRequest ¶
type ExchangeRequest struct {
SubjectToken string
SubjectTokenType TokenType // default access_token
ActorToken string // optional ⇒ delegation
ActorTokenType TokenType // required iff ActorToken set
Resource []string
Audience []string
Scope []string
RequestedTokenType TokenType
}
ExchangeRequest is one RFC 8693 token-exchange request.
type ExchangeResult ¶
type ExchangeResult struct {
AccessToken string
IssuedTokenType TokenType
TokenType string // "Bearer" | "DPoP" | "N_A"
ExpiresIn time.Duration
Scope []string
RefreshToken string
// contains filtered or unexported fields
}
ExchangeResult is the parsed RFC 8693 token-exchange response.
func (*ExchangeResult) Decode ¶
func (r *ExchangeResult) Decode(v any) error
Decode unmarshals the full token-endpoint response JSON into v for access to non-standard fields.
type ExchangeValidator ¶
type ExchangeValidator struct {
// contains filtered or unexported fields
}
ExchangeValidator verifies inbound RFC 8693 requests for a resource/gateway that mints its own tokens.
func NewExchangeValidator ¶
func NewExchangeValidator(opts ExchangeValidatorOptions) (*ExchangeValidator, error)
NewExchangeValidator validates opts and returns a validator.
func (*ExchangeValidator) Validate ¶
func (v *ExchangeValidator) Validate(ctx context.Context, r *http.Request) (*ExchangeGrant, error)
Validate parses and authorizes the request, returning the grant to sign.
type ExchangeValidatorOptions ¶
type ExchangeValidatorOptions struct {
KeysJWKS []byte
JWKSURI string
Issuer string
JWKSCacheTTL time.Duration
AllowedAlgorithms []string
ClockSkew time.Duration
Policy ExchangePolicy
Audit Emitter
// Audience, when non-empty, is the set of acceptable audiences for the
// INBOUND subject/actor tokens (this STS's own identifier(s)). A token whose
// aud does not intersect Audience is rejected (RFC 9700 audience restriction;
// prevents a token minted for another resource from being exchanged here).
// When empty, tokens for ANY audience from the trusted issuer are accepted, a
// deployment risk the consumer MUST mitigate (e.g. via Policy). Strongly
// recommended to set this.
Audience []string
}
ExchangeValidatorOptions configures an ExchangeValidator.
type FuncEmitter ¶
FuncEmitter lets you pass a closure as an Emitter without defining a type.
type HTTPContext ¶
type HTTPContext struct {
Method string `json:"method,omitempty"`
Path string `json:"path,omitempty"`
RemoteAddr string `json:"remote_addr,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
}
HTTPContext holds request metadata attached to an Event.
type HandlerOptions ¶
type HandlerOptions struct {
// AllowInsecureLocalhost mirrors ValidateOptions.AllowInsecureLocalhost.
AllowInsecureLocalhost bool
// CacheControl overrides the default Cache-Control header ("public, max-age=3600").
CacheControl string
// EnableCORS enables permissive CORS headers on the discovery endpoint.
EnableCORS bool
}
HandlerOptions configures NewMetadataHandler. The zero value is strict-RFC.
type IssuedToken ¶
type IssuedToken struct {
AccessToken string
IssuedTokenType TokenType
TokenType string // "Bearer" | "DPoP" | "N_A"
ExpiresIn time.Duration
Scope []string
RefreshToken string
}
IssuedToken is a token the consumer signed from an ExchangeGrant, to be written as the RFC 8693 response. aoa does not sign; the caller provides AccessToken.
type ProtectedResourceMetadata ¶
type ProtectedResourceMetadata struct {
Resource string `json:"resource"`
AuthorizationServers []string `json:"authorization_servers,omitempty"`
JWKSURI string `json:"jwks_uri,omitempty"`
ScopesSupported []string `json:"scopes_supported,omitempty"`
BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"`
ResourceSigningAlgValuesSupported []string `json:"resource_signing_alg_values_supported,omitempty"`
ResourceName string `json:"resource_name,omitempty"`
ResourceDocumentation string `json:"resource_documentation,omitempty"`
ResourcePolicyURI string `json:"resource_policy_uri,omitempty"`
ResourceTOSURI string `json:"resource_tos_uri,omitempty"`
TLSClientCertBoundAccessTokens bool `json:"tls_client_certificate_bound_access_tokens,omitempty"`
AuthorizationDetailsTypesSupported []string `json:"authorization_details_types_supported,omitempty"`
DPoPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported,omitempty"`
DPoPBoundAccessTokensRequired bool `json:"dpop_bound_access_tokens_required,omitempty"`
SignedMetadata string `json:"signed_metadata,omitempty"`
// Extra holds non-standard metadata values merged into the JSON output.
// Typed fields take precedence over same-key entries in Extra.
Extra map[string]any `json:"-"`
}
ProtectedResourceMetadata is the RFC 9728 Protected Resource Metadata document. Resource is required; all other fields are optional.
func (ProtectedResourceMetadata) MarshalJSON ¶
func (m ProtectedResourceMetadata) MarshalJSON() ([]byte, error)
MarshalJSON merges Extra into the typed JSON output. Typed fields win on collision.
func (ProtectedResourceMetadata) Validate ¶
func (m ProtectedResourceMetadata) Validate() error
Validate checks the document against RFC 9728 par.1.2 / par.2. For localhost dev environments, use ValidateWithOptions(ValidateOptions{AllowInsecureLocalhost: true}).
func (ProtectedResourceMetadata) ValidateWithOptions ¶
func (m ProtectedResourceMetadata) ValidateWithOptions(opts ValidateOptions) error
type TokenExchanger ¶
type TokenExchanger struct {
// contains filtered or unexported fields
}
TokenExchanger performs RFC 8693 token exchange against an AS token endpoint.
func NewTokenExchanger ¶
func NewTokenExchanger(cfg ExchangeConfig) (*TokenExchanger, error)
NewTokenExchanger validates cfg and returns an exchanger.
func (*TokenExchanger) Exchange ¶
func (x *TokenExchanger) Exchange(ctx context.Context, req ExchangeRequest) (*ExchangeResult, error)
Exchange runs one token exchange and returns the issued token.
type TokenLookup ¶
type TokenLookup int
TokenLookup selects where the bearer token is read from.
const (
HeaderBearer TokenLookup = iota
)
type TokenType ¶
type TokenType string
TokenType is an RFC 8693 par.3 token-type URI used for subject_token_type, actor_token_type, requested_token_type, and the response's issued_token_type.
const ( TokenTypeAccessToken TokenType = "urn:ietf:params:oauth:token-type:access_token" TokenTypeRefreshToken TokenType = "urn:ietf:params:oauth:token-type:refresh_token" TokenTypeIDToken TokenType = "urn:ietf:params:oauth:token-type:id_token" TokenTypeJWT TokenType = "urn:ietf:params:oauth:token-type:jwt" TokenTypeSAML1 TokenType = "urn:ietf:params:oauth:token-type:saml1" TokenTypeSAML2 TokenType = "urn:ietf:params:oauth:token-type:saml2" )
type ValidateOptions ¶
type ValidateOptions struct {
// AllowInsecureLocalhost permits http:// for localhost. Dev/test only.
AllowInsecureLocalhost bool
}
ValidateOptions controls Validate behaviour. The zero value is strict-RFC.