diff --git a/docs/changelogs/v1.5.0-prerelease2.mdx b/docs/changelogs/v1.5.0-prerelease2.mdx
index 303502a14d..2a860a273b 100644
--- a/docs/changelogs/v1.5.0-prerelease2.mdx
+++ b/docs/changelogs/v1.5.0-prerelease2.mdx
@@ -19,6 +19,7 @@ description: "v1.5.0-prerelease2 changelog - 2026-04-08"
## ✨ Features
+- **Model Alias** — Map model names to provider-specific identifiers (deployment names, inference profile ARNs, fine-tuned model IDs, custom model names, etc.) via per-key alias config
- **Realtime Support** — Add WebSocket, WebRTC, and client secret handlers with session state management and transport context helpers
- **Fireworks AI Provider** — Add Fireworks AI as a first-class provider with native completions, responses, embeddings, and image generations (thanks [@ivanetchart](https://github.com/ivanetchart)!)
- **Per-User OAuth Consent** — Add per-user OAuth consent flow with identity selection and MCP authentication
@@ -48,8 +49,24 @@ description: "v1.5.0-prerelease2 changelog - 2026-04-08"
- **Data Race Fix** — Fix race in data reading from fasthttp request for integrations
- **Model Listing** — Unify /api/models and /api/models/details listing behavior
+
+**v1.5.0 contains multiple breaking changes.** See the [v1.5.0 Migration Guide](/migration-guides/v1.5.0) for full before/after examples and a migration checklist.
+
+
+## Breaking Changes in This Release
+
+This prerelease introduces 3 additional breaking changes on top of those in prerelease1. See the **[v1.5.0 Migration Guide](/migration-guides/v1.5.0)** for full before/after examples, automatic migration details, and a step-by-step checklist.
+
+| # | Breaking Change | Affected |
+|---|---|---|
+| [9](/migration-guides/v1.5.0#breaking-change-9-provider-deployments-removed-migrate-to-aliases) | Provider `deployments` removed — migrate Azure, Bedrock, Vertex, and Replicate deployment maps to the unified top-level `aliases` field | `config.json`, REST API, Go SDK |
+| [10](/migration-guides/v1.5.0#breaking-change-10-go-sdk-extrafields-model-fields-renamed) | Go SDK: `ExtraFields.ModelRequested` replaced by `OriginalModelRequested` + `ResolvedModelUsed` | Go SDK |
+| [11](/migration-guides/v1.5.0#breaking-change-11-go-sdk-streamaccumulatorresult-field-renamed) | Go SDK: `StreamAccumulatorResult.Model` replaced by `RequestedModel` + `ResolvedModel` | Go SDK |
+
+---
+- feat: add model alias support — map model names to provider-specific identifiers per key
- feat: add Fireworks AI as a first-class provider (thanks [@ivanetchart](https://github.com/ivanetchart)!)
- feat: add realtime provider interfaces, schemas, and engine hooks
- feat: add session log storage and realtime request normalization
@@ -72,6 +89,7 @@ description: "v1.5.0-prerelease2 changelog - 2026-04-08"
+- feat: add model alias storage and encryption in key config
- feat: add per-user OAuth consent flow with identity selection and MCP authentication
- feat: add access profiles for fine-grained permission control
- feat: add user level OAuth for MCP gateway
diff --git a/docs/docs.json b/docs/docs.json
index a5d408231f..c7a5f85ae4 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -165,6 +165,7 @@
"mcp/overview",
"mcp/connecting-to-servers",
"mcp/oauth",
+ "mcp/per-user-oauth",
"mcp/tool-execution",
"mcp/agent-mode",
"mcp/code-mode",
diff --git a/docs/mcp/connecting-to-servers.mdx b/docs/mcp/connecting-to-servers.mdx
index 119b46f32f..4b620c4cec 100644
--- a/docs/mcp/connecting-to-servers.mdx
+++ b/docs/mcp/connecting-to-servers.mdx
@@ -16,8 +16,8 @@ Bifrost supports three connection protocols, each with different authentication
| Type | Description | Best For | Auth Support |
|------|-------------|----------|--------------|
| **STDIO** | Spawns a subprocess and communicates via stdin/stdout | Local tools, CLI utilities, scripts | None |
-| **HTTP** | Sends requests to an HTTP endpoint | Remote APIs, microservices, cloud functions | Headers, OAuth 2.0 |
-| **SSE** | Server-Sent Events for persistent connections | Real-time data, streaming tools | Headers, OAuth 2.0 |
+| **HTTP** | Sends requests to an HTTP endpoint | Remote APIs, microservices, cloud functions | Headers, OAuth 2.0, Per-User OAuth |
+| **SSE** | Server-Sent Events for persistent connections | Real-time data, streaming tools | Headers, OAuth 2.0, Per-User OAuth |
### STDIO Connections
@@ -50,9 +50,10 @@ STDIO connections launch external processes and communicate via standard input/o
HTTP connections communicate with MCP servers via HTTP requests. Ideal for remote services and microservices.
-HTTP connections support two authentication methods:
+HTTP connections support three authentication methods:
- **Header-based authentication**: Static headers (API keys, custom tokens)
-- **OAuth 2.0**: Dynamic token-based authentication with automatic token refresh
+- **OAuth 2.0**: Shared token managed by an admin, with automatic refresh
+- **Per-User OAuth**: Each end-user authenticates with their own credentials
#### Header-Based Authentication
@@ -107,11 +108,23 @@ Use OAuth 2.0 for secure, user-based authentication with automatic token refresh
[→ Learn more about OAuth authentication →](./oauth)
**Use Cases:**
-- User-delegated access
-- Third-party service integrations
-- Secure credential management
+- Shared service integrations where all users access the same account
+- Admin-managed third-party connections
- Compliance with OAuth 2.0 standards
+#### Per-User OAuth
+
+Use per-user OAuth when each end-user should access the upstream service under their own account (e.g., each user's personal Notion workspace or GitHub repos). Bifrost acts as an OAuth 2.1 Authorization Server — users authenticate through a consent flow and their tokens are stored per-identity.
+
+Per-user OAuth is configured through the Web UI only (Bifrost runs a test OAuth flow and pre-fetches tools at setup time).
+
+[→ Learn more about Per-User OAuth →](./per-user-oauth)
+
+**Use Cases:**
+- Multi-tenant apps where users access their own data
+- Personal integrations (Notion, GitHub, Google Drive)
+- Scenarios requiring per-user audit trails and token isolation
+
**Overall HTTP Use Cases:**
- Remote API integrations
- Cloud-hosted MCP services
@@ -120,7 +133,7 @@ Use OAuth 2.0 for secure, user-based authentication with automatic token refresh
### SSE Connections
-Server-Sent Events (SSE) connections provide real-time, persistent connections to MCP servers. Like HTTP connections, SSE supports both header-based and OAuth authentication.
+Server-Sent Events (SSE) connections provide real-time, persistent connections to MCP servers. Like HTTP connections, SSE supports header-based authentication, OAuth 2.0, and per-user OAuth.
#### Header-Based Authentication
@@ -159,7 +172,13 @@ Server-Sent Events (SSE) connections provide real-time, persistent connections t
- Real-time market data
- Live system monitoring
- Event-driven workflows
-- User-authenticated streaming connections
+- Shared streaming connections managed by an admin
+
+#### Per-User OAuth
+
+Same as HTTP — each user authenticates under their own account. Configured through the Web UI only.
+
+[→ Learn more about Per-User OAuth →](./per-user-oauth)
[→ Learn more about OAuth authentication →](./oauth)
diff --git a/docs/mcp/gateway-url.mdx b/docs/mcp/gateway-url.mdx
index ff169c4c60..e9ce76cfd2 100644
--- a/docs/mcp/gateway-url.mdx
+++ b/docs/mcp/gateway-url.mdx
@@ -289,6 +289,22 @@ The MCP Server dynamically updates its tool registry from the tool manager.
---
+## Per-User OAuth for MCP Clients
+
+When at least one MCP server is configured with `per_user_oauth`, the `/mcp` endpoint automatically advertises OAuth support via standard discovery headers. OAuth-capable MCP clients (Claude Code, Cursor, and others) detect this automatically — no manual configuration is needed on the client side.
+
+When an unauthenticated client connects, Bifrost responds with a `401` that points to the discovery endpoints:
+
+```
+WWW-Authenticate: Bearer resource_metadata="https://your-bifrost-domain/.well-known/oauth-protected-resource"
+```
+
+The client then fetches the OAuth metadata and kicks off a consent flow where the user can attach an identity and connect their upstream services. Subsequent requests use a Bifrost-issued session token as the Bearer credential.
+
+See [Per-User OAuth →](./per-user-oauth) for the full flow and identity options.
+
+---
+
## Security Considerations
diff --git a/docs/mcp/oauth.mdx b/docs/mcp/oauth.mdx
index 1aacb1cdd7..4b87de9fa2 100644
--- a/docs/mcp/oauth.mdx
+++ b/docs/mcp/oauth.mdx
@@ -5,6 +5,10 @@ description: "Configure OAuth 2.0 authentication for MCP HTTP and SSE connection
icon: "lock"
---
+
+This page covers **server-level OAuth**, where an admin authenticates once and the token is shared across all requests to that MCP server. If you need each end-user to authenticate with their own credentials (e.g., personal Notion or GitHub accounts), see [Per-User OAuth](./per-user-oauth).
+
+
## Overview
OAuth 2.0 authentication enables secure, user-delegated access to MCP servers. Bifrost handles:
diff --git a/docs/mcp/overview.mdx b/docs/mcp/overview.mdx
index 4bac6808db..6ad5f0213e 100644
--- a/docs/mcp/overview.mdx
+++ b/docs/mcp/overview.mdx
@@ -40,6 +40,9 @@ By default, Bifrost does NOT automatically execute tool calls. All tool executio
Secure OAuth 2.0 authentication with automatic token refresh
+
+ Let each end-user authenticate with upstream services under their own credentials
+
Execute tools with full control over approval and conversation flow
diff --git a/docs/mcp/per-user-oauth.mdx b/docs/mcp/per-user-oauth.mdx
new file mode 100644
index 0000000000..103226121d
--- /dev/null
+++ b/docs/mcp/per-user-oauth.mdx
@@ -0,0 +1,192 @@
+---
+title: "Per-User OAuth"
+sidebarTitle: "Per-User OAuth"
+description: "Let each end-user authenticate with upstream MCP services under their own credentials. Works with both the MCP Gateway and LLM Gateway."
+icon: "users"
+---
+
+## Overview
+
+**Per-user OAuth** lets each end-user connect to upstream MCP services (Notion, GitHub, etc.) using their own credentials. Instead of a single shared admin token, every user gets their own access — scoped to their account, their data.
+
+This is different from [server-level OAuth](./oauth), where an admin authenticates once and every request uses the same shared token:
+
+| | Server-level OAuth | Per-user OAuth |
+|---|---|---|
+| Who authenticates | Admin, once | Each end-user individually |
+| Token scope | Shared across all requests | Per-user, per-service |
+| Identity required | No | Yes (VK, User ID, or session) |
+| Persists across sessions | Yes (background refresh) | Yes, when tied to VK or User ID |
+| Works with MCP Gateway | Yes | Yes |
+| Works with LLM Gateway | Yes | Yes |
+
+---
+
+## Setup
+
+Per-user OAuth is configured through the Web UI only. During setup, Bifrost runs a test OAuth flow and pre-fetches the available tools from the upstream service — this is why file-based config is not supported for this auth type.
+
+
+
+
+1. Navigate to **MCP Gateway** and click **New MCP Server**
+2. Select **HTTP** or **SSE** as the connection type and enter the server URL
+3. Set **Auth Type** to **Per-User OAuth**
+4. Fill in the OAuth application credentials:
+ - **Client ID** — your upstream OAuth app's client ID
+ - **Client Secret** — optional for PKCE flows
+ - **Authorize URL** — upstream authorization endpoint (or leave blank for auto-discovery)
+ - **Token URL** — upstream token endpoint (or leave blank for auto-discovery)
+ - **Scopes** — comma-separated list of requested scopes
+5. Click **Create** — Bifrost runs a test OAuth flow to validate the config and pre-fetches the tool list
+6. Complete the authorization in your browser
+7. Save the MCP client
+
+
+
+
+
+
+
+If your upstream server supports OAuth Discovery (RFC 8414), you can leave the authorize and token URLs blank and provide only the **Server URL**. Bifrost will discover the endpoints automatically.
+
+
+---
+
+## How it works: MCP Gateway
+
+When you expose Bifrost as an MCP server (via the `/mcp` endpoint) and at least one MCP client is configured with `per_user_oauth`, Bifrost becomes an **OAuth 2.1 Authorization Server**. OAuth-capable MCP clients like Claude Code and Cursor detect this automatically — no manual configuration required on the client side.
+
+The full flow involves three distinct phases: **discovery** (the client finds Bifrost's OAuth endpoints), **consent** (the user attaches an identity and connects upstream services), and **authenticated use** (all subsequent tool calls carry the user's tokens transparently). The diagram below shows all three phases end to end.
+
+
+
+### First connection: the consent flow
+
+The first time a client connects, Bifrost walks the user through a two-step consent screen:
+
+**Step 1 — Identity selection**
+
+The user chooses how to identify themselves for this session:
+
+- **Virtual Key** — ties upstream tokens to the VK permanently; tokens survive session restarts and work across the LLM Gateway too
+- **User ID** — a self-declared identifier with the same persistence guarantees as a VK
+- **Skip** — no identity attached; tokens are scoped to this session only and won't carry over to other sessions or the LLM Gateway
+
+
+
+**Step 2 — Connect upstream services**
+
+The user sees all per-user OAuth MCP servers available on their Virtual Key. They can connect all of them at once or just the ones they want right now.
+
+
+
+For each selected service, the user is redirected to the upstream OAuth provider (Notion, GitHub, etc.) to authorize access. After authorizing, they return to Bifrost and can connect additional services or finish.
+
+**Step 3 — Done**
+
+Bifrost issues a 24-hour session token. The MCP client receives this token and proceeds normally. All subsequent tool calls use the user's upstream tokens transparently.
+
+### Lazy auth for skipped services
+
+If the user skips a service during consent — or a new per-user MCP server is added later — Bifrost handles it lazily. When a tool call hits a service the user hasn't authenticated with yet, Bifrost returns an auth URL in the tool result instead of executing the tool:
+
+```
+Authentication required for Notion. Open this URL to connect:
+https://your-bifrost-domain.com/api/oauth/per-user/upstream/authorize?...
+```
+
+
+
+The user opens the URL, completes the upstream OAuth flow, and Bifrost saves the token against their session identity. The next tool call proceeds without any re-auth. This lazy pattern is the same one used by the LLM Gateway — the only difference is the auth URL surfaces as a tool result message rather than an API response field.
+
+---
+
+## How it works: LLM Gateway
+
+When using per-user OAuth through the LLM Gateway (`/v1/chat/completions`), there is no upfront consent screen. Auth is **entirely lazy** — Bifrost waits until a tool actually needs a token before asking for one. This is also the same pattern used when a service is skipped during MCP Gateway consent.
+
+The pattern is simple: every request carries an identity header, and any tool call to an unauthenticated service returns an auth URL instead of a result. The user completes auth once at that URL; all subsequent calls to that service execute normally. The diagram below shows the full cycle.
+
+
+
+1. The user makes a request with an identity header attached (required — see below)
+2. The LLM suggests a tool call to a per-user OAuth service
+3. If no token exists for that user + service, Bifrost returns an `mcp_auth_required` response with an `authorize_url` **instead of executing the tool** — the rest of the LLM response still comes through normally
+
+
+
+4. The user opens the URL and completes the upstream OAuth flow
+5. Bifrost saves the token against their identity — no action needed on your side
+6. On the next request, the tool call executes normally — no re-auth, no special handling required
+
+### Identity is required
+
+The LLM Gateway has no session management, so an identity must be declared on every request. Without one, Bifrost has no stable key to look up or store tokens against.
+
+Pass one of:
+
+```bash
+# Virtual Key (recommended — also works with MCP Gateway)
+-H "x-bf-virtual-key: vk_your_key"
+
+# Self-declared User ID
+-H "X-Bf-User-Id: user_123"
+```
+
+
+**Enterprise**: When enterprise user identity is configured, the user's identity is automatically attached as the User ID — no manual header required.
+
+
+---
+
+## Cross-gateway token sharing
+
+Tokens are stored against an **identity** (Virtual Key or User ID), not against a gateway. This means:
+
+- Authenticate via the **LLM Gateway** with a VK → that token is immediately usable on the **MCP Gateway** with the same VK
+- Authenticate via the **MCP Gateway** consent flow with a VK → that VK works on the **LLM Gateway** with no re-auth needed
+
+The only exception is **Skip** (session-only) auth: those tokens are not associated with any persistent identity and cannot be used from the LLM Gateway.
+
+| Identity mode | Set via | Cross-gateway portable | Persists across sessions |
+|---|---|---|---|
+| Virtual Key | Consent screen or `x-bf-virtual-key` header | Yes | Yes |
+| User ID | Consent screen or `X-Bf-User-Id` header | Yes | Yes |
+| Skip (MCP Gateway only) | Consent screen | No | No |
+
+---
+
+## Config reference
+
+Per-user OAuth is configured on the MCP client via `auth_type`. When `auth_type` is `per_user_oauth`, an `oauth_config_id` linking to the OAuth credentials is required (set automatically during UI setup):
+
+```json
+{
+ "mcp": {
+ "mcp_clients": [
+ {
+ "name": "notion",
+ "connection_type": "http",
+ "connection_string": "https://mcp.notion.so/sse",
+ "auth_type": "per_user_oauth",
+ "oauth_config_id": "oauth_cfg_abc123",
+ "tools_to_execute": ["*"]
+ }
+ ]
+ }
+}
+```
+
+| Field | Type | Description |
+|---|---|---|
+| `auth_type` | string | Set to `"per_user_oauth"` |
+| `oauth_config_id` | string | ID of the OAuth config created during UI setup |
+
+---
+
+## Next Steps
+
+- [Server-level OAuth →](./oauth) — admin authenticates once, shared token for all requests
+- [MCP Gateway URL →](./gateway-url) — expose Bifrost as an MCP server for Claude Code and Cursor
+- [Tool Filtering →](./filtering) — control which per-user tools are available per Virtual Key
diff --git a/docs/media/ui-mcp-per-user-oauth-consent-identity.png b/docs/media/ui-mcp-per-user-oauth-consent-identity.png
new file mode 100644
index 0000000000..af2cb0de32
Binary files /dev/null and b/docs/media/ui-mcp-per-user-oauth-consent-identity.png differ
diff --git a/docs/media/ui-mcp-per-user-oauth-consent-mcps.png b/docs/media/ui-mcp-per-user-oauth-consent-mcps.png
new file mode 100644
index 0000000000..014ef8f74a
Binary files /dev/null and b/docs/media/ui-mcp-per-user-oauth-consent-mcps.png differ
diff --git a/docs/media/ui-mcp-per-user-oauth-flow-llm.svg b/docs/media/ui-mcp-per-user-oauth-flow-llm.svg
new file mode 100644
index 0000000000..56361a60ec
--- /dev/null
+++ b/docs/media/ui-mcp-per-user-oauth-flow-llm.svg
@@ -0,0 +1,89 @@
+
diff --git a/docs/media/ui-mcp-per-user-oauth-flow-mcp.svg b/docs/media/ui-mcp-per-user-oauth-flow-mcp.svg
new file mode 100644
index 0000000000..9ecc358757
--- /dev/null
+++ b/docs/media/ui-mcp-per-user-oauth-flow-mcp.svg
@@ -0,0 +1,122 @@
+
diff --git a/docs/media/ui-mcp-per-user-oauth-llm-prompt-llm.png b/docs/media/ui-mcp-per-user-oauth-llm-prompt-llm.png
new file mode 100644
index 0000000000..b11ecc2a86
Binary files /dev/null and b/docs/media/ui-mcp-per-user-oauth-llm-prompt-llm.png differ
diff --git a/docs/media/ui-mcp-per-user-oauth-llm-prompt-mcp.png b/docs/media/ui-mcp-per-user-oauth-llm-prompt-mcp.png
new file mode 100644
index 0000000000..2bb7fd1c96
Binary files /dev/null and b/docs/media/ui-mcp-per-user-oauth-llm-prompt-mcp.png differ
diff --git a/docs/media/ui-mcp-per-user-oauth-setup.png b/docs/media/ui-mcp-per-user-oauth-setup.png
new file mode 100644
index 0000000000..eda00d45f9
Binary files /dev/null and b/docs/media/ui-mcp-per-user-oauth-setup.png differ
diff --git a/docs/openapi/openapi.json b/docs/openapi/openapi.json
index 978c0d4a9f..da486a8e81 100644
--- a/docs/openapi/openapi.json
+++ b/docs/openapi/openapi.json
@@ -32016,6 +32016,70 @@
}
}
},
+ "/api/mcp/client/{id}/complete-oauth": {
+ "post": {
+ "operationId": "completeMCPClientOAuth",
+ "summary": "Complete MCP client OAuth flow",
+ "description": "Completes the OAuth flow for an MCP client after the user has authorized the request.\nThis endpoint should be called after the OAuth provider redirects back to the callback endpoint\nand the OAuth token has been stored. It retrieves the pending MCP client configuration and\nestablishes the connection with the OAuth-provided credentials.\n",
+ "tags": [
+ "MCP",
+ "OAuth"
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "MCP client ID",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "MCP client connected successfully with OAuth",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SuccessResponse"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "OAuth not authorized yet or MCP client not found in pending OAuth clients",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BifrostError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "MCP client not found in pending OAuth clients or OAuth config not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BifrostError"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BifrostError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/oauth/callback": {
"get": {
"operationId": "handleOAuthCallback",
@@ -32063,20 +32127,911 @@
}
],
"responses": {
- "200": {
- "description": "OAuth authorization successful. Returns HTML page that closes the authorization window.",
- "content": {
- "text/html": {
+ "200": {
+ "description": "OAuth authorization successful. Returns HTML page that closes the authorization window.",
+ "content": {
+ "text/html": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "OAuth authorization failed or missing required parameters",
+ "content": {
+ "text/html": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/oauth/config/{id}/status": {
+ "get": {
+ "operationId": "getOAuthConfigStatus",
+ "summary": "Get OAuth config status",
+ "description": "Retrieves the current status of an OAuth configuration.\nShows whether the OAuth flow is pending, authorized, or failed,\nand includes token expiration and scopes if authorized.\n",
+ "tags": [
+ "OAuth"
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "OAuth config ID",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OAuth config status retrieved successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/OAuthConfigStatus"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "OAuth config not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ManagementErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BifrostError"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "operationId": "revokeOAuthConfig",
+ "summary": "Revoke OAuth config",
+ "description": "Revokes an OAuth configuration and its associated access token.\nAfter revocation, the MCP client will no longer be able to use this OAuth token.\n",
+ "tags": [
+ "OAuth"
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "OAuth config ID",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OAuth token revoked successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SuccessResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BifrostError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/oauth/per-user/register": {
+ "post": {
+ "operationId": "registerPerUserOAuthClient",
+ "summary": "Register OAuth client (RFC 7591)",
+ "description": "Dynamic Client Registration per RFC 7591. MCP clients (Claude Code, Cursor, etc.)\ncall this endpoint to obtain a `client_id` before initiating the authorization flow.\n\nThis endpoint is only available when at least one MCP client is configured with\n`auth_type: per_user_oauth`. Returns `404` otherwise.\n\nAuthentication is not required — this is part of the unauthenticated OAuth bootstrap flow.\n",
+ "tags": [
+ "OAuth",
+ "Per-User OAuth"
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "description": "Dynamic Client Registration request per RFC 7591.\nMCP clients (Claude Code, Cursor, etc.) call this to obtain a client_id\nbefore initiating the authorization flow.\n",
+ "required": [
+ "redirect_uris"
+ ],
+ "properties": {
+ "client_name": {
+ "type": "string",
+ "description": "Human-readable name of the client application",
+ "example": "Claude Code"
+ },
+ "redirect_uris": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of allowed redirect URIs for this client",
+ "example": [
+ "http://localhost:54321/callback"
+ ]
+ },
+ "grant_types": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Supported grant types. Defaults to [\"authorization_code\"]",
+ "example": [
+ "authorization_code"
+ ]
+ },
+ "response_types": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Supported response types",
+ "example": [
+ "code"
+ ]
+ },
+ "token_endpoint_auth_method": {
+ "type": "string",
+ "description": "Token endpoint authentication method. Always \"none\" (public client)",
+ "example": "none"
+ },
+ "scope": {
+ "type": "string",
+ "description": "Space-separated list of requested scopes",
+ "example": "mcp:read mcp:write"
+ }
+ }
+ },
+ "example": {
+ "client_name": "Claude Code",
+ "redirect_uris": [
+ "http://localhost:54321/callback"
+ ],
+ "grant_types": [
+ "authorization_code"
+ ],
+ "response_types": [
+ "code"
+ ],
+ "token_endpoint_auth_method": "none"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Client registered successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "description": "Dynamic Client Registration response per RFC 7591",
+ "properties": {
+ "client_id": {
+ "type": "string",
+ "description": "Issued client identifier",
+ "example": "550e8400-e29b-41d4-a716-446655440000"
+ },
+ "client_name": {
+ "type": "string",
+ "description": "Human-readable name of the client application"
+ },
+ "redirect_uris": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Registered redirect URIs"
+ },
+ "grant_types": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Registered grant types"
+ },
+ "response_types": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Registered response types"
+ },
+ "token_endpoint_auth_method": {
+ "type": "string",
+ "description": "Token endpoint authentication method (always \"none\")"
+ }
+ }
+ },
+ "example": {
+ "client_id": "550e8400-e29b-41d4-a716-446655440000",
+ "client_name": "Claude Code",
+ "redirect_uris": [
+ "http://localhost:54321/callback"
+ ],
+ "grant_types": [
+ "authorization_code"
+ ],
+ "token_endpoint_auth_method": "none"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BifrostError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "No per-user OAuth MCP clients configured",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "Config store is disabled",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ManagementErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/oauth/per-user/authorize": {
+ "get": {
+ "operationId": "authorizePerUserOAuth",
+ "summary": "Authorization endpoint (OAuth 2.1)",
+ "description": "OAuth 2.1 authorization endpoint. Validates the request parameters, creates a\nbrowser-bound `PendingFlow` record (15-minute TTL), and redirects the user to\nthe Bifrost consent screen at `/oauth/consent?flow_id=xxx`.\n\n**PKCE is required** — `code_challenge` and `code_challenge_method=S256` must\nbe provided. Plain code challenges are not supported.\n\nA `__bifrost_flow_secret` HttpOnly SameSite=Lax cookie is set on redirect to\nbind the consent flow to the initiating browser session (CSRF protection).\n\nAuthentication is not required — this is part of the unauthenticated OAuth bootstrap flow.\n",
+ "tags": [
+ "OAuth",
+ "Per-User OAuth"
+ ],
+ "parameters": [
+ {
+ "name": "response_type",
+ "in": "query",
+ "required": true,
+ "description": "Must be `code`",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "code"
+ ]
+ }
+ },
+ {
+ "name": "client_id",
+ "in": "query",
+ "required": true,
+ "description": "Client ID obtained from the registration endpoint",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "redirect_uri",
+ "in": "query",
+ "required": true,
+ "description": "Must match a URI registered for this client",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "code_challenge",
+ "in": "query",
+ "required": true,
+ "description": "PKCE code challenge (Base64URL-encoded SHA-256 of the code verifier)",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "code_challenge_method",
+ "in": "query",
+ "required": true,
+ "description": "Must be `S256`",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "S256"
+ ]
+ }
+ },
+ {
+ "name": "state",
+ "in": "query",
+ "required": false,
+ "description": "Opaque value to maintain state between request and callback (CSRF protection)",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "302": {
+ "description": "Redirect to consent screen at `/oauth/consent?flow_id=xxx`",
+ "headers": {
+ "Location": {
+ "schema": {
+ "type": "string"
+ },
+ "description": "URL of the consent screen"
+ },
+ "Set-Cookie": {
+ "schema": {
+ "type": "string"
+ },
+ "description": "`__bifrost_flow_secret` HttpOnly SameSite=Lax cookie for browser binding"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BifrostError"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "No per-user OAuth MCP clients configured, or unknown client_id",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "Config store is disabled",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ManagementErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/oauth/per-user/token": {
+ "post": {
+ "operationId": "exchangePerUserOAuthToken",
+ "summary": "Token endpoint (OAuth 2.1)",
+ "description": "OAuth 2.1 token endpoint. Exchanges a single-use authorization code (5-minute TTL)\nfor a Bifrost-issued access token (24-hour TTL) using PKCE verification.\n\nThe request body must be `application/x-www-form-urlencoded`.\n\nThe returned `access_token` is the Bearer token to use on subsequent `/mcp` requests.\nIt carries the user's upstream service tokens (Notion, GitHub, etc.) linked to their\nidentity (Virtual Key or User ID) from the consent flow.\n\nAuthentication is not required — this is part of the unauthenticated OAuth bootstrap flow.\n",
+ "tags": [
+ "OAuth",
+ "Per-User OAuth"
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "grant_type",
+ "code",
+ "code_verifier"
+ ],
+ "properties": {
+ "grant_type": {
+ "type": "string",
+ "description": "Must be `authorization_code`",
+ "enum": [
+ "authorization_code"
+ ]
+ },
+ "code": {
+ "type": "string",
+ "description": "Authorization code received in the redirect callback"
+ },
+ "redirect_uri": {
+ "type": "string",
+ "description": "Must match the redirect_uri used in the authorize request (if provided)"
+ },
+ "client_id": {
+ "type": "string",
+ "description": "Client ID (optional — code is already bound to the client)"
+ },
+ "code_verifier": {
+ "type": "string",
+ "description": "PKCE code verifier — the raw secret whose SHA-256 matches the code_challenge"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Token issued successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "description": "OAuth 2.1 token response from the token endpoint",
+ "properties": {
+ "access_token": {
+ "type": "string",
+ "description": "Bifrost-issued access token (24h TTL). Use as Bearer token on /mcp requests."
+ },
+ "token_type": {
+ "type": "string",
+ "description": "Token type, always \"Bearer\"",
+ "example": "Bearer"
+ },
+ "expires_in": {
+ "type": "integer",
+ "description": "Seconds until the access token expires (86400 for 24h)",
+ "example": 86400
+ },
+ "scope": {
+ "type": "string",
+ "description": "Space-separated scopes granted"
+ }
+ }
+ },
+ "example": {
+ "access_token": "abc123xyz...",
+ "token_type": "Bearer",
+ "expires_in": 86400,
+ "scope": "mcp:read mcp:write"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid grant, expired code, PKCE failure, or unsupported grant type",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "string",
+ "enum": [
+ "invalid_grant",
+ "invalid_request",
+ "unsupported_grant_type"
+ ]
+ },
+ "error_description": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "No per-user OAuth MCP clients configured",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Server error or session creation failed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "string",
+ "enum": [
+ "server_error"
+ ]
+ },
+ "error_description": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/oauth/per-user/upstream/authorize": {
+ "get": {
+ "operationId": "authorizeUpstreamPerUserOAuth",
+ "summary": "Upstream OAuth proxy — authorize with upstream service",
+ "description": "Initiates an OAuth flow with an upstream MCP service (Notion, GitHub, etc.)\non behalf of the current user. Used during the consent flow (via \"Connect\" buttons\non the MCPs page) and at runtime when a tool call is made to an unauthenticated service.\n\n**Consent flow** — provide `flow_id` (from the pending consent flow). The browser-binding\ncookie (`__bifrost_flow_secret`) is validated.\n\n**Runtime flow** — provide `session` (the Bifrost session ID from the token endpoint).\nUsed when a service was skipped during consent and needs to be connected later.\n\nOn success, redirects the user to the upstream provider's authorize URL. After the user\ngrants access, the upstream callback lands at `/api/oauth/callback`, stores the upstream\ntoken against the user's identity, and redirects back to the consent screen (consent flow)\nor returns an authorization success page (runtime flow).\n\nAuthentication is not required — cookie/session validation is performed instead.\n",
+ "tags": [
+ "OAuth",
+ "Per-User OAuth"
+ ],
+ "parameters": [
+ {
+ "name": "mcp_client_id",
+ "in": "query",
+ "required": true,
+ "description": "ID of the per-user OAuth MCP client to authenticate with",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "flow_id",
+ "in": "query",
+ "required": false,
+ "description": "Pending consent flow ID. Required if `session` is not provided.\nThe `__bifrost_flow_secret` cookie must be present and match the flow.\n",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "session",
+ "in": "query",
+ "required": false,
+ "description": "Bifrost session ID (from the token endpoint). Required if `flow_id` is not provided.\nUsed for runtime (post-consent) upstream authorization.\n",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "302": {
+ "description": "Redirect to upstream OAuth provider's authorize URL",
+ "headers": {
+ "Location": {
+ "schema": {
+ "type": "string"
+ },
+ "description": "Upstream provider authorization URL with PKCE parameters"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BifrostError"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Invalid or expired flow/session",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ManagementErrorResponse"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Browser-binding cookie mismatch (CSRF protection)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ManagementErrorResponse"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "MCP client not found or not configured for per-user OAuth",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ManagementErrorResponse"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "Config store is disabled",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ManagementErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/oauth/consent": {
+ "get": {
+ "operationId": "getConsentIdentityPage",
+ "summary": "Consent identity selection page",
+ "description": "Renders the identity selection screen where the user chooses how to identify\nthemselves for the session: Virtual Key, User ID, or Skip (session-only auth).\n\nThe `__bifrost_flow_secret` HttpOnly cookie set during `/api/oauth/per-user/authorize`\nmust be present — it binds the consent flow to the initiating browser.\n\nThe Skip option is only shown when `enforce_auth_on_inference` is `false` in config.\n",
+ "tags": [
+ "Per-User OAuth",
+ "Consent Flow"
+ ],
+ "parameters": [
+ {
+ "name": "flow_id",
+ "in": "query",
+ "required": true,
+ "description": "Pending flow ID from the authorize redirect",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "error",
+ "in": "query",
+ "required": false,
+ "description": "Error message to display (used on redirect-back from failed form submissions)",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Identity selection HTML page",
+ "content": {
+ "text/html": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Missing or expired flow_id",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Browser-binding cookie mismatch",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/oauth/consent/mcps": {
+ "get": {
+ "operationId": "getConsentMCPsPage",
+ "summary": "Consent MCP services page",
+ "description": "Renders the MCP services connection screen. Shows all per-user OAuth MCP servers\navailable on the user's Virtual Key (or all servers if no VK was selected).\nEach service shows a \"Connect\" link or a \"Connected ✓\" badge.\n\nRequires the `__bifrost_flow_secret` browser-binding cookie.\n",
+ "tags": [
+ "Per-User OAuth",
+ "Consent Flow"
+ ],
+ "parameters": [
+ {
+ "name": "flow_id",
+ "in": "query",
+ "required": true,
+ "description": "Pending flow ID",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "MCP services connection HTML page",
+ "content": {
+ "text/html": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Missing or expired flow_id",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Browser-binding cookie mismatch",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/oauth/per-user/consent/vk": {
+ "post": {
+ "operationId": "submitConsentVirtualKey",
+ "summary": "Submit Virtual Key identity",
+ "description": "Validates the submitted Virtual Key and links it to the pending flow as the user's\nidentity. On success, redirects to the MCPs page. On failure, redirects back to the\nidentity page with an error message.\n\nRequest body is `application/x-www-form-urlencoded` (browser form submission).\n",
+ "tags": [
+ "Per-User OAuth",
+ "Consent Flow"
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "flow_id",
+ "vk"
+ ],
+ "properties": {
+ "flow_id": {
+ "type": "string",
+ "description": "Pending flow ID"
+ },
+ "vk": {
+ "type": "string",
+ "description": "Virtual Key value (validated against the database)"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "302": {
+ "description": "Redirect to `/oauth/consent/mcps?flow_id=xxx` on success, or back to\n`/oauth/consent?flow_id=xxx&error=...` on failure.\n",
+ "headers": {
+ "Location": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/oauth/per-user/consent/user-id": {
+ "post": {
+ "operationId": "submitConsentUserID",
+ "summary": "Submit User ID identity",
+ "description": "Links a self-declared User ID to the pending flow as the user's identity.\nOn success, redirects to the MCPs page.\n\nThe User ID is self-declared with no server-side verification — it matches\nthe trust model of the `X-Bf-User-Id` header in the LLM Gateway path.\n\nRequest body is `application/x-www-form-urlencoded` (browser form submission).\n",
+ "tags": [
+ "Per-User OAuth",
+ "Consent Flow"
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "flow_id",
+ "user_id"
+ ],
+ "properties": {
+ "flow_id": {
+ "type": "string",
+ "description": "Pending flow ID"
+ },
+ "user_id": {
+ "type": "string",
+ "description": "Self-declared user identifier (max 255 characters)"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "302": {
+ "description": "Redirect to `/oauth/consent/mcps?flow_id=xxx` on success, or back to\n`/oauth/consent?flow_id=xxx&error=...` on failure.\n",
+ "headers": {
+ "Location": {
"schema": {
"type": "string"
}
}
}
- },
- "400": {
- "description": "OAuth authorization failed or missing required parameters",
- "content": {
- "text/html": {
+ }
+ }
+ }
+ },
+ "/api/oauth/per-user/consent/skip": {
+ "post": {
+ "operationId": "skipConsentIdentity",
+ "summary": "Skip identity selection",
+ "description": "Skips identity selection and proceeds directly to the MCPs page. Upstream service\ntokens will be stored against the session token only (not a persistent identity),\nso they will not carry over to other sessions or the LLM Gateway.\n\nOnly available when `enforce_auth_on_inference` is `false` in config. Returns a\nredirect back to the identity page with an error if auth enforcement is enabled.\n\nRequest body is `application/x-www-form-urlencoded` (browser form submission).\n",
+ "tags": [
+ "Per-User OAuth",
+ "Consent Flow"
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "flow_id"
+ ],
+ "properties": {
+ "flow_id": {
+ "type": "string",
+ "description": "Pending flow ID"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "302": {
+ "description": "Redirect to `/oauth/consent/mcps?flow_id=xxx` on success, or back to\n`/oauth/consent?flow_id=xxx&error=...` if identity enforcement is required.\n",
+ "headers": {
+ "Location": {
"schema": {
"type": "string"
}
@@ -32086,38 +33041,78 @@
}
}
},
- "/api/oauth/config/{id}/status": {
- "get": {
- "operationId": "getOAuthConfigStatus",
- "summary": "Get OAuth config status",
- "description": "Retrieves the current status of an OAuth configuration.\nShows whether the OAuth flow is pending, authorized, or failed,\nand includes token expiration and scopes if authorized.\n",
+ "/api/oauth/per-user/consent/submit": {
+ "post": {
+ "operationId": "submitConsent",
+ "summary": "Finalize consent flow",
+ "description": "Finalizes the consent flow atomically:\n1. Creates a `TablePerUserOAuthSession` (24h Bifrost session token)\n2. Transfers upstream tokens from the flow proxy to the session\n3. Issues a single-use `TablePerUserOAuthCode` (5-minute TTL, PKCE-bound)\n4. Deletes the `PendingFlow`\n5. Redirects to the MCP client's `redirect_uri` with `code` and `state`\n\nThe MCP client then exchanges the code at `/api/oauth/per-user/token`.\n\nRequest body is `application/x-www-form-urlencoded` (browser form submission).\n",
"tags": [
- "OAuth"
+ "Per-User OAuth",
+ "Consent Flow"
],
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": true,
- "description": "OAuth config ID",
- "schema": {
- "type": "string"
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "flow_id"
+ ],
+ "properties": {
+ "flow_id": {
+ "type": "string",
+ "description": "Pending flow ID"
+ }
+ }
+ }
}
}
- ],
+ },
"responses": {
- "200": {
- "description": "OAuth config status retrieved successfully",
+ "302": {
+ "description": "Redirect to the MCP client's registered `redirect_uri` with\n`?code=xxx&state=yyy` query parameters.\n",
+ "headers": {
+ "Location": {
+ "schema": {
+ "type": "string"
+ },
+ "description": "MCP client callback URL with code and state"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/OAuthConfigStatus"
+ "$ref": "#/components/schemas/BifrostError"
}
}
}
},
- "404": {
- "description": "OAuth config not found",
+ "403": {
+ "description": "Browser-binding cookie mismatch",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ManagementErrorResponse"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Consent flow already submitted (duplicate submission prevention)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ManagementErrorResponse"
+ }
+ }
+ }
+ },
+ "410": {
+ "description": "Consent flow expired",
"content": {
"application/json": {
"schema": {
@@ -32137,42 +33132,212 @@
}
}
}
- },
- "delete": {
- "operationId": "revokeOAuthConfig",
- "summary": "Revoke OAuth config",
- "description": "Revokes an OAuth configuration and its associated access token.\nAfter revocation, the MCP client will no longer be able to use this OAuth token.\n",
+ }
+ },
+ "/.well-known/oauth-protected-resource": {
+ "get": {
+ "operationId": "getOAuthProtectedResourceMetadata",
+ "summary": "Protected Resource Metadata (RFC 9728)",
+ "description": "Returns the OAuth 2.0 Protected Resource Metadata document per RFC 9728.\n\nMCP clients fetch this after receiving a `401` response from `/mcp` (with a\n`WWW-Authenticate: Bearer resource_metadata=\".../.well-known/oauth-protected-resource\"`\nheader). The response tells the client which authorization server(s) protect the\n`/mcp` resource so it can proceed with discovery.\n\nReturns `404` when no MCP clients are configured with `auth_type: per_user_oauth`.\n",
"tags": [
- "OAuth"
+ "OAuth",
+ "Per-User OAuth"
],
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "required": true,
- "description": "OAuth config ID",
- "schema": {
- "type": "string"
+ "responses": {
+ "200": {
+ "description": "Protected resource metadata",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "description": "OAuth 2.0 Protected Resource Metadata per RFC 9728.\nReturned by /.well-known/oauth-protected-resource to tell MCP clients\nwhich authorization server(s) protect the /mcp endpoint.\n",
+ "properties": {
+ "resource": {
+ "type": "string",
+ "description": "URL of the protected resource (Bifrost's /mcp endpoint)",
+ "example": "https://your-bifrost-domain.com/mcp"
+ },
+ "authorization_servers": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of authorization server issuer URLs",
+ "example": [
+ "https://your-bifrost-domain.com"
+ ]
+ },
+ "scopes_supported": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Scopes supported by this resource",
+ "example": [
+ "mcp:read",
+ "mcp:write"
+ ]
+ },
+ "bearer_methods_supported": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Supported methods for passing Bearer tokens",
+ "example": [
+ "header"
+ ]
+ }
+ }
+ },
+ "example": {
+ "resource": "https://your-bifrost-domain.com/mcp",
+ "authorization_servers": [
+ "https://your-bifrost-domain.com"
+ ],
+ "scopes_supported": [
+ "mcp:read",
+ "mcp:write"
+ ],
+ "bearer_methods_supported": [
+ "header"
+ ]
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "No per-user OAuth MCP clients configured",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
}
}
+ }
+ }
+ },
+ "/.well-known/oauth-authorization-server": {
+ "get": {
+ "operationId": "getOAuthAuthorizationServerMetadata",
+ "summary": "Authorization Server Metadata (RFC 8414)",
+ "description": "Returns the OAuth 2.0 Authorization Server Metadata document per RFC 8414.\n\nAfter fetching the Protected Resource Metadata, MCP clients fetch this endpoint\nto discover Bifrost's OAuth endpoints (register, authorize, token) and capabilities\n(PKCE methods, grant types, etc.).\n\nReturns `404` when no MCP clients are configured with `auth_type: per_user_oauth`.\n",
+ "tags": [
+ "OAuth",
+ "Per-User OAuth"
],
"responses": {
"200": {
- "description": "OAuth token revoked successfully",
+ "description": "Authorization server metadata",
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/SuccessResponse"
+ "type": "object",
+ "description": "OAuth 2.0 Authorization Server Metadata per RFC 8414.\nReturned by /.well-known/oauth-authorization-server to let MCP clients\ndiscover Bifrost's OAuth endpoints and capabilities.\n",
+ "properties": {
+ "issuer": {
+ "type": "string",
+ "description": "Authorization server issuer URL (Bifrost base URL)",
+ "example": "https://your-bifrost-domain.com"
+ },
+ "authorization_endpoint": {
+ "type": "string",
+ "description": "Authorization endpoint URL",
+ "example": "https://your-bifrost-domain.com/api/oauth/per-user/authorize"
+ },
+ "token_endpoint": {
+ "type": "string",
+ "description": "Token endpoint URL",
+ "example": "https://your-bifrost-domain.com/api/oauth/per-user/token"
+ },
+ "registration_endpoint": {
+ "type": "string",
+ "description": "Dynamic client registration endpoint URL",
+ "example": "https://your-bifrost-domain.com/api/oauth/per-user/register"
+ },
+ "response_types_supported": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "code"
+ ]
+ },
+ "grant_types_supported": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "authorization_code"
+ ]
+ },
+ "code_challenge_methods_supported": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Supported PKCE methods (only S256)",
+ "example": [
+ "S256"
+ ]
+ },
+ "token_endpoint_auth_methods_supported": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Supported token endpoint auth methods (public clients only)",
+ "example": [
+ "none"
+ ]
+ },
+ "scopes_supported": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "mcp:read",
+ "mcp:write"
+ ]
+ }
+ }
+ },
+ "example": {
+ "issuer": "https://your-bifrost-domain.com",
+ "authorization_endpoint": "https://your-bifrost-domain.com/api/oauth/per-user/authorize",
+ "token_endpoint": "https://your-bifrost-domain.com/api/oauth/per-user/token",
+ "registration_endpoint": "https://your-bifrost-domain.com/api/oauth/per-user/register",
+ "response_types_supported": [
+ "code"
+ ],
+ "grant_types_supported": [
+ "authorization_code"
+ ],
+ "code_challenge_methods_supported": [
+ "S256"
+ ],
+ "token_endpoint_auth_methods_supported": [
+ "none"
+ ],
+ "scopes_supported": [
+ "mcp:read",
+ "mcp:write"
+ ]
}
}
}
},
- "500": {
- "description": "Internal server error",
+ "404": {
+ "description": "No per-user OAuth MCP clients configured",
"content": {
- "application/json": {
+ "text/plain": {
"schema": {
- "$ref": "#/components/schemas/BifrostError"
+ "type": "string"
}
}
}
@@ -40394,6 +41559,16 @@
}
}
},
+ "NotFound": {
+ "description": "Resource not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BifrostError"
+ }
+ }
+ }
+ },
"InternalError": {
"description": "Internal server error",
"content": {
@@ -52123,9 +53298,10 @@
"enum": [
"none",
"headers",
- "oauth"
+ "oauth",
+ "per_user_oauth"
],
- "description": "Authentication type for MCP connections:\n- none: No authentication\n- headers: Header-based authentication (API keys, custom headers, etc.)\n- oauth: OAuth 2.0 authentication\n"
+ "description": "Authentication type for MCP connections:\n- none: No authentication\n- headers: Header-based authentication (API keys, custom headers, etc.)\n- oauth: OAuth 2.0 authentication (shared admin token)\n- per_user_oauth: Per-user OAuth 2.1 (each end-user authenticates individually)\n"
},
"OAuthConfigRequest": {
"type": "object",
diff --git a/docs/openapi/openapi.yaml b/docs/openapi/openapi.yaml
index bc6bb5bae1..7142399a8b 100644
--- a/docs/openapi/openapi.yaml
+++ b/docs/openapi/openapi.yaml
@@ -626,13 +626,45 @@ paths:
$ref: './paths/management/mcp.yaml#/client-by-id'
/api/mcp/client/{id}/reconnect:
$ref: './paths/management/mcp.yaml#/client-reconnect'
+ /api/mcp/client/{id}/complete-oauth:
+ $ref: './paths/management/mcp.yaml#/client-complete-oauth'
- # MCP - OAuth
+ # MCP - OAuth (server-level)
/api/oauth/callback:
$ref: './paths/management/oauth.yaml#/oauth-callback'
/api/oauth/config/{id}/status:
$ref: './paths/management/oauth.yaml#/oauth-config-status'
+ # Per-User OAuth 2.1 Authorization Server
+ /api/oauth/per-user/register:
+ $ref: './paths/management/oauth.yaml#/per-user-oauth-register'
+ /api/oauth/per-user/authorize:
+ $ref: './paths/management/oauth.yaml#/per-user-oauth-authorize'
+ /api/oauth/per-user/token:
+ $ref: './paths/management/oauth.yaml#/per-user-oauth-token'
+ /api/oauth/per-user/upstream/authorize:
+ $ref: './paths/management/oauth.yaml#/per-user-oauth-upstream-authorize'
+
+ # Per-User OAuth Consent Flow (browser UI)
+ /oauth/consent:
+ $ref: './paths/management/oauth.yaml#/consent-identity-page'
+ /oauth/consent/mcps:
+ $ref: './paths/management/oauth.yaml#/consent-mcps-page'
+ /api/oauth/per-user/consent/vk:
+ $ref: './paths/management/oauth.yaml#/consent-submit-vk'
+ /api/oauth/per-user/consent/user-id:
+ $ref: './paths/management/oauth.yaml#/consent-submit-user-id'
+ /api/oauth/per-user/consent/skip:
+ $ref: './paths/management/oauth.yaml#/consent-skip'
+ /api/oauth/per-user/consent/submit:
+ $ref: './paths/management/oauth.yaml#/consent-submit'
+
+ # OAuth Discovery (RFC 9728 + RFC 8414)
+ /.well-known/oauth-protected-resource:
+ $ref: './paths/management/oauth.yaml#/oauth-protected-resource-metadata'
+ /.well-known/oauth-authorization-server:
+ $ref: './paths/management/oauth.yaml#/oauth-authorization-server-metadata'
+
# Governance - Virtual Keys
/api/governance/virtual-keys:
$ref: './paths/management/governance.yaml#/virtual-keys'
@@ -794,6 +826,12 @@ components:
application/json:
schema:
$ref: './schemas/inference/common.yaml#/BifrostError'
+ NotFound:
+ description: Resource not found
+ content:
+ application/json:
+ schema:
+ $ref: './schemas/inference/common.yaml#/BifrostError'
InternalError:
description: Internal server error
content:
diff --git a/docs/openapi/paths/management/oauth.yaml b/docs/openapi/paths/management/oauth.yaml
index 2224d1cd46..9a9de7b4da 100644
--- a/docs/openapi/paths/management/oauth.yaml
+++ b/docs/openapi/paths/management/oauth.yaml
@@ -104,3 +104,666 @@ oauth-config-status:
$ref: '../../schemas/management/common.yaml#/SuccessResponse'
'500':
$ref: '../../openapi.yaml#/components/responses/InternalError'
+
+# ─── Per-User OAuth 2.1 Authorization Server ───────────────────────────────
+# These endpoints implement RFC 7591 (dynamic registration), RFC 7636 (PKCE),
+# and the OAuth 2.1 authorization code flow. MCP clients use them automatically
+# when connecting to Bifrost's /mcp endpoint. Only active when at least one MCP
+# client is configured with auth_type: per_user_oauth.
+
+per-user-oauth-register:
+ post:
+ operationId: registerPerUserOAuthClient
+ summary: Register OAuth client (RFC 7591)
+ description: |
+ Dynamic Client Registration per RFC 7591. MCP clients (Claude Code, Cursor, etc.)
+ call this endpoint to obtain a `client_id` before initiating the authorization flow.
+
+ This endpoint is only available when at least one MCP client is configured with
+ `auth_type: per_user_oauth`. Returns `404` otherwise.
+
+ Authentication is not required — this is part of the unauthenticated OAuth bootstrap flow.
+ tags:
+ - OAuth
+ - Per-User OAuth
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/oauth.yaml#/PerUserOAuthClientRegistrationRequest'
+ example:
+ client_name: "Claude Code"
+ redirect_uris: ["http://localhost:54321/callback"]
+ grant_types: ["authorization_code"]
+ response_types: ["code"]
+ token_endpoint_auth_method: "none"
+ responses:
+ '201':
+ description: Client registered successfully
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/oauth.yaml#/PerUserOAuthClientRegistrationResponse'
+ example:
+ client_id: "550e8400-e29b-41d4-a716-446655440000"
+ client_name: "Claude Code"
+ redirect_uris: ["http://localhost:54321/callback"]
+ grant_types: ["authorization_code"]
+ token_endpoint_auth_method: "none"
+ '400':
+ $ref: '../../openapi.yaml#/components/responses/BadRequest'
+ '404':
+ description: No per-user OAuth MCP clients configured
+ content:
+ text/plain:
+ schema:
+ type: string
+ '503':
+ description: Config store is disabled
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/common.yaml#/ErrorResponse'
+
+per-user-oauth-authorize:
+ get:
+ operationId: authorizePerUserOAuth
+ summary: Authorization endpoint (OAuth 2.1)
+ description: |
+ OAuth 2.1 authorization endpoint. Validates the request parameters, creates a
+ browser-bound `PendingFlow` record (15-minute TTL), and redirects the user to
+ the Bifrost consent screen at `/oauth/consent?flow_id=xxx`.
+
+ **PKCE is required** — `code_challenge` and `code_challenge_method=S256` must
+ be provided. Plain code challenges are not supported.
+
+ A `__bifrost_flow_secret` HttpOnly SameSite=Lax cookie is set on redirect to
+ bind the consent flow to the initiating browser session (CSRF protection).
+
+ Authentication is not required — this is part of the unauthenticated OAuth bootstrap flow.
+ tags:
+ - OAuth
+ - Per-User OAuth
+ parameters:
+ - name: response_type
+ in: query
+ required: true
+ description: Must be `code`
+ schema:
+ type: string
+ enum: [code]
+ - name: client_id
+ in: query
+ required: true
+ description: Client ID obtained from the registration endpoint
+ schema:
+ type: string
+ - name: redirect_uri
+ in: query
+ required: true
+ description: Must match a URI registered for this client
+ schema:
+ type: string
+ - name: code_challenge
+ in: query
+ required: true
+ description: PKCE code challenge (Base64URL-encoded SHA-256 of the code verifier)
+ schema:
+ type: string
+ - name: code_challenge_method
+ in: query
+ required: true
+ description: Must be `S256`
+ schema:
+ type: string
+ enum: [S256]
+ - name: state
+ in: query
+ required: false
+ description: Opaque value to maintain state between request and callback (CSRF protection)
+ schema:
+ type: string
+ responses:
+ '302':
+ description: Redirect to consent screen at `/oauth/consent?flow_id=xxx`
+ headers:
+ Location:
+ schema:
+ type: string
+ description: URL of the consent screen
+ Set-Cookie:
+ schema:
+ type: string
+ description: "`__bifrost_flow_secret` HttpOnly SameSite=Lax cookie for browser binding"
+ '400':
+ $ref: '../../openapi.yaml#/components/responses/BadRequest'
+ '404':
+ description: No per-user OAuth MCP clients configured, or unknown client_id
+ content:
+ text/plain:
+ schema:
+ type: string
+ '503':
+ description: Config store is disabled
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/common.yaml#/ErrorResponse'
+
+per-user-oauth-token:
+ post:
+ operationId: exchangePerUserOAuthToken
+ summary: Token endpoint (OAuth 2.1)
+ description: |
+ OAuth 2.1 token endpoint. Exchanges a single-use authorization code (5-minute TTL)
+ for a Bifrost-issued access token (24-hour TTL) using PKCE verification.
+
+ The request body must be `application/x-www-form-urlencoded`.
+
+ The returned `access_token` is the Bearer token to use on subsequent `/mcp` requests.
+ It carries the user's upstream service tokens (Notion, GitHub, etc.) linked to their
+ identity (Virtual Key or User ID) from the consent flow.
+
+ Authentication is not required — this is part of the unauthenticated OAuth bootstrap flow.
+ tags:
+ - OAuth
+ - Per-User OAuth
+ requestBody:
+ required: true
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ required:
+ - grant_type
+ - code
+ - code_verifier
+ properties:
+ grant_type:
+ type: string
+ description: Must be `authorization_code`
+ enum: [authorization_code]
+ code:
+ type: string
+ description: Authorization code received in the redirect callback
+ redirect_uri:
+ type: string
+ description: Must match the redirect_uri used in the authorize request (if provided)
+ client_id:
+ type: string
+ description: Client ID (optional — code is already bound to the client)
+ code_verifier:
+ type: string
+ description: PKCE code verifier — the raw secret whose SHA-256 matches the code_challenge
+ responses:
+ '200':
+ description: Token issued successfully
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/oauth.yaml#/PerUserOAuthTokenResponse'
+ example:
+ access_token: "abc123xyz..."
+ token_type: "Bearer"
+ expires_in: 86400
+ scope: "mcp:read mcp:write"
+ '400':
+ description: Invalid grant, expired code, PKCE failure, or unsupported grant type
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: string
+ enum: [invalid_grant, invalid_request, unsupported_grant_type]
+ error_description:
+ type: string
+ '404':
+ description: No per-user OAuth MCP clients configured
+ content:
+ text/plain:
+ schema:
+ type: string
+ '500':
+ description: Server error or session creation failed
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: string
+ enum: [server_error]
+ error_description:
+ type: string
+
+per-user-oauth-upstream-authorize:
+ get:
+ operationId: authorizeUpstreamPerUserOAuth
+ summary: Upstream OAuth proxy — authorize with upstream service
+ description: |
+ Initiates an OAuth flow with an upstream MCP service (Notion, GitHub, etc.)
+ on behalf of the current user. Used during the consent flow (via "Connect" buttons
+ on the MCPs page) and at runtime when a tool call is made to an unauthenticated service.
+
+ **Consent flow** — provide `flow_id` (from the pending consent flow). The browser-binding
+ cookie (`__bifrost_flow_secret`) is validated.
+
+ **Runtime flow** — provide `session` (the Bifrost session ID from the token endpoint).
+ Used when a service was skipped during consent and needs to be connected later.
+
+ On success, redirects the user to the upstream provider's authorize URL. After the user
+ grants access, the upstream callback lands at `/api/oauth/callback`, stores the upstream
+ token against the user's identity, and redirects back to the consent screen (consent flow)
+ or returns an authorization success page (runtime flow).
+
+ Authentication is not required — cookie/session validation is performed instead.
+ tags:
+ - OAuth
+ - Per-User OAuth
+ parameters:
+ - name: mcp_client_id
+ in: query
+ required: true
+ description: ID of the per-user OAuth MCP client to authenticate with
+ schema:
+ type: string
+ - name: flow_id
+ in: query
+ required: false
+ description: |
+ Pending consent flow ID. Required if `session` is not provided.
+ The `__bifrost_flow_secret` cookie must be present and match the flow.
+ schema:
+ type: string
+ - name: session
+ in: query
+ required: false
+ description: |
+ Bifrost session ID (from the token endpoint). Required if `flow_id` is not provided.
+ Used for runtime (post-consent) upstream authorization.
+ schema:
+ type: string
+ responses:
+ '302':
+ description: Redirect to upstream OAuth provider's authorize URL
+ headers:
+ Location:
+ schema:
+ type: string
+ description: Upstream provider authorization URL with PKCE parameters
+ '400':
+ $ref: '../../openapi.yaml#/components/responses/BadRequest'
+ '401':
+ description: Invalid or expired flow/session
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/common.yaml#/ErrorResponse'
+ '403':
+ description: Browser-binding cookie mismatch (CSRF protection)
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/common.yaml#/ErrorResponse'
+ '404':
+ description: MCP client not found or not configured for per-user OAuth
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/common.yaml#/ErrorResponse'
+ '503':
+ description: Config store is disabled
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/common.yaml#/ErrorResponse'
+
+# ─── Per-User OAuth Consent Flow (browser UI) ──────────────────────────────
+# These endpoints serve HTML pages and handle form submissions for the
+# multi-step consent flow. They are browser-facing, not JSON API endpoints.
+# All endpoints validate the __bifrost_flow_secret browser-binding cookie.
+
+consent-identity-page:
+ get:
+ operationId: getConsentIdentityPage
+ summary: Consent identity selection page
+ description: |
+ Renders the identity selection screen where the user chooses how to identify
+ themselves for the session: Virtual Key, User ID, or Skip (session-only auth).
+
+ The `__bifrost_flow_secret` HttpOnly cookie set during `/api/oauth/per-user/authorize`
+ must be present — it binds the consent flow to the initiating browser.
+
+ The Skip option is only shown when `enforce_auth_on_inference` is `false` in config.
+ tags:
+ - Per-User OAuth
+ - Consent Flow
+ parameters:
+ - name: flow_id
+ in: query
+ required: true
+ description: Pending flow ID from the authorize redirect
+ schema:
+ type: string
+ - name: error
+ in: query
+ required: false
+ description: Error message to display (used on redirect-back from failed form submissions)
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Identity selection HTML page
+ content:
+ text/html:
+ schema:
+ type: string
+ '400':
+ description: Missing or expired flow_id
+ content:
+ text/plain:
+ schema:
+ type: string
+ '403':
+ description: Browser-binding cookie mismatch
+ content:
+ text/plain:
+ schema:
+ type: string
+
+consent-mcps-page:
+ get:
+ operationId: getConsentMCPsPage
+ summary: Consent MCP services page
+ description: |
+ Renders the MCP services connection screen. Shows all per-user OAuth MCP servers
+ available on the user's Virtual Key (or all servers if no VK was selected).
+ Each service shows a "Connect" link or a "Connected ✓" badge.
+
+ Requires the `__bifrost_flow_secret` browser-binding cookie.
+ tags:
+ - Per-User OAuth
+ - Consent Flow
+ parameters:
+ - name: flow_id
+ in: query
+ required: true
+ description: Pending flow ID
+ schema:
+ type: string
+ responses:
+ '200':
+ description: MCP services connection HTML page
+ content:
+ text/html:
+ schema:
+ type: string
+ '400':
+ description: Missing or expired flow_id
+ content:
+ text/plain:
+ schema:
+ type: string
+ '403':
+ description: Browser-binding cookie mismatch
+ content:
+ text/plain:
+ schema:
+ type: string
+
+consent-submit-vk:
+ post:
+ operationId: submitConsentVirtualKey
+ summary: Submit Virtual Key identity
+ description: |
+ Validates the submitted Virtual Key and links it to the pending flow as the user's
+ identity. On success, redirects to the MCPs page. On failure, redirects back to the
+ identity page with an error message.
+
+ Request body is `application/x-www-form-urlencoded` (browser form submission).
+ tags:
+ - Per-User OAuth
+ - Consent Flow
+ requestBody:
+ required: true
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ required: [flow_id, vk]
+ properties:
+ flow_id:
+ type: string
+ description: Pending flow ID
+ vk:
+ type: string
+ description: Virtual Key value (validated against the database)
+ responses:
+ '302':
+ description: |
+ Redirect to `/oauth/consent/mcps?flow_id=xxx` on success, or back to
+ `/oauth/consent?flow_id=xxx&error=...` on failure.
+ headers:
+ Location:
+ schema:
+ type: string
+
+consent-submit-user-id:
+ post:
+ operationId: submitConsentUserID
+ summary: Submit User ID identity
+ description: |
+ Links a self-declared User ID to the pending flow as the user's identity.
+ On success, redirects to the MCPs page.
+
+ The User ID is self-declared with no server-side verification — it matches
+ the trust model of the `X-Bf-User-Id` header in the LLM Gateway path.
+
+ Request body is `application/x-www-form-urlencoded` (browser form submission).
+ tags:
+ - Per-User OAuth
+ - Consent Flow
+ requestBody:
+ required: true
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ required: [flow_id, user_id]
+ properties:
+ flow_id:
+ type: string
+ description: Pending flow ID
+ user_id:
+ type: string
+ description: Self-declared user identifier (max 255 characters)
+ responses:
+ '302':
+ description: |
+ Redirect to `/oauth/consent/mcps?flow_id=xxx` on success, or back to
+ `/oauth/consent?flow_id=xxx&error=...` on failure.
+ headers:
+ Location:
+ schema:
+ type: string
+
+consent-skip:
+ post:
+ operationId: skipConsentIdentity
+ summary: Skip identity selection
+ description: |
+ Skips identity selection and proceeds directly to the MCPs page. Upstream service
+ tokens will be stored against the session token only (not a persistent identity),
+ so they will not carry over to other sessions or the LLM Gateway.
+
+ Only available when `enforce_auth_on_inference` is `false` in config. Returns a
+ redirect back to the identity page with an error if auth enforcement is enabled.
+
+ Request body is `application/x-www-form-urlencoded` (browser form submission).
+ tags:
+ - Per-User OAuth
+ - Consent Flow
+ requestBody:
+ required: true
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ required: [flow_id]
+ properties:
+ flow_id:
+ type: string
+ description: Pending flow ID
+ responses:
+ '302':
+ description: |
+ Redirect to `/oauth/consent/mcps?flow_id=xxx` on success, or back to
+ `/oauth/consent?flow_id=xxx&error=...` if identity enforcement is required.
+ headers:
+ Location:
+ schema:
+ type: string
+
+consent-submit:
+ post:
+ operationId: submitConsent
+ summary: Finalize consent flow
+ description: |
+ Finalizes the consent flow atomically:
+ 1. Creates a `TablePerUserOAuthSession` (24h Bifrost session token)
+ 2. Transfers upstream tokens from the flow proxy to the session
+ 3. Issues a single-use `TablePerUserOAuthCode` (5-minute TTL, PKCE-bound)
+ 4. Deletes the `PendingFlow`
+ 5. Redirects to the MCP client's `redirect_uri` with `code` and `state`
+
+ The MCP client then exchanges the code at `/api/oauth/per-user/token`.
+
+ Request body is `application/x-www-form-urlencoded` (browser form submission).
+ tags:
+ - Per-User OAuth
+ - Consent Flow
+ requestBody:
+ required: true
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ required: [flow_id]
+ properties:
+ flow_id:
+ type: string
+ description: Pending flow ID
+ responses:
+ '302':
+ description: |
+ Redirect to the MCP client's registered `redirect_uri` with
+ `?code=xxx&state=yyy` query parameters.
+ headers:
+ Location:
+ schema:
+ type: string
+ description: MCP client callback URL with code and state
+ '400':
+ $ref: '../../openapi.yaml#/components/responses/BadRequest'
+ '403':
+ description: Browser-binding cookie mismatch
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/common.yaml#/ErrorResponse'
+ '409':
+ description: Consent flow already submitted (duplicate submission prevention)
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/common.yaml#/ErrorResponse'
+ '410':
+ description: Consent flow expired
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/common.yaml#/ErrorResponse'
+ '500':
+ $ref: '../../openapi.yaml#/components/responses/InternalError'
+
+# ─── OAuth Discovery (RFC 9728 + RFC 8414) ─────────────────────────────────
+# These well-known endpoints enable MCP clients to auto-discover Bifrost's
+# OAuth configuration. Only active when at least one MCP client is configured
+# with auth_type: per_user_oauth.
+
+oauth-protected-resource-metadata:
+ get:
+ operationId: getOAuthProtectedResourceMetadata
+ summary: Protected Resource Metadata (RFC 9728)
+ description: |
+ Returns the OAuth 2.0 Protected Resource Metadata document per RFC 9728.
+
+ MCP clients fetch this after receiving a `401` response from `/mcp` (with a
+ `WWW-Authenticate: Bearer resource_metadata=".../.well-known/oauth-protected-resource"`
+ header). The response tells the client which authorization server(s) protect the
+ `/mcp` resource so it can proceed with discovery.
+
+ Returns `404` when no MCP clients are configured with `auth_type: per_user_oauth`.
+ tags:
+ - OAuth
+ - Per-User OAuth
+ responses:
+ '200':
+ description: Protected resource metadata
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/oauth.yaml#/ProtectedResourceMetadata'
+ example:
+ resource: "https://your-bifrost-domain.com/mcp"
+ authorization_servers: ["https://your-bifrost-domain.com"]
+ scopes_supported: ["mcp:read", "mcp:write"]
+ bearer_methods_supported: ["header"]
+ '404':
+ description: No per-user OAuth MCP clients configured
+ content:
+ text/plain:
+ schema:
+ type: string
+
+oauth-authorization-server-metadata:
+ get:
+ operationId: getOAuthAuthorizationServerMetadata
+ summary: Authorization Server Metadata (RFC 8414)
+ description: |
+ Returns the OAuth 2.0 Authorization Server Metadata document per RFC 8414.
+
+ After fetching the Protected Resource Metadata, MCP clients fetch this endpoint
+ to discover Bifrost's OAuth endpoints (register, authorize, token) and capabilities
+ (PKCE methods, grant types, etc.).
+
+ Returns `404` when no MCP clients are configured with `auth_type: per_user_oauth`.
+ tags:
+ - OAuth
+ - Per-User OAuth
+ responses:
+ '200':
+ description: Authorization server metadata
+ content:
+ application/json:
+ schema:
+ $ref: '../../schemas/management/oauth.yaml#/AuthorizationServerMetadata'
+ example:
+ issuer: "https://your-bifrost-domain.com"
+ authorization_endpoint: "https://your-bifrost-domain.com/api/oauth/per-user/authorize"
+ token_endpoint: "https://your-bifrost-domain.com/api/oauth/per-user/token"
+ registration_endpoint: "https://your-bifrost-domain.com/api/oauth/per-user/register"
+ response_types_supported: ["code"]
+ grant_types_supported: ["authorization_code"]
+ code_challenge_methods_supported: ["S256"]
+ token_endpoint_auth_methods_supported: ["none"]
+ scopes_supported: ["mcp:read", "mcp:write"]
+ '404':
+ description: No per-user OAuth MCP clients configured
+ content:
+ text/plain:
+ schema:
+ type: string
diff --git a/docs/openapi/schemas/management/oauth.yaml b/docs/openapi/schemas/management/oauth.yaml
index 84b2d00d0f..59e23ab351 100644
--- a/docs/openapi/schemas/management/oauth.yaml
+++ b/docs/openapi/schemas/management/oauth.yaml
@@ -2,12 +2,13 @@
MCPAuthType:
type: string
- enum: [none, headers, oauth]
+ enum: [none, headers, oauth, per_user_oauth]
description: |
Authentication type for MCP connections:
- none: No authentication
- headers: Header-based authentication (API keys, custom headers, etc.)
- - oauth: OAuth 2.0 authentication
+ - oauth: OAuth 2.0 authentication (shared admin token)
+ - per_user_oauth: Per-user OAuth 2.1 (each end-user authenticates individually)
OAuthConfigRequest:
type: object
@@ -130,3 +131,175 @@ OAuthToken:
type: string
format: date-time
description: When the token was last refreshed
+
+# Per-User OAuth 2.1 Authorization Server schemas
+
+PerUserOAuthClientRegistrationRequest:
+ type: object
+ description: |
+ Dynamic Client Registration request per RFC 7591.
+ MCP clients (Claude Code, Cursor, etc.) call this to obtain a client_id
+ before initiating the authorization flow.
+ required:
+ - redirect_uris
+ properties:
+ client_name:
+ type: string
+ description: Human-readable name of the client application
+ example: Claude Code
+ redirect_uris:
+ type: array
+ items:
+ type: string
+ description: List of allowed redirect URIs for this client
+ example: ["http://localhost:54321/callback"]
+ grant_types:
+ type: array
+ items:
+ type: string
+ description: Supported grant types. Defaults to ["authorization_code"]
+ example: ["authorization_code"]
+ response_types:
+ type: array
+ items:
+ type: string
+ description: Supported response types
+ example: ["code"]
+ token_endpoint_auth_method:
+ type: string
+ description: Token endpoint authentication method. Always "none" (public client)
+ example: none
+ scope:
+ type: string
+ description: Space-separated list of requested scopes
+ example: "mcp:read mcp:write"
+
+PerUserOAuthClientRegistrationResponse:
+ type: object
+ description: Dynamic Client Registration response per RFC 7591
+ properties:
+ client_id:
+ type: string
+ description: Issued client identifier
+ example: "550e8400-e29b-41d4-a716-446655440000"
+ client_name:
+ type: string
+ description: Human-readable name of the client application
+ redirect_uris:
+ type: array
+ items:
+ type: string
+ description: Registered redirect URIs
+ grant_types:
+ type: array
+ items:
+ type: string
+ description: Registered grant types
+ response_types:
+ type: array
+ items:
+ type: string
+ description: Registered response types
+ token_endpoint_auth_method:
+ type: string
+ description: Token endpoint authentication method (always "none")
+
+PerUserOAuthTokenResponse:
+ type: object
+ description: OAuth 2.1 token response from the token endpoint
+ properties:
+ access_token:
+ type: string
+ description: Bifrost-issued access token (24h TTL). Use as Bearer token on /mcp requests.
+ token_type:
+ type: string
+ description: Token type, always "Bearer"
+ example: Bearer
+ expires_in:
+ type: integer
+ description: Seconds until the access token expires (86400 for 24h)
+ example: 86400
+ scope:
+ type: string
+ description: Space-separated scopes granted
+
+ProtectedResourceMetadata:
+ type: object
+ description: |
+ OAuth 2.0 Protected Resource Metadata per RFC 9728.
+ Returned by /.well-known/oauth-protected-resource to tell MCP clients
+ which authorization server(s) protect the /mcp endpoint.
+ properties:
+ resource:
+ type: string
+ description: URL of the protected resource (Bifrost's /mcp endpoint)
+ example: "https://your-bifrost-domain.com/mcp"
+ authorization_servers:
+ type: array
+ items:
+ type: string
+ description: List of authorization server issuer URLs
+ example: ["https://your-bifrost-domain.com"]
+ scopes_supported:
+ type: array
+ items:
+ type: string
+ description: Scopes supported by this resource
+ example: ["mcp:read", "mcp:write"]
+ bearer_methods_supported:
+ type: array
+ items:
+ type: string
+ description: Supported methods for passing Bearer tokens
+ example: ["header"]
+
+AuthorizationServerMetadata:
+ type: object
+ description: |
+ OAuth 2.0 Authorization Server Metadata per RFC 8414.
+ Returned by /.well-known/oauth-authorization-server to let MCP clients
+ discover Bifrost's OAuth endpoints and capabilities.
+ properties:
+ issuer:
+ type: string
+ description: Authorization server issuer URL (Bifrost base URL)
+ example: "https://your-bifrost-domain.com"
+ authorization_endpoint:
+ type: string
+ description: Authorization endpoint URL
+ example: "https://your-bifrost-domain.com/api/oauth/per-user/authorize"
+ token_endpoint:
+ type: string
+ description: Token endpoint URL
+ example: "https://your-bifrost-domain.com/api/oauth/per-user/token"
+ registration_endpoint:
+ type: string
+ description: Dynamic client registration endpoint URL
+ example: "https://your-bifrost-domain.com/api/oauth/per-user/register"
+ response_types_supported:
+ type: array
+ items:
+ type: string
+ example: ["code"]
+ grant_types_supported:
+ type: array
+ items:
+ type: string
+ example: ["authorization_code"]
+ code_challenge_methods_supported:
+ type: array
+ items:
+ type: string
+ description: Supported PKCE methods (only S256)
+ example: ["S256"]
+ token_endpoint_auth_methods_supported:
+ type: array
+ items:
+ type: string
+ description: Supported token endpoint auth methods (public clients only)
+ example: ["none"]
+ scopes_supported:
+ type: array
+ items:
+ type: string
+ example: ["mcp:read", "mcp:write"]