Skip to content

MCP: prefer static Authorization header over stored OAuth tokens#609

Open
romzes5000 wants to merge 1 commit intomattermost:masterfrom
romzes5000:fix/mcp-static-auth-ignores-oauth-kv
Open

MCP: prefer static Authorization header over stored OAuth tokens#609
romzes5000 wants to merge 1 commit intomattermost:masterfrom
romzes5000:fix/mcp-static-auth-ignores-oauth-kv

Conversation

@romzes5000
Copy link
Copy Markdown

@romzes5000 romzes5000 commented Apr 9, 2026

Summary

When an MCP server is configured with a custom Authorization header (e.g. Outline API key ol_api_*), the HTTP client must not wrap requests with oauth2.Transport using a previously stored per-user OAuth token from the plugin KV store.

Without this, switching from OAuth to token-only auth leaves stale mcp_oauth_token_v1_* rows; the plugin still calls createOAuthConfig / token refresh on every request, which can fail (e.g. DCR rate limits) even though the admin intends to use only the static header.

Changes

  • If custom headers include Authorization (case-insensitive), skip loading the OAuth token and do not wrap with oauth2.Transport.
  • Add TestAuthorizationHeaderInCustomHeaders.

Test steps

  1. Configure MCP with OAuth, complete flow (token in KV).
  2. Add Authorization: Bearer <api key> in custom headers and remove OAuth client fields if any.
  3. Requests should succeed using the header without requiring manual KV cleanup.

Made with Cursor

Summary by CodeRabbit

  • Improvements
    • Enhanced authorization header handling to detect and respect custom Authorization headers (case-insensitive) in HTTP requests.
    • Updated OAuth token behavior to conditionally apply based on whether a static Authorization header is provided, preventing conflicts and improving request handling flexibility.
    • Added comprehensive test coverage for authorization header detection across various scenarios.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@romzes5000 romzes5000 force-pushed the fix/mcp-static-auth-ignores-oauth-kv branch from 8e283f9 to 56a1164 Compare April 9, 2026 19:04
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

Warning

Rate limit exceeded

@romzes5000 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 6 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 17 minutes and 6 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: d34af89b-0ed5-4fd9-ab2c-ccb28fcec7c7

📥 Commits

Reviewing files that changed from the base of the PR and between 56a1164 and 2e8dbbd.

📒 Files selected for processing (3)
  • mcp/http_client.go
  • mcp/http_client_test.go
  • mcp/oauth_transport.go
📝 Walkthrough

Walkthrough

The changes introduce a helper function to detect custom authorization headers and update the HTTP client to conditionally control OAuth token loading behavior. A new preferStaticAuthorizationHeader flag is passed to the authentication transport, which decides whether to load OAuth tokens based on whether an Authorization header already exists in the provided custom headers.

Changes

Cohort / File(s) Summary
HTTP Client Configuration
mcp/http_client.go
Added authorizationHeaderInCustomHeaders helper function for case-insensitive detection of Authorization headers in custom headers map. Updated httpClientForMCP to compute preferStaticAuthorizationHeader flag and pass it to authenticationTransport constructor.
Authentication Transport Logic
mcp/oauth_transport.go
Added preferStaticAuthorizationHeader boolean field to authenticationTransport struct. Modified RoundTrip method to conditionally skip OAuth token loading when flag is true, and only wrap requests with oauth2.Transport when flag is false and token exists.
Test Coverage
mcp/http_client_test.go
New test file with TestAuthorizationHeaderInCustomHeaders covering nil/empty/populated headers maps, case-insensitive Authorization header detection, and unrelated custom header scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Fix MCP OAuth detection for bare 401 responses #518: Both PRs modify the authenticationTransport.RoundTrip method in mcp/oauth_transport.go to change token-loading and authorization behavior, though this PR adds conditional logic based on static headers while the other addresses HTTP 401 response handling.

Suggested labels

Setup Cloud Test Server

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: prioritizing static Authorization headers over stored OAuth tokens in MCP client configuration.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

When an MCP server is configured with a custom Authorization header (e.g.
API key), do not wrap requests with oauth2.Transport using a previously
stored per-user OAuth token. That stale KV entry still caused createOAuthConfig
and token refresh after switching from OAuth to token-only auth.

Adds a unit test for header detection.
@romzes5000 romzes5000 force-pushed the fix/mcp-static-auth-ignores-oauth-kv branch from 56a1164 to 2e8dbbd Compare April 9, 2026 19:07
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
mcp/http_client_test.go (1)

12-31: Add one transport-level regression test for the actual auth bypass path.

This validates the helper well, but it would be valuable to also assert that when Authorization is present, authenticationTransport.RoundTrip skips token load / OAuth wrapping (not just key detection).

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

In `@mcp/http_client_test.go` around lines 12 - 31, Add a transport-level
regression test that verifies authenticationTransport.RoundTrip skips token
loading/OAuth wrapping when an Authorization header is present: create a new
test (e.g. TestAuthenticationTransportSkipsTokenLoadWhenAuthorizationPresent)
that constructs an authenticationTransport with a fake token provider whose
LoadToken method toggles a flag and a fake base RoundTripper that records the
request it receives; build an *http.Request containing the Authorization header
via the custom headers path used by authorizationHeaderInCustomHeaders, call
authenticationTransport.RoundTrip(req), and assert that the fake token provider
was not called and the outgoing request still contains the original
Authorization header (i.e. no OAuth wrapping occurred).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@mcp/http_client_test.go`:
- Around line 12-31: Add a transport-level regression test that verifies
authenticationTransport.RoundTrip skips token loading/OAuth wrapping when an
Authorization header is present: create a new test (e.g.
TestAuthenticationTransportSkipsTokenLoadWhenAuthorizationPresent) that
constructs an authenticationTransport with a fake token provider whose LoadToken
method toggles a flag and a fake base RoundTripper that records the request it
receives; build an *http.Request containing the Authorization header via the
custom headers path used by authorizationHeaderInCustomHeaders, call
authenticationTransport.RoundTrip(req), and assert that the fake token provider
was not called and the outgoing request still contains the original
Authorization header (i.e. no OAuth wrapping occurred).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: b8e65933-4f08-4522-96af-72d718a00ad5

📥 Commits

Reviewing files that changed from the base of the PR and between 284b321 and 56a1164.

📒 Files selected for processing (3)
  • mcp/http_client.go
  • mcp/http_client_test.go
  • mcp/oauth_transport.go

Copy link
Copy Markdown

@edgarbellot edgarbellot left a comment

Choose a reason for hiding this comment

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

@romzes5000 Thanks for the contribution! The approach of skipping oauth2.Transport when a static Authorization header is present makes sense and the tests look clean.

I left a couple of comments on areas where the new flag could be checked more broadly to keep the static-auth path fully isolated from the OAuth machinery. We might need to update the test to cover the edge cases mentioned in the comments. Happy to provide more context if needed :)

Comment thread mcp/oauth_transport.go
}

// If we get a 401, force an actual error so we can handle it. Include the header info in the error
if resp.StatusCode == http.StatusUnauthorized {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice work on skipping the OAuth token load and oauth2.Transport when a static Authorization header is present - the outgoing request path (67-86) looks correct.

However, should we also guard the 401 response handling block (109-131) with the same preferStaticAuthorizationHeader check? Right now, if a static-auth MCP server responds with a 401 (e.g. expired API key), the transport still returns an mcpUnauthorized error. The caller in createSession (293-302) then catches it via errors.As and calls InitiateOAuthFlow, which triggers outbound HTTP requests for metadata discovery (discoverProtectedResourceMetadata, discoverAuthorizationServerMetadata) and potentially DCR (loadOrCreateClientCredentials).

This means a compromised MCP server could craft its 401 WWW-Authenticate header to point the plugin at an attacker-controlled URL, causing the Mattermost server to make outbound requests to that URL during discovery. The MattermostTransport does block reserved IPs, which limits internal SSRF, but external endpoints are still reachable.

Would it make sense to short-circuit the 401 path when preferStaticAuthorizationHeader is true - something like returning a plain error instead of mcpUnauthorized? For example:

if resp.StatusCode == http.StatusUnauthorized {
    if t.preferStaticAuthorizationHeader {
        drainAndCloseResponseBody(resp)
        return nil, fmt.Errorf("static authorization rejected by server (HTTP 401)")
    }
    // ... existing OAuth 401 handling
}

Comment thread mcp/http_client.go
headers map[string]string
}

func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is a pre-existing issue in headerTransport, but I think this PR makes it worth flagging because the credential at stake changes significantly.

Go's http.Client strips the Authorization header on cross-domain redirects (since Go 1.12) before calling Transport.RoundTrip(). However, headerTransport.RoundTrip unconditionally re-adds all configured headers - including Authorization - on every call. Since http.Client.Do() calls RoundTrip again for each redirect hop, this effectively bypasses Go's built-in protection.

Before this PR, the Authorization header would typically be overwritten by oauth2.Transport with a short-lived OAuth token, so a leaked credential would expire quickly. With preferStaticAuthorizationHeader set to true, oauth2.Transport is skipped and the static API key flows through unchanged. That means a compromised MCP server responding with a 302 Location: https://attacker.com/steal would receive the long-lived API key in the redirected request - silently, with no error or log.

As mentioned in the other comment, the MattermostTransport blocks reserved/internal IPs, which mitigates internal SSRF, but external hosts are fully reachable. There's also no CheckRedirect policy on the http.Client built by httpClientForMCP, so Go's default 10-redirect limit applies with no domain restrictions.

Would it make sense to add a CheckRedirect policy that strips credentials on cross-domain redirects? Something like:

httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
    if len(via) >= 10 {
        return errors.New("stopped after 10 redirects")
    }
    if len(via) > 0 && req.URL.Host != via[0].URL.Host {
        req.Header.Del("Authorization")
    }
    return nil
}

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