Skip to content

fix: Shim for MCP Auth#1872

Draft
edwinjosechittilappilly wants to merge 1 commit into
mainfrom
fix-mcp-auth
Draft

fix: Shim for MCP Auth#1872
edwinjosechittilappilly wants to merge 1 commit into
mainfrom
fix-mcp-auth

Conversation

@edwinjosechittilappilly

@edwinjosechittilappilly edwinjosechittilappilly commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

fix: Shim for MCP Auth

Summary by CodeRabbit

  • New Features

    • Enhanced JWT authentication token handling for MCP requests with improved forwarding mechanisms across internal proxying.
  • Chores

    • Added configuration support for customizable JWT header names in MCP authentication flows.

@github-actions github-actions Bot added backend 🔷 Issues related to backend services (OpenSearch, Langflow, APIs) bug 🔴 Something isn't working. labels Jun 15, 2026
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds an ASGI middleware (_ForwardJWTToMCPMiddleware) that copies the inbound Authorization JWT into a configurable alternate header (X-OpenRAG-JWT by default) before delegating to the MCP sub-app. mount_mcp() wraps the MCP HTTP app with this middleware. The /v1 authentication dependency gains a fallback to read that same forwarded header.

Changes

MCP JWT Forwarding Shim

Layer / File(s) Summary
Forwarded JWT header config
src/config/settings.py
Adds get_mcp_forwarded_jwt_header() returning the OPENRAG_MCP_JWT_HEADER env var with a default of X-OpenRAG-JWT.
ASGI middleware and mount wiring
src/app/routes/mcp.py
Implements _ForwardJWTToMCPMiddleware, which reads Authorization from scope["headers"] and copies it into the configured forwarded-JWT header when absent; mount_mcp() wraps mcp_http_app with this middleware before mounting at /mcp.
Auth dependency fallback
src/dependencies.py
get_api_key_user_async now falls back to get_mcp_forwarded_jwt_header() when the primary gateway JWT header is missing or empty.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing a shim for MCP authentication by forwarding JWT tokens across FastMCP's proxying behavior.
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 fix-mcp-auth

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added bug 🔴 Something isn't working. and removed bug 🔴 Something isn't working. labels Jun 15, 2026

@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: 2

🤖 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 `@src/app/routes/mcp.py`:
- Around line 44-52: The code currently only appends the Authorization header as
the target when the target header is not present (when has_target is false),
which allows client-supplied target headers to take precedence. Instead, you
need to strip any inbound target header from the headers list first, then
unconditionally append the Authorization value as the target header when auth is
present. Modify the logic to filter the headers list to remove any headers where
the lowercased name equals the target value (removing client-supplied target
headers), then if auth is not none, append it as the target header in the
modified scope headers list.

In `@src/dependencies.py`:
- Around line 854-859: The JWT header retrieval logic in the raw_jwt assignment
uses `or` to fallback from the primary JWT auth header to the MCP forwarded JWT
header. However, if the primary header (from get_jwt_auth_header()) contains
only whitespace, it evaluates as truthy and prevents the fallback, causing valid
MCP forwarded JWTs to be ignored. Fix this by stripping whitespace from the
primary header before using the `or` operator, so that whitespace-only values
are treated as absent and the fallback to get_mcp_forwarded_jwt_header() is
properly triggered.
🪄 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

Run ID: 637bc3bf-7c6e-46f3-9ed5-a609e4a2c7b1

📥 Commits

Reviewing files that changed from the base of the PR and between 14b1646 and 4839369.

📒 Files selected for processing (3)
  • src/app/routes/mcp.py
  • src/config/settings.py
  • src/dependencies.py

Comment thread src/app/routes/mcp.py
Comment on lines +44 to +52
for name, value in headers:
lname = name.lower()
if lname == b"authorization":
auth = value
elif lname == target:
has_target = True
if auth is not None and not has_target:
scope = dict(scope)
scope["headers"] = list(headers) + [(target, auth)]

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Overwrite client-supplied forwarded JWT headers.

has_target lets an inbound X-OpenRAG-JWT value win over the authenticated Authorization value. After FastMCP strips Authorization, /v1 can consume that client-controlled fallback header as identity/roles. Strip any inbound target header, then append only the value copied from Authorization.

🛡️ Proposed fix
-        headers = scope.get("headers", [])
+        headers = list(scope.get("headers", []))
         auth = None
-        has_target = False
+        forwarded_headers = []
         for name, value in headers:
             lname = name.lower()
             if lname == b"authorization":
                 auth = value
-            elif lname == target:
-                has_target = True
-        if auth is not None and not has_target:
+                forwarded_headers.append((name, value))
+            elif lname != target:
+                forwarded_headers.append((name, value))
+        if auth and auth.strip():
             scope = dict(scope)
-            scope["headers"] = list(headers) + [(target, auth)]
+            scope["headers"] = forwarded_headers + [(target, auth)]
+        elif len(forwarded_headers) != len(headers):
+            scope = dict(scope)
+            scope["headers"] = forwarded_headers
🤖 Prompt for 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.

In `@src/app/routes/mcp.py` around lines 44 - 52, The code currently only appends
the Authorization header as the target when the target header is not present
(when has_target is false), which allows client-supplied target headers to take
precedence. Instead, you need to strip any inbound target header from the
headers list first, then unconditionally append the Authorization value as the
target header when auth is present. Modify the logic to filter the headers list
to remove any headers where the lowercased name equals the target value
(removing client-supplied target headers), then if auth is not none, append it
as the target header in the modified scope headers list.

Comment thread src/dependencies.py
Comment on lines +854 to +859
# Primary: the gateway-forwarded JWT header (default Authorization). Fallback:
# the header the /mcp shim copies the JWT into, because FastMCP strips
# Authorization before proxying an MCP tool call to this /v1 handler.
raw_jwt = request.headers.get(get_jwt_auth_header(), "") or request.headers.get(
get_mcp_forwarded_jwt_header(), ""
)

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Treat blank primary JWT headers as absent before falling back.

A whitespace-only primary header is truthy for or, so a valid MCP forwarded JWT is ignored and the request falls through as unauthenticated.

🔧 Proposed fix
-    raw_jwt = request.headers.get(get_jwt_auth_header(), "") or request.headers.get(
-        get_mcp_forwarded_jwt_header(), ""
-    )
+    primary_jwt = request.headers.get(get_jwt_auth_header(), "").strip()
+    raw_jwt = primary_jwt or request.headers.get(get_mcp_forwarded_jwt_header(), "").strip()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Primary: the gateway-forwarded JWT header (default Authorization). Fallback:
# the header the /mcp shim copies the JWT into, because FastMCP strips
# Authorization before proxying an MCP tool call to this /v1 handler.
raw_jwt = request.headers.get(get_jwt_auth_header(), "") or request.headers.get(
get_mcp_forwarded_jwt_header(), ""
)
# Primary: the gateway-forwarded JWT header (default Authorization). Fallback:
# the header the /mcp shim copies the JWT into, because FastMCP strips
# Authorization before proxying an MCP tool call to this /v1 handler.
primary_jwt = request.headers.get(get_jwt_auth_header(), "").strip()
raw_jwt = primary_jwt or request.headers.get(get_mcp_forwarded_jwt_header(), "").strip()
🤖 Prompt for 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.

In `@src/dependencies.py` around lines 854 - 859, The JWT header retrieval logic
in the raw_jwt assignment uses `or` to fallback from the primary JWT auth header
to the MCP forwarded JWT header. However, if the primary header (from
get_jwt_auth_header()) contains only whitespace, it evaluates as truthy and
prevents the fallback, causing valid MCP forwarded JWTs to be ignored. Fix this
by stripping whitespace from the primary header before using the `or` operator,
so that whitespace-only values are treated as absent and the fallback to
get_mcp_forwarded_jwt_header() is properly triggered.

@edwinjosechittilappilly edwinjosechittilappilly marked this pull request as draft June 15, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend 🔷 Issues related to backend services (OpenSearch, Langflow, APIs) bug 🔴 Something isn't working.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant