auth

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 29, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package auth: OIDC Back-Channel Logout 1.0 receiver.

Spec: https://openid.net/specs/openid-connect-backchannel-1_0.html

The receiver accepts the AS-initiated POST that fires when a session is revoked, validates the carried logout_token JWT, and invokes registered listeners with the (sub, sid) tuple that identifies which session ended. Application-level code (e.g., panyam/mcpkit's experimental/ext/events lib) hooks the listener to do the actual downstream work — kill webhook subscriptions, evict caches, log audit events.

The handler is deliberately separate from JWTValidator and IntrospectionValidator: BCL logout_tokens have non-overlapping claim constraints with access tokens (events claim required, nonce forbidden, sub-or-sid one-of), and BCL semantically isn't request authentication — it's a server-to-server notification on its own URL. Sharing a validator with access tokens would just produce an option bag of "is this a logout_token? then enforce these extra rules".

Mount example:

h, err := auth.NewBackChannelLogoutHandler(auth.BackChannelLogoutConfig{
    Issuer:   "http://keycloak.example.com/realms/tenant-a",
    Audience: "mcp-event-server",
    JWKSURL:  "http://keycloak.example.com/realms/tenant-a/protocol/openid-connect/certs",
})
if err != nil { ... }
h.RegisterListener(func(ctx context.Context, sub, sid string) {
    log.Printf("session ended: sub=%s sid=%s", sub, sid)
    // walk subscriptions, fire PostTerminated, etc.
})
mux.Handle("/backchannel-logout", h)

Package auth provides MCP authorization support backed by oneauth. It implements mcpkit's AuthValidator, ClaimsProvider, TokenSource, and ExtensionProvider interfaces as thin adapters over oneauth's JWT, OAuth, and discovery primitives.

This is a separate Go module (github.com/panyam/mcpkit/auth) so that the core mcpkit module stays zero-auth-deps. Import this package only when you need JWT validation, OAuth flows, or PRM endpoints.

Index

Constants

View Source
const BackChannelLogoutEventURI = "http://schemas.openid.net/event/backchannel-logout"

BackChannelLogoutEventURI is the JWT `events` claim member that every OIDC Back-Channel Logout token MUST carry (spec § 2.4).

Variables

View Source
var ErrIssMismatch = errors.New("RFC 9207 iss does not match authorization server issuer")

ErrIssMismatch is returned by the OAuth callback when the RFC 9207 `iss` query parameter is present in the authorization response but does not match the issuer identifier of the authorization server the client sent the user to.

This is the OAuth-callback analogue of an issuer-mismatch attack (RFC 9207 Mix-Up Protection): an attacker substitutes a different AS's redirect with a code that LOOKS valid (matching state, correct shape) but `iss` carries the attacker's AS identifier. Without this check the client would proceed to token exchange against the wrong AS and accept a token minted by an unintended issuer.

Wrapped with diagnostic context (PRM issuer, observed iss) so callers logging the error can see what was rejected; consumers branching on the failure mode should use errors.Is.

View Source
var ErrIssMissing = errors.New("RFC 9207 iss missing despite authorization server advertising support")

ErrIssMissing is returned when the authorization server advertised `authorization_response_iss_parameter_supported: true` in its AS metadata but the authorization response omitted the `iss` query parameter. RFC 9207 §2.4 says clients MUST treat the parameter as REQUIRED when the AS advertises support — silently accepting the omission would degrade the mix-up protection the advertisement promised.

Distinct from ErrIssMismatch so audit / log consumers can tell "advertised but absent" apart from "present but wrong" — different failure modes, different remediation.

View Source
var ErrPRMResourceMismatch = errors.New("PRM resource does not match MCP server URL")

ErrPRMResourceMismatch is returned by DiscoverMCPAuth when the Protected Resource Metadata document's `resource` field points at a URL that differs from the MCP server URL the client is connecting to.

Per RFC 8707 §2 (Resource Indicators) and the upstream `auth/resource-mismatch` conformance scenario, clients MUST validate that PRM `resource` matches the protected resource they're trying to access — otherwise a server-controlled PRM can redirect the client to an attacker-controlled authorization server that issues tokens for a different audience (a token-recipient-confusion attack).

Comparison normalizes scheme + host to lowercase (RFC 3986 §6.2.2.1) and ignores a trailing slash on the path. An empty PRM.Resource is treated as "no binding asserted" and accepted — some early PRM emitters omit the field, and rejecting outright would break working integrations.

View Source
var RegisterClient = client.RegisterClient

RegisterClient is re-exported from oneauth/client. Performs RFC 7591 Dynamic Client Registration against the given endpoint.

Functions

func DefaultClientRegistration

func DefaultClientRegistration() client.ClientRegistrationRequest

DefaultClientRegistration returns a sensible default DCR request for an MCP client. This remains in mcpkit because the defaults are MCP-specific (client name, redirect URIs, grant types, auth method, application_type).

ApplicationType is set to "native" per SEP-837: MCP clients are CLI/SDK (installed) clients, not web-server-hosted clients.

func MountAuth

func MountAuth(mux *http.ServeMux, cfg AuthConfig)

MountAuth registers the OAuth Protected Resource Metadata endpoint (RFC 9728) on the given mux. This enables MCP clients to discover the authorization server via the well-known URI.

Per MCP spec (2025-11-25): MCP servers MUST implement RFC 9728 PRM.

Usage:

mux := http.NewServeMux()
mux.Handle("/mcp", srv.Handler(core.WithStreamableHTTP(true)))
auth.MountAuth(mux, auth.AuthConfig{
    ResourceURI:          "https://mcp.example.com",
    AuthorizationServers: []string{"https://auth.example.com"},
    ScopesSupported:      []string{"tools:read", "tools:call", "admin:write"},
    MCPPath:              "/mcp",
})

func NewToolScopeMiddleware added in v0.2.41

func NewToolScopeMiddleware(lookup ToolDefLookup, opts ...ToolScopeOption) server.Middleware

NewToolScopeMiddleware returns a server middleware that enforces per-tool OAuth scopes (declared via core.ToolDef.RequiredScopes and optionally core.ToolDef.AcceptedScopes) for tools/call requests. It runs pre-dispatch and short-circuits with *core.AuthError (HTTP 403 + WWW-Authenticate: insufficient_scope) when the request's claims don't satisfy the gate.

Gate semantics:

  • RequiredScopes alone: AND — the caller must hold every listed scope.
  • AcceptedScopes non-empty: OR — the caller satisfies the gate by holding ANY scope in AcceptedScopes. Supports scope hierarchies (a parent `repo` scope satisfies a tool nominally requiring `repo:read`). AcceptedScopes is gate-only — it never appears in the 403 challenge, keeping re-auth guidance least-privilege.
  • AcceptedScopes nil or empty: falls back to the AND semantics above. The two-state is deliberate so allocating []string{} cannot silently bypass enforcement.

Per SEP-2643 (FineGrainedAuth UC2): the scope step-up is fully described by the WWW-Authenticate challenge. The body is a JSON-RPC error with the authorization-denial classification metadata only — no remediationHints, because the scopes are already in the WWW-Authenticate header.

Non-tools/call requests pass through unchanged. Unknown tools also pass through (the dispatcher returns method-not-found for those).

Example:

srv := server.NewServer(info,
    server.WithAuth(jwtValidator),
    server.WithMiddleware(auth.NewToolScopeMiddleware(srv.Registry(),
        auth.WithIncludeGrantedScopes(true), // optional
    )),
)
srv.RegisterTool(core.ToolDef{
    Name:           "update_doc",
    RequiredScopes: []string{"docs:write"},
    AcceptedScopes: []string{"docs:write", "docs"}, // optional OR hierarchy
}, handler)

func ParseWWWAuthenticate

func ParseWWWAuthenticate(header string) (resourceMetadata string, scopes []string, err error)

ParseWWWAuthenticate extracts the resource_metadata URL and scopes from a WWW-Authenticate: Bearer header value. Used by MCP clients to discover the PRM endpoint after receiving a 401.

Per spec: clients MUST use resource_metadata from WWW-Authenticate when present.

Delegates to core.ParseWWWAuthenticate (core module) — the parser lives in core so the client transport can use it without depending on the auth sub-module.

func RequireAllScopes

func RequireAllScopes(ctx context.Context, scopes ...string) error

RequireAllScopes checks that all given scopes are present in the context's claims.

func RequireScope

func RequireScope(ctx context.Context, scope string) error

RequireScope checks if the context's authenticated claims include the given scope. Returns nil if the scope is present, or an *core.AuthError with HTTP 403 and a WWW-Authenticate header with error="insufficient_scope" if missing.

Per MCP spec (2025-11-25): servers SHOULD respond with 403 and WWW-Authenticate when a client has a valid token but insufficient scopes.

Usage in a tool handler:

func adminTool(ctx core.ToolContext, req core.ToolRequest) (core.ToolResponse, error) {
    if err := auth.RequireScope(ctx, "admin:write"); err != nil {
        return core.ErrorResult(err.Error()), nil
    }
    // ... admin operation ...
}

func UnionScopes added in v0.2.47

func UnionScopes(a, b []string) []string

UnionScopes returns the set union of a and b preserving first-seen order: every element of a comes first in its original order, then every element of b that did not already appear in a. Duplicates within either input are collapsed. Returns nil when both inputs are empty so callers can treat the "no scopes anywhere" case uniformly. The returned slice is always a fresh allocation — callers may safely retain and mutate it without affecting the inputs.

Primarily used by NewToolScopeMiddleware to compose granted-and-required scope sets for the WWW-Authenticate challenge under WithIncludeGrantedScopes. Custom middleware that builds its own scope challenges can use it directly.

func ValidatePKCES256

func ValidatePKCES256(meta *client.ASMetadata) error

ValidatePKCES256 checks that the AS supports PKCE with S256 (C11/C12). Per MCP spec: clients MUST verify code_challenge_methods_supported includes "S256", and MUST refuse to proceed if it is absent.

func WWWAuth401

func WWWAuth401(resourceMetadataURL string, scopes ...string) string

WWWAuth401 builds a WWW-Authenticate header value for 401 Unauthorized responses. Per MCP spec (2025-11-25): includes resource_metadata URL and optional scopes.

Example output:

Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="tools:read admin:write"

func WWWAuth403

func WWWAuth403(resourceMetadataURL string, scopes ...string) string

WWWAuth403 builds a WWW-Authenticate header value for 403 Forbidden responses with insufficient scope. Per MCP spec (2025-11-25) and RFC 6750 §3.1, includes the `error="insufficient_scope"` token plus the required scopes, and optionally the `resource_metadata="<URL>"` link to the server's RFC 9728 PRM document so callers don't have to fall back to the well-known path to discover it.

resourceMetadataURL empty omits the `resource_metadata` segment entirely. This is symmetric with WWWAuth401, which always emits the link, and matches the shape modelcontextprotocol/typescript-sdk PR 1624 (the SEP-2350 server-side reference impl) emits on its own scope-challenge 403s — see RFC 9728 §5.1 for the discovery semantics this enables.

Stateless-wire 403s are the load-bearing case: there is no preceding 401 to learn the PRM link from, so the very first request that lands a 403 needs the link inline to discover the AS.

Example output:

Bearer error="insufficient_scope", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="admin:write files:read"

Types

type AuthConfig

type AuthConfig struct {
	// ResourceURI is the canonical URI of this MCP server (RFC 8707 resource indicator).
	ResourceURI string

	// AuthorizationServers lists the authorization server URLs this server trusts.
	// At least one is required per RFC 9728.
	AuthorizationServers []string

	// ScopesSupported lists the OAuth scopes this server understands.
	ScopesSupported []string

	// MCPPath is the MCP endpoint path (e.g., "/mcp"). Used to construct
	// the well-known URI: /.well-known/oauth-protected-resource/<mcpPath>
	MCPPath string

	// Validator, if set, is automatically wired with ScopesSupported as its
	// AllScopes field (if not already populated). This ensures that 401
	// WWW-Authenticate responses include the full scope list, allowing clients
	// to request broad scopes upfront and avoid scope step-up round-trips.
	//
	// Without this wiring, Validator.AllScopes defaults to empty and 401
	// responses omit the scope= parameter, forcing clients through a
	// narrow-to-broad scope dance. See #50.
	Validator *JWTValidator
}

AuthConfig configures the OAuth well-known endpoints for an MCP server.

type AuthExtension

type AuthExtension struct{}

AuthExtension declares the MCP auth extension with its spec version and stability level. Register it on the server to advertise auth support in the initialize response.

Usage:

srv := core.NewServer(info,
    core.WithAuth(jwtValidator),
    core.WithExtension(auth.AuthExtension{}),
)

func (AuthExtension) Extension

func (AuthExtension) Extension() core.Extension

Extension returns the MCP auth extension metadata.

type BackChannelLogoutConfig added in v0.2.47

type BackChannelLogoutConfig struct {
	// Issuer is the expected `iss` claim value (the AS issuer URL).
	// Required. Mirrors the same iss the AS publishes on its
	// /.well-known/openid-configuration document.
	Issuer string

	// Audience is the expected `aud` claim value — typically the
	// resource server's client_id registered with the AS.
	// Required. The spec mandates aud validation against the
	// registered client_id (§ 2.6).
	Audience string

	// JWKSURL is the AS JWKS endpoint used to fetch the signing
	// key for the logout_token via its kid header. Required.
	// Wrapped in oneauth's JWKSKeyStore for cached lookups.
	JWKSURL string

	// JTIStore stores seen `jti` values for replay protection.
	// Default: a fresh MemoryJTIStore. Multi-replica deployments
	// should swap for a Redis / SQL backed impl so a replay seen
	// on replica A is also rejected on replica B.
	JTIStore JTIStore

	// ReplayWindow is the TTL applied to recorded jtis — how long
	// a jti remains visible to JTIStore.Seen. Default 10 minutes.
	// Set this to at least 2x the AS's max-clock-skew tolerance so
	// a delayed-replay attack can't slip past the eviction.
	ReplayWindow time.Duration

	// AllowedClockSkew is the leeway applied to exp / iat / nbf
	// checks during JWT validation. Default 60s. The spec is silent
	// on a concrete value; a minute is the common floor across OIDC
	// libraries.
	AllowedClockSkew time.Duration

	// Now returns the current time. Defaults to time.Now. Tests
	// inject a fixed clock so exp / iat / replay-window assertions
	// are deterministic.
	Now func() time.Time
}

BackChannelLogoutConfig configures the receiver. Required fields have a "required" callout; the rest pick defaults that match the spec's MUST window plus a small clock-skew leeway.

type BackChannelLogoutHandler added in v0.2.47

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

BackChannelLogoutHandler implements http.Handler for the OIDC Back-Channel Logout 1.0 receiver endpoint. Construct via NewBackChannelLogoutHandler; mount on any http.ServeMux at the path the AS was configured to POST to.

func NewBackChannelLogoutHandler added in v0.2.47

func NewBackChannelLogoutHandler(cfg BackChannelLogoutConfig) (*BackChannelLogoutHandler, error)

NewBackChannelLogoutHandler constructs a receiver from cfg. Returns an error when a required field is missing or empty.

func (*BackChannelLogoutHandler) RegisterListener added in v0.2.47

func (h *BackChannelLogoutHandler) RegisterListener(fn LogoutListener)

RegisterListener appends a listener invoked for every valid logout_token. Listeners run synchronously in registration order; short callbacks only, or hand off to a goroutine.

func (*BackChannelLogoutHandler) ServeHTTP added in v0.2.47

ServeHTTP implements the OIDC BCL POST endpoint per spec § 2.7. The handler returns:

  • 200 OK with `Cache-Control: no-store` on successful validation
  • 400 Bad Request with a categorical reason on any validation failure
  • 405 Method Not Allowed on non-POST methods

The 400 response body is a JSON object {"error":"..."} where the error string identifies which check failed — useful for operators debugging an AS misconfiguration without leaking secrets.

type ClientCredentialsSource

type ClientCredentialsSource = client.ClientCredentialsSource

Type aliases re-exported from oneauth/client for backward compatibility. These types were moved to oneauth as part of mcpkit#158 (generic OAuth pushdown).

type ClientCredentialsTokenSource added in v0.2.47

type ClientCredentialsTokenSource struct {
	// ServerURL is the MCP server URL (used for PRM discovery).
	ServerURL string

	// ClientID identifies the client at the AS. Required.
	ClientID string

	// ClientSecret authenticates the client via HTTP Basic / form POST.
	// Mutually exclusive with PrivateKeyPEM.
	ClientSecret string

	// PrivateKeyPEM is the PKCS#8 PEM-encoded private key used to mint
	// the client_assertion JWT. Mutually exclusive with ClientSecret.
	// Must match the public key the AS associates with ClientID.
	PrivateKeyPEM string

	// SigningAlgorithm is the JWS alg for the assertion (e.g. "ES256", "RS256").
	// Required when PrivateKeyPEM is set. Must match the AS's registered
	// signing_alg for this client.
	SigningAlgorithm string

	// KeyID, when non-empty, is emitted as the JWS `kid` header so the AS
	// can pick the right registered key. Optional.
	KeyID string

	// Scopes to request. If empty, inherits from PRM scopes_supported.
	Scopes []string

	// HTTPClient overrides the discovery and token-request HTTP client.
	// Useful in tests with httptest.Server.
	HTTPClient *http.Client

	// AllowInsecure permits http:// AS endpoints. Required for the
	// conformance mock AS; production deployments should leave this false
	// so AS HTTPS is enforced.
	AllowInsecure bool

	// ASMetadataStore caches AS metadata across discovery calls when
	// multiple sources target the same AS.
	ASMetadataStore client.ASMetadataStore

	// OnToken fires after a successful token fetch by the underlying
	// ClientCredentialsSource. Use to persist the credential outside the
	// in-memory cache.
	OnToken func(*client.ServerCredential)
	// contains filtered or unexported fields
}

ClientCredentialsTokenSource implements core.TokenSource for the OAuth 2.0 client_credentials grant (RFC 6749 §4.4, SEP-1046). Use when the MCP client is a backend service authenticating with its own identity rather than acting on behalf of a human user. The flow has no browser, no consent screen, and no refresh_token: tokens are minted directly by the AS in exchange for the client's credentials and refetched on expiry.

Two client-authentication variants, selected by which field the caller sets:

  • ClientSecret (client_secret_basic / client_secret_post per RFC 6749 §2.3.1)
  • PrivateKeyPEM (private_key_jwt per RFC 7523 §2.2)

PRM discovery, AS metadata fetch, and token caching are inherited from oneauth's ClientCredentialsSource; this type adds the MCP-specific glue (PRM → AS resolution, scope selection, issuer-as-audience for the JWT assertion per RFC 7523bis).

func (*ClientCredentialsTokenSource) Token added in v0.2.47

Token returns a cached access token if still valid, or fetches a fresh one via the configured client_credentials variant. Implements core.TokenSource.

func (*ClientCredentialsTokenSource) TokenForScopes added in v0.2.47

func (s *ClientCredentialsTokenSource) TokenForScopes(scopes []string) (string, error)

TokenForScopes invalidates the cached token, merges the requested scopes with the existing set, and fetches a fresh token. Implements core.ScopeAwareTokenSource so the mcpkit client transport can step up scope on a 403 with insufficient_scope.

type ClientRegistrationRequest

type ClientRegistrationRequest = client.ClientRegistrationRequest

Type aliases re-exported from oneauth/client for backward compatibility. The DCR types and RegisterClient function were moved to oneauth as part of mcpkit#158 (generic OAuth pushdown). MCP-specific defaults remain here.

type ClientRegistrationResponse

type ClientRegistrationResponse = client.ClientRegistrationResponse

type DiscoverOption

type DiscoverOption func(*discoverConfig)

DiscoverOption configures DiscoverMCPAuth.

func WithASMetadataStore added in v0.1.22

func WithASMetadataStore(s client.ASMetadataStore) DiscoverOption

WithASMetadataStore enables caching of authorization server metadata across multiple discovery calls. When multiple MCP servers share the same authorization server, the first discovery fetches from the network and subsequent discoveries hit the cache.

Typical usage: share a single store across all OAuthTokenSource instances in a process that connects to multiple MCP servers behind one IdP.

cache := client.NewMemoryASMetadataStore(0) // default TTL 1h
info1, _ := auth.DiscoverMCPAuth(url1, auth.WithASMetadataStore(cache))
info2, _ := auth.DiscoverMCPAuth(url2, auth.WithASMetadataStore(cache))
// If url1 and url2 share an AS, info2 hits the cache — no second fetch.

func WithHTTPClient

func WithHTTPClient(c *http.Client) DiscoverOption

WithHTTPClient sets a custom HTTP client for discovery requests. Use in tests with httptest.Server to avoid real network calls.

type EnterpriseManagedTokenSource added in v0.2.47

type EnterpriseManagedTokenSource struct {
	// ServerURL is the MCP server URL. Used for PRM discovery and as the
	// `resource` form value on the IdP token exchange.
	ServerURL string

	// ClientID identifies this MCP client at the MCP authorization server.
	// Required.
	ClientID string

	// ClientSecret authenticates the MCP client at the AS via
	// `client_secret_basic` (RFC 6749 §2.3.1) on the jwt-bearer grant
	// request. Required.
	ClientSecret string

	// IdpClientID is the client identity sent on the IdP token exchange.
	// SEP-990 §6.1 notes the IdP "needs to be aware of the MCP Client's
	// client_id that it normally uses with the MCP Server"; this field
	// names the client at the IdP, which may differ from ClientID above.
	// Optional — empty falls back to the unauthenticated token-exchange
	// path (AuthMethodNone), which the conformance fixture accepts.
	IdpClientID string

	// IdpIDToken is the signed ID token issued by the IdP for the user.
	// Required. Sent as the RFC 8693 `subject_token`.
	IdpIDToken string

	// IdpTokenEndpoint is the URL of the IdP token endpoint that performs
	// the RFC 8693 token exchange. Required. The conformance scenario
	// provides this directly in MCP_CONFORMANCE_CONTEXT rather than
	// requiring a separate IdP discovery step.
	IdpTokenEndpoint string

	// AllowInsecure permits http:// AS endpoints. The conformance mock AS
	// uses HTTP; production deployments leave this false.
	AllowInsecure bool

	// HTTPClient overrides the discovery and token-request HTTP client.
	HTTPClient *http.Client

	// ASMetadataStore caches AS metadata across discovery calls.
	ASMetadataStore client.ASMetadataStore
	// contains filtered or unexported fields
}

EnterpriseManagedTokenSource implements core.TokenSource for the SEP-990 enterprise-managed authorization flow. The MCP client presents an upstream IdP-issued ID token, exchanges it for an ID-JAG via RFC 8693 at the IdP, then redeems the ID-JAG at the MCP authorization server via RFC 7523 §2.1 (jwt-bearer grant) using `client_secret_basic` for client authentication.

This is the two-stage chain in plain terms:

IdP /token   (RFC 8693)   id_token → id-jag
AS  /token   (RFC 7523)   id-jag   → MCP access token

The first stage is "the IdP vouches for the user". The second stage is "the AS vouches for the MCP client acting on the user's behalf at the MCP server." See:

https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/enterprise-managed-authorization.mdx

Token caching is keyed on the MCP access token's expiry. When the access token expires the source re-runs the full two-stage chain — the ID-JAG is single-use so we never cache it across calls.

func (*EnterpriseManagedTokenSource) Token added in v0.2.47

Token returns a cached access token if still valid, or runs the full two-stage chain (token exchange at IdP → jwt-bearer grant at AS). Implements core.TokenSource.

func (*EnterpriseManagedTokenSource) TokenForScopes added in v0.2.47

func (s *EnterpriseManagedTokenSource) TokenForScopes(_ []string) (string, error)

TokenForScopes invalidates the cached access token and re-runs the chain. SEP-990 doesn't have a scope step-up story today; the implementation re-runs the same flow because the AS may issue a token with different scope based on the (already-issued) ID-JAG.

type IntrospectionConfig added in v0.2.47

type IntrospectionConfig struct {
	// IntrospectionURL is the OAuth 2.0 Token Introspection endpoint
	// (RFC 7662). Required.
	IntrospectionURL string

	// ClientID and ClientSecret authenticate this resource server to
	// the introspection endpoint via HTTP Basic auth
	// (client_secret_basic). Both required.
	ClientID     string
	ClientSecret string

	// HTTPClient overrides the HTTP client used for introspection
	// requests. Nil means http.DefaultClient. Set this when a deployment
	// needs custom timeouts, mTLS, or to point at a sidecar proxy.
	HTTPClient *http.Client

	// CacheTTL bounds the staleness window for introspection responses.
	// A revoked token MAY remain "active" in the cache for up to
	// CacheTTL after the authorization server actually revokes it —
	// this is the load-bearing knob for the prod-events walkthrough's
	// "token revocation as auth-visibility step" demo. Default 0 (no
	// caching, every request hits the AS). Recommended 30s.
	CacheTTL time.Duration

	// ResourceMetadataURL is included in WWW-Authenticate headers on
	// 401 responses (RFC 9728). Same field as on JWTConfig.
	ResourceMetadataURL string

	// RequiredScopes are checked on every request (global minimum).
	// 403 + WWW-Authenticate scope hint on missing scopes.
	RequiredScopes []string

	// AllScopes is the complete set of scopes this server supports.
	// Included in WWW-Authenticate 401 headers for client scope
	// selection — same shape as JWTConfig.AllScopes.
	AllScopes []string

	// TenantMapper extracts (tenant, subject) from the introspection
	// response. nil means the default realm-from-issuer-URL mapper —
	// the right choice for any Keycloak deployment whose realm =
	// tenant. Override when a deployment maps tenant from a custom
	// claim, but note that oneauth's IntrospectionResult today carries
	// only RFC 7662 well-known fields; custom-claim mapping requires
	// extending oneauth (tracked as a future enhancement).
	TenantMapper TenantMapper
}

IntrospectionConfig configures an IntrospectionValidator. Construction goes through NewIntrospectionValidator — the zero value is not safe (no endpoint, no credentials).

type IntrospectionValidator added in v0.2.47

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

IntrospectionValidator validates MCP requests using bearer tokens against an RFC 7662 OAuth 2.0 Token Introspection endpoint. It implements mcpcore.AuthValidator and mcpcore.ClaimsProvider by wrapping oneauth's IntrospectionValidator (which owns the wire format and the introspection-response cache) and mapping the result into mcpkit's core.Claims shape.

Use IntrospectionValidator instead of JWTValidator when:

  • Token revocation must be visible synchronously (introspection's cache TTL bounds revocation lag; JWT/JWKS validation can't see revocation until the token expires).
  • The resource server cannot reach the JWKS endpoint but can reach a server-to-server introspection endpoint authenticated with client credentials.
  • The authorization server issues opaque (non-JWT) tokens.

Tenant encoding: the returned core.Claims carries Subject and Tenant as separate typed fields. Subject is the raw OAuth sub; Tenant is the realm parsed from the introspection response's iss claim (Keycloak's .../realms/<realm> URL shape) when the default mapper is used, or whatever the configured TenantMapper produces. Consumers that need a string-form principal (session-binding, webhook canonical-key) call auth.PrincipalFor(claims) — the single encoder that handles the "<tenant>/<sub>" case and the "no-tenant → bare-subject" fallback.

Usage:

validator := auth.NewIntrospectionValidator(auth.IntrospectionConfig{
    IntrospectionURL: "https://auth.example.com/oauth/introspect",
    ClientID:         "mcp-resource-server",
    ClientSecret:     os.Getenv("OAUTH_CLIENT_SECRET"),
    CacheTTL:         30 * time.Second,
})
srv := mcp.NewServer(info, mcp.WithAuth(validator))

func NewIntrospectionValidator added in v0.2.47

func NewIntrospectionValidator(cfg IntrospectionConfig) *IntrospectionValidator

NewIntrospectionValidator constructs a validator backed by oneauth's IntrospectionValidator. Required fields (IntrospectionURL, ClientID, ClientSecret) MUST be set on cfg; this constructor does not panic on missing fields — the resulting validator will reject every request with a 401 because the underlying introspection call fails. Callers SHOULD validate cfg before construction.

func (*IntrospectionValidator) Claims added in v0.2.47

Claims implements mcpcore.ClaimsProvider. Returns the claims that the most recent Validate(r) call stashed for the same bearer token, or nil if there is no such record. Like JWTValidator.Claims, this is a one-shot read: LoadAndDelete drops the entry so the next per-request lookup misses (the validator's recentClaims is not a persistent cache — that's the introspection layer's job).

func (*IntrospectionValidator) Validate added in v0.2.47

func (v *IntrospectionValidator) Validate(r *http.Request) error

Validate implements mcpcore.AuthValidator. Extracts the bearer token, calls the introspection endpoint via oneauth (honors the configured CacheTTL), maps the response into core.Claims using IntrospectionConfig.TenantMapper, and checks RequiredScopes. Stashes the claims for Claims(r) to retrieve on the same request.

Error semantics mirror JWTValidator: 401 + WWW-Authenticate for every authentication failure (missing header, inactive token, network error, missing subject); 403 + scope-hint for insufficient scope.

type JTIStore added in v0.2.47

type JTIStore interface {
	// Seen returns whether the jti has been recorded within its TTL
	// window. Used by the BCL receiver to reject replays. Spec §2.6:
	// "If the Logout Token has been seen before, [the receiver] MUST
	// reject the request as a replay."
	Seen(ctx context.Context, req SeenRequest) (SeenResponse, error)

	// Record stores a jti with the given TTL. Re-recording an existing
	// jti is allowed and resets its TTL — the spec does not require
	// monotonic-only recording, and forcing it would complicate the
	// in-memory impl for no observable gain.
	Record(ctx context.Context, req RecordRequest) (RecordResponse, error)
}

JTIStore is the storage seam behind logout_token replay protection. OIDC Back-Channel Logout 1.0 § 2.6 says the receiver "MUST verify that no `jti` claim in the [Logout] Token has been seen before". Implementations must therefore retain seen jtis at least as long as the spec-allowed replay window (the spec is silent on a concrete window; we default to twice the max permitted clock skew, which is the same minimum-floor every BCL impl seems to converge on).

gRPC-style request/response signatures so the in-memory default in this file can be swapped for a Redis / SQL / shared-cache impl without changing call sites. Forward-compatible with a future remote JTIStore that wraps a gRPC client — ctx threads cancellation and trace context to the storage backend.

type JWTConfig

type JWTConfig struct {
	// JWKSURL is the authorization server's JWKS endpoint for key discovery.
	JWKSURL string

	// Issuer is the expected "iss" claim in tokens.
	Issuer string

	// Audience is the expected "aud" claim — this MCP server's canonical URI (RFC 8707).
	Audience string

	// ResourceMetadataURL is the URL of this server's Protected Resource Metadata
	// document (RFC 9728). Included in WWW-Authenticate headers.
	ResourceMetadataURL string

	// RequiredScopes are checked on every request (global minimum).
	RequiredScopes []string

	// AllScopes is the complete set of scopes this server supports.
	// Included in WWW-Authenticate 401 headers for client scope selection.
	AllScopes []string

	// TracerProvider optionally enables SEP-414 instrumentation of
	// token validation: a sub-span per JWKS key lookup plus
	// mcp.auth.* attributes (method / subject / issuer / scopes /
	// cache_hit) on the active dispatch span set up by
	// server.WithTracerProvider. Nil (or core.NoopTracerProvider{})
	// is the default — zero spans, zero attributes, zero allocation
	// added to the validation path. ext/auth depends on the core
	// abstraction only for its own spans.
	TracerProvider mcpcore.TracerProvider

	// OneauthTracerProvider opts oneauth's internal work (JWKS
	// HTTP fetch, refresh, signature verify, etc.) into emitting
	// its own spans, threading them through oneauth v0.1.14's
	// tracing surface via keys.WithTracerProvider on the
	// JWKSKeyStore. Nil = oneauth-internal work stays opaque (the
	// pre-v0.1.14 behavior); ext/auth's own auth.jwks_lookup span
	// still emits if TracerProvider above is set, just without
	// child spans showing oneauth's internal HTTP / parsing work.
	//
	// Pass commonotel.UnderlyingOTelTP(tp) when you want oneauth's
	// spans to share the same OTel pipeline as the rest of the
	// process — the typical production setup.
	//
	// Type is the OTel SDK's trace.TracerProvider directly (not
	// mcpkit's core.TracerProvider) because oneauth's API takes the
	// SDK type, and ext/auth already pulls OTel transitively
	// through oneauth v0.1.14.
	OneauthTracerProvider oteltrace.TracerProvider
}

JWTConfig configures a JWTValidator.

type JWTValidator

type JWTValidator struct {

	// ResourceMetadataURL is included in WWW-Authenticate headers on 401 responses.
	ResourceMetadataURL string

	// RequiredScopes are checked on every request. Empty means no global scope requirement.
	RequiredScopes []string

	// AllScopes is included in WWW-Authenticate 401 headers to guide client scope selection.
	// Per spec: clients use this to request scopes upfront, reducing step-up round-trips.
	AllScopes []string

	// CacheTTL enables a validated-token cache when > 0. Tokens validated
	// within the TTL window return cached claims without re-verifying the
	// JWT signature. This avoids redundant crypto during LLM agent loops
	// making rapid sequential tool calls with the same token.
	// Default: 0 (no caching). Recommended: 30s-60s.
	CacheTTL time.Duration

	// CacheMaxSize limits the number of cached tokens to prevent memory
	// growth under token flooding. When the cache exceeds this size, new
	// tokens are validated but not cached. Default: 1000.
	// Future: consider hashicorp/golang-lru for proper LRU eviction.
	CacheMaxSize int
	// contains filtered or unexported fields
}

JWTValidator validates MCP requests using JWT Bearer tokens. It resolves verification keys from a JWKS endpoint by kid and applies issuer / audience / scope checks before stamping the claims on the active dispatch span.

We resolve keys + verify the JWT directly here (rather than going through oneauth's TokenValidator) because the validation surface MCP wants is narrow and kid-based: a single JWKS, a fixed expected issuer + audience, and the scope-array vs space-delimited-string fallback Keycloak emits. The oneauth dependency stays scoped to the JWKS key store + utility helpers.

Usage:

validator := auth.NewJWTValidator(auth.JWTConfig{
    JWKSURL:             "https://auth.example.com/.well-known/jwks.json",
    Issuer:              "https://auth.example.com",
    Audience:            "https://mcp.example.com",
    ResourceMetadataURL: "https://mcp.example.com/.well-known/oauth-protected-resource/mcp",
})
srv := mcpcore.NewServer(info, core.WithAuth(validator))

func NewJWTValidator

func NewJWTValidator(cfg JWTConfig) *JWTValidator

NewJWTValidator creates a JWTValidator backed by oneauth's JWT validation and JWKS key discovery.

func (*JWTValidator) Claims

func (v *JWTValidator) Claims(r *http.Request) *mcpcore.Claims

Claims implements mcpmcpcore.ClaimsProvider. Returns the claims parsed during the most recent Validate call for the same token.

func (*JWTValidator) Start

func (v *JWTValidator) Start()

Start begins background JWKS key refresh. Call this once at startup.

func (*JWTValidator) Stop

func (v *JWTValidator) Stop()

Stop halts background JWKS key refresh.

func (*JWTValidator) Validate

func (v *JWTValidator) Validate(r *http.Request) error

Validate implements mcpcore.AuthValidator. Extracts the Bearer token, validates signature/issuer/audience/expiry via oneauth, checks required scopes, and stashes parsed claims for Claims() to read.

type LogoutListener added in v0.2.47

type LogoutListener func(ctx context.Context, sub, sid string)

LogoutListener is invoked synchronously after a logout_token validates. sub is the OIDC subject claim (may be empty if the token only carried sid); sid is the OIDC session ID claim (may be empty if the token only carried sub). At least one is guaranteed non-empty by the spec-mandated sub-or-sid check in ServeHTTP.

ctx is the request context; cancellation propagates through it. Listeners that do expensive work should spawn their own goroutine — the receiver does not impose a deadline, but slow listeners extend the AS-side BCL POST latency, which can lead to AS retries or alerts depending on the AS implementation.

type MCPAuthInfo

type MCPAuthInfo struct {
	// ResourceMetadataURL is the PRM endpoint URL (from WWW-Authenticate or well-known).
	ResourceMetadataURL string

	// PRM is the parsed Protected Resource Metadata document.
	PRM *ProtectedResourceMetadata

	// AuthorizationServers from the PRM document.
	AuthorizationServers []string

	// ASMetadata is the discovered authorization server metadata.
	ASMetadata *client.ASMetadata
}

MCPAuthInfo holds the combined discovery results for an MCP server's auth configuration.

func DiscoverMCPAuth

func DiscoverMCPAuth(serverURL string, opts ...DiscoverOption) (*MCPAuthInfo, error)

DiscoverMCPAuth performs the MCP-specific discovery chain:

  1. Send unauthenticated request to serverURL, expect 401
  2. Parse WWW-Authenticate header for resource_metadata and scope
  3. If no resource_metadata in header, try well-known URIs
  4. Fetch PRM document
  5. Extract authorization_servers and scopes_supported
  6. Discover AS metadata via oneauth DiscoverAS (RFC 8414 + OIDC fallback)

Per MCP spec (2025-11-25): clients MUST support both WWW-Authenticate and well-known URI discovery. Must use resource_metadata from header when present.

type MemoryCredentialStore added in v0.1.24

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

MemoryCredentialStore is a simple in-memory implementation of client.CredentialStore. Used as the default backing store for OAuthTokenSource when no external CredStore is provided, so the refresh_token grant path (AuthClient.GetToken -> refreshTokenLocked) has a place to read the current credential from.

Not suitable for cross-process or cross-restart persistence. For those, consumers should provide their own CredStore (e.g., a file-backed one).

Zero value is ready to use. NewMemoryCredentialStore returns an equivalent initialized value for callers who prefer explicit construction.

func NewMemoryCredentialStore added in v0.1.24

func NewMemoryCredentialStore() *MemoryCredentialStore

NewMemoryCredentialStore returns a new, empty MemoryCredentialStore.

func (*MemoryCredentialStore) GetCredential added in v0.1.24

func (s *MemoryCredentialStore) GetCredential(serverURL string) (*client.ServerCredential, error)

GetCredential returns the stored credential for serverURL, or nil if none. Returns a copy so callers cannot mutate cached state through the pointer.

func (*MemoryCredentialStore) ListServers added in v0.1.24

func (s *MemoryCredentialStore) ListServers() ([]string, error)

ListServers returns the set of server URLs with stored credentials.

func (*MemoryCredentialStore) RemoveCredential added in v0.1.24

func (s *MemoryCredentialStore) RemoveCredential(serverURL string) error

RemoveCredential deletes the stored credential for serverURL. No-op if no entry exists.

func (*MemoryCredentialStore) Save added in v0.1.24

func (s *MemoryCredentialStore) Save() error

Save is a no-op for in-memory storage.

func (*MemoryCredentialStore) SetCredential added in v0.1.24

func (s *MemoryCredentialStore) SetCredential(serverURL string, cred *client.ServerCredential) error

SetCredential stores the credential for serverURL, overwriting any previous entry. Stores a copy so the caller cannot mutate cached state.

type MemoryJTIStore added in v0.2.47

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

MemoryJTIStore is the default in-memory JTIStore implementation — a map of jti → expiry, swept lazily on each Seen / Record call. Suitable for single-process deployments and tests. Multi-replica setups should swap for a Redis / shared-cache impl so a replay caught on replica A is also rejected on replica B.

Thread-safe via an internal RWMutex. Lazy sweep keeps memory bounded to the rate of unique jtis arriving during the active TTL window — there is no background goroutine, which keeps the store lifecycle equal to the receiver's lifecycle (no shutdown dance required).

func NewMemoryJTIStore added in v0.2.47

func NewMemoryJTIStore() *MemoryJTIStore

NewMemoryJTIStore constructs a fresh in-memory JTIStore.

func (*MemoryJTIStore) Record added in v0.2.47

Record persists the jti with the given TTL. Re-recording is allowed; the new TTL replaces the old one.

func (*MemoryJTIStore) Seen added in v0.2.47

Seen reports whether the jti exists and is unexpired. Lazily evicts an entry that is past its TTL so the next Record call for the same jti starts a fresh window.

type OAuthTokenSource

type OAuthTokenSource struct {
	// ServerURL is the MCP server URL (used for PRM discovery and as resource indicator).
	ServerURL string

	// ClientID for pre-registered clients. Takes priority over CIMD and DCR.
	ClientID string

	// ClientSecret for confidential clients (empty for public clients).
	ClientSecret string

	// ClientMetadataURL is a CIMD URL (draft-ietf-oauth-client-id-metadata-document).
	// When set and no ClientID is provided, this URL is used as the client_id.
	// Per MCP spec: SHOULD support Client ID Metadata Documents.
	ClientMetadataURL string

	// EnableDCR enables Dynamic Client Registration (RFC 7591) as a fallback
	// when no ClientID or ClientMetadataURL is set. Per MCP spec: MAY support DCR.
	EnableDCR bool

	// DCRMeta overrides the default DCR registration request.
	// If nil, DefaultClientRegistration() is used.
	DCRMeta *ClientRegistrationRequest

	// Scopes to request. Leave empty for the default lazy behavior: the
	// source acquires no token until a server 401/403 selects the scope via
	// WWW-Authenticate, then uses that (falling back to PRM scopes_supported
	// only when the challenge carries no scope=). Setting Scopes explicitly
	// pins them and acquires eagerly on the first Token call. TokenForScopes
	// merges challenge scopes into this set as step-up occurs.
	Scopes []string

	// CredStore persists tokens across sessions (optional).
	CredStore client.CredentialStore

	// ASMetadataStore caches authorization server metadata across discovery
	// calls. Share a single store across multiple OAuthTokenSource instances
	// connecting to MCP servers behind the same AS to avoid redundant
	// discovery fetches. Optional.
	ASMetadataStore client.ASMetadataStore

	// OnToken is an optional callback invoked after a successful token
	// acquisition or refresh by the underlying oneauth AuthClient. Use
	// this to persist tokens to an external store (disk, database, secret
	// manager) without implementing a full CredentialStore.
	//
	// Fires ONLY for the refresh_token grant path in the underlying
	// AuthClient — not for initial LoginWithBrowser calls in today's
	// OAuthTokenSource.Token() flow (which re-runs the full browser flow
	// instead of using refresh tokens). This is a latent capability until
	// the browser-login flow learns to use refresh tokens (follow-up).
	//
	// Thread safety: the callback is invoked synchronously while the
	// underlying AuthClient mutex is held (same contract as
	// CredentialStore.SetCredential). Callbacks must not re-enter
	// AuthClient or OAuthTokenSource methods, or they will deadlock.
	//
	// See oneauth#82 for the underlying pushdown. Issue #137.
	OnToken func(*client.ServerCredential)

	// OpenBrowser opens the authorization URL (nil = platform default).
	OpenBrowser func(url string) error

	// HTTPClient is used for discovery and DCR requests.
	// If nil, http.DefaultClient is used.
	HTTPClient *http.Client

	// AllowInsecure skips HTTPS enforcement on AS endpoints.
	// Only set this for local development/testing.
	AllowInsecure bool
	// contains filtered or unexported fields
}

OAuthTokenSource implements core.TokenSource using the full MCP OAuth flow: PRM discovery → AS metadata → PKCE authorization code → token.

Per MCP spec (2025-11-25): clients MUST implement PKCE with S256, MUST include resource parameter, MUST verify PKCE support in AS metadata.

Acquisition is lazy: the source defers the OAuth flow until a server challenge selects the scope (see Token). The flow, once a challenge arms the source, is:

  1. Fetch Protected Resource Metadata (PRM, RFC 9728)
  2. Discover Authorization Server metadata (RFC 8414 / OIDC)
  3. Validate PKCE S256 support
  4. Resolve client identity (pre-registered → CIMD → DCR)
  5. Run PKCE authorization code flow via oneauth LoginWithBrowser

func (*OAuthTokenSource) Close added in v0.1.22

func (s *OAuthTokenSource) Close() error

Close stops any background goroutines started by underlying token sources (e.g., proactive refresh in ClientCredentialsSource). Safe to call multiple times. Implements io.Closer.

After Close, subsequent Token() calls still work — they fall back to reactive refresh. Close is typically called from the owning Client.Close() on shutdown.

func (*OAuthTokenSource) Invalidate added in v0.2.47

func (s *OAuthTokenSource) Invalidate()

Invalidate implements core.InvalidatingTokenSource. It drops cached discovery, the cached access token, the AuthClient handle, and the in-memory credential store entry, so the next Token call re-runs the full discovery + auth flow. DoWithAuthRetry calls this before re-issuing Token on a 401 — necessary for SEP-2352 AS-change re-discovery to actually fire, since the cached authInfo would otherwise mask the AS swap.

DCR client credentials are intentionally NOT cleared here. resolveClientID handles that lazily, comparing the current AS against the cached dcrAS and dropping the cache only on mismatch. This keeps Invalidate cheap for the common case (same AS, fresh token needed) while still meeting SEP-2352 when the AS truly changes.

The armed flag (Token's lazy gate) is intentionally NOT reset: the transport calls Invalidate then TokenForScopes on a 401, and re-arming would be redundant, but more importantly an already-armed source must stay armed across a step-up retry so the re-issued Token acquires rather than re-deferring with core.ErrNoTokenYet.

Safe to call multiple times.

func (*OAuthTokenSource) Token

func (s *OAuthTokenSource) Token() (string, error)

Token implements core.TokenSource.

Acquisition is LAZY by default: until a server challenge selects the scope, Token returns ("", core.ErrNoTokenYet) instead of running the OAuth flow. The transport treats that sentinel as "send this request without an Authorization header", the server replies 401 with a per-operation WWW-Authenticate scope=, and the auth-retry path calls TokenForScopes to arm the source and acquire with that exact scope (RFC 6750 §3.1). This avoids pre-acquiring with the broader PRM scopes_supported catalog before any operation has stated what it needs. Setting explicit Scopes on the source opts out of laziness — a caller that pins scopes acquires eagerly on the first Token call.

Once armed (or with explicit Scopes), attempts are ordered cheap-to-expensive:

  1. In-memory cached token still valid -> return it
  2. CredStore has a non-expired credential (fast path) -> cache + return
  3. Refresh path: if the stored credential has a refresh token and the scope set still covers s.Scopes, exchange it for a new access token via AuthClient.GetToken -> refreshTokenLocked
  4. Full LoginWithBrowser flow (opens a browser tab)

The refresh path requires a non-nil CredentialStore on the AuthClient; when s.CredStore is nil, an internal in-memory store is used so refresh tokens issued by LoginWithBrowser can be exercised on subsequent calls without forcing external persistence.

func (*OAuthTokenSource) TokenForScopes

func (s *OAuthTokenSource) TokenForScopes(scopes []string) (string, error)

TokenForScopes implements core.ScopeAwareTokenSource. Invalidates the cached token and the stored credential (if any), merges the requested scopes into the existing scope set, and triggers a fresh OAuth flow. Stored-credential invalidation is required so the refresh path cannot silently return a token scoped to the old grant — step-up must re-run LoginWithBrowser to widen the scope set.

Calling this also arms the source for acquisition (see Token's lazy gate). The transport routes every 401 on a scope-aware source through here — including 401s whose WWW-Authenticate carries no scope=, where scopes is empty. That empty call is a no-op on the scope set but still arms the source, so the subsequent Token acquires using the PRM scopes_supported fallback rather than deferring with core.ErrNoTokenYet.

type ProtectedResourceMetadata

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

ProtectedResourceMetadata is the JSON structure served by the PRM endpoint (RFC 9728).

type RecordRequest added in v0.2.47

type RecordRequest struct {
	JTI string
	TTL time.Duration
}

RecordRequest carries the jti + TTL to persist. TTL is the duration the jti must remain visible to Seen() — typically set from the BCL handler's configured replay window, NOT from the logout_token's `exp` claim (because a malicious AS could shrink exp and turn a replay window into nothing).

type RecordResponse added in v0.2.47

type RecordResponse struct{}

RecordResponse is a marker; success is err=nil. No Found-style field because the spec doesn't require any signal on the record path (overwriting an existing jti is fine — the handler already rejected the replay via Seen()).

type SeenRequest added in v0.2.47

type SeenRequest struct {
	JTI string
}

SeenRequest carries the jti being checked. JTI is the bare `jti` claim value from the logout_token — callers do not need to prefix or namespace it; the store treats the value as opaque.

type SeenResponse added in v0.2.47

type SeenResponse struct {
	Found bool
}

SeenResponse reports whether the jti has been recorded within its TTL. Found=false is the success path (jti is fresh, proceed to Record). Found=true means the BCL handler MUST respond 400.

type TenantMapper added in v0.2.47

type TenantMapper func(*apiauth.IntrospectionResult) (tenant, subject string)

TenantMapper extracts (tenant, subject) from an introspection response. The validator stamps these into the returned core.Claims as Claims.Tenant and Claims.Subject respectively — both typed fields, no string-encoding at this layer. Single-tenant deployments return tenant="" and the validator stamps Tenant="" without further processing.

Mappers MUST NOT return subject == "" — Validate rejects the request with 401 when no subject is recoverable, since downstream consumers (events canonical-key, session-binding) require a non-empty principal identity.

The default mapper (realm-from-issuer) is sufficient for every Keycloak deployment whose realm == tenant; override only when a deployment maps tenant from a custom claim (e.g., an admin-mapped "organization" attribute). Until a concrete consumer needs the override, callers should leave IntrospectionConfig.TenantMapper nil — the default is documented and stable.

type ToolDefLookup added in v0.2.41

type ToolDefLookup interface {
	ToolDef(name string) (core.ToolDef, bool)
}

ToolDefLookup is the minimal interface NewToolScopeMiddleware needs from the server's tool registry. *server.Registry satisfies this.

type ToolScopeOption added in v0.2.47

type ToolScopeOption func(*toolScopeConfig)

ToolScopeOption configures NewToolScopeMiddleware. Use the With* functions in this package; the underlying type is opaque so future fields can be added without breaking callers that pass zero options.

func WithIncludeGrantedScopes added in v0.2.47

func WithIncludeGrantedScopes(v bool) ToolScopeOption

WithIncludeGrantedScopes opts the middleware into advertising the union of (caller's currently-granted scopes ∪ tool's required scopes) in the 403 WWW-Authenticate scope parameter. Default is false (per-operation, matching SEP-2350 semantics).

When to opt in: facing non-mcpkit clients that may overwrite their scope set on every challenge instead of accumulating it. The classic broken behavior is "client sees insufficient_scope=docs:write, re-requests a token for ONLY docs:write, loses the docs:read it already had." Union-on-challenge defends against that by re-stating the granted set every time. this option is the server-side counterpart for deployments that can't wait for every client to upgrade.

When to leave off: mcpkit's own clients accumulate scopes correctly (the OAuthTokenSource.TokenForScopes contract enforces it), so mcpkit-on-mcpkit deployments gain nothing from the union. Leaving it off also keeps the challenge minimal, which suits least-privilege re-auth.

Interaction with AcceptedScopes: AcceptedScopes (gate-only) NEVER appears in the challenge regardless of this option. The union is over granted scopes and required scopes; tolerated alternates stay private to the server.

func WithResourceMetadataURL added in v0.3.0

func WithResourceMetadataURL(url string) ToolScopeOption

WithResourceMetadataURL plumbs the server's RFC 9728 Protected Resource Metadata URL into the 403 challenge the middleware emits. When set, the WWW-Authenticate header carries `resource_metadata="<URL>"` alongside `error="insufficient_scope"` and `scope="..."`, so clients hitting a first-call 403 on the stateless wire (no preceding 401) can discover the authorization server without falling back to the well-known path.

Empty (default) omits the segment — the 401 path's PRM advertisement is the only discovery hint. This is correct when every client passes through a 401 before any 403, but breaks down on SEP-2575 stateless wire where the first tools/call can be a 403 with no prior context.

Typically the same URL passed to JWTValidator.ResourceMetadataURL — the two configs live on separate components by design (JWTValidator emits 401s, NewToolScopeMiddleware emits 403s) but referencing the same PRM document.

Jump to

Keyboard shortcuts

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