Skip to content

Strip authorization requests before they reach authorization server#1543

Merged
ignaciojimenezr merged 9 commits intoMCPJam:mainfrom
ignaciojimenezr:debugging-oauth
Mar 18, 2026
Merged

Strip authorization requests before they reach authorization server#1543
ignaciojimenezr merged 9 commits intoMCPJam:mainfrom
ignaciojimenezr:debugging-oauth

Conversation

@ignaciojimenezr
Copy link
Collaborator

Addresses #1535

The OAuth Debugger's mergeHeaders function merges connection-level custom headers into all requests — including those going to the Authorization Server. If a user sets a custom Authorization: Bearer header (intended for the MCP server), it leaks into Authorization Server requests where Bearer tokens have no defined meaning — RFC 6750 defines them exclusively for accessing "protected resources hosted by the resource server."

In the case of SuperTokens (#1535), the token endpoint receives the unexpected Authorization: Bearer eyJhbG... header and fails with IllegalArgumentException: Illegal base64 character 20, which likely means SuperTokens is trying to base64-decode the Bearer value as client credentials.

Fix: Strip Authorization: Bearer headers from requests before they reach the Authorization Server.

@chelojimenez
Copy link
Contributor

chelojimenez commented Mar 6, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copy link
Collaborator

@prathmeshpatel prathmeshpatel left a comment

Choose a reason for hiding this comment

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

@ignaciojimenezr : feel free to just iterate on this with claude/codex

[do] Header stripping is not case-insensitive — this will still leak on AUTHORIZATION, authORIZATION, etc.

HTTP headers are case-insensitive per RFC 7230 §3.2. You only delete Authorization and authorization, but a user could configure AUTHORIZATION or any other casing in their custom headers.

Fix this with a case-insensitive filter:

if (isAuthServer) {
  for (const key of Object.keys(headers)) {
    if (key.toLowerCase() === "authorization") {
      delete headers[key];
    }
  }
}

[do] Boolean trap — isAuthServer = false as a positional arg is fragile and invites regressions

Every future mergeHeaders call site requires the author to remember "oh right, pass true for auth server requests." If they forget, the header silently leaks again. This is the exact class of bug that regresses.

Suggestion: make it explicit with two named functions, or at minimum use an options object:

mergeHeaders(requestHeaders, { stripAuth: true })
// or better:
mergeHeadersForAuthServer(requestHeaders)
mergeHeadersForMcpServer(requestHeaders)

This makes intent self-documenting and grep-able.

[do] Tests only cover the 2025-11-25 state machine, but the fix was applied to all three

You modified debug-oauth-2025-03-26.ts, debug-oauth-2025-06-18.ts, and debug-oauth-2025-11-25.ts — but the test file only imports and tests debug-oauth-2025-11-25. If one of the other two files has a subtle difference or a missed call site, you won't catch it. Add at least a smoke test for the other two machines, or extract mergeHeaders into the shared helpers.ts module so there's a single implementation to test.

[do] Verify: does loggingFetch (used by the SDK's discoverOAuthProtectedResourceMetadata) also need stripping?

In debug-oauth-2025-06-18.ts:685 and debug-oauth-2025-11-25.ts:830, you have a loggingFetch wrapper that calls mergeHeaders(normalizeHeaders(...)) without isAuthServer = true. This wrapper is passed to the MCP SDK's discoverOAuthProtectedResourceMetadata. The resource metadata endpoint (/.well-known/oauth-protected-resource) lives on the MCP server's domain, so it's probably fine — but please confirm the SDK doesn't follow any redirects or links that could send this to the auth server. Add a comment explaining why true is not needed here.

@ignaciojimenezr
Copy link
Collaborator Author

  1. Case-insensitive stripping — Done. mergeHeadersForAuthServer now iterates all keys with key.toLowerCase() === "authorization" and deletes any match. Handles AUTHORIZATION, authORIZATION, etc.

  2. Boolean trap → named functions — Done. Replaced the single mergeHeaders(headers, isAuthServer) with two explicit functions:

mergeHeaders(customHeaders, requestHeaders) — for MCP server requests (keeps all headers)
mergeHeadersForAuthServer(customHeaders, requestHeaders) — for auth server requests (strips Authorization)
Self-documenting and grep-able, as suggested.

  1. Single shared implementation + unit tests — Went with the "extract to shared helpers" approach. Both functions live in shared/helpers.ts — all 3 state machines import from there, so there's one implementation to test. Added unit tests (merge-headers-unit.test.ts) that test the shared functions directly, covering standard/mixed/all-caps casing, header merging, and undefined handling. The existing integration test still exercises the full 2025-11-25 state machine end-to-end.

  2. loggingFetch does NOT need stripping — Confirmed. loggingFetch is only passed to the SDK's discoverOAuthProtectedResourceMetadata, which fetches /.well-known/oauth-protected-resource from the MCP server's own domain. The SDK does not follow redirects or links to the Authorization Server from this endpoint. Added inline comments in both 2025-06-18 (line 679) and 2025-11-25 (line 824) explaining why mergeHeaders (not mergeHeadersForAuthServer) is correct here. The 2025-03-26 machine has a separate comment noting it doesn't support RFC 9728 at all, so no loggingFetch path exists.

@ignaciojimenezr ignaciojimenezr marked this pull request as ready for review March 10, 2026 02:21
@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. bug Something isn't working labels Mar 10, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds three exported helpers: mergeHeaders(customHeaders, requestHeaders), mergeHeadersForAuthServer(customHeaders, requestHeaders) (strips Authorization case-insensitively), and mergeHeadersForResourceMetadataRequest(serverUrl, requestUrl, customHeaders, requestHeaders) (chooses merge strategy by origin). Replaces in-file header merging across multiple OAuth debug state machines to use these helpers and updates call sites to the new signatures. Adds unit and integration tests covering header merging, case-insensitive Authorization stripping, and origin-based behavior for resource metadata requests. No other public APIs were changed.

📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts (1)

909-916: ⚠️ Potential issue | 🟠 Major

Don't copy customHeaders into synthetic response headers.

These placeholders are persisted as lastResponse / httpHistory.response, so this re-exposes user-supplied Authorization values in logs even when the actual auth-server request stripped them on the wire. lastRequest already holds the attempted request headers. The later token/authenticated catches repeat the same shape.

🛡️ Safer placeholder response
               const errorResponse = {
                 status: 0,
                 statusText: "Network Error",
-                headers: mergeHeaders(customHeaders, {}),
+                headers: {},
                 body: {
                   error: error instanceof Error ? error.message : String(error),
                 },
               };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts`
around lines 909 - 916, The synthetic errorResponse currently copies
user-supplied customHeaders via mergeHeaders(customHeaders, {}), which can
persist sensitive Authorization values; update the construction of errorResponse
(variable errorResponse) to avoid copying customHeaders — replace headers:
mergeHeaders(customHeaders, {}) with either an empty headers object or a
sanitized/redacted headers object (e.g., {}) so only non-sensitive metadata is
stored (the original request headers remain available in lastRequest); ensure
references to mergeHeaders and customHeaders are removed from this synthetic
response.
🧹 Nitpick comments (1)
mcpjam-inspector/client/src/lib/oauth/__tests__/merge-headers-unit.test.ts (1)

15-35: Add a casing-mismatch override test.

The suite only checks exact-case collisions. authorization vs Authorization is the HTTP-relevant failure mode, and it is the one that would let duplicate auth headers slip through here.

🧪 Example coverage to add
 describe("mergeHeaders", () => {
+  it("treats header names case-insensitively when request headers override custom headers", () => {
+    const result = mergeHeaders(
+      { authorization: "Bearer old-token" },
+      { Authorization: "Bearer new-token" },
+    );
+
+    expect(
+      Object.keys(result).filter((key) => key.toLowerCase() === "authorization"),
+    ).toHaveLength(1);
+    expect(result.Authorization).toBe("Bearer new-token");
+  });
+
   it("returns request headers overriding custom headers", () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpjam-inspector/client/src/lib/oauth/__tests__/merge-headers-unit.test.ts`
around lines 15 - 35, Add a unit test to cover case-insensitive header
collisions for mergeHeaders: call mergeHeaders with a custom headers object
containing a lowercased "authorization" and a request headers object containing
"Authorization" (or vice versa) and assert that the request header value wins
(the resulting headers include the request's Authorization value and key).
Reference the mergeHeaders function and add this new it(...) test alongside the
existing tests to ensure casing-mismatch overrides behave like exact-case
collisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts`:
- Around line 432-434: Wrap the "idle" case body in braces to confine
declarations to the case block; specifically, change the switch case for "idle"
so that const initialRequestHeaders and const initialRequest (and any other
declarations in that case) are declared inside a braced block (case "idle": {
... }) to satisfy Biome's noSwitchDeclarations rule and avoid TDZ/lexical
scoping issues.

In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts`:
- Around line 578-580: The switch case that declares const initialRequestHeaders
(using mergeHeaders and "Content-Type") must be wrapped in a block to avoid
lexical-scope leakage; open a brace immediately after the case label and close
it before the break so the const is scoped to that case (i.e., surround the
entire case body that contains initialRequestHeaders with { ... } and keep the
existing break inside the braces).

In `@mcpjam-inspector/client/src/lib/oauth/state-machines/shared/helpers.ts`:
- Around line 232-239: mergeHeaders currently merges by exact key which treats
header names as case-sensitive; modify mergeHeaders to normalize header names
(e.g., lowercase) so requestHeaders override customHeaders case-insensitively:
iterate customHeaders and requestHeaders, insert keys into a new map using
key.toLowerCase() so later entries (requestHeaders) replace earlier ones, and
return that normalized map; refer to the mergeHeaders function to implement this
normalization when building the result.
- Around line 250-260: The current mergeHeadersForAuthServer removes every
Authorization header after merging; change it to only strip a connection-level
Bearer token that originated from customHeaders so request-specific auth (e.g.
Basic or registration tokens) remains. Concretely: after computing merged =
mergeHeaders(customHeaders, requestHeaders), check customHeaders
(case-insensitive for the header name) for an Authorization value and if that
value begins with "Bearer " (case-insensitive), then remove the corresponding
key from merged; otherwise leave merged.Authorization intact. Use the function
name mergeHeadersForAuthServer and the parameters customHeaders and
requestHeaders to locate and implement this conditional removal.

---

Outside diff comments:
In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts`:
- Around line 909-916: The synthetic errorResponse currently copies
user-supplied customHeaders via mergeHeaders(customHeaders, {}), which can
persist sensitive Authorization values; update the construction of errorResponse
(variable errorResponse) to avoid copying customHeaders — replace headers:
mergeHeaders(customHeaders, {}) with either an empty headers object or a
sanitized/redacted headers object (e.g., {}) so only non-sensitive metadata is
stored (the original request headers remain available in lastRequest); ensure
references to mergeHeaders and customHeaders are removed from this synthetic
response.

---

Nitpick comments:
In `@mcpjam-inspector/client/src/lib/oauth/__tests__/merge-headers-unit.test.ts`:
- Around line 15-35: Add a unit test to cover case-insensitive header collisions
for mergeHeaders: call mergeHeaders with a custom headers object containing a
lowercased "authorization" and a request headers object containing
"Authorization" (or vice versa) and assert that the request header value wins
(the resulting headers include the request's Authorization value and key).
Reference the mergeHeaders function and add this new it(...) test alongside the
existing tests to ensure casing-mismatch overrides behave like exact-case
collisions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3b0bc3d-ce2d-4b2e-a1fe-1165d80cf8bd

📥 Commits

Reviewing files that changed from the base of the PR and between 4b2fabe and b4f3074.

📒 Files selected for processing (6)
  • mcpjam-inspector/client/src/lib/oauth/__tests__/merge-headers-auth-server.test.ts
  • mcpjam-inspector/client/src/lib/oauth/__tests__/merge-headers-unit.test.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/shared/helpers.ts

Comment on lines +432 to 434
const initialRequestHeaders = mergeHeaders(customHeaders, {
"Content-Type": "application/json",
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate the file
find . -name "debug-oauth-2025-06-18.ts" -type f

Repository: MCPJam/inspector

Length of output: 140


🏁 Script executed:

# Check if the file exists and view the relevant lines
if [ -f "mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts" ]; then
  echo "File found. Checking lines 430-440 for context:"
  sed -n '430,440p' "mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts" | cat -n
else
  echo "File not found at expected path"
fi

Repository: MCPJam/inspector

Length of output: 573


🏁 Script executed:

# Get broader context to see the switch structure
if [ -f "mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts" ]; then
  echo "Checking lines 420-450 for full switch context:"
  sed -n '420,450p' "mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts" | cat -n
fi

Repository: MCPJam/inspector

Length of output: 1314


Wrap this case body in braces.

The const initialRequestHeaders and const initialRequest declarations inside the "idle" case bypass proper lexical scoping and trigger Biome's noSwitchDeclarations rule. Adding braces constrains them to the case scope and resolves the TDZ hazard:

case "idle": {
  const initialRequestHeaders = mergeHeaders(customHeaders, {
    "Content-Type": "application/json",
  });
  // ... rest of case body
}
🧰 Tools
🪛 Biome (2.4.6)

[error] 432-434: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts`
around lines 432 - 434, Wrap the "idle" case body in braces to confine
declarations to the case block; specifically, change the switch case for "idle"
so that const initialRequestHeaders and const initialRequest (and any other
declarations in that case) are declared inside a braced block (case "idle": {
... }) to satisfy Biome's noSwitchDeclarations rule and avoid TDZ/lexical
scoping issues.

Comment on lines +578 to 580
const initialRequestHeaders = mergeHeaders(customHeaders, {
"Content-Type": "application/json",
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -path "*/mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts" -type f

Repository: MCPJam/inspector

Length of output: 140


🏁 Script executed:

sed -n '560,600p' ./mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts

Repository: MCPJam/inspector

Length of output: 1384


🏁 Script executed:

sed -n '575,620p' ./mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts

Repository: MCPJam/inspector

Length of output: 1573


Wrap this case body in braces.

Lexical declarations in unbraced switch cases create scope pollution across all subsequent cases and temporal dead zone hazards. Biome's noSwitchDeclarations rule flags this; wrap the entire case body in braces to resolve.

🧰 Tools
🪛 Biome (2.4.6)

[error] 578-580: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts`
around lines 578 - 580, The switch case that declares const
initialRequestHeaders (using mergeHeaders and "Content-Type") must be wrapped in
a block to avoid lexical-scope leakage; open a brace immediately after the case
label and close it before the break so the const is scoped to that case (i.e.,
surround the entire case body that contains initialRequestHeaders with { ... }
and keep the existing break inside the braces).

Comment on lines +250 to +260
export function mergeHeadersForAuthServer(
customHeaders: Record<string, string> | undefined,
requestHeaders: Record<string, string> = {},
): Record<string, string> {
const merged = mergeHeaders(customHeaders, requestHeaders);
for (const key of Object.keys(merged)) {
if (key.toLowerCase() === "authorization") {
delete merged[key];
}
}
return merged;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Strip the leaked MCP bearer token, not every auth-server credential.

This helper deletes any Authorization header after the merge, so an explicit Basic ... client-auth header or registration access token would disappear too. The scrub should target connection-level Bearer headers from customHeaders, then leave request-specific auth intact.

🎯 Narrow the scrub to the problematic header
 export function mergeHeadersForAuthServer(
   customHeaders: Record<string, string> | undefined,
   requestHeaders: Record<string, string> = {},
 ): Record<string, string> {
-  const merged = mergeHeaders(customHeaders, requestHeaders);
-  for (const key of Object.keys(merged)) {
-    if (key.toLowerCase() === "authorization") {
-      delete merged[key];
-    }
-  }
-  return merged;
+  const safeCustomHeaders: Record<string, string> = { ...(customHeaders ?? {}) };
+
+  for (const key of Object.keys(safeCustomHeaders)) {
+    if (
+      key.toLowerCase() === "authorization" &&
+      /^Bearer\s/i.test(safeCustomHeaders[key] ?? "")
+    ) {
+      delete safeCustomHeaders[key];
+    }
+  }
+
+  return mergeHeaders(safeCustomHeaders, requestHeaders);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpjam-inspector/client/src/lib/oauth/state-machines/shared/helpers.ts`
around lines 250 - 260, The current mergeHeadersForAuthServer removes every
Authorization header after merging; change it to only strip a connection-level
Bearer token that originated from customHeaders so request-specific auth (e.g.
Basic or registration tokens) remains. Concretely: after computing merged =
mergeHeaders(customHeaders, requestHeaders), check customHeaders
(case-insensitive for the header name) for an Authorization value and if that
value begins with "Bearer " (case-insensitive), then remove the corresponding
key from merged; otherwise leave merged.Authorization intact. Use the function
name mergeHeadersForAuthServer and the parameters customHeaders and
requestHeaders to locate and implement this conditional removal.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts (1)

526-556: ⚠️ Potential issue | 🟡 Minor

Keep recorded request headers identical to the actual wire headers.

Line 526 builds requestHeaders, but Line 553 sends a different header object (MCP-Protocol-Version is added only there). This makes HTTP history inaccurate during retries.

Proposed fix
-                const requestHeaders = mergeHeadersForAuthServer(
-                  customHeaders,
-                  {},
-                );
+                const requestHeaders = mergeHeadersForAuthServer(customHeaders, {
+                  "MCP-Protocol-Version": "2025-03-26",
+                });
@@
-                const response = await proxyFetch(url, {
-                  method: "GET",
-                  headers: mergeHeadersForAuthServer(customHeaders, {
-                    "MCP-Protocol-Version": "2025-03-26",
-                  }),
-                });
+                const response = await proxyFetch(url, {
+                  method: "GET",
+                  headers: requestHeaders,
+                });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts`
around lines 526 - 556, The recorded request headers differ from the actual sent
headers because you build requestHeaders with
mergeHeadersForAuthServer(customHeaders, {}) but call proxyFetch with
mergeHeadersForAuthServer(customHeaders, { "MCP-Protocol-Version": "2025-03-26"
}); fix by creating a single headers object (e.g., call
mergeHeadersForAuthServer(customHeaders, { "MCP-Protocol-Version": "2025-03-26"
}) once), use that same object when setting lastRequest and updating
updatedHistoryForRetry (references: mergeHeadersForAuthServer,
updatedHistoryForRetry, updateState, proxyFetch) so the stored httpHistory
exactly matches the wire headers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@mcpjam-inspector/client/src/lib/oauth/__tests__/merge-headers-auth-server.test.ts`:
- Around line 12-162: Replace the bespoke test mocks with the shared client mock
presets: import and apply mcpApiPresets and storePresets from
client/src/test/mocks at the top of the test (before importing
createDebugOAuthStateMachine) instead of manually mocking
"../state-machines/shared/helpers" and
"@modelcontextprotocol/sdk/client/auth.js"; remove the custom proxyFetch/helpers
and SDK mocks and rely on the preset mocks to provide proxyFetch,
generateRandomString, generateCodeChallenge, loadPreregisteredCredentials and
discoverOAuthProtectedResourceMetadata behavior so createTestMachine and
createDebugOAuthStateMachine continue to work with
serverUrl/serverName/redirectUrl/customHeaders/registrationStrategy as before.

In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts`:
- Around line 909-913: The synthetic network-error response construction is
copying request-side customHeaders into errorResponse.headers (via
mergeHeaders(customHeaders, {})), which can leak secrets; update the error
path(s) where errorResponse is built (the instances referencing errorResponse
and mergeHeaders(customHeaders, {})) to not include request headers—use an empty
response header set or a safe/set of server-origin headers instead (e.g.,
replace mergeHeaders(customHeaders, {}) with {} or mergeHeaders({}, {}) and/or
explicitly omit sensitive keys like Authorization/Api-Key), and apply the same
change to the other occurrences that build errorResponse in this file.

In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts`:
- Around line 1244-1248: The synthetic network-error response currently copies
customHeaders into errorResponse.headers (via mergeHeaders(customHeaders, {})),
which can leak sensitive values like Authorization or cookies; update the code
that builds the errorResponse object (the variable named errorResponse) to
sanitize headers first by removing sensitive keys ("authorization", "cookie",
"set-cookie", etc.) before calling mergeHeaders, and apply the same sanitization
logic to the other synthetic responses at the same pattern (the similar
constructions around the other occurrences). Ensure you centralize the filter (a
small helper like sanitizeHeaders or reuse an existing header util) and use that
helper in each place instead of passing customHeaders directly.

In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts`:
- Around line 1411-1415: The synthetic error responses are incorrectly seeding
request custom headers into response metadata by setting errorResponse.headers =
mergeHeaders(customHeaders, {}); update each errorResponse construction (the
variable named errorResponse where headers are assigned) to avoid including
request credentials: replace mergeHeaders(customHeaders, {}) with a safe
response-only headers object (e.g., an empty/explicitly constructed headers map
or mergeHeaders({}, responseSafeHeaders)) so request-specific customHeaders are
not copied into the synthetic response; do this for every occurrence that uses
mergeHeaders(customHeaders, {}) (the three errorResponse blocks flagged).

---

Outside diff comments:
In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts`:
- Around line 526-556: The recorded request headers differ from the actual sent
headers because you build requestHeaders with
mergeHeadersForAuthServer(customHeaders, {}) but call proxyFetch with
mergeHeadersForAuthServer(customHeaders, { "MCP-Protocol-Version": "2025-03-26"
}); fix by creating a single headers object (e.g., call
mergeHeadersForAuthServer(customHeaders, { "MCP-Protocol-Version": "2025-03-26"
}) once), use that same object when setting lastRequest and updating
updatedHistoryForRetry (references: mergeHeadersForAuthServer,
updatedHistoryForRetry, updateState, proxyFetch) so the stored httpHistory
exactly matches the wire headers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de5c9b71-3166-4200-b4cf-70a2779ea333

📥 Commits

Reviewing files that changed from the base of the PR and between b4f3074 and da70951.

📒 Files selected for processing (6)
  • mcpjam-inspector/client/src/lib/oauth/__tests__/merge-headers-auth-server.test.ts
  • mcpjam-inspector/client/src/lib/oauth/__tests__/merge-headers-unit.test.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/shared/helpers.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • mcpjam-inspector/client/src/lib/oauth/tests/merge-headers-unit.test.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/shared/helpers.ts

Comment on lines +12 to +162
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { OAuthFlowState } from "../state-machines/types";
import { EMPTY_OAUTH_FLOW_STATE } from "../state-machines/types";

// Track all proxyFetch calls so we can inspect headers
let proxyFetchCalls: Array<{ url: string; options: RequestInit }> = [];

// Mock the helpers module to intercept proxyFetch
vi.mock("../state-machines/shared/helpers", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../state-machines/shared/helpers")>();
return {
...actual,
proxyFetch: vi.fn(async (url: string, options: RequestInit = {}) => {
proxyFetchCalls.push({ url, options });

// Return 401 with WWW-Authenticate for MCP server requests
if (
url.includes("mcp-server.example.com") &&
!url.includes(".well-known")
) {
return {
status: 401,
statusText: "Unauthorized",
headers: {
"www-authenticate":
'Bearer resource_metadata="https://mcp-server.example.com/.well-known/oauth-protected-resource"',
},
body: null,
ok: false,
};
}

// Return resource metadata for /.well-known/oauth-protected-resource
if (url.includes("oauth-protected-resource")) {
return {
status: 200,
statusText: "OK",
headers: { "content-type": "application/json" },
body: {
resource: "https://mcp-server.example.com",
authorization_servers: ["https://auth-server.example.com"],
},
ok: true,
};
}

// Return auth server metadata
if (
url.includes("oauth-authorization-server") ||
url.includes("openid-configuration")
) {
return {
status: 200,
statusText: "OK",
headers: { "content-type": "application/json" },
body: {
issuer: "https://auth-server.example.com",
authorization_endpoint: "https://auth-server.example.com/authorize",
token_endpoint: "https://auth-server.example.com/token",
registration_endpoint: "https://auth-server.example.com/register",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
code_challenge_methods_supported: ["S256"],
token_endpoint_auth_methods_supported: ["none"],
},
ok: true,
};
}

// Return success for client registration
if (url.includes("/register")) {
return {
status: 200,
statusText: "OK",
headers: { "content-type": "application/json" },
body: {
client_id: "test-client-id",
client_secret: "test-client-secret",
token_endpoint_auth_method: "client_secret_post",
},
ok: true,
};
}

// Return success for token exchange
if (url.includes("/token")) {
return {
status: 200,
statusText: "OK",
headers: { "content-type": "application/json" },
body: {
access_token: "new-access-token",
token_type: "Bearer",
expires_in: 3600,
},
ok: true,
};
}

// Default 404
return {
status: 404,
statusText: "Not Found",
headers: {},
body: null,
ok: false,
};
}),
generateRandomString: vi.fn(() => "mock-random-string"),
generateCodeChallenge: vi.fn(async () => "mock-code-challenge"),
loadPreregisteredCredentials: vi.fn(() => ({
clientId: undefined,
clientSecret: undefined,
})),
};
});

// Mock the MCP SDK's discoverOAuthProtectedResourceMetadata
// This SDK function is called during resource metadata discovery.
// It receives a loggingFetch wrapper, but we mock the whole function
// to return valid metadata directly.
vi.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
discoverOAuthProtectedResourceMetadata: vi.fn(async () => ({
resource: "https://mcp-server.example.com",
authorization_servers: ["https://auth-server.example.com"],
})),
}));

// Import after mocks are set up
import { createDebugOAuthStateMachine } from "../state-machines/debug-oauth-2025-11-25";

// Helper: create a state machine with custom headers and track state updates
function createTestMachine(customHeaders: Record<string, string>) {
let state: OAuthFlowState = { ...EMPTY_OAUTH_FLOW_STATE };

const machine = createDebugOAuthStateMachine({
state,
getState: () => state,
updateState: (updates) => {
state = { ...state, ...updates };
},
serverUrl: "https://mcp-server.example.com",
serverName: "test-server",
redirectUrl: "http://localhost:3000/oauth/callback/debug",
customHeaders,
registrationStrategy: "dcr",
});

return { machine, getState: () => state };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align client test setup with repository mock preset policy.

This test builds bespoke mocks but does not use the shared client mock presets (mcpApiPresets, storePresets). Please switch setup to the standard preset fixtures for consistency and maintainability.

As per coding guidelines mcpjam-inspector/client/**/__tests__/*.test.{ts,tsx}: Use mock presets from client/src/test/mocks/ including mcpApiPresets and storePresets in client tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mcpjam-inspector/client/src/lib/oauth/__tests__/merge-headers-auth-server.test.ts`
around lines 12 - 162, Replace the bespoke test mocks with the shared client
mock presets: import and apply mcpApiPresets and storePresets from
client/src/test/mocks at the top of the test (before importing
createDebugOAuthStateMachine) instead of manually mocking
"../state-machines/shared/helpers" and
"@modelcontextprotocol/sdk/client/auth.js"; remove the custom proxyFetch/helpers
and SDK mocks and rely on the preset mocks to provide proxyFetch,
generateRandomString, generateCodeChallenge, loadPreregisteredCredentials and
discoverOAuthProtectedResourceMetadata behavior so createTestMachine and
createDebugOAuthStateMachine continue to work with
serverUrl/serverName/redirectUrl/customHeaders/registrationStrategy as before.

@ignaciojimenezr ignaciojimenezr changed the title OAuth Debugger sending Strip Authorizationrequests before they reach authorization server Mar 18, 2026
@ignaciojimenezr ignaciojimenezr changed the title Strip Authorizationrequests before they reach authorization server Strip authorization requests before they reach authorization server Mar 18, 2026
@prathmeshpatel
Copy link
Collaborator

Let's fix the 4 major comments from CodeRabbit and then you're good

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts`:
- Around line 680-687: The fetch wrapper loggingFetch used when calling
discoverOAuthProtectedResourceMetadata must avoid leaking Authorization across
cross-origin metadata hops: when resolving metadataOptions.resourceMetadataUrl
(or the URL passed into loggingFetch) compare its origin to the MCP server
origin used by your code and, if the origins differ, use
mergeHeadersForAuthServer instead of mergeHeaders to build mergedHeaders
(replace the mergeHeaders call that takes customHeaders and
normalizeHeaders(init.headers) inside loggingFetch); this origin check should
also be applied at the other occurrence around lines 748-751 that currently uses
mergeHeaders so that cross-origin requests do not carry Authorization headers.

In
`@mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts`:
- Around line 825-833: The loggingFetch used by
discoverOAuthProtectedResourceMetadata currently builds mergedHeaders via
mergeHeaders(customHeaders, normalizeHeaders(init.headers)) which assumes
same-origin, risking Authorization header leakage to cross-origin
resourceMetadataUrl; update loggingFetch to apply origin-aware header stripping
like mergeHeadersForAuthServer (or reuse its logic) so that Authorization and
other sensitive headers are removed when the request target origin differs from
the MCP server origin (i.e., compare resourceMetadataUrl origin to the server
origin used by loggingFetch), and ensure the same change is applied to the other
occurrence at the code referenced around lines 892–895.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f04d0052-6737-4690-82fb-53469d498f94

📥 Commits

Reviewing files that changed from the base of the PR and between 0c57cf1 and 5a1a239.

📒 Files selected for processing (4)
  • mcpjam-inspector/client/src/lib/oauth/__tests__/merge-headers-auth-server.test.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-03-26.ts
  • mcpjam-inspector/client/src/lib/oauth/tests/merge-headers-auth-server.test.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
mcpjam-inspector/client/src/lib/oauth/state-machines/shared/helpers.ts (1)

269-279: ⚠️ Potential issue | 🟠 Major

Do not drop request-specific Authorization headers for auth-server calls.

At Line 273–Line 277, removing every Authorization header after merge also removes intentional per-request credentials (for example, Basic auth) when they are explicitly passed in requestHeaders. Sanitize leaked connection-level auth from customHeaders before merging, then let request headers win.

🔧 Focused fix
 export function mergeHeadersForAuthServer(
   customHeaders: Record<string, string> | undefined,
   requestHeaders: Record<string, string> = {},
 ): Record<string, string> {
-  const merged = mergeHeaders(customHeaders, requestHeaders);
-  for (const key of Object.keys(merged)) {
-    if (key.toLowerCase() === "authorization") {
-      delete merged[key];
-    }
-  }
-  return merged;
+  const safeCustomHeaders: Record<string, string> = { ...(customHeaders ?? {}) };
+
+  for (const [key, value] of Object.entries(safeCustomHeaders)) {
+    if (
+      key.toLowerCase() === "authorization" &&
+      /^Bearer\s/i.test(value ?? "")
+    ) {
+      delete safeCustomHeaders[key];
+    }
+  }
+
+  return mergeHeaders(safeCustomHeaders, requestHeaders);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpjam-inspector/client/src/lib/oauth/state-machines/shared/helpers.ts`
around lines 269 - 279, The current mergeHeadersForAuthServer unconditionally
deletes any Authorization header from the merged result, which removes
request-specific credentials; instead, remove/sanitize Authorization from the
customHeaders input before merging so requestHeaders can override it: create a
shallow copy of customHeaders, strip any header whose key lowercased equals
"authorization" from that copy, then call mergeHeaders(copyOfCustomHeaders,
requestHeaders) so any Authorization provided in requestHeaders is preserved;
refer to mergeHeadersForAuthServer, mergeHeaders, customHeaders, and
requestHeaders when implementing this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@mcpjam-inspector/client/src/lib/oauth/state-machines/shared/helpers.ts`:
- Around line 269-279: The current mergeHeadersForAuthServer unconditionally
deletes any Authorization header from the merged result, which removes
request-specific credentials; instead, remove/sanitize Authorization from the
customHeaders input before merging so requestHeaders can override it: create a
shallow copy of customHeaders, strip any header whose key lowercased equals
"authorization" from that copy, then call mergeHeaders(copyOfCustomHeaders,
requestHeaders) so any Authorization provided in requestHeaders is preserved;
refer to mergeHeadersForAuthServer, mergeHeaders, customHeaders, and
requestHeaders when implementing this change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81ee5444-15c8-421e-9a42-663c4cd4557f

📥 Commits

Reviewing files that changed from the base of the PR and between 5a1a239 and 3a18b19.

📒 Files selected for processing (4)
  • mcpjam-inspector/client/src/lib/oauth/__tests__/merge-headers-unit.test.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-11-25.ts
  • mcpjam-inspector/client/src/lib/oauth/state-machines/shared/helpers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • mcpjam-inspector/client/src/lib/oauth/state-machines/debug-oauth-2025-06-18.ts

@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Mar 18, 2026
@ignaciojimenezr ignaciojimenezr merged commit cf547a4 into MCPJam:main Mar 18, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working lgtm This PR has been approved by a maintainer size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants