-
Notifications
You must be signed in to change notification settings - Fork 0
Pitfall invalid scope disguised as 302
Affects: OAuth authorization flow
Discovered: v1.2.9 (2026-07-05)
Fixed in: v1.2.10
After fixing the wrong redirect URI in v1.2.9, Claude.ai still showed "Authorization with the MCP server failed" — even though nginx logs showed a clean HTTP 302 from /authorize.
The nginx log line for a successful authorization and for a failed one are identical:
POST /authorize HTTP/1.1" 302
The difference is only in the Location header:
| Outcome | Location header |
|---|---|
| Success | https://claude.ai/api/mcp/auth_callback?code=XXXX&state=... |
| Failure | https://claude.ai/api/mcp/auth_callback?error=invalid_scope&state=... |
Because the redirect goes to Claude.ai's domain, the server never sees the callback. The only observable symptom server-side is: Anthropic's IPs (160.79.106.34–39) call /authorize but never call /token.
Claude.ai requested the full historical scope list regardless of what the server advertised:
scope=content.read+content.write+site.admin+system.admin
Before v1.2.10, the server still treated system.admin as a separate tier above site.admin. resolveClientScope picked system.admin, compared it against the client's registered max (site.admin), and returned invalid_scope — which HandleAuthorize then sent as a redirect to the client's callback URI with ?error=invalid_scope.
The client (Claude.ai) received the error, aborted the flow, and showed "Authorization failed". The /token endpoint was never called.
If Claude.ai says "Authorization failed" and you see 302s from /authorize in nginx but no /token call from Anthropic IPs:
# Simulate the exact authorize request Claude.ai sends
curl -sk -o /dev/null -w '%{url_effective}' \
"https://mcp.arleo.eu/authorize?response_type=code \
&client_id=claude-admin \
&redirect_uri=https://claude.ai/api/mcp/auth_callback \
&scope=content.read+content.write+site.admin+system.admin \
&state=test \
&code_challenge=n4sk8wlI_n8S-GaUfbNTr6dL7X5enlRgJe27i8U6Bhg \
&code_challenge_method=S256"A broken response looks like:
https://claude.ai/api/mcp/auth_callback?error=invalid_scope&state=test
A working response looks like:
https://claude.ai/api/mcp/auth_callback?code=XXXX&state=test
Two changes in v1.2.10:
Instead of returning invalid_scope when the requested scope exceeds the client's max, the server now clamps to the client's maximum allowed scope and issues the token. RFC 6749 §3.3 explicitly permits the authorization server to grant a subset of the requested scope.
// Before: rejected with invalid_scope
if !allowedScope(scope, c.Scope) {
return "", fmt.Errorf("invalid_scope")
}
// After: clamp to client's max
if !allowedScope(scope, c.Scope) {
scope = c.Scope
}system.admin is no longer a separate exposed tier. It was collapsed into site.admin, and incoming system.admin values are normalized to site.admin. The active scope hierarchy is:
| Rank | Scope | OAuth |
|---|---|---|
| 0 | anonymous | none |
| 1 | content.read |
required |
| 2 | content.write |
required |
| 3 | site.admin |
required |
Never trust HTTP 302 from /authorize as proof of a successful flow. Always inspect the Location header for ?error=. The smoke test probe_authorize should check for the absence of error= in the redirect location, not just the 302 status code.
Related issue: #121