Skip to main content

Overview

auth_type: "oauth" covers server-level OAuth: the admin authenticates once during MCP client setup, Bifrost stores the resulting token, and every subsequent request to that MCP server uses the same token regardless of which caller hit Bifrost. If you need each end-user to authenticate themselves (personal Notion workspace, personal GitHub repos, etc.), use Per-User OAuth instead. This auth type is only valid for HTTP and SSE connections. What Bifrost handles for you:
  • Automatic token refresh before expiration
  • PKCE for public clients (no client secret)
  • Client ID Metadata Documents, so Bifrost can sign in without registering a client first
  • Dynamic Client Registration (RFC 7591)
  • OAuth discovery from server URLs (.well-known/oauth-authorization-server, .well-known/openid-configuration)
  • Secure token storage (encrypted at rest)

OAuth flow

Bifrost implements the Authorization Code flow:

Configuration

  1. Navigate to MCP Gateway and click New MCP Server
  2. Pick HTTP or SSE as the connection type, fill in the Connection URL
  3. Set Auth Type to OAuth 2.0
  4. Fill in the OAuth fields:
    • Client ID (optional — leave blank and Bifrost registers itself, through its client metadata document or Dynamic Client Registration)
    • Use Bifrost’s client metadata document (off by default — turn on to use the document even when the server does not advertise support; see below)
    • Client Secret (optional — omit for PKCE public clients)
    • Authorize URL (optional — leave blank to use OAuth discovery)
    • Token URL (optional — same)
    • Scopes (comma-separated)
  5. Click Create — Bifrost runs the OAuth dance in a popup
  6. Sign in and authorize on the upstream provider
  7. The popup closes and the MCP client is persisted with the token
OAuth flow popup opened from the MCP client creation step, landing on the upstream provider's consent screen
By default this connection is per-call (a fresh connection and credential resolution per tool call, no shared upstream connection to keep alive) — see Session Stickiness to make it sticky instead.

PKCE for public clients

For applications without a client secret, omit client_secret and Bifrost will automatically generate PKCE code verifiers:

Client ID Metadata Documents

Bifrost publishes a client metadata document describing itself: its name, the redirect URI it signs in with, and that it is a public client with no secret. An authorization server that supports these documents accepts the document’s address as the client ID and reads it during sign-in, so nothing has to be registered beforehand and there is no client secret to manage. When Client ID is left blank, Bifrost uses its document automatically when all of these hold, and falls back to Dynamic Client Registration otherwise:
  • the authorization server advertises support for client metadata documents;
  • Bifrost is reachable over public HTTPS (a local or plain-HTTP address cannot be fetched by the authorization server);
  • a token URL you entered by hand belongs to that same authorization server. Bifrost does not check a hand-entered authorization URL, so make sure it belongs to the same server too.
Turn on Use Bifrost’s client metadata document ("use_client_metadata_document": true in oauth_config) to use the document even when the authorization server does not advertise support, for example when its endpoints cannot be discovered and you enter them by hand:
  • The authorization server has to fetch the document from Bifrost, so Bifrost must be reachable over public HTTPS. Set mcp_external_client_url if Bifrost sits behind a proxy; on a local or plain-HTTP address the setting is refused when you save the client.
  • It replaces the client ID and secret, so it cannot be combined with a Client ID.
  • The authorization server must actually accept client metadata documents. If it does not, sign-in fails on its page; enter a client ID registered with that authorization server instead. Turning the setting off is not enough when the server advertises support, since Bifrost then still uses the document automatically.
If Bifrost’s public address changes later, the client ID moves to the document at the new address on the next sign-in, and existing sessions have to sign in again.

Dynamic Client Registration (RFC 7591)

If your OAuth provider supports DCR, omit client_id and client_secret and provide a registration_url (or just a server_url for discovery):
Bifrost will:
  1. Discover OAuth endpoints from server_url (if needed)
  2. Send the OAuth resource indicator during authorization and token exchange only when resource is provided.
  3. Register a new client via registration_url
  4. Continue with the standard authorize / token exchange flow
The redirect_uri Bifrost registers with the upstream provider is locked to Bifrost’s current public URL (mcp_external_client_url, or the request Host header if unset). If you change Bifrost’s public URL later, the upstream provider will reject the next authorize call with “Invalid redirect URI”. Plain reauthorization reuses the registered client, so it cannot fix the mismatch on its own: use Reauthorize with a new client so Bifrost re-runs DCR against the new URL. (For manually registered credentials, add the new redirect URI in the provider’s dashboard instead, then reauthorize.)

OAuth discovery

If only client_id and server_url are provided, Bifrost will probe in order:
  1. <server_url>/.well-known/oauth-authorization-server (RFC 8414)
  2. <server_url>/.well-known/openid-configuration
  3. MCP server metadata returned by the server itself
Bifrost only uses an authorization server’s metadata when the issuer it names is exactly the authorization server it was fetched for. A provider whose metadata names a different issuer, even one that differs only by a trailing slash, fails discovery. Ask the provider to correct its metadata. Entering its endpoints by hand also works, but then Bifrost has no issuer to check sign-in responses against, so it cannot confirm they come from that authorization server.

Authorization server changes

Bifrost remembers which authorization server issued an MCP client’s OAuth credentials, and checks that the MCP server still points to it each time a sign-in or reauthorization starts. The check needs the MCP server’s authorization metadata: if Bifrost cannot read it, the sign-in goes ahead with the stored authorization server. An MCP client set up before this check existed is tied to its authorization server once the token URL it uses matches the one the MCP server names. If the MCP server has moved to a different authorization server:
  • Client ID Metadata Document: Bifrost’s client ID carries over when the new authorization server supports Client ID Metadata Documents. Otherwise Bifrost falls back to Dynamic Client Registration with it.
  • Dynamic Client Registration: Bifrost registers a new client with the new authorization server. If the new server offers no registration, or registration fails, the sign-in fails; register a client with it yourself and enter its credentials as below.
  • Credentials entered by hand: the sign-in fails with an error naming both authorization servers. Register a client with the new one, set its client_id / client_secret on the MCP client (and its authorize_url / token_url, if they differ from the old server’s), then reauthorize. The reauthorization binds the MCP client to the new authorization server.
The move happens when the sign-in starts. Once the MCP client has moved to the new authorization server, every credential bound to it is signed out, including every end user’s on a per_user_oauth server, since Bifrost cannot use tokens from the old authorization server with the new one. Bifrost does not revoke them at the old authorization server. They stay signed out even if that sign-in is then abandoned or denied. A move that fails, such as one with credentials entered by hand for the old server, signs nobody out. If the MCP server lists several authorization servers, the one its credentials belong to stays in use for as long as it is listed, even when another listed server answers first.

OAuth servers on localhost or a private network

Bifrost’s OAuth requests to an MCP server (discovery, dynamic client registration, token exchange, and refresh) reach only public addresses by default. Every hop after the first comes from the previous response, so the MCP server decides where Bifrost sends them. To use an OAuth-protected MCP server that runs on the same host or inside your network, list its host, IP address, or CIDR range in the OAuth private network allowlist:
  • Entries are hostnames, IP addresses, or CIDR ranges, with no scheme or port (localhost, 127.0.0.1, ::1, 10.20.0.0/16, mcp.internal.example).
  • The allowlist applies only to MCP servers added by an authenticated admin or declared in config.json. Servers added while dashboard authentication was off stay public-only.
  • Changing the allowlist over the API or in the UI requires dashboard authentication.
  • Link-local and cloud metadata addresses (for example 169.254.169.254) are always refused, and an entry that covers them is rejected.
  • Plain MCP connections without OAuth are not affected: an authenticated admin can already add http://localhost:... servers.
Open MCP Settings and add each entry under OAuth Private Network Allowlist (press Enter or comma after each one), then save.

Token management

Status

Status values:
  • pending — admin hasn’t authorized yet
  • authorized — token is valid and active
  • failed — authorization failed or token is invalid
  • revoked — the token was revoked (via DELETE); the config row is retained with no live token
When a stored token permanently dies later (refresh rejected, provider-side revocation), the status flips on the token row, not on the OAuth config: the token moves to needs_reauth and the MCP client’s connection state shows needs_reauth. The OAuth config itself stays authorized.

Automatic refresh

Bifrost refreshes access tokens automatically using the stored refresh token, in two layers:
  • In the background — a worker periodically refreshes tokens that are about to expire, so active clients always have a valid token ready.
  • On use — if a token is already expired when a request needs it, Bifrost refreshes it inline before forwarding the request.
Background refresh only runs while the MCP client is enabled. Disabling a client pauses it; on re-enable, the token is refreshed on first use. If a client stays disabled long enough for the provider to expire the idle refresh token, reauthorization is required. Transient refresh failures (network blips, provider hiccups) keep retrying silently. Only a permanent rejection (the provider refuses the refresh token outright) flips the token to needs_reauth. A successful background refresh doesn’t just update the stored token: for shared clients it immediately recycles the live connection so it starts using the fresh credential. The recycle is make-before-break for HTTP and SSE clients, so the connection keeps serving tool calls throughout, and token expiry normally passes with zero failed calls. In a multi-node deployment, only the node that performed the refresh recycles its own connection; the other nodes’ connections heal on their next auth failure via the retry described in Auth failure recovery.
Some providers only issue a refresh token when you explicitly ask for one. Google is the canonical case: without access_type=offline&prompt=consent on the authorize URL, the grant contains only an access token, and the client lands in needs_reauth at every token expiry (roughly hourly for Google). Bifrost preserves any query parameters already present on the configured authorize_url, so append the provider’s offline-access parameters there, e.g. "authorize_url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent".

Rotation

PUT /api/mcp/client/{id} accepts an oauth_config block for clients with auth_type oauth or per_user_oauth (400 for any other auth type). Any field can be rotated: client_id, client_secret, authorize_url, token_url, registration_url, resource, scopes. Rotation applies the changed fields in place on the same OAuth config row; it does not create a new row and does not re-run discovery or client registration.
Semantics:
  • Unset fields preserve stored values. client_id / client_secret follow the SecretVar masked-placeholder convention (sending back the redacted value from a GET means “keep”); the other fields treat empty as “not provided”.
  • Any actual change cascades. Every token bound to that OAuth config flips to needs_reauth, regardless of auth mode: the shared connection token, every per-user token, and the retained admin discovery credential alike. Shared clients surface it on the next reconnect; per-user callers get the standard reauth URL on their next tool call.
  • A no-op round-trip is safe. Re-sending the stored values does not cascade anything.
  • Cannot run while the client is (or is being) disabled (400; enable the client first, or send the enable and rotation as separate requests).
The same rotation applies to config.json-declared clients: editing the oauth_config block of an already-authorized client rotates the stored config at the next boot and cascades needs_reauth, with a boot warning that existing sessions must re-authenticate.
Rotating client_id or client_secret immediately signs out every current session on the MCP client, shared and per-user alike. Everyone re-authenticates against the new credentials; for the shared connection itself, that means clicking Reauthorize (below).

Reauthorization

A shared OAuth client whose credential permanently dies lands in the needs_reauth connection state: the client was authorized and connected at least once, but the token can no longer be refreshed (provider-side revocation, expired refresh token, or a credential rotation). This is distinct from pending_verification, which means initial setup never completed. needs_reauth is sticky: the health monitor and enable/disable toggles will not flip the client back to healthy or unstable, and the Reconnect action is disabled for it (reconnecting cannot help when the credential itself is dead). Only a human redoing consent clears it. From the dashboard, open the server’s actions menu (⋯) in the MCP Server Catalog and choose Reauthorize. The client’s State column and its sheet show a Needs Reauth badge until then. Bifrost redoes the OAuth consent flow in a popup against the currently stored credentials, and on completion reconnects the client.
MCP Server Catalog with a shared OAuth client in Needs Reauth state and Reauthorize highlighted in its actions menu
The same flow is scriptable via POST /api/mcp/client/{id}/reauthorize ({id} = MCP client ID):
The response is the same pending_oauth payload the create flow returns (oauth_config_id, authorize_url, expires_at, complete_url, status_url, next_steps): open authorize_url in a browser, poll status_url until authorized, then POST complete_url. On completion the client reconnects and the response reads "MCP client re-authorized and reconnected successfully". Error cases: 400 if the client’s auth_type is not OAuth-based, or if it never completed initial authorization (use initiate-verification instead); 404 for an unknown ID; 503 when no OAuth provider is configured. For per_user_oauth clients the endpoint repairs the retained admin discovery credential instead, and is gated accordingly; see Per-User OAuth.

Reauthorizing with a new client

Reauthorization reuses the client_id already stored on the OAuth config. That is the right default, but it cannot recover a client whose provider has stopped recognising that client_id altogether. Authorization servers that hand out clients through Dynamic Client Registration commonly keep the registry in memory, so a provider restart can drop every client it ever issued. Once that happens the stored client_id is rejected everywhere it is used:
  • background refresh fails, and the credential lands in needs_reauth with a status reason like provider rejected the refresh (HTTP 401, invalid_client: Invalid client_id)
  • Reauthorize fails the same way, because the authorize request and the code exchange behind it both present the same unknown client
The repair is to register a replacement client first. From the dashboard, open the server’s actions menu and choose Reauthorize with a new client. Bifrost registers a fresh client at the provider’s registration_url, writes it over the stored credentials, and then runs the normal consent flow against it. The credential block flags this case: when a needs_reauth credential’s status reason names invalid_client, it says so and points at this action.
Replacing the client_id marks every credential Bifrost stored under the old one needs_reauth and stops using it, since none of them can be refreshed against a client the provider no longer associates them with. It does not revoke the access tokens the provider already issued: Bifrost never calls the provider’s revocation endpoint (see Revoke), so those can stay valid at the provider until they expire. On a shared oauth server that is the one connection credential. On a per_user_oauth server it is every end user’s credential as well, and each of them will have to sign in again. Plain Reauthorize leaves end users alone; only this action touches them.
The same flow is scriptable via POST /api/mcp/client/{id}/reregister ({id} = MCP client ID):
The response is the same pending_oauth payload as reauthorize, plus registered_client_id and previous_client_id so you can confirm which client the consent you are about to complete belongs to. Drive it identically: open authorize_url, poll status_url until authorized, then POST complete_url. It is a separate endpoint rather than a flag on reauthorize because the two differ in what they destroy, and that difference belongs somewhere access control and audit logs can see it. Error cases: 400 if the client’s auth_type is not OAuth-based, if it never completed initial authorization (use initiate-verification instead), if the OAuth config has no registration_url (the provider offers nowhere to register, so only a manually issued client_id can be used), or if the provider refuses the registration request (answers it with a 4xx); 404 for an unknown ID; 503 when no OAuth provider is configured. A provider that cannot be reached, or that answers with a 5xx, is a 500: nothing in the request was wrong, and nothing was registered or changed, so it is safe to retry. So is a 500 whose message starts failed to persist the newly registered oauth client: Bifrost is unchanged (the stored client_id and every token are as they were), but the provider did register a replacement before the local store failed, so it holds a registration Bifrost will never use, and each retry adds one more. Bifrost cannot delete those; remove them at the provider if it lists registered clients.
One 500 is not safe to retry. The replacement client is registered and stored before the consent flow is opened, and a registration made at the provider cannot be undone. If opening the flow then fails, the new client_id is already installed and every token bound to the config is already needs_reauth. The error message says so and names both client_ids.Do not call reregister again from that state, because it would register yet another client. Call reauthorize instead: it runs consent against whatever client is stored, which is now the replacement.

Revoke

This deletes the stored token from Bifrost and marks the OAuth configuration revoked (the config row is kept, not deleted). Bifrost does not call the upstream provider’s revocation endpoint — revoke at the provider’s dashboard if you need the upstream token invalidated there.

Provider snippets

GitHub

Google


Public URL configuration

The redirect_uri Bifrost registers and the consent URLs it builds are derived from the request Host header by default. Behind a reverse proxy, override them with:
  • mcp_external_client_url — public base URL Bifrost uses both for the consent pages it surfaces and as the redirect_uri registered with upstream providers
See Reverse Proxy configuration → for the full reference.
Changing mcp_external_client_url after an upstream provider has been registered breaks already-authorized clients registered through DCR or by hand. Upstream providers lock the redirect_uri to whatever was registered during DCR. To recover a DCR client, use Reauthorize with a new client so Bifrost registers again with the new URL (plain reauthorization reuses the registered client and cannot fix the mismatch). For manually registered credentials, add the new redirect URI at the provider’s dashboard, then reauthorize. Clients using Bifrost’s client metadata document need no action: their client ID follows the new address at the next sign-in, and existing sessions sign in again.

Troubleshooting

  • Ensure auth_type is exactly "oauth"
  • Confirm oauth_config is on the request body
  • Provide authorize_url or a server_url Bifrost can discover from
  • Check that the refresh token is still valid (some providers expire refresh tokens after long idle)
  • If the client was disabled for a long stretch, background refresh was paused for it — the refresh token may have expired at the provider in the meantime
  • If the provider never issued a refresh token at all, the client will hit this at every access-token expiry; append the provider’s offline-access parameters to authorize_url (see the warning under Automatic refresh)
  • Verify scopes are still sufficient
  • Re-authorize: choose Reauthorize from the client’s actions menu (or POST /api/mcp/client/{id}/reauthorize); see Reauthorization
  • Confirm Bifrost is reachable at the registered redirect URI (DNS, firewall, reverse-proxy headers)
  • Check mcp_external_client_url matches what was registered upstream
  • Look at Bifrost logs for oauth errors
You changed Bifrost’s public URL after the upstream client was registered. Use Reauthorize with a new client so Bifrost re-runs DCR with the new URL; for manually registered credentials, add the new redirect URI at the provider’s dashboard and then reauthorize.
The provider’s response named a different authorization server than the one Bifrost sent the user to, or left that out although the provider says it always includes it. Bifrost refuses such a response before using the authorization code. If it keeps happening for one provider, its configuration is inconsistent: check that the issuer in its OAuth metadata matches the address the MCP server points to.
The provider has stopped recognising the client_id itself, not just the token issued under it, so redoing consent with the same client fails at the code exchange the same way the refresh did. This is common with Dynamic Client Registration, where the provider’s client registry is often in memory and does not survive a restart.Use Reauthorize with a new client to register a replacement client and run consent against it. Note that this signs out every credential bound to the server, including every end user’s on a per_user_oauth server.If the provider has no registration_url, register a client in its dashboard by hand, set the new client_id / client_secret on the MCP client, and then reauthorize.

API reference


Security notes

  • Tokens are stored encrypted at rest (set BIFROST_ENCRYPTION_KEY)
  • PKCE is enforced automatically for public clients
  • The OAuth state parameter is verified server-side for CSRF protection
  • Bifrost compares the issuer an authorization response names with the authorization server it sent the user to, and refuses a mismatch. A response without an issuer is refused when that authorization server advertises that it always sends one; authorization servers that do not send it are not checked this way
  • Use HTTPS — most upstream providers refuse HTTP redirect URIs in production
  • Request only the scopes your tools need

Next Steps

  • Per-User OAuth — when each user should authenticate themselves
  • Headers — when there’s no OAuth, just a static key
  • MCP Sessions — per-user credential lifecycle (does not surface server-level OAuth)