Skip to content

feat: add JWT Bearer authentication path for /mcp with session validation and context injection#4508

Open
Pratham-Mishra04 wants to merge 1 commit into
06-17-feat_adds_mcp_oauth2_consent_flow_backendfrom
06-17-feat_wires_jwt_authentication_on__mcp_endpoint
Open

feat: add JWT Bearer authentication path for /mcp with session validation and context injection#4508
Pratham-Mishra04 wants to merge 1 commit into
06-17-feat_adds_mcp_oauth2_consent_flow_backendfrom
06-17-feat_wires_jwt_authentication_on__mcp_endpoint

Conversation

@Pratham-Mishra04

Copy link
Copy Markdown
Collaborator

Summary

This PR adds JWT Bearer token authentication to the /mcp endpoint, enabling Bifrost-issued JWTs to be used as a first-class authentication mechanism alongside existing VK/header credentials. It also introduces a session validation gate that prevents per-user upstream flows (OAuth and header submission) from being initiated unless the caller's identity has been actively verified for the current request.

Changes

  • Added extractBearerJWT to detect and extract JWT Bearer tokens from the Authorization header, distinguishing them from VK credentials by the eyJ prefix.
  • Added verifyMCPJWT to validate RS256-signed JWTs against the active signing key, enforcing expiry, issued-at, key ID, and RFC 8707 audience checks against the /mcp resource URL.
  • Added injectJWTContext to translate verified JWT claims (bf_mode=user|vk|session) into the same BifrostContext keys that header-based auth sets, ensuring downstream resolvers (governance, per-user OAuth, tool-group filtering) work without modification.
  • Introduced mcpAuthResult to carry the authenticated MCP server, JWT claims, and resolved VK out of getMCPServerForRequest, replacing the previous single return value.
  • Refactored getMCPServerForRequest to implement a prioritized auth flow: JWT → oauth-strict rejection → VK/header → anonymous dev mode. In oauth strict mode, non-JWT requests are rejected with a WWW-Authenticate header per RFC 9728.
  • Added BifrostContextKeyOAuth2JWTAuthenticated and BifrostContextKeyOAuth2JWTSessionValidated context keys. The session-validated key is set only when a user-mode JWT's sub is confirmed against an active dashboard session.
  • Added a session validation gate in both per_user_oauth.go and per_user_headers.go: when a request is JWT-authenticated in user mode, upstream flows cannot be initiated unless BifrostContextKeyOAuth2JWTSessionValidated is present.
  • Extracted ensureVKMCPServerByValue to allow VK server lookup by value from both the header path and the JWT VK path.
  • Removed the stale comment block about Bifrost not being an OAuth authorization server.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

go version
go test ./...
  • Send a request to /mcp with a valid Bifrost-issued JWT in Authorization: Bearer <token> and confirm the appropriate context keys are set and the correct scoped server is used.
  • Send a request with bf_mode=user JWT and no active dashboard session; confirm a 401 is returned with a message indicating no active session.
  • Send a request with bf_mode=user JWT and a mismatched session user; confirm rejection.
  • Configure MCPServerAuthMode=oauth and send a request with a VK header; confirm rejection with a WWW-Authenticate header.
  • Attempt to initiate a per-user OAuth or header submission flow with a user-mode JWT that has not been session-validated; confirm the flow is blocked.
  • Confirm anonymous dev-mode access (EnforceAuthOnInference=false, no credentials) still works.

Breaking changes

  • Yes
  • No

getMCPServerForRequest now returns *mcpAuthResult instead of *server.MCPServer. Any code calling this method directly must be updated. When MCPServerAuthMode is set to oauth, header-based credentials are no longer accepted at the /mcp endpoint.

Security considerations

  • JWT verification enforces RS256 signature, expiry, issued-at, key ID, and RFC 8707 audience binding to the /mcp resource URL, preventing token reuse across resources.
  • User-mode JWTs require an active dashboard session whose user ID matches bf_sub, preventing replay of valid tokens after session expiry.
  • The session validation gate on per-user upstream flows ensures that even a structurally valid JWT cannot trigger credential submission flows without a confirmed live session.
  • WWW-Authenticate headers are emitted on auth failures in discovery-enabled modes, per RFC 9728.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Pratham-Mishra04 commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 14d7d8ff-a693-4931-b894-8df131449e4c

📥 Commits

Reviewing files that changed from the base of the PR and between 1f9653e and b156095.

📒 Files selected for processing (5)
  • core/mcp/credstore/per_user_headers.go
  • core/mcp/credstore/per_user_oauth.go
  • core/schemas/bifrost.go
  • transports/bifrost-http/handlers/mcpserver.go
  • transports/bifrost-http/handlers/oauth2_jwt.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • core/schemas/bifrost.go
  • core/mcp/credstore/per_user_headers.go
  • core/mcp/credstore/per_user_oauth.go
  • transports/bifrost-http/handlers/oauth2_jwt.go
  • transports/bifrost-http/handlers/mcpserver.go

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added JWT-based authorization for the /mcp JSON-RPC and SSE endpoints, including strict claim verification and audience matching to the canonical resource.
    • Added request context markers indicating JWT authentication and whether user-session validation succeeded.
  • Security Enhancements

    • Enforced user-mode JWT session validation before initiating per-user OAuth re-auth/header submissions.
    • Auth/scoping failures now return 401 Unauthorized; SSE requests are rejected if identity injection fails.
    • Fail-closed handling for inactive virtual-key values.
  • Refactor

    • Consolidated MCP endpoint authentication/scoping into a unified selection flow; tightened /mcp route registration.

Walkthrough

Adds RS256 JWT authentication for the /mcp endpoint. Two new BifrostContextKey constants are defined. A new oauth2_jwt.go file implements JWT extraction, verification, and BifrostContext injection by bf_mode. getMCPServerForRequest is refactored to return an mcpAuthResult with JWT claims and enforces auth-mode rules. Per-user credential resolvers add session-validation preconditions for JWT user-mode requests.

Changes

MCP JWT Authentication

Layer / File(s) Summary
New JWT context-key constants
core/schemas/bifrost.go
Adds BifrostContextKeyOAuth2JWTAuthenticated and BifrostContextKeyOAuth2JWTSessionValidated exported constants to signal JWT auth state on /mcp requests.
JWT claims, verification, and context injection
transports/bifrost-http/handlers/oauth2_jwt.go
New file with jwtMCPClaims, extractBearerJWT, verifyMCPJWT (RS256 + kid + audience check), injectJWTContext (bf_mode dispatch into BifrostContext), and RFC 9728 wwwAuthenticateValue.
MCP handler auth refactor
transports/bifrost-http/handlers/mcpserver.go
Introduces mcpAuthResult struct; rewrites getMCPServerForRequest to return JWT claims, enforce auth-mode rules and oauth-strict rejection, and add ensureVKMCPServerByValue; handleMCPServer and handleMCPServerSSE consume the result and inject JWT context, returning 401 on failure; RegisterRoutes simplified; ensureVKMCPServer rejects inactive keys.
Per-user credential resolver guards
core/mcp/credstore/per_user_oauth.go, core/mcp/credstore/per_user_headers.go
Both ConnectionHeaders and buildAuthRequiredError add a precondition blocking upstream auth flows when JWT user-mode auth is present but session validation has not been confirmed.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MCPServerHandler
  participant getMCPServerForRequest
  participant verifyMCPJWT
  participant injectJWTContext
  participant CredStore

  Client->>MCPServerHandler: POST/GET /mcp (Authorization: Bearer eyJ...)
  MCPServerHandler->>getMCPServerForRequest: fasthttp.RequestCtx
  getMCPServerForRequest->>verifyMCPJWT: rawToken + Config store
  verifyMCPJWT-->>getMCPServerForRequest: jwtMCPClaims (bf_mode, scope, aud)
  getMCPServerForRequest-->>MCPServerHandler: mcpAuthResult{server, claims, vk}
  MCPServerHandler->>injectJWTContext: bifrostCtx + claims + vk
  injectJWTContext-->>MCPServerHandler: sets UserID/VK/SessionID + JWTAuthenticated=true
  MCPServerHandler->>CredStore: ConnectionHeaders(ctx)
  CredStore->>CredStore: check JWTAuthenticated + SessionValidated
  alt Session not validated
    CredStore-->>MCPServerHandler: error (identity not verified)
    MCPServerHandler-->>Client: 401 Unauthorized
  else Session validated
    CredStore-->>MCPServerHandler: upstream auth headers
    MCPServerHandler-->>Client: MCP response
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • maximhq/bifrost#3656: Both PRs make code changes in the MCP credstore per-user OAuth path—main PR adds a JWT "session validated" precondition in core/mcp/credstore/per_user_oauth.go, while the retrieved PR refactors that same per-user OAuth handling into the new credstore resolver infrastructure.
  • maximhq/bifrost#3703: Both PRs modify the per-user header auth "inline 401/submission flow" path in the credstore (the retrieved PR introduces perUserHeadersResolver and its MCPAuthRequiredError, while the main PR adds a JWT session-validation precondition before buildAuthRequiredError starts that same flow).
  • maximhq/bifrost#3724: Main PR's JWT session-validation precondition changes in core/mcp/credstore/per_user_headers.go and per_user_oauth.go build directly on the per-user headers flow and credential store introduced by the retrieved PR.

Suggested reviewers

  • akshaydeo
  • danpiths
  • roroghost17

🐇 A token arrives, signed and sealed tight,
RS256 guards the /mcp with might.
The session must pass, the JWT checked,
No unvalidated user shall be neglected.
Hop hop, the context is set just right! 🔑

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main feature: adding JWT Bearer authentication with session validation and context injection to the /mcp endpoint.
Description check ✅ Passed The PR description comprehensively covers all required sections: clear summary, detailed changes with design rationale, type of change, affected areas, testing instructions, breaking changes, and security considerations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 06-17-feat_wires_jwt_authentication_on__mcp_endpoint

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

The auth flow is well-structured: JWT signature, expiry, issuer, key ID, and audience are all verified before any identity is injected into the BifrostContext, and the session-validation gate correctly fails closed for user-mode flows that lack a confirmed live session.

No correctness or security regressions were found. The new JWT path correctly chains signature verification → mode-specific server selection → context injection, and the session gate in the credstores reliably blocks new upstream flow initiations without blocking existing tool calls. The two findings are minor: a redundant guard that is always true, and a double DB lookup for the same VK on cold cache. Neither affects auth outcomes.

No files require special attention; the most complex logic is in transports/bifrost-http/handlers/mcpserver.go and oauth2_jwt.go, both of which are well-commented and consistent.

Important Files Changed

Filename Overview
transports/bifrost-http/handlers/oauth2_jwt.go New file implementing JWT extraction, RS256 verification with issuer/audience pinning, and context injection; logic is well-structured with one minor concern about setting a governance-owned context key.
transports/bifrost-http/handlers/mcpserver.go getMCPServerForRequest refactored into a clean three-tier auth flow; VK-mode JWT path performs two separate DB round-trips for the same key; redundant inner discoveryEnabled guard in the JWT error branch.
core/mcp/credstore/per_user_oauth.go Session-validation gate correctly placed inside the flow-initiation branch; does not block retrieval of already-valid tokens, matching the stated design intent.
core/mcp/credstore/per_user_headers.go Session-validation gate mirrors per_user_oauth.go correctly; only fires on new submission-flow initiation for JWT user-mode requests.
core/schemas/bifrost.go Two new context key constants added with accurate inline documentation; correctly placed relative to surrounding MCP session keys.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["/mcp request"] --> B{extractBearerJWT\nstarts with eyJ?}
    B -- "yes + discoveryEnabled" --> C[verifyMCPJWT\nRS256 · iss · aud · kid · exp · iat]
    C -- error --> D["WWW-Authenticate header\n401 Unauthorized"]
    C -- ok --> E{bf_mode?}
    E -- user --> F{sessionUserID\nfrom SCIM middleware?}
    F -- "present, matches sub" --> G[sessionValidated = true]
    F -- "present, mismatch" --> D
    F -- absent --> H[sessionValidated = false]
    G --> I{bf_mode=session + enforceVK?}
    H --> I
    E -- session --> I
    I -- yes --> D
    I -- no --> J[mcpAuthResult + jwtClaims]
    E -- vk --> K[GetVirtualKey by ID\nIsActiveValue check]
    K -- inactive/not found --> D
    K -- ok --> L[ensureVKMCPServerByValue\ncache or GetVirtualKeyByValue]
    L --> J
    B -- "no / not discoveryEnabled" --> M{authMode == oauth?}
    M -- yes --> D
    M -- no --> N[getVKFromRequest]
    N -- "vk='' + !enforceVK" --> O[globalMCPServer anonymous]
    N -- "vk=''" --> D
    N -- vk present --> P[ensureVKMCPServerByValue]
    P --> Q[mcpAuthResult vk-only]
    J --> R[injectJWTContext\nset UserID / VK / SessionID\n+ JWT auth flags]
    R -- error --> D
    R -- ok --> S[HandleMessage / HandleConnection]
    O --> S
    Q --> S
    S --> T{per-user flow initiation needed?}
    T -- "jwtAuth=true, mode=user, sessionValidated=false" --> D
    T -- otherwise --> U[Proceed with upstream call]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["/mcp request"] --> B{extractBearerJWT\nstarts with eyJ?}
    B -- "yes + discoveryEnabled" --> C[verifyMCPJWT\nRS256 · iss · aud · kid · exp · iat]
    C -- error --> D["WWW-Authenticate header\n401 Unauthorized"]
    C -- ok --> E{bf_mode?}
    E -- user --> F{sessionUserID\nfrom SCIM middleware?}
    F -- "present, matches sub" --> G[sessionValidated = true]
    F -- "present, mismatch" --> D
    F -- absent --> H[sessionValidated = false]
    G --> I{bf_mode=session + enforceVK?}
    H --> I
    E -- session --> I
    I -- yes --> D
    I -- no --> J[mcpAuthResult + jwtClaims]
    E -- vk --> K[GetVirtualKey by ID\nIsActiveValue check]
    K -- inactive/not found --> D
    K -- ok --> L[ensureVKMCPServerByValue\ncache or GetVirtualKeyByValue]
    L --> J
    B -- "no / not discoveryEnabled" --> M{authMode == oauth?}
    M -- yes --> D
    M -- no --> N[getVKFromRequest]
    N -- "vk='' + !enforceVK" --> O[globalMCPServer anonymous]
    N -- "vk=''" --> D
    N -- vk present --> P[ensureVKMCPServerByValue]
    P --> Q[mcpAuthResult vk-only]
    J --> R[injectJWTContext\nset UserID / VK / SessionID\n+ JWT auth flags]
    R -- error --> D
    R -- ok --> S[HandleMessage / HandleConnection]
    O --> S
    Q --> S
    S --> T{per-user flow initiation needed?}
    T -- "jwtAuth=true, mode=user, sessionValidated=false" --> D
    T -- otherwise --> U[Proceed with upstream call]
Loading

Reviews (6): Last reviewed commit: "feat: wires jwt authentication on /mcp e..." | Re-trigger Greptile

Comment thread transports/bifrost-http/handlers/oauth2_jwt.go
Comment thread transports/bifrost-http/handlers/oauth2_jwt.go
Comment thread transports/bifrost-http/handlers/mcpserver.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@transports/bifrost-http/handlers/mcpserver.go`:
- Around line 598-607: The issue is that the code unconditionally rejects
user-mode JWT calls lacking a browser session by returning an error when
sessionUserID is empty, which prevents valid JWTs from reaching per-user
resolvers. Instead, restructure the MCPAuthModeUser check to only validate the
session when one exists: remove the early return error when sessionUserID is
empty, and only perform the session matching validation and set the
BifrostContextKeyOAuth2JWTSessionValidated flag if sessionUserID is not empty.
This allows valid user-mode JWTs without a dashboard session to proceed and lets
upstream OAuth/header resolvers apply their own authentication guards.
- Around line 667-684: The ensureVKMCPServerByValue and ensureVKMCPServer
methods lack a security check for inactive virtual keys that exists in the JWT
path. After calling GetVirtualKeyByValue in the ensureVKMCPServer method (around
line 698), add a validation check using vk.IsActiveValue() to ensure the virtual
key is active before caching or returning the server. If the virtual key is not
active, return an error with an appropriate message to fail closed on inactive
keys, matching the pattern used in other handlers like oauth2_consent.go and
governance/resolver.go.

In `@transports/bifrost-http/handlers/oauth2_jwt.go`:
- Around line 64-72: In the ParseWithClaims call with the key validation
callback function, replace the broad RSA family check (currently checking
*jwt.SigningMethodRSA) with a specific RS256 algorithm check by verifying
t.Method.Alg() equals "RS256" or using the specific RS256 signing method type.
Additionally, remove the jwt.WithIssuedAt() option and instead add explicit
validation within the callback function to ensure the iat (issued at) claim is
present and non-zero in the claims struct before returning the public key,
rejecting tokens that lack this required claim.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bed1a51b-929f-4dda-94db-038b2f0c1b73

📥 Commits

Reviewing files that changed from the base of the PR and between df31ae6 and d6a46f9.

📒 Files selected for processing (5)
  • core/mcp/credstore/per_user_headers.go
  • core/mcp/credstore/per_user_oauth.go
  • core/schemas/bifrost.go
  • transports/bifrost-http/handlers/mcpserver.go
  • transports/bifrost-http/handlers/oauth2_jwt.go

Comment thread transports/bifrost-http/handlers/mcpserver.go
Comment thread transports/bifrost-http/handlers/mcpserver.go
Comment thread transports/bifrost-http/handlers/oauth2_jwt.go Outdated
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 06-17-feat_wires_jwt_authentication_on__mcp_endpoint branch from d6a46f9 to 5d1f810 Compare June 18, 2026 07:34
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 06-17-feat_adds_mcp_oauth2_consent_flow_backend branch from df31ae6 to 6ebebaf Compare June 18, 2026 07:34
@coderabbitai coderabbitai Bot requested a review from roroghost17 June 18, 2026 07:36
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 06-17-feat_adds_mcp_oauth2_consent_flow_backend branch from 76885d3 to d82df7b Compare June 18, 2026 12:22
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 06-17-feat_wires_jwt_authentication_on__mcp_endpoint branch from b156095 to 8a84174 Compare June 18, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants