fix(oauth): fix invalid_scope and invalid_token errors for user OAuth clients

Three bugs fixed for social-login users connecting Claude.ai via OAuth:

1. User OAuth clients now allow 'admin' scope by default — previously
   defaulted to ['read', 'write'], causing invalid_scope when Claude.ai
   requests 'admin' scope during authorization.

2. Fix JWT tokens with 'aud' claim being rejected by middleware — when
   Claude.ai sends RFC 8707 resource parameter, the issued JWT gets an
   aud claim. validate_access_token() now sets verify_aud=False to
   prevent PyJWT InvalidAudienceError from silently marking valid tokens
   as invalid in _is_valid_token().

3. Fix NameError in user_mcp_handler tools/call scope check — key_info
   was only defined in the mhu_ auth path but referenced in tools/call
   for both paths. Replaced with key_scopes variable set by both mhu_
   and JWT auth paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-26 00:37:43 +03:30
parent 4a5381d765
commit a806671e2d
3 changed files with 8 additions and 2 deletions

View File

@@ -2946,7 +2946,7 @@ async def dashboard_user_oauth_clients_create(request: Request) -> Response:
client_name = body.get("client_name", "").strip() client_name = body.get("client_name", "").strip()
redirect_uris_raw = body.get("redirect_uris", "") redirect_uris_raw = body.get("redirect_uris", "")
scopes = body.get("scopes", ["read", "write"]) scopes = body.get("scopes", ["read", "write", "admin"])
if not client_name: if not client_name:
return JSONResponse({"error": "Client name required"}, status_code=400) return JSONResponse({"error": "Client name required"}, status_code=400)

View File

@@ -139,6 +139,7 @@ class TokenManager:
"verify_signature": True, "verify_signature": True,
"verify_exp": True, "verify_exp": True,
"verify_nbf": True, "verify_nbf": True,
"verify_aud": False, # Server is the resource server; no external aud check needed
}, },
) )

View File

@@ -210,6 +210,9 @@ async def user_mcp_handler(request: Request) -> Response:
api_key = auth_header[7:] # Strip "Bearer " api_key = auth_header[7:] # Strip "Bearer "
# Shared scope tracking — set by whichever auth path succeeds
key_scopes: list[str] = []
# Try mhu_ API key first, then fall back to OAuth JWT token # Try mhu_ API key first, then fall back to OAuth JWT token
if api_key.startswith("mhu_"): if api_key.startswith("mhu_"):
try: try:
@@ -234,6 +237,7 @@ async def user_mcp_handler(request: Request) -> Response:
_jsonrpc_error(None, -32600, "API key does not match user"), _jsonrpc_error(None, -32600, "API key does not match user"),
status_code=403, status_code=403,
) )
key_scopes = key_info.get("scopes", "read").split()
else: else:
# Try OAuth JWT token (issued after consent flow via GitHub/Google login) # Try OAuth JWT token (issued after consent flow via GitHub/Google login)
try: try:
@@ -258,6 +262,7 @@ async def user_mcp_handler(request: Request) -> Response:
_jsonrpc_error(None, -32600, "Token user mismatch"), _jsonrpc_error(None, -32600, "Token user mismatch"),
status_code=403, status_code=403,
) )
key_scopes = jwt_payload.get("scope", "read").split()
except pyjwt.ExpiredSignatureError: except pyjwt.ExpiredSignatureError:
return JSONResponse( return JSONResponse(
_jsonrpc_error(None, -32600, "Token expired"), _jsonrpc_error(None, -32600, "Token expired"),
@@ -360,7 +365,7 @@ async def user_mcp_handler(request: Request) -> Response:
return JSONResponse(_jsonrpc_error(req_id, -32601, f"Tool '{tool_name}' not found")) return JSONResponse(_jsonrpc_error(req_id, -32601, f"Tool '{tool_name}' not found"))
required_scope = tool_def.required_scope required_scope = tool_def.required_scope
key_scopes = key_info.get("scopes", "").split() # key_scopes is set during authentication (both mhu_ and JWT paths)
scope_hierarchy = {"read": 1, "write": 2, "admin": 3} scope_hierarchy = {"read": 1, "write": 2, "admin": 3}
required_level = scope_hierarchy.get(required_scope, 0) required_level = scope_hierarchy.get(required_scope, 0)