Skip to content

Add RFC 6750 error to the /mcp 401 WWW-Authenticate header - #30

Merged
aterga merged 2 commits into
mainfrom
claude/mcp-spec-compliance-9202ka
Jul 4, 2026
Merged

aterga merged 2 commits into
mainfrom
claude/mcp-spec-compliance-9202ka

Conversation

@aterga

@aterga aterga commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

What & why

Helps MCP clients trigger re-authorization inline (a reconnect prompt in the chat) instead of making the user dig into connector settings when their token expires.

The /mcp 401 already carried the RFC 9728 discovery pointer (WWW-Authenticate: Bearer resource_metadata="…"), and #29 made it readable cross-origin. The one missing piece was the RFC 6750 §3 error field in the header — clients that key their auto-reprompt off the challenge string couldn't tell "token expired → re-authorize" from "no token / other."

Change

In require_token, when a bearer token was presented but is invalid/expired, the challenge now includes the error code:

WWW-Authenticate: Bearer error="invalid_token",
  error_description="The access token is invalid or expired",
  resource_metadata="<base>/.well-known/oauth-protected-resource"

A request with no token still gets a bare challenge (RFC 6750: omit the error code when no credentials were sent):

WWW-Authenticate: Bearer resource_metadata="<base>/.well-known/oauth-protected-resource"

The {"error":"invalid_token"} JSON body is unchanged. No new dependencies; ~a few lines in require_token.

Testing

  • cargo build clean (one pre-existing macro warning).
  • Verified against a local server:
    • POST /mcp with no Authorization → WWW-Authenticate: Bearer resource_metadata="…" (no error field).
    • POST /mcp with an invalid bearer token → WWW-Authenticate: Bearer error="invalid_token", error_description="The access token is invalid or expired", resource_metadata="…".
    • Both still carry the /mcp CORS headers, incl. access-control-expose-headers: www-authenticate,mcp-session-id, so browser clients can read the challenge.

Notes

  • Branch restarted from main after Add CORS to /mcp so browser-based MCP connectors can probe it #29's squash-merge; fresh PR.
  • Server-side signal only — whether a client renders an inline reconnect vs. a settings trip is that client's UX choice. Fully silent re-auth (no user gesture) would still require refresh tokens, intentionally deferred.

🤖 Generated with Claude Code

https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym


Generated by Claude Code

When a bearer token is presented but invalid/expired, include
error="invalid_token" (+ error_description) in the WWW-Authenticate
challenge, in addition to the RFC 9728 resource_metadata pointer, so MCP
clients can distinguish "expired -> re-authorize" from "no token" and
prompt an inline reconnect rather than sending the user to settings. A
request with no token still gets a bare challenge (RFC 6750: omit the error
code when no credentials were sent). The header is already exposed
cross-origin via the /mcp CORS layer, so browser-based clients can read it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the /mcp bearer-token gate to include an RFC 6750 error="invalid_token" (plus error_description) in the WWW-Authenticate challenge only when a bearer token was presented but is invalid/expired, while keeping the bare RFC 9728 resource_metadata challenge for requests with no token. This helps MCP clients distinguish “token expired → re-authorize” from “no credentials provided”.

Changes:

  • Track whether a token was presented and branch the WWW-Authenticate header accordingly.
  • Add error="invalid_token" and an error_description to the WWW-Authenticate header for invalid/expired tokens.
  • Preserve the existing JSON body ({"error":"invalid_token"}) and discovery pointer (resource_metadata=...).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/auth.rs Resolved
Comment thread src/auth.rs Resolved
Address PR #30 review:
- Parse the Authorization scheme case-insensitively (RFC 7235 §2.1), so
  `Authorization: bearer <token>` is recognized as a token (previously a
  case-sensitive strip_prefix("Bearer ") treated it as no token, which also
  mis-set had_token and dropped the error="invalid_token" challenge).
- Extract the WWW-Authenticate challenge into a pure `bearer_challenge`
  helper and unit-test both branches (bare vs error="invalid_token"), to
  lock in the RFC 6750/9728 header formatting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
@aterga
aterga marked this pull request as ready for review July 4, 2026 18:23
Copilot AI review requested due to automatic review settings July 4, 2026 18:23
@aterga
aterga merged commit c9f42fd into main Jul 4, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment thread src/auth.rs
Comment on lines +890 to +893
.and_then(|h| {
let (scheme, rest) = h.split_once(' ')?;
scheme.eq_ignore_ascii_case("Bearer").then(|| rest.trim().to_owned())
});
Comment thread src/auth.rs

let no_token = super::bearer_challenge(false);
assert!(no_token.starts_with("Bearer "));
assert!(!no_token.contains("error="), "a bare challenge must omit the error code: {no_token}");
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.

3 participants