Documentation
¶
Index ¶
- Constants
- Variables
- func ApplicationTypeFor(redirectURI string) string
- func GeneratePKCEChallenge() (verifier, challenge string, err error)
- func IsClientMetadataURL(clientID string) bool
- func NewAdminConfiguredHTTPClient(timeout time.Duration) *http.Client
- func SetDiscoveryDialContextForTests(dial func(ctx context.Context, network, addr string) (net.Conn, error))
- func SetLogger(l schemas.Logger)
- func SetPrivateTargetAllowlist(allow *network.Allowlist)
- func ValidatePKCEChallenge(verifier, challenge string) bool
- type DynamicClientRegistrationRequest
- type DynamicClientRegistrationResponse
- type OAuth2Provider
- func (p *OAuth2Provider) BuildAdminUpstreamAuthorizeURL(ctx context.Context, flowID string) (string, error)
- func (p *OAuth2Provider) BuildUpstreamAuthorizeURL(ctx context.Context, flowID string) (string, error)
- func (p *OAuth2Provider) CheckAuthorizationResponseIssuer(ctx context.Context, state, iss string, hasIss bool) error
- func (p *OAuth2Provider) CompleteOAuthFlow(ctx context.Context, state, code string) error
- func (p *OAuth2Provider) CompleteUserOAuthFlow(ctx context.Context, state string, code string) (string, error)
- func (p *OAuth2Provider) EvictExchangedToken(mode schemas.MCPAuthMode, identity, mcpClientID string)
- func (p *OAuth2Provider) EvictUserToken(mode schemas.MCPAuthMode, identity, mcpClientID string)
- func (p *OAuth2Provider) EvictUserTokenByID(tokenID string)
- func (p *OAuth2Provider) EvictUserTokensByMCPClient(mcpClientID string)
- func (p *OAuth2Provider) EvictUserTokensByUser(userID string)
- func (p *OAuth2Provider) EvictUserTokensByVirtualKey(virtualKeyID string)
- func (p *OAuth2Provider) ExchangeAdminCredential(ctx *schemas.BifrostContext, config *schemas.MCPClientConfig) (*schemas.OAuth2TokenExchangeResponse, error)
- func (p *OAuth2Provider) FlushUserTokenCache()
- func (p *OAuth2Provider) ForceRefreshAccessToken(ctx *schemas.BifrostContext, config *schemas.MCPClientConfig) error
- func (p *OAuth2Provider) GetAccessToken(ctx context.Context, oauthConfigID string) (string, error)
- func (p *OAuth2Provider) GetAdminAccessToken(ctx context.Context, mcpClientID string) (string, error)
- func (p *OAuth2Provider) GetExchangedAccessToken(ctx *schemas.BifrostContext, config *schemas.MCPClientConfig) (string, error)
- func (p *OAuth2Provider) GetPendingMCPClient(oauthConfigID string) (*schemas.MCPClientConfig, error)
- func (p *OAuth2Provider) GetUserAccessTokenByMode(ctx context.Context, mode schemas.MCPAuthMode, identity, mcpClientID string) (string, error)
- func (p *OAuth2Provider) InitiateOAuthFlow(ctx context.Context, config *schemas.OAuth2Config) (*schemas.OAuth2FlowInitiation, error)
- func (p *OAuth2Provider) InitiateUserOAuthFlow(ctx context.Context, oauthConfigID string, mcpClientID string, ...) (*schemas.OAuth2FlowInitiation, string, error)
- func (p *OAuth2Provider) RefreshAccessToken(ctx context.Context, tokenID string) error
- func (p *OAuth2Provider) RemovePendingMCPClient(oauthConfigID string) error
- func (p *OAuth2Provider) ReregisterDynamicClient(ctx context.Context, oauthConfigID, redirectURI string) (previousClientID, newClientID string, rotated bool, err error)
- func (p *OAuth2Provider) RetainExchangeAdminCredential(ctx context.Context, config *schemas.MCPClientConfig, ...) error
- func (p *OAuth2Provider) RevokeToken(ctx context.Context, oauthConfigID string) error
- func (p *OAuth2Provider) SetClientMetadataDocumentPaths(callbackPath, documentPath string)
- func (p *OAuth2Provider) SetCredentialsRotatedHook(hook func(ctx context.Context, mcpClientID string))
- func (p *OAuth2Provider) SetOnTokenWritten(cb func(ctx context.Context, tokenID string))
- func (p *OAuth2Provider) SetTempTokenService(svc *temptoken.Service)
- func (p *OAuth2Provider) SetTokenExchangeIdPResolver(r schemas.TokenExchangeIdPResolver)
- func (p *OAuth2Provider) StorePendingMCPClient(oauthConfigID string, mcpClientConfig schemas.MCPClientConfig) error
- func (p *OAuth2Provider) TokenExchangeAvailable() bool
- func (p *OAuth2Provider) ValidateToken(ctx context.Context, oauthConfigID string) (bool, error)
- func (p *OAuth2Provider) ValidateTokenExchangeConfig(ctx context.Context, cfg *schemas.MCPTokenExchangeConfig) error
- type OAuthMetadata
- type OAuthTokenRefreshWorker
- func (w *OAuthTokenRefreshWorker) SetLookAheadWindow(window time.Duration)
- func (w *OAuthTokenRefreshWorker) SetOnTokenRefreshed(cb func(mcpClientID, authMode string))
- func (w *OAuthTokenRefreshWorker) SetRefreshInterval(interval time.Duration)
- func (w *OAuthTokenRefreshWorker) SetShouldRefreshGate(gate func(ctx context.Context) bool)
- func (w *OAuthTokenRefreshWorker) Start(ctx context.Context)
- func (w *OAuthTokenRefreshWorker) Stop()
- type PerUserOAuthSweepWorker
- type PermanentOAuthError
- type ResourceMetadata
- type TokenExchangeConfigValidator
Constants ¶
const ClientName = "Bifrost MCP Gateway"
ClientName is Bifrost's client_name in DCR and its Client ID Metadata Document.
Variables ¶
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") )
var ( // 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") // 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
ApplicationTypeFor returns "native" for a loopback redirectURI, else "web" (SEP-837).
func GeneratePKCEChallenge ¶
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
IsClientMetadataURL reports whether clientID is a Client ID Metadata Document URL.
func NewAdminConfiguredHTTPClient ¶ added in v1.11.0
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 SetPrivateTargetAllowlist ¶ added in v1.9.0
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 ¶
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
func (p *OAuth2Provider) ExchangeAdminCredential(ctx *schemas.BifrostContext, config *schemas.MCPClientConfig) (*schemas.OAuth2TokenExchangeResponse, error)
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 ¶
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 ¶
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
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.