oauth

package
v6.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 17, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Overview

Package oauth provides small, composable OAuth 2.0/2.1 primitives used by AppTheory components (e.g. Remote MCP protected resources) and by Autheory (an OAuth Authorization Server built on AppTheory).

The package intentionally focuses on wire-level helpers: - RFC9728 protected resource discovery (WWW-Authenticate + metadata) - RFC8414 authorization server metadata helpers - RFC7591 Dynamic Client Registration request/response types + validation - PKCE utilities

Index

Constants

View Source
const (

	// MCPPath is the conventional AppTheory MCP endpoint path.
	MCPPath = "/mcp"

	// OAuthProtectedResourcePath is the generic RFC 9728 protected-resource
	// metadata path.
	OAuthProtectedResourcePath = "/.well-known/oauth-protected-resource"

	// OAuthProtectedResourceMCPPath is the RFC 9728 path-scoped metadata path
	// for the conventional MCP endpoint.
	OAuthProtectedResourceMCPPath = "/.well-known/oauth-protected-resource/mcp"

	// OAuthAuthorizationServerMCPPath is the RFC 8414 path-scoped metadata path
	// for an MCP authorization server.
	OAuthAuthorizationServerMCPPath = "/.well-known/oauth-authorization-server/mcp"
)
View Source
const ContextKeyBearerClaims = "oauth.bearer_claims" //nolint:gosec // context key, not a credential

ContextKeyBearerClaims is set on apptheory.Context when a claims validator is configured.

View Source
const ContextKeyBearerToken = "oauth.bearer_token" //nolint:gosec // context key, not a credential

ContextKeyBearerToken is set on apptheory.Context by RequireBearerTokenMiddleware.

Variables

View Source
var (
	// ErrMissingBearerToken is returned when an Authorization header is missing.
	ErrMissingBearerToken = errors.New("missing bearer token")
	// ErrInvalidAuthorizationHeader is returned when the Authorization header cannot be parsed as Bearer.
	ErrInvalidAuthorizationHeader = errors.New("invalid authorization header")
	// ErrInvalidBearerToken indicates an unknown or otherwise invalid bearer token.
	ErrInvalidBearerToken = errors.New("invalid bearer token")
	// ErrBearerTokenExpired indicates an expired bearer token.
	ErrBearerTokenExpired = errors.New("bearer token expired")
	// ErrBearerTokenInvalidAudience indicates a bearer token is not bound to the required resource.
	ErrBearerTokenInvalidAudience = errors.New("bearer token invalid audience")
	// ErrBearerTokenInsufficientScope indicates a bearer token lacks a required scope.
	ErrBearerTokenInsufficientScope = errors.New("bearer token insufficient scope")
	// ErrInvalidURL is returned when a required URL is not a valid absolute URL.
	ErrInvalidURL = errors.New("invalid url")

	// ErrAuthorizationCodeNotFound indicates an unknown/consumed authorization code.
	ErrAuthorizationCodeNotFound = errors.New("authorization code not found")
	// ErrAuthorizationCodeExpired indicates an expired authorization code.
	ErrAuthorizationCodeExpired = errors.New("authorization code expired")

	// ErrRefreshTokenNotFound indicates an unknown/revoked refresh token.
	ErrRefreshTokenNotFound = errors.New("refresh token not found")
	// ErrRefreshTokenExpired indicates an expired refresh token.
	ErrRefreshTokenExpired = errors.New("refresh token expired")
)

Functions

func AuthorizationServerMetadataHandler

func AuthorizationServerMetadataHandler(md *AuthorizationServerMetadata) apptheory.Handler

AuthorizationServerMetadataHandler returns an AppTheory handler that serves the RFC8414 authorization server metadata document.

func BearerTokenFromHeaders

func BearerTokenFromHeaders(headers map[string][]string) (string, error)

BearerTokenFromHeaders extracts the bearer token from normalized headers.

func BootstrapOIDCDiscovery added in v6.1.0

func BootstrapOIDCDiscovery(ctx context.Context, discoveryURL string, issuer string, client *http.Client) (jwksURI string, err error)

BootstrapOIDCDiscovery fetches an OIDC discovery document, verifies that the document's issuer equals the configured issuer, and returns the document's jwks_uri. The discovery URL and the returned key-set URL must both be absolute HTTPS URLs; a document that omits jwks_uri, an issuer mismatch, and any redirect all fail closed.

A non-nil client is caller-owned and keeps its own connection policy, so a caller can point the bootstrap at a loopback server for local development; when it is nil the framework installs its own transport, which refuses any endpoint host that is not a public address.

func CanonicalResourceURL

func CanonicalResourceURL(raw string) string

CanonicalResourceURL trims whitespace and a trailing slash.

func CanonicalizeIssuerURL

func CanonicalizeIssuerURL(raw string) (string, bool)

CanonicalizeIssuerURL trims trailing slashes from an issuer/base URL.

func InsufficientScopeWWWAuthenticate added in v6.1.0

func InsufficientScopeWWWAuthenticate(scopes []string, resourceMetadataURL string) string

InsufficientScopeWWWAuthenticate builds the RFC 6750 (RFC 9728-extended) insufficient_scope challenge carrying the route-derived remediation scope set.

func NewMCPProtectedResourceDiscoveryHandler

func NewMCPProtectedResourceDiscoveryHandler(config MCPServerConfig) (apptheory.Handler, error)

NewMCPProtectedResourceDiscoveryHandler creates the request-time RFC 9728 discovery handler used by namespace MCP applications. The protected resource URL is reconstructed from AppTheory's canonical request headers; no resource origin is accepted in config.

func NewOpaqueToken

func NewOpaqueToken() (string, error)

NewOpaqueToken generates a URL-safe opaque token suitable for authorization codes and refresh tokens.

func NewPKCECodeVerifier

func NewPKCECodeVerifier() (string, error)

NewPKCECodeVerifier generates a random PKCE code verifier (length ~43).

func NormalizeRequestOrigin

func NormalizeRequestOrigin(raw string) (string, bool)

NormalizeRequestOrigin validates and canonicalizes a request-derived origin. It accepts HTTPS, plus HTTP only for loopback hosts, and removes scheme- default ports.

func PKCEChallengeS256

func PKCEChallengeS256(verifier string) (string, error)

PKCEChallengeS256 computes the S256 code challenge for a verifier.

func PKCEVerifyS256

func PKCEVerifyS256(verifier, expectedChallenge string) (bool, error)

PKCEVerifyS256 verifies a verifier matches an expected S256 challenge.

func ProtectedResourceMetadataHandler

func ProtectedResourceMetadataHandler(md *ProtectedResourceMetadata) apptheory.Handler

ProtectedResourceMetadataHandler returns an AppTheory handler that serves the RFC9728 protected resource metadata document.

func ProtectedResourceMetadataURLForRequest

func ProtectedResourceMetadataURLForRequest(headers map[string][]string) (string, bool)

ProtectedResourceMetadataURLForRequest derives an absolute root `/.well-known/oauth-protected-resource` URL from common proxy headers.

Prefer using ResourceMetadataURLFromMcpEndpoint with an explicit MCP endpoint URL for AWS Remote MCP deployments. This helper is intentionally root-only and does not attempt to infer path-scoped protected resources from request paths.

func ProtectedResourceWWWAuthenticate

func ProtectedResourceWWWAuthenticate(resourceMetadataURL string) string

ProtectedResourceWWWAuthenticate builds the RFC9728 MCP-style discovery challenge.

Example:

Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"

func RFC9728ResourceMetadataURL

func RFC9728ResourceMetadataURL(resourceURL string) (string, bool)

RFC9728ResourceMetadataURL derives the protected resource metadata URL from any absolute protected resource identifier URL per RFC 9728 section 3.

For example:

https://api.example.com/mcp      -> https://api.example.com/.well-known/oauth-protected-resource/mcp
https://api.example.com/mcp/Arch -> https://api.example.com/.well-known/oauth-protected-resource/mcp/Arch

func RegisterMCPServer

func RegisterMCPServer(app *apptheory.SecureApp, handler apptheory.Handler, config MCPServerConfig) error

RegisterMCPServer registers the conventional authenticated MCP POST route and both public RFC 9728 discovery routes on a SecureApp. The fixed posture split is deliberate: clients must fetch discovery before they have a token, while the MCP endpoint uses SecureApp's authenticated posture.

func RequireBearerTokenMiddleware

func RequireBearerTokenMiddleware(opts RequireBearerTokenOptions) apptheory.Middleware

RequireBearerTokenMiddleware enforces `Authorization: Bearer <token>` and, when missing/invalid, returns 401 with the MCP 2025-06-18 compatible WWW-Authenticate challenge that points to protected resource metadata.

A ResourceMetadataURLTemplate is compiled here, at registration, and a malformed template panics: the middleware never serves a request whose challenge would advertise a broken or partially resolved discovery URL.

func ResourceMetadataURLFromMcpEndpoint

func ResourceMetadataURLFromMcpEndpoint(mcpEndpoint string) (string, bool)

ResourceMetadataURLFromMcpEndpoint derives the protected resource metadata URL from an MCP endpoint URL.

This compatibility alias intentionally performs no `/mcp` suffix validation; any absolute protected resource URL is accepted and transformed per RFC 9728.

func ValidateDynamicClientRegistrationRequest

func ValidateDynamicClientRegistrationRequest(req *DynamicClientRegistrationRequest, policy DynamicClientRegistrationPolicy) error

ValidateDynamicClientRegistrationRequest validates an RFC7591 request against a policy.

func ValidatePKCECodeVerifier

func ValidatePKCECodeVerifier(verifier string) error

ValidatePKCECodeVerifier validates a verifier against RFC7636 constraints.

Types

type AutheoryProvider added in v6.1.0

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

AutheoryProvider is the Autheory implementation of Provider.

func NewAutheoryProvider added in v6.1.0

func NewAutheoryProvider(opts AutheoryProviderOptions) (*AutheoryProvider, error)

NewAutheoryProvider returns a fail-closed Autheory provider adapter. The issuer must be an absolute URL; Discovery refuses a non-https discovery URL, a document issuer that does not match, and any redirect.

func (*AutheoryProvider) AuthorizationURL added in v6.1.0

func (p *AutheoryProvider) AuthorizationURL(req AuthorizationRequest) (string, error)

AuthorizationURL builds the upstream authorize URL. The adapter always sends resource (Autheory requires it, RFC 8707); a hint whose name collides with a required or reserved authorize parameter is refused as invalid_request, and a non-colliding hint is still forwarded.

func (*AutheoryProvider) Discovery added in v6.1.0

func (p *AutheoryProvider) Discovery(ctx context.Context) (*ProviderMetadata, error)

Discovery fetches and verifies the upstream RFC 8414 document, then caches the result. Every published endpoint must be an https URL and the document issuer must equal the configured issuer before anything is trusted.

func (*AutheoryProvider) ExchangeCode added in v6.1.0

func (p *AutheoryProvider) ExchangeCode(ctx context.Context, req CodeExchangeRequest) (*TokenSet, error)

ExchangeCode performs the upstream authorization-code exchange with PKCE.

func (*AutheoryProvider) Refresh added in v6.1.0

func (p *AutheoryProvider) Refresh(ctx context.Context, req RefreshRequest) (*TokenSet, error)

Refresh rotates the recorded upstream material, presenting the OAuth client that owns it.

func (*AutheoryProvider) ValidateAccessToken added in v6.1.0

func (p *AutheoryProvider) ValidateAccessToken(
	ctx context.Context,
	token string,
	expected TokenExpectations,
) (*TokenClaims, error)

ValidateAccessToken verifies the token against the expectation set and returns safe claims. The expectation set selects (and caches) one RS256/JWKS validator, so the key cache is reused across calls with the same expectations. Any validator failure is an upstream_error: a token the upstream cannot vouch for is never accepted, and the reason is never forwarded to the client.

type AutheoryProviderOptions added in v6.1.0

type AutheoryProviderOptions struct {
	// Issuer is the expected upstream issuer, for example
	// https://auth.autheory.app. It is required.
	Issuer string

	// ClientID is the adapter's own upstream client registration id. The adapter
	// never derives a request's client id from it: a request that omits its
	// upstream client id fails closed with invalid_request.
	ClientID string

	// DiscoveryURL overrides the discovery document URL. Zero value means the
	// issuer plus /.well-known/oauth-authorization-server.
	DiscoveryURL string

	// TokenEndpoint, JWKSURI, and AuthorizationEndpoint override the endpoints
	// the discovery document publishes. Each must be an https URL.
	TokenEndpoint         string
	JWKSURI               string
	AuthorizationEndpoint string

	// HTTPClient is reused for every discovery, token, and JWKS call. Redirects
	// are refused regardless; a caller-supplied transport keeps its own
	// connection policy.
	HTTPClient *http.Client

	// AllowPrivateHosts disables the framework's non-public-host guard on the
	// calls the adapter makes with the framework's own transport. It is off by
	// default; local development and loopback test suites opt in explicitly.
	AllowPrivateHosts bool

	// FetchTimeout bounds one discovery, token, or JWKS call. Zero means the
	// framework default shared with discovery and JWKS fetching.
	FetchTimeout time.Duration

	// Now overrides the clock used for token expiry and key caching. Zero means
	// time.Now().UTC.
	Now func() time.Time

	// AllowedAuthorizedParties is an exact-match allow-list applied to the
	// validated token's azp claim (falling back to client_id). It is the default
	// for expectation sets that do not carry their own list.
	AllowedAuthorizedParties []string
}

AutheoryProviderOptions configures NewAutheoryProvider.

type AuthorizationCodeRecord

type AuthorizationCodeRecord struct {
	Code                string
	ClientID            string
	RedirectURI         string
	Resource            string
	CodeChallenge       string
	CodeChallengeMethod string
	ExpiresAt           time.Time
}

AuthorizationCodeRecord stores the data needed to redeem an authorization code.

type AuthorizationCodeStore

type AuthorizationCodeStore interface {
	Put(ctx context.Context, rec *AuthorizationCodeRecord) error
	Consume(ctx context.Context, code string) (*AuthorizationCodeRecord, error)
}

AuthorizationCodeStore stores short-lived authorization codes.

type AuthorizationRequest added in v6.1.0

type AuthorizationRequest struct {
	UpstreamClientID    string
	CallbackURL         string
	CodeChallenge       string
	CodeChallengeMethod string
	State               string
	Resource            string
	Scopes              []string
	Hints               map[string]string
}

AuthorizationRequest is one upstream authorize request. State is a fresh upstream state distinct from the client's state, and Hints carries opaque provider hints (for example account_id): a provider derives authentication account context from its own validated binding, never from a hint.

type AuthorizationServerMetadata

type AuthorizationServerMetadata struct {
	Issuer                            string   `json:"issuer"`
	AuthorizationEndpoint             string   `json:"authorization_endpoint,omitempty"`
	TokenEndpoint                     string   `json:"token_endpoint,omitempty"`
	RegistrationEndpoint              string   `json:"registration_endpoint,omitempty"`
	JWKSURI                           string   `json:"jwks_uri,omitempty"`
	RevocationEndpoint                string   `json:"revocation_endpoint,omitempty"`
	DeviceAuthorizationEndpoint       string   `json:"device_authorization_endpoint,omitempty"`
	ResponseTypesSupported            []string `json:"response_types_supported,omitempty"`
	GrantTypesSupported               []string `json:"grant_types_supported,omitempty"`
	TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
	CodeChallengeMethodsSupported     []string `json:"code_challenge_methods_supported,omitempty"`
	ScopesSupported                   []string `json:"scopes_supported,omitempty"`
	SubjectTypesSupported             []string `json:"subject_types_supported,omitempty"`
	IDTokenSigningAlgValuesSupported  []string `json:"id_token_signing_alg_values_supported,omitempty"`
	// AuthorizationResponseIssParameterSupported advertises RFC 9207 `iss` on
	// authorization responses. It is opt-in and false by default: omitempty
	// keeps every document that does not set it byte-identical to its pre-RFC
	// 9207 shape (an absent member means false, RFC 8414 section 2), so only a
	// surface that provably sends `iss` sets it. The field is a typed bool, so
	// the wire value is always the JSON boolean `true` while present: no
	// non-boolean state can reach the document.
	AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"`
}

AuthorizationServerMetadata is the RFC8414 Authorization Server metadata document.

func NewAuthorizationServerMetadata

func NewAuthorizationServerMetadata(issuer string, opts ...AuthorizationServerMetadataOption) (*AuthorizationServerMetadata, error)

NewAuthorizationServerMetadata builds a Claude-compatible RFC8414 document with conventional root endpoints (/authorize, /token, /register) derived from the issuer/base URL. Additional RFC 8414 members such as revocation_endpoint and device_authorization_endpoint are opt-in via AuthorizationServerMetadataOption: the framework implements no revocation or device-flow endpoints, so the default document advertises only what it serves.

func (*AuthorizationServerMetadata) MarshalJSONBytes

func (m *AuthorizationServerMetadata) MarshalJSONBytes() ([]byte, error)

MarshalJSONBytes marshals the metadata document to JSON bytes.

type AuthorizationServerMetadataOption

type AuthorizationServerMetadataOption func(*AuthorizationServerMetadata, *url.URL) error

AuthorizationServerMetadataOption configures the RFC 8414 authorization server metadata document built by NewAuthorizationServerMetadata. Options run in order after the conventional fields are derived from the issuer base URL; base is the canonical issuer URL.

func WithDeviceAuthorizationEndpoint

func WithDeviceAuthorizationEndpoint(explicit ...string) AuthorizationServerMetadataOption

WithDeviceAuthorizationEndpoint adds the RFC 8628 device_authorization_endpoint to the metadata document. With no argument it derives the conventional /device path from the issuer base URL; pass exactly one explicit absolute URL to point at a non-conventional path. Passing more than one explicit URL is rejected with ErrInvalidURL. The framework implements no device-flow endpoint, so this option is opt-in: the default document does not advertise one.

func WithRevocationEndpoint

func WithRevocationEndpoint(explicit ...string) AuthorizationServerMetadataOption

WithRevocationEndpoint adds the RFC 8414 revocation_endpoint to the metadata document. With no argument it derives the conventional /revoke path from the issuer base URL; pass exactly one explicit absolute URL to point at a non-conventional path. Passing more than one explicit URL is rejected with ErrInvalidURL. The framework implements no revocation endpoint, so this option is opt-in: the default document does not advertise one.

type BearerTokenClaims

type BearerTokenClaims struct {
	Subject   string
	Audience  string
	Scopes    []string
	ExpiresAt time.Time
}

BearerTokenClaims is the deterministic, local claim shape AppTheory uses for contract-pinned bearer validation. Token is intentionally not included so the safe context value cannot leak credentials.

func BearerTokenClaimsFromContext

func BearerTokenClaimsFromContext(c *apptheory.Context) (BearerTokenClaims, bool)

BearerTokenClaimsFromContext returns safe bearer claims set by RequireBearerTokenMiddleware when ClaimsValidator is configured.

type BearerTokenClaimsValidator

type BearerTokenClaimsValidator func(ctx *apptheory.Context, token string) (*BearerTokenClaims, error)

BearerTokenClaimsValidator validates a Bearer access token and returns safe, non-secret claims for downstream handlers.

func NewJWKSValidator added in v6.1.0

func NewJWKSValidator(opts JWKSValidatorOptions) (BearerTokenClaimsValidator, error)

NewJWKSValidator returns a fail-closed RS256/JWKS claims validator.

The returned validator resolves signing keys from a bounded kid cache that RefreshCooldown rate-limits, verifies RSASSA-PKCS1-v1_5/SHA-256 signatures, and refuses the request on every doubt: a malformed token, a non-RS256 algorithm, a missing kid, an unknown or inconsistent key, a signature mismatch, an issuer mismatch, an expired/not-yet-valid token, an audience or scope mismatch, or a disallowed azp. It never returns claims for a token it could not fully verify, and it never logs the token or key material.

func NewMemoryBearerTokenValidator

func NewMemoryBearerTokenValidator(records []BearerTokenRecord, opts BearerTokenValidationOptions) BearerTokenClaimsValidator

NewMemoryBearerTokenValidator returns a deterministic local bearer validator over the provided token records.

type BearerTokenRecord

type BearerTokenRecord struct {
	Token     string
	Subject   string
	Audience  string
	Scope     string
	Scopes    []string
	ExpiresAt time.Time
}

BearerTokenRecord seeds a deterministic in-memory bearer-token validator.

type BearerTokenValidationOptions

type BearerTokenValidationOptions struct {
	RequiredAudience string
	RequiredScopes   []string
	// RequiredAudienceFunc returns the route-derived audience the token must be
	// bound to. When set it takes precedence over RequiredAudience, so audience
	// can be the exact matched route URL.
	RequiredAudienceFunc func(*apptheory.Context) (string, error)
	// RequiredScopesFunc returns the route-derived scope set the token must
	// carry. When set it takes precedence over RequiredScopes.
	RequiredScopesFunc func(*apptheory.Context) ([]string, error)
	Now                func() time.Time
}

BearerTokenValidationOptions configures NewMemoryBearerTokenValidator.

type BearerTokenValidator

type BearerTokenValidator func(ctx *apptheory.Context, token string) error

BearerTokenValidator validates a Bearer access token.

The validator receives the route-aware AppTheory context, so a validator can derive the expected issuer, audience, and scope set from the matched route instead of a process-global value.

type CodeExchangeRequest added in v6.1.0

type CodeExchangeRequest struct {
	Code             string
	UpstreamClientID string
	CallbackURL      string
	CodeVerifier     string
}

CodeExchangeRequest is one server-side upstream code exchange.

type DynamicClientRegistrationPolicy

type DynamicClientRegistrationPolicy struct {
	AllowedRedirectURIs []string
	RequirePublicClient bool // token_endpoint_auth_method must be "none"
	RequireRefreshToken bool // grant_types must include "refresh_token" (if grant_types provided)
}

DynamicClientRegistrationPolicy controls DCR validation.

func ClaudeDynamicClientRegistrationPolicy

func ClaudeDynamicClientRegistrationPolicy() DynamicClientRegistrationPolicy

ClaudeDynamicClientRegistrationPolicy returns a day-1 policy compatible with Claude connectors.

type DynamicClientRegistrationRequest

type DynamicClientRegistrationRequest struct {
	ClientName              string   `json:"client_name,omitempty"`
	RedirectURIs            []string `json:"redirect_uris,omitempty"`
	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method,omitempty"`
	GrantTypes              []string `json:"grant_types,omitempty"`
	ResponseTypes           []string `json:"response_types,omitempty"`
	Scope                   string   `json:"scope,omitempty"`
}

DynamicClientRegistrationRequest is an RFC7591 client registration request.

type DynamicClientRegistrationResponse

type DynamicClientRegistrationResponse struct {
	ClientID string `json:"client_id"`
	// ClientSecret is omitted for public clients.
	ClientSecret          string `json:"client_secret,omitempty"`
	ClientIDIssuedAt      int64  `json:"client_id_issued_at,omitempty"`
	ClientSecretExpiresAt int64  `json:"client_secret_expires_at,omitempty"`
}

DynamicClientRegistrationResponse is a minimal RFC7591 response.

type JWKSValidatorOptions added in v6.1.0

type JWKSValidatorOptions struct {
	// Issuer is the expected token issuer. It is required.
	Issuer string

	// JWKSURI is the absolute HTTPS JSON Web Key Set URL. When it is empty,
	// DiscoveryURL bootstraps it.
	JWKSURI string

	// DiscoveryURL is the absolute OIDC discovery document URL. It is consulted
	// only when JWKSURI is empty, and its document issuer must equal Issuer
	// before its jwks_uri is trusted.
	DiscoveryURL string

	// HTTPClient is reused for every discovery and JWKS fetch. Redirects are
	// disabled regardless: a 3xx response fails closed.
	HTTPClient *http.Client

	// Now overrides the clock used for exp/nbf, cache expiry, and cooldown.
	Now func() time.Time

	// ClockSkew is the tolerance applied to exp and nbf.
	ClockSkew time.Duration

	// FetchTimeout bounds one discovery or JWKS fetch. Zero means the default of
	// 10s, shared with the TypeScript and Python validators; an HTTPClient that
	// already carries its own Timeout keeps that Timeout unless FetchTimeout is
	// set explicitly.
	FetchTimeout time.Duration

	// AllowPrivateHosts disables the non-public-host check on the framework's
	// discovery/JWKS transport. It is off by default and the framework never
	// enables it: local development and the repository's loopback test suites
	// opt in explicitly. A caller-supplied HTTPClient keeps its own connection
	// policy either way.
	AllowPrivateHosts bool

	// CacheTTL is how long a fetched key stays usable before a refresh. Zero
	// means the default.
	CacheTTL time.Duration

	// RefreshCooldown bounds how often one unknown kid may trigger fetches.
	// Zero means the default.
	RefreshCooldown time.Duration

	// MaxRefreshPerKid bounds the refresh fetches a single unknown kid may
	// trigger beyond the fetch that populates the cache. Zero means the default
	// of 1.
	MaxRefreshPerKid int

	// MaxKidLength bounds a kid string before it may enter any cache. Zero means
	// the default of 256.
	MaxKidLength int

	// MaxWindowEntries bounds the per-kid refresh-window map. Zero means the
	// default of 1024.
	MaxWindowEntries int

	// MaxKeyEntries bounds the cached signing keys. Zero means the default of
	// 128.
	MaxKeyEntries int

	// MaxUnknownKidFetches is the global key-set fetch budget inside one
	// UnknownKidFetchWindow. Zero means the default of 32.
	MaxUnknownKidFetches int

	// UnknownKidFetchWindow is the rolling window of MaxUnknownKidFetches. Zero
	// means the default of 60s.
	UnknownKidFetchWindow time.Duration

	// AllowedAZP is an exact-match allow-list for the token's azp claim, falling
	// back to client_id. An empty list disables the check.
	AllowedAZP []string

	// RequiredAudience derives the route-bound audience. A non-empty result must
	// equal one aud entry exactly; there is no substring or wildcard match.
	RequiredAudience func(*apptheory.Context) (string, error)

	// RequiredScopes derives the route-bound scope set. Every entry must be
	// present in the token.
	RequiredScopes func(*apptheory.Context) ([]string, error)
}

JWKSValidatorOptions configures NewJWKSValidator.

type MCPServerConfig

type MCPServerConfig struct {
	// MCPPath overrides the conventional /mcp endpoint path.
	MCPPath string

	// AuthorizationServerIssuer is the configured OAuth authorization server
	// issuer advertised by RFC 9728 discovery.
	AuthorizationServerIssuer string

	// JWKSURI is the configured JSON Web Key Set URL used to validate access
	// tokens. It is not the protected resource's own key set and is therefore
	// not advertised by RFC 9728 discovery.
	JWKSURI string

	// AllowedHostnames is an optional trusted-host allowlist for discovery.
	// When it is empty the discovery origin is derived from the
	// deployment-authoritative `host` header alone, and forwarded-host headers
	// are ignored. When it is non-empty, forwarded-host headers may be honored,
	// but only a request whose canonical authority is on this list produces
	// metadata; anything else fails closed with HTTP 400
	// `{"error":"invalid_request_host"}`.
	AllowedHostnames []string
}

MCPServerConfig configures AppTheory's secure MCP route bundle. The protected resource origin is derived from each request by default (from the deployment-authoritative `host` header) while the authorization server and JWKS remain install config.

type MemoryAuthorizationCodeStore

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

MemoryAuthorizationCodeStore is an in-memory AuthorizationCodeStore.

func NewMemoryAuthorizationCodeStore

func NewMemoryAuthorizationCodeStore() *MemoryAuthorizationCodeStore

func (*MemoryAuthorizationCodeStore) Consume

func (*MemoryAuthorizationCodeStore) Put

type MemoryRefreshTokenStore

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

MemoryRefreshTokenStore is an in-memory RefreshTokenStore.

func NewMemoryRefreshTokenStore

func NewMemoryRefreshTokenStore() *MemoryRefreshTokenStore

func (*MemoryRefreshTokenStore) Consume

func (*MemoryRefreshTokenStore) Delete

func (s *MemoryRefreshTokenStore) Delete(_ context.Context, token string) error

func (*MemoryRefreshTokenStore) Get

func (*MemoryRefreshTokenStore) Put

type ProtectedResourceMetadata

type ProtectedResourceMetadata struct {
	Resource               string   `json:"resource"`
	AuthorizationServers   []string `json:"authorization_servers"`
	JWKSURI                string   `json:"jwks_uri,omitempty"`
	ScopesSupported        []string `json:"scopes_supported,omitempty"`
	BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"`
}

ProtectedResourceMetadata is the RFC9728 discovery document hosted by a protected resource server.

func NewProtectedResourceMetadata

func NewProtectedResourceMetadata(resource string, authorizationServers []string) (*ProtectedResourceMetadata, error)

NewProtectedResourceMetadata creates a minimal metadata document. It requires: - resource: an absolute URL that identifies the protected resource (for MCP this is typically the `/mcp` endpoint) - authorizationServers: one or more OAuth AS issuer/base URLs

func (*ProtectedResourceMetadata) MarshalJSONBytes

func (m *ProtectedResourceMetadata) MarshalJSONBytes() ([]byte, error)

MarshalJSONBytes marshals the metadata document to JSON bytes.

type Provider added in v6.1.0

type Provider interface {
	// Discovery returns the upstream metadata after verifying the discovery
	// document: the document issuer must equal the configured issuer before any
	// published endpoint is trusted.
	Discovery(ctx context.Context) (*ProviderMetadata, error)

	// AuthorizationURL builds the absolute upstream authorize URL a browser is
	// redirected to from the facade's /authorize surface.
	AuthorizationURL(req AuthorizationRequest) (string, error)

	// ExchangeCode performs the server-side upstream code exchange, presenting
	// the facade's own client and PKCE verifier.
	ExchangeCode(ctx context.Context, req CodeExchangeRequest) (*TokenSet, error)

	// Refresh performs an upstream refresh presenting the OAuth client that owns
	// the recorded upstream material.
	Refresh(ctx context.Context, req RefreshRequest) (*TokenSet, error)

	// ValidateAccessToken validates an upstream access token against the caller's
	// expectations and returns safe, non-secret claims. It never mutates the
	// token.
	ValidateAccessToken(ctx context.Context, token string, expected TokenExpectations) (*TokenClaims, error)
}

Provider is the provider-neutral upstream IdP adapter SPI of the MCP auth facade (DESIGN §6, contract fixture facade-provider-spi.v1.json).

The SPI is deliberately the minimum needed to derive the facade from the upstream requirements, so provider-specific policy travels through hints and typed errors rather than framework branches. Every method is fail-closed: it never relaxes a check, never mutates the token it is given, and never forwards an upstream response body to its caller. Failures are reported as a typed *ProviderError carrying the provider error class the facade maps onto the closed client-facing vocabulary.

type ProviderError added in v6.1.0

type ProviderError struct {
	Class   ProviderErrorClass
	Op      string
	Message string
	// contains filtered or unexported fields
}

ProviderError is the typed error every provider adapter returns. Message is a fixed adapter string and never contains any part of an upstream response body; the attached cause is only ever a transport failure (an errors.Is / errors.As target), so rendering an error can never echo upstream material.

func AsProviderError added in v6.1.0

func AsProviderError(err error) (*ProviderError, bool)

AsProviderError reports whether err is (or wraps) a *ProviderError.

func NewProviderError added in v6.1.0

func NewProviderError(class ProviderErrorClass, op string, message string) *ProviderError

NewProviderError returns a ProviderError for class and operation with a fixed message.

func (*ProviderError) Error added in v6.1.0

func (e *ProviderError) Error() string

Error implements error. The rendered string is the class, the operation, the fixed message, and the transport cause: no upstream response body is ever part of it.

func (*ProviderError) HTTPStatus added in v6.1.0

func (e *ProviderError) HTTPStatus() int

HTTPStatus maps the class onto the facade's client-facing status.

func (*ProviderError) Unwrap added in v6.1.0

func (e *ProviderError) Unwrap() error

Unwrap returns the underlying cause so errors.Is/errors.As can classify the transport failure that produced the class.

type ProviderErrorClass added in v6.1.0

type ProviderErrorClass string

ProviderErrorClass is the closed provider error vocabulary of DESIGN §6. It maps onto the facade's client-facing classes and HTTP statuses.

const (
	// ProviderUpstreamError is an upstream provider failure (facade HTTP 502).
	ProviderUpstreamError ProviderErrorClass = "upstream_error"
	// ProviderTemporarilyUnavailable is a transient, retryable provider failure
	// (facade HTTP 503).
	ProviderTemporarilyUnavailable ProviderErrorClass = "temporarily_unavailable"
	// ProviderInvalidGrant means the provider definitively rejected the grant
	// (terminal; facade HTTP 400).
	ProviderInvalidGrant ProviderErrorClass = "invalid_grant"
	// ProviderInvalidRequest means the adapter was called with a request it
	// cannot send; it never reaches the upstream (facade HTTP 400).
	ProviderInvalidRequest ProviderErrorClass = "invalid_request"
	// ProviderReauthRequired means the provider reported the recorded material
	// stale or replaced (terminal; facade HTTP 400).
	ProviderReauthRequired ProviderErrorClass = "reauth_required"
)

ProviderErrorClass values. Every class is reachable from an explicit adapter decision; none is inferred from a raw upstream body.

type ProviderMetadata added in v6.1.0

type ProviderMetadata struct {
	Issuer                                     string   `json:"issuer"`
	AuthorizationEndpoint                      string   `json:"authorization_endpoint"`
	TokenEndpoint                              string   `json:"token_endpoint"`
	JWKSURI                                    string   `json:"jwks_uri"`
	CodeChallengeMethodsSupported              []string `json:"code_challenge_methods_supported"`
	AuthorizationResponseIssParameterSupported bool     `json:"authorization_response_iss_parameter_supported"`
	ResourceParameterSupported                 bool     `json:"resource_parameter_supported"`
}

ProviderMetadata is the verified upstream discovery document subset the facade relies on.

type RefreshRequest added in v6.1.0

type RefreshRequest struct {
	RefreshMaterial  string
	UpstreamClientID string
}

RefreshRequest is one upstream refresh. UpstreamClientID is the OAuth client that owns the recorded upstream material: the facade's own client for facade-opened records, the client's own id for pre-facade records.

type RefreshTokenRecord

type RefreshTokenRecord struct {
	Token     string
	ClientID  string
	Subject   string
	Resource  string
	ExpiresAt time.Time
}

RefreshTokenRecord stores long-lived refresh token state.

type RefreshTokenStore

type RefreshTokenStore interface {
	Put(ctx context.Context, rec *RefreshTokenRecord) error
	Get(ctx context.Context, token string) (*RefreshTokenRecord, error)
	Consume(ctx context.Context, token string) (*RefreshTokenRecord, error)
	Delete(ctx context.Context, token string) error
}

RefreshTokenStore stores refresh tokens. Implementations should support rotation by Consume+Put.

type RequireBearerTokenOptions

type RequireBearerTokenOptions struct {
	// ResourceMetadataURL is used to build the RFC9728 discovery challenge.
	//
	// The middleware reads no environment variable. When this and
	// ResourceMetadataURLFunc are both empty the challenge is the bare
	// `Bearer` form with no resource_metadata parameter, so a missing
	// configuration fails closed to an unadvertised discovery endpoint rather
	// than to a value an injected MCP_ENDPOINT could influence.
	ResourceMetadataURL string

	// ResourceMetadataURLFunc returns the route-derived protected-resource
	// metadata URL for the matched route. When set it takes precedence over
	// ResourceMetadataURLTemplate and ResourceMetadataURL, so the challenge is
	// route-derived rather than family-wide.
	ResourceMetadataURLFunc func(*apptheory.Context) (string, error)

	// ResourceMetadataURLTemplate is an absolute HTTPS URL template whose
	// `{param}` placeholders are interpolated from the matched route's path
	// parameters, so a per-actor route (`/mcp/{actor}`) advertises that actor's
	// own RFC 9728 metadata URL with no hand-rolled resolver. It is explicit
	// application configuration: the middleware reads no environment variable.
	//
	// Pass the CDK construct's route-derived template straight through, for
	// example `AppTheoryRemoteMcpServer.protectedResourceMetadataUrl` on the
	// adopter's own configuration surface.
	//
	// The template is validated when the middleware is constructed — it must be
	// an absolute HTTPS URL with no userinfo, query, fragment, or percent
	// escape and no "." or ".." path segment, and its placeholders must appear
	// in the path and name route parameters. A malformed template panics at
	// registration rather than advertising a broken discovery URL, so a bad
	// template fails closed at startup instead of at request time.
	//
	// At request time a placeholder that the matched route does not supply, or
	// whose value is not a safe single path segment (see
	// isSafeResourceMetadataURLTemplateValue), makes the middleware emit the
	// bare `Bearer` challenge. It never advertises a literal placeholder.
	//
	// When set it takes precedence over ResourceMetadataURL; ResourceMetadataURLFunc
	// still takes precedence over it.
	ResourceMetadataURLTemplate string

	// Validator is called for every request. If it is omitted, or if it returns
	// an error, the request is rejected with 401.
	Validator BearerTokenValidator

	// ClaimsValidator is preferred for new code because it can distinguish
	// unauthorized token failures from forbidden audience/scope failures and
	// expose safe claims to downstream handlers.
	ClaimsValidator BearerTokenClaimsValidator

	// RequiredScopesFunc returns the route-derived scope set used to build the
	// 403 insufficient_scope remediation challenge. It is consulted only when a
	// validator reports ErrBearerTokenInsufficientScope.
	RequiredScopesFunc func(*apptheory.Context) ([]string, error)
	// contains filtered or unexported fields
}

RequireBearerTokenOptions configures RequireBearerTokenMiddleware.

type TokenClaims added in v6.1.0

type TokenClaims struct {
	Subject         string    `json:"subject"`
	Audience        string    `json:"audience"`
	Scopes          []string  `json:"scopes"`
	ExpiresAt       time.Time `json:"expires_at"`
	AuthorizedParty string    `json:"authorized_party"`
}

TokenClaims is the safe, non-secret claim subset ValidateAccessToken returns.

type TokenExpectations added in v6.1.0

type TokenExpectations struct {
	ExpectedIssuer           string
	ExpectedAudience         string
	RequiredScopes           []string
	AllowedAuthorizedParties []string
}

TokenExpectations is the caller's expectation set for access-token validation. AllowedAuthorizedParties is an exact-match allow-list for the token's azp claim (falling back to client_id); an empty list disables that check.

type TokenSet added in v6.1.0

type TokenSet struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	Scope        string `json:"scope"`
	IDToken      string `json:"id_token"`
}

TokenSet is one upstream token response. The adapter never drops an upstream field the facade may need: the access token is returned to the client byte-for-byte unmodified, while refresh material and any id_token never reach the client.

Directories

Path Synopsis
Package store defines the five pluggable store SPIs of the MCP auth facade (DESIGN §7, contract fixture facade-store-spi.v1.json) and the framework's in-memory reference implementations.
Package store defines the five pluggable store SPIs of the MCP auth facade (DESIGN §7, contract fixture facade-store-spi.v1.json) and the framework's in-memory reference implementations.

Jump to

Keyboard shortcuts

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