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)
- 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
- Web UI
- API
- config.json
- Navigate to MCP Gateway and click New MCP Server
- Pick HTTP or SSE as the connection type, fill in the Connection URL
- Set Auth Type to OAuth 2.0
- Fill in the OAuth fields:
- Client ID (optional — leave blank for Dynamic Client Registration)
- Client Secret (optional — omit for PKCE public clients)
- Authorize URL (optional — leave blank to use OAuth discovery)
- Token URL (optional — same)
- Scopes (comma-separated)
- Click Create — Bifrost runs the OAuth dance in a popup
- Sign in and authorize on the upstream provider
- The popup closes and the MCP client is persisted with the token

PKCE for public clients
For applications without a client secret, omitclient_secret and Bifrost will automatically generate PKCE code verifiers:
Dynamic Client Registration (RFC 7591)
If your OAuth provider supports DCR, omitclient_id and client_secret and provide a registration_url (or just a server_url for discovery):
- Discover OAuth endpoints from
server_url(if needed) - Send the OAuth
resourceindicator during authorization and token exchange only whenresourceis provided. - Register a new client via
registration_url - Continue with the standard authorize / token exchange flow
OAuth discovery
If onlyclient_id and server_url are provided, Bifrost will probe in order:
<server_url>/.well-known/oauth-authorization-server(RFC 8414)<server_url>/.well-known/openid-configuration- MCP server metadata returned by the server itself
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.
- Web UI
- API
- config.json
Open MCP Settings and add each entry under OAuth Private Network Allowlist (press Enter or comma after each one), then save.
Token management
Status
pending— admin hasn’t authorized yetauthorized— token is valid and activefailed— authorization failed or token is invalidrevoked— the token was revoked (via DELETE); the config row is retained with no live token
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.
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.
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.
- Unset fields preserve stored values.
client_id/client_secretfollow 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).
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.
Reauthorization
A shared OAuth client whose credential permanently dies lands in theneeds_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 client and click Reauthorize: Bifrost redoes the OAuth consent flow in a popup against the currently stored credentials, and on completion reconnects the client.

POST /api/mcp/client/{id}/reauthorize ({id} = MCP client ID):
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 theclient_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_reauthwith a status reason likeprovider 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
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.
The same flow is scriptable via POST /api/mcp/client/{id}/reregister ({id} = MCP client ID):
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.
Revoke
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
- Configuration
- Provider setup
- Configuration
- Provider setup
Public URL configuration
Theredirect_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 theredirect_uriregistered with upstream providers
Troubleshooting
Token refresh fails / tools say oauth token expired
Token refresh fails / tools say oauth token expired
- 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: click Reauthorize on the client (or
POST /api/mcp/client/{id}/reauthorize); see Reauthorization
Callback hangs at /api/oauth/callback
Callback hangs at /api/oauth/callback
- Confirm Bifrost is reachable at the registered redirect URI (DNS, firewall, reverse-proxy headers)
- Check
mcp_external_client_urlmatches what was registered upstream - Look at Bifrost logs for
oautherrors
Invalid redirect URI from the upstream provider
Invalid redirect URI from the upstream provider
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.
API reference
Security notes
- Tokens are stored encrypted at rest (set
BIFROST_ENCRYPTION_KEY) - PKCE is enforced automatically for public clients
- The OAuth
stateparameter is verified server-side for CSRF protection - 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)

