oauth2

package
v1.11.1 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: Apache-2.0 Imports: 34 Imported by: 2

Documentation

Index

Constants

View Source
const ClientName = "Bifrost MCP Gateway"

ClientName is Bifrost's client_name in DCR and its Client ID Metadata Document.

Variables

View Source
var (
	// ErrIssuerMismatch marks authorization server metadata that names another issuer (RFC 8414 3.3).
	ErrIssuerMismatch = errors.New("authorization server metadata names another issuer")
	// ErrAuthorizationResponseIssuer marks an authorization response from an unexpected issuer (RFC 9207).
	ErrAuthorizationResponseIssuer = errors.New("the authorization response did not come from the expected authorization server")
	// ErrAuthorizationServerChanged marks pre-registered credentials whose authorization server changed (SEP-2352).
	ErrAuthorizationServerChanged = errors.New("the MCP server's authorization server changed")
)
View Source
var (
	// ErrDynamicRegistrationUnavailable means the config has no registration
	// endpoint to ask. Fixable by setting a registration_url, or by
	// reauthorizing with the stored client instead. Worded as the first clause
	// of the message it has always produced, so wrapping it leaves that
	// message byte-for-byte what it was.
	ErrDynamicRegistrationUnavailable = errors.New("this MCP server's OAuth provider has no registration endpoint, so a new client cannot be registered")

	// ErrDynamicRegistrationRejected means the provider answered the
	// registration request with a 4xx: it understood the request and refused
	// it (registration closed to anonymous clients, metadata it will not
	// accept). A 5xx is deliberately not this: the provider failing is not the
	// provider saying no.
	ErrDynamicRegistrationRejected = errors.New("the oauth provider rejected the registration request")

	// ErrClientMetadataDocumentWithClientID means use_client_metadata_document was set with a client_id:
	// the document's URL is the client_id, so the two cannot both apply.
	ErrClientMetadataDocumentWithClientID = errors.New("use_client_metadata_document cannot be combined with a client_id")

	// ErrClientMetadataDocumentUnavailable means use_client_metadata_document was set but Bifrost's
	// document is not at an address an authorization server can fetch (public HTTPS, not loopback).
	ErrClientMetadataDocumentUnavailable = errors.New("the client metadata document needs Bifrost on a public HTTPS address: set the MCP external URL")
)

The two ReregisterDynamicClient failures a caller can do something about. Everything else it returns (a config that would not load, stored scopes that would not parse, a registration endpoint that could not be reached or fell over, a replacement that could not be persisted) is Bifrost's or the network's problem, and an HTTP caller should hear about it as a 5xx rather than be told its own request was wrong.

Declared here rather than beside the OAuth2 sentinels in core/schemas because nothing outside the registration path produces or consumes them.

Functions

func ApplicationTypeFor added in v1.11.0

func ApplicationTypeFor(redirectURI string) string

ApplicationTypeFor returns "native" for a loopback redirectURI, else "web" (SEP-837).

func GeneratePKCEChallenge

func GeneratePKCEChallenge() (verifier, challenge string, err error)

GeneratePKCEChallenge generates code_verifier and code_challenge for PKCE (RFC 7636) Returns:

  • verifier: Random 128-character string (stored securely, never sent to server)
  • challenge: SHA256 hash of verifier, base64url encoded (sent in authorization request)

func IsClientMetadataURL added in v1.11.0

func IsClientMetadataURL(clientID string) bool

IsClientMetadataURL reports whether clientID is a Client ID Metadata Document URL.

func NewAdminConfiguredHTTPClient added in v1.11.0

func NewAdminConfiguredHTTPClient(timeout time.Duration) *http.Client

NewAdminConfiguredHTTPClient returns a client for an endpoint an authenticated administrator configured (such as a trusted identity provider's JWKS): the MCP OAuth chain's proxy handling and SSRF guard, honoring the private-target allowlist. Build one per request so allowlist changes apply immediately.

func SetDiscoveryDialContextForTests added in v1.9.0

func SetDiscoveryDialContextForTests(dial func(ctx context.Context, network, addr string) (net.Conn, error))

SetDiscoveryDialContextForTests overrides the dialer behind every OAuth discovery, registration and token-exchange client. Test-only: other packages whose tests stand up loopback-bound OAuth servers call it from their TestMain, the same way core/mcp exposes SetDialContextForTests. Pass nil to restore the guarded dialer.

func SetLogger

func SetLogger(l schemas.Logger)

func SetPrivateTargetAllowlist added in v1.9.0

func SetPrivateTargetAllowlist(allow *network.Allowlist)

SetPrivateTargetAllowlist replaces the private-target allowlist for MCP OAuth discovery, registration and token requests. Pass nil to permit only public destinations. Build the allowlist with network.ValidatePrivateTargetAllowlist.

The allowlist is checked only when a connection is dialed, so the allowlisted transport is replaced as well: a keep-alive connection approved under the previous list must not serve a later request. Requests already in flight finish on the old transport, whose pool no new request draws from.

func ValidatePKCEChallenge

func ValidatePKCEChallenge(verifier, challenge string) bool

ValidatePKCEChallenge validates that a code_verifier matches the expected code_challenge Used during testing or debugging

Types

type DynamicClientRegistrationRequest

type DynamicClientRegistrationRequest struct {
	ClientName              string   `json:"client_name"`
	RedirectURIs            []string `json:"redirect_uris"`
	GrantTypes              []string `json:"grant_types"`
	ResponseTypes           []string `json:"response_types"`
	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method"`
	ApplicationType         string   `json:"application_type,omitempty"` // OIDC: "web" or "native"; MCP requires it (SEP-837)
	Scope                   string   `json:"scope,omitempty"`
	LogoURI                 string   `json:"logo_uri,omitempty"`
	ClientURI               string   `json:"client_uri,omitempty"`
	Contacts                []string `json:"contacts,omitempty"`
}

DynamicClientRegistrationRequest represents the client registration request (RFC 7591)

type DynamicClientRegistrationResponse

type DynamicClientRegistrationResponse struct {
	ClientID                string `json:"client_id"`
	ClientSecret            string `json:"client_secret,omitempty"`
	ClientIDIssuedAt        int64  `json:"client_id_issued_at,omitempty"`
	ClientSecretExpiresAt   int64  `json:"client_secret_expires_at,omitempty"`
	RegistrationAccessToken string `json:"registration_access_token,omitempty"`
	RegistrationClientURI   string `json:"registration_client_uri,omitempty"`
}

DynamicClientRegistrationResponse represents the server's response (RFC 7591)

func RegisterDynamicClient

func RegisterDynamicClient(ctx context.Context, registrationURL string, req *DynamicClientRegistrationRequest) (*DynamicClientRegistrationResponse, error)

RegisterDynamicClient performs dynamic client registration with the OAuth provider (RFC 7591) This allows Bifrost to automatically register as an OAuth client without manual setup.

Parameters:

  • ctx: Context for the registration request
  • registrationURL: The registration endpoint (discovered or user-provided)
  • req: Client registration details

Returns client_id and optional client_secret that can be used for OAuth flows.

type OAuth2Provider

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

OAuth2Provider implements the schemas.OAuth2Provider interface It provides OAuth 2.0 authentication functionality with database persistence

func NewOAuth2Provider

func NewOAuth2Provider(configStore configstore.ConfigStore, logger schemas.Logger) *OAuth2Provider

NewOAuth2Provider creates a new OAuth provider instance

func (*OAuth2Provider) BuildAdminUpstreamAuthorizeURL added in v1.5.9

func (p *OAuth2Provider) BuildAdminUpstreamAuthorizeURL(ctx context.Context, flowID string) (string, error)

BuildAdminUpstreamAuthorizeURL is BuildUpstreamAuthorizeURL's admin-mode counterpart: reconstructs the upstream provider authorization URL for a pending flow_mode='admin' row, used by the MCP client reauthorize endpoint to hand OAuth2Authorizer's popup a real provider URL to open directly (unlike the per-user page-based flow, which navigates to a Bifrost page that itself resolves the upstream URL later). Reads through GetOauthFlowByID rather than widening BuildUpstreamAuthorizeURL's own GetOauthUserSessionByID call: that lookup is deliberately scoped away from admin-mode rows (see its doc comment) so a caller-supplied flow ID from a per-user-facing endpoint can never reach an admin flow's PKCE state.

func (*OAuth2Provider) BuildUpstreamAuthorizeURL added in v1.3.11

func (p *OAuth2Provider) BuildUpstreamAuthorizeURL(ctx context.Context, flowID string) (string, error)

BuildUpstreamAuthorizeURL reconstructs the upstream provider authorization URL for a pending per-user OAuth flow. Called by the frontend sessions tab when the user clicks "Authenticate" — at which point the flow row already exists (from a prior InitiateUserOAuthFlow), the CSRF state + PKCE verifier are stored on it, and we just need to hand the user the upstream redirect.

The code_challenge is recomputed deterministically from the stored verifier so we don't have to persist it separately.

func (*OAuth2Provider) CheckAuthorizationResponseIssuer added in v1.11.0

func (p *OAuth2Provider) CheckAuthorizationResponseIssuer(ctx context.Context, state, iss string, hasIss bool) error

CheckAuthorizationResponseIssuer applies RFC 9207 2.4 to state's authorization response before its code or error is used. A flow with no recorded issuer is not checked.

func (*OAuth2Provider) CompleteOAuthFlow

func (p *OAuth2Provider) CompleteOAuthFlow(ctx context.Context, state, code string) error

CompleteOAuthFlow handles the OAuth callback and exchanges code for tokens Supports PKCE verification. Handles the admin-mode flow — the shared client's production authorize and a per-user client's bootstrap-test authorize alike (see the FlowMode field comment on TableMCPOauthFlow).

func (*OAuth2Provider) CompleteUserOAuthFlow added in v1.3.1

func (p *OAuth2Provider) CompleteUserOAuthFlow(ctx context.Context, state string, code string) (string, error)

CompleteUserOAuthFlow handles the OAuth callback for a per-user flow. It looks up the session by state, exchanges code for tokens, and returns a session token.

func (*OAuth2Provider) EvictExchangedToken added in v1.5.9

func (p *OAuth2Provider) EvictExchangedToken(mode schemas.MCPAuthMode, identity, mcpClientID string)

EvictExchangedToken removes every cached exchanged token for one binding, regardless of which subject bearer token each entry was cached under (see userTokenCacheKey's doc comment on subjectDiscriminator) — the caller here (ForceRefreshAccessToken) knows the binding but not a specific token's fingerprint.

func (*OAuth2Provider) EvictUserToken added in v1.5.9

func (p *OAuth2Provider) EvictUserToken(mode schemas.MCPAuthMode, identity, mcpClientID string)

EvictUserToken drops the cached access token for one (mode, identity, mcp client) binding. Side-effect only and safe to call when the cache is absent; the next lookup for the binding reads the database.

func (*OAuth2Provider) EvictUserTokenByID added in v1.5.9

func (p *OAuth2Provider) EvictUserTokenByID(tokenID string)

EvictUserTokenByID drops the cached access token backed by the given token row ID, if any binding currently holds it. Side-effect only and safe to call when the cache is absent or the ID is not cached.

func (*OAuth2Provider) EvictUserTokensByMCPClient added in v1.5.9

func (p *OAuth2Provider) EvictUserTokensByMCPClient(mcpClientID string)

EvictUserTokensByMCPClient drops every cached access token bound to the given MCP client, across all auth modes and identities, with its held ID-JAG grants and discovered upstream token endpoint. Used after client-level mutations that invalidate its token rows as a set, such as credential rotation, access reconciliation, or client deletion. Side-effect only and safe to call when the cache is absent.

func (*OAuth2Provider) EvictUserTokensByUser added in v1.5.9

func (p *OAuth2Provider) EvictUserTokensByUser(userID string)

EvictUserTokensByUser drops every cached user-mode access token bound to the given user, across all MCP clients. Used after user-level mutations that orphan or delete the user's token rows as a set. Side-effect only and safe to call when the cache is absent.

func (*OAuth2Provider) EvictUserTokensByVirtualKey added in v1.5.9

func (p *OAuth2Provider) EvictUserTokensByVirtualKey(virtualKeyID string)

EvictUserTokensByVirtualKey drops every cached vk-mode access token bound to the given virtual key, across all MCP clients. Used after virtual key mutations that orphan or delete its token rows as a set. Side-effect only and safe to call when the cache is absent.

func (*OAuth2Provider) ExchangeAdminCredential added in v1.5.9

ExchangeAdminCredential performs a single uncached exchange of the admin's own identity-provider token ("verify as yourself", never associated with any identity binding), producing the admin bootstrap credential used for verification + tool discovery. The token is the one the request carries or, when it carries none but was authenticated through the admin's own sign-in (storedSubjectAllowed), the one stored from that sign-in, retried once as exchangeStoredSubject describes. The full token response is returned so the caller can retain it via RetainExchangeAdminCredential once verification succeeds. A request with neither, as one made with an API key, gets ErrExchangeSubjectTokenMissing; one whose stored sign-in has lapsed gets ErrStoredSignInExpired.

func (*OAuth2Provider) FlushUserTokenCache added in v1.5.9

func (p *OAuth2Provider) FlushUserTokenCache()

FlushUserTokenCache drops every cached per-user access token. The coarse fallback for mutations whose blast radius cannot be scoped to one client or virtual key. Side-effect only.

func (*OAuth2Provider) ForceRefreshAccessToken added in v1.5.9

func (p *OAuth2Provider) ForceRefreshAccessToken(ctx *schemas.BifrostContext, config *schemas.MCPClientConfig) error

ForceRefreshAccessToken resolves the MCP OAuth token row backing config — the shared token linked to config.OauthConfigID for MCPAuthTypeOauth (same lookup GetAccessToken performs), or the caller's per-identity token for MCPAuthTypePerUserOauth (same lookup GetUserAccessTokenByMode performs, with (mode, identity) derived from ctx) — and refreshes it unconditionally via RefreshAccessToken, regardless of whether the token's own ExpiresAt says it's still good. RefreshAccessToken itself never consults ExpiresAt — only its callers (GetAccessToken, GetUserAccessTokenByMode) gate on it before deciding to call it — so resolving the right token row and going straight to RefreshAccessToken is the entire "force" behavior.

func (*OAuth2Provider) GetAccessToken

func (p *OAuth2Provider) GetAccessToken(ctx context.Context, oauthConfigID string) (string, error)

GetAccessToken retrieves the access token for a given oauth_config_id

func (*OAuth2Provider) GetAdminAccessToken added in v1.5.9

func (p *OAuth2Provider) GetAdminAccessToken(ctx context.Context, mcpClientID string) (string, error)

GetAdminAccessToken is GetAccessToken's admin-mode counterpart: resolves the retained bootstrap-verification credential for a per-user client's periodic tool-discovery refresh (ClientToolSyncer.performSync), rather than the shared-mode production credential. Keyed by the MCP client ID, which every admin row carries regardless of whether an oauth_configs template sits behind the credential.

func (*OAuth2Provider) GetExchangedAccessToken added in v1.5.9

func (p *OAuth2Provider) GetExchangedAccessToken(ctx *schemas.BifrostContext, config *schemas.MCPClientConfig) (string, error)

GetExchangedAccessToken returns an upstream access token for a token_exchange client, exchanging the caller's identity-provider token for one scoped to the client's audience. A request that carried no such token but was authenticated through the user's own sign-in exchanges the token stored from that sign-in instead (storedSubjectAllowed). Cached per (auth mode, identity, mcp client) binding in the same in-memory cache as per-user OAuth lookups — exchanged tokens have no database row, so the cached entry (with the expiry-as-miss validator) is the only local state, and a miss simply performs a fresh exchange.

func (*OAuth2Provider) GetPendingMCPClient

func (p *OAuth2Provider) GetPendingMCPClient(oauthConfigID string) (*schemas.MCPClientConfig, error)

GetPendingMCPClient retrieves an MCP client config by oauth_config_id. Returns nil if no pending config is found. Unlike before PKCE/expiry fields moved off TableOauthConfig, this no longer applies its own expiry gate: the only caller (completeMCPClientOAuth) already requires oauthConfig.Status=="authorized" before reaching this call, which is a stronger, more truthful signal than a raw timestamp comparison — a config row that reached "authorized" represents a real completed OAuth exchange regardless of how much wall-clock time has passed since the bootstrap stash was written.

func (*OAuth2Provider) GetUserAccessTokenByMode added in v1.3.11

func (p *OAuth2Provider) GetUserAccessTokenByMode(ctx context.Context, mode schemas.MCPAuthMode, identity, mcpClientID string) (string, error)

GetUserAccessTokenByMode retrieves the upstream access token using exactly one identity column determined by mode. No fallback chain. Filters status='active' so orphaned rows never satisfy a lookup.

Reads are served from an in-memory cache when possible: a cached entry whose expiry has passed is dropped and the lookup falls through to the database path, which owns every expiry and refresh decision. Failed lookups are never cached, so a caller completing OAuth (or a token being reactivated) becomes visible on the very next call.

func (*OAuth2Provider) InitiateOAuthFlow

func (p *OAuth2Provider) InitiateOAuthFlow(ctx context.Context, config *schemas.OAuth2Config) (*schemas.OAuth2FlowInitiation, error)

InitiateOAuthFlow creates an OAuth config and returns the authorization URL Supports OAuth discovery and PKCE

func (*OAuth2Provider) InitiateUserOAuthFlow added in v1.3.1

func (p *OAuth2Provider) InitiateUserOAuthFlow(ctx context.Context, oauthConfigID string, mcpClientID string, redirectURI string, flowMode schemas.MCPAuthMode) (*schemas.OAuth2FlowInitiation, string, error)

InitiateUserOAuthFlow creates or refreshes the per-user OAuth flow row for a given (mode, identity, mcp_client) binding and returns the auth landing URL.

Determinism: there is exactly one flow row per binding. If one already exists (from a prior auth attempt), it's updated in place — fresh CSRF state, fresh PKCE verifier, status reset to 'pending'. Otherwise a new row is inserted. Reauth never duplicates rows; revoke deletes them.

The function errors out cleanly on any misconfig (missing identity, unknown mode, missing template config) — no fallbacks, no generated identities.

func (*OAuth2Provider) RefreshAccessToken

func (p *OAuth2Provider) RefreshAccessToken(ctx context.Context, tokenID string) error

RefreshAccessToken refreshes any MCP OAuth token row — the single shared client credential (AuthMode="shared") or a per-identity credential (AuthMode "user"/"vk"/"session") alike — looked up by the token row's own primary-key ID. This is the one refresh path for every kind of MCP OAuth credential: GetAccessToken and GetUserAccessTokenByMode both funnel their lazy pre-flight refresh through here, unifying what used to be two separate functions (one keyed by oauth_config_id and reachable only via TableOauthConfig.TokenID, one keyed by token ID directly).

Never writes to TableOauthConfig: the template config row (client_id, token_url, etc.) is read-only input to the token exchange here. Credential health lives entirely on the token row's own Status field.

The actual refresh runs on a context detached from any single caller — via DoChan rather than Do — because it is shared work: with Do, the first caller's ctx backs the whole call, so that caller disconnecting (client abort, gateway timeout) would cancel the in-flight upstream request and deliver that same cancellation error to every other concurrent caller waiting on the same token, even though their own requests are still live. Each caller here instead only ever stops waiting on its own ctx; the shared refresh keeps running to completion (bounded by refreshAccessTokenTimeout) for whoever else still needs its result.

func (*OAuth2Provider) RemovePendingMCPClient

func (p *OAuth2Provider) RemovePendingMCPClient(oauthConfigID string) error

RemovePendingMCPClient clears the pending MCP client config from the oauth config This is called after OAuth completion to clean up

func (*OAuth2Provider) ReregisterDynamicClient added in v1.9.0

func (p *OAuth2Provider) ReregisterDynamicClient(ctx context.Context, oauthConfigID, redirectURI string) (previousClientID, newClientID string, rotated bool, err error)

ReregisterDynamicClient obtains a fresh client_id (and secret, if the provider issues one) from the config's RFC 7591 registration endpoint and writes it over the stored credentials in place. Returns the client_id it replaced and the one it installed, so the caller can report the swap, and whether anything was actually rotated, so the caller can invalidate what it holds in memory exactly when the stored tokens were invalidated (see below).

redirectURI is the one the consent following this call will present. Pass the same value that goes to InitiateUserOAuthFlow; "" falls back to the URI stored at bootstrap.

This is the recovery path for an authorization server that no longer recognises the client_id it issued through dynamic registration — a registry that did not survive a restart, or a client an admin revoked upstream. Once that happens the stored client_id is rejected at the token endpoint on every refresh AND at the authorize endpoint on every reauthorization, so redoing consent with it cannot recover the connection; only a new registration can. Deliberately operator-invoked rather than triggered off the provider's error response: authorization servers do not report the RFC 6749 error codes consistently enough to decide credential replacement on, and silently discarding a client_id an admin set by hand would be worse than leaving it in place.

Persisted through RotateMCPOAuthConfig, which cascades every token bound to this config to needs_reauth in the same transaction. That cascade is the point, not a side effect: tokens issued to the previous client_id cannot be refreshed once it is replaced, and on a per_user_oauth server that means every end user's credential, not just the admin's. A provider that answers with the client_id it already issued rotates nothing and cascades nothing.

func (*OAuth2Provider) RetainExchangeAdminCredential added in v1.5.9

func (p *OAuth2Provider) RetainExchangeAdminCredential(ctx context.Context, config *schemas.MCPClientConfig, response *schemas.OAuth2TokenExchangeResponse) error

RetainExchangeAdminCredential persists the outcome of a successful admin verification as the retained auth_mode='admin' token row for config — the discovery credential the tool syncer and OAuthTokenRefreshWorker keep alive, mirroring what PromoteSharedOauthTokenToAdmin does for per-user OAuth bootstrap. Upserts: a repair replaces the existing row's credential.

func (*OAuth2Provider) RevokeToken

func (p *OAuth2Provider) RevokeToken(ctx context.Context, oauthConfigID string) error

RevokeToken revokes the OAuth token

func (*OAuth2Provider) SetClientMetadataDocumentPaths added in v1.11.0

func (p *OAuth2Provider) SetClientMetadataDocumentPaths(callbackPath, documentPath string)

SetClientMetadataDocumentPaths tells the provider where, under Bifrost's base URL, the OAuth callback and its Client ID Metadata Document are served, so a metadata-document client_id can follow the base a sign-in uses (followClientMetadataDocument).

func (*OAuth2Provider) SetCredentialsRotatedHook added in v1.11.0

func (p *OAuth2Provider) SetCredentialsRotatedHook(hook func(ctx context.Context, mcpClientID string))

SetCredentialsRotatedHook installs hook, run after the provider itself rotates an MCP client's OAuth credentials (a metadata-document client_id following Bifrost's address, or a sign-in moving the config to a new authorization server), so the caller can drop that client's cached tokens and connection as it does for its own rotations. nil removes it.

func (*OAuth2Provider) SetOnTokenWritten added in v1.11.0

func (p *OAuth2Provider) SetOnTokenWritten(cb func(ctx context.Context, tokenID string))

SetOnTokenWritten registers a callback invoked after this provider writes a token row (a refresh, a re-authorization, a status change or a deletion), with the row's ID, once the row is written and dropped from this process's cache. That eviction reaches only this process; a deployment running several processes over one token store uses the callback to have the others drop the row too. The callback runs inline on the write path, including inside refreshes other callers are waiting on, so it must return promptly. Safe to call at any time; nil clears it.

func (*OAuth2Provider) SetTempTokenService added in v1.3.11

func (p *OAuth2Provider) SetTempTokenService(svc *temptoken.Service)

SetTempTokenService installs the temp-token service used by InitiateUserOAuthFlow to mint the mcp_auth token embedded in the auth-page URL fragment. Called by server startup once both services have been constructed (the provider is built first by lib/config.go, the service later by the HTTP transport).

func (*OAuth2Provider) SetTokenExchangeIdPResolver added in v1.5.9

func (p *OAuth2Provider) SetTokenExchangeIdPResolver(r schemas.TokenExchangeIdPResolver)

SetTokenExchangeIdPResolver installs the identity-provider resolver that backs delegated token exchange. Called once at server startup after the identity integration is constructed; nil (never installed) leaves the token_exchange auth type unavailable.

func (*OAuth2Provider) StorePendingMCPClient

func (p *OAuth2Provider) StorePendingMCPClient(oauthConfigID string, mcpClientConfig schemas.MCPClientConfig) error

StorePendingMCPClient stores an MCP client config that's waiting for OAuth completion The config is persisted in the database (oauth_configs.mcp_client_config_json) to support multi-instance deployments where OAuth callback may hit a different server instance.

func (*OAuth2Provider) TokenExchangeAvailable added in v1.5.9

func (p *OAuth2Provider) TokenExchangeAvailable() bool

TokenExchangeAvailable reports whether delegated token exchange can run.

func (*OAuth2Provider) ValidateToken

func (p *OAuth2Provider) ValidateToken(ctx context.Context, oauthConfigID string) (bool, error)

ValidateToken checks if the token is still valid

func (*OAuth2Provider) ValidateTokenExchangeConfig added in v1.11.0

func (p *OAuth2Provider) ValidateTokenExchangeConfig(ctx context.Context, cfg *schemas.MCPTokenExchangeConfig) error

ValidateTokenExchangeConfig checks cfg with the installed resolver when it implements TokenExchangeConfigValidator, so the create and edit paths can refuse settings before they are saved. With no resolver, or one that does not validate, it returns nil.

type OAuthMetadata

type OAuthMetadata struct {
	AuthorizationURL string   `json:"authorization_endpoint"`
	TokenURL         string   `json:"token_endpoint"`
	RegistrationURL  *string  `json:"registration_endpoint,omitempty"`
	ScopesSupported  []string `json:"scopes_supported,omitempty"`
	Resource         string   `json:"resource,omitempty"`
	Issuer           string   `json:"issuer,omitempty"`
	ResponseTypes    []string `json:"response_types_supported,omitempty"`
	GrantTypes       []string `json:"grant_types_supported,omitempty"`
	TokenAuthMethods []string `json:"token_endpoint_auth_methods_supported,omitempty"`
	PKCEMethods      []string `json:"code_challenge_methods_supported,omitempty"`
	// Accepts an HTTPS URL to a Client ID Metadata Document as client_id (MCP 2025-11-25+).
	ClientIDMetadataDocumentSupported bool `json:"client_id_metadata_document_supported,omitempty"`
	// Sends iss in authorization responses (RFC 9207).
	IssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"`

	AuthorizationServers []string `json:"-"` // every authorization server the MCP server advertises; this metadata is the first that answered
}

OAuthMetadata contains discovered OAuth configuration from authorization server

func DiscoverOAuthMetadata

func DiscoverOAuthMetadata(ctx context.Context, serverURL string) (*OAuthMetadata, error)

DiscoverOAuthMetadata performs OAuth 2.0 discovery for the given MCP server URL Following RFC 8414 (Authorization Server Discovery) and RFC 9728 (Protected Resource Metadata)

Parameters:

  • ctx: Context for the discovery requests
  • serverURL: The MCP server URL to discover OAuth configuration from
  • logger: Logger for discovery progress (can be nil for silent operation)

The discovery process: 1. Attempt to connect to MCP server, expect 401 with WWW-Authenticate header 2. Parse WWW-Authenticate header for resource_metadata URL and scopes 3. Fetch resource metadata to get authorization server URLs 4. Try .well-known discovery if resource metadata is not available 5. Fetch authorization server metadata from discovered URLs 6. Return complete OAuth configuration

type OAuthTokenRefreshWorker added in v1.5.9

type OAuthTokenRefreshWorker struct {

	// AuthModes restricts proactive refresh to tokens whose AuthMode is in
	// this set. Defaults to {"shared", "admin"} — shared-client tokens and
	// retained per_user_oauth bootstrap credentials (auth_mode='admin') are
	// the only ones with no live caller to trigger a lazy/inline refresh:
	// admin-mode tokens back only the periodic tool-discovery syncer, never
	// real end-user traffic, so nothing else would ever refresh them.
	// Widening this further (e.g. to include "user"/"vk"/"session") opts
	// specific per-identity auth modes into the same proactive sweep.
	//
	// Set this before calling Start, not after: the background goroutine
	// reads this field directly on every sweep with no synchronization, so a
	// mutation concurrent with a running sweep is a data race. Every current
	// caller already treats this as write-once-before-Start; there is no
	// supported way to change an already-started worker's scope short of
	// Stop and constructing a new worker.
	AuthModes []string
	// contains filtered or unexported fields
}

OAuthTokenRefreshWorker manages automatic token refresh for expiring OAuth tokens

func NewOAuthTokenRefreshWorker added in v1.5.9

func NewOAuthTokenRefreshWorker(provider *OAuth2Provider, logger schemas.Logger) *OAuthTokenRefreshWorker

NewOAuthTokenRefreshWorker creates a new token refresh worker

func (*OAuthTokenRefreshWorker) SetLookAheadWindow added in v1.5.9

func (w *OAuthTokenRefreshWorker) SetLookAheadWindow(window time.Duration)

SetLookAheadWindow updates the look-ahead window for token expiry (for testing)

func (*OAuthTokenRefreshWorker) SetOnTokenRefreshed added in v1.5.9

func (w *OAuthTokenRefreshWorker) SetOnTokenRefreshed(cb func(mcpClientID, authMode string))

SetOnTokenRefreshed registers a callback invoked after each successful proactive token refresh, with the owning MCP client ID and the token's auth mode. It lets the serving layer react to a freshened credential, for example by recycling a connection that captured the old one. Tokens with no MCP client ID (legacy rows) never fire the callback. Safe to call at any time, including while the worker is running; passing nil clears the callback.

func (*OAuthTokenRefreshWorker) SetRefreshInterval added in v1.5.9

func (w *OAuthTokenRefreshWorker) SetRefreshInterval(interval time.Duration)

SetRefreshInterval updates the refresh check interval (for testing)

func (*OAuthTokenRefreshWorker) SetShouldRefreshGate added in v1.5.9

func (w *OAuthTokenRefreshWorker) SetShouldRefreshGate(gate func(ctx context.Context) bool)

SetShouldRefreshGate installs a predicate consulted at the start of every sweep tick; a tick proceeds only when the gate is unset or returns true. Lets a deployment with multiple workers sharing the same token store restrict sweeps to one at a time, avoiding concurrent refreshes of the same token. Safe to call at any time, including while the worker is running; passing nil restores the default (always run).

func (*OAuthTokenRefreshWorker) Start added in v1.5.9

func (w *OAuthTokenRefreshWorker) Start(ctx context.Context)

Start begins the token refresh worker in a background goroutine

func (*OAuthTokenRefreshWorker) Stop added in v1.5.9

func (w *OAuthTokenRefreshWorker) Stop()

Stop gracefully stops the token refresh worker. Safe to call multiple times — guarded by sync.Once so a redundant call from a secondary shutdown path can't panic by re-closing the channel.

type PerUserOAuthSweepWorker added in v1.3.11

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

PerUserOAuthSweepWorker periodically purges stale OAuth state:

  • expired pending flow rows (mcp_oauth_flows where ExpiresAt < now and status='pending')
  • abandoned bootstrap configs (oauth_configs left 'pending'/'failed' with no MCP client, live flow, or token referencing them, older than abandonedOauthConfigGrace)
  • orphaned token rows older than OrphanRetention (mcp_oauth_tokens where status='orphaned')

Pending-flow expiry is short (driven by the row's own ExpiresAt, default 15min) so the flow tick is fast, and abandoned configs ride the same tick. Orphan retention is operator-tunable (default 30 days) and the orphan tick is slow.

func NewPerUserOAuthSweepWorker added in v1.3.11

func NewPerUserOAuthSweepWorker(provider *OAuth2Provider, orphanRetention time.Duration, logger schemas.Logger) *PerUserOAuthSweepWorker

NewPerUserOAuthSweepWorker creates a sweep worker with sensible defaults. orphanRetention <= 0 disables the orphan-token sweep.

func (*PerUserOAuthSweepWorker) SetFlowSweepInterval added in v1.3.11

func (w *PerUserOAuthSweepWorker) SetFlowSweepInterval(d time.Duration)

SetFlowSweepInterval updates the pending-flow sweep cadence (for testing). Non-positive durations are ignored — run() feeds the field straight into time.NewTicker, which panics on d <= 0.

func (*PerUserOAuthSweepWorker) SetOrphanSweepInterval added in v1.3.11

func (w *PerUserOAuthSweepWorker) SetOrphanSweepInterval(d time.Duration)

SetOrphanSweepInterval updates the orphan-token sweep cadence (for testing). Same non-positive guard as SetFlowSweepInterval.

func (*PerUserOAuthSweepWorker) Start added in v1.3.11

func (w *PerUserOAuthSweepWorker) Start(ctx context.Context)

Start begins the sweep worker in a background goroutine.

func (*PerUserOAuthSweepWorker) Stop added in v1.3.11

func (w *PerUserOAuthSweepWorker) Stop()

Stop gracefully stops the sweep worker. sync.Once guards against double-close panics when called from multiple shutdown paths.

type PermanentOAuthError added in v1.2.37

type PermanentOAuthError struct {
	StatusCode int
	Body       string
}

PermanentOAuthError indicates the OAuth provider rejected the request in a way that requires user re-authorization (e.g. revoked refresh token, invalid_grant). Distinct from transient network failures which should be retried.

func (*PermanentOAuthError) Error added in v1.2.37

func (e *PermanentOAuthError) Error() string

type ResourceMetadata

type ResourceMetadata struct {
	Resource             string   `json:"resource,omitempty"`
	AuthorizationServers []string `json:"authorization_servers"`
	ScopesSupported      []string `json:"scopes_supported,omitempty"`
	Scopes               []string `json:"scopes,omitempty"` // Alternative field name
}

ResourceMetadata contains metadata from protected resource

type TokenExchangeConfigValidator added in v1.11.0

type TokenExchangeConfigValidator interface {
	ValidateTokenExchangeConfig(ctx context.Context, cfg *schemas.MCPTokenExchangeConfig) error
}

TokenExchangeConfigValidator is optionally implemented by a TokenExchangeIdPResolver to check an MCP client's token_exchange settings when the client is created or edited, without exchanging a token. An error refuses the save, and its text is returned to the editor.

Jump to

Keyboard shortcuts

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