From a806671e2d256e20c4c7364b162a06a2c24ffc3d Mon Sep 17 00:00:00 2001 From: airano Date: Thu, 26 Feb 2026 00:37:43 +0330 Subject: [PATCH] fix(oauth): fix invalid_scope and invalid_token errors for user OAuth clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- core/dashboard/routes.py | 2 +- core/oauth/token_manager.py | 1 + core/user_endpoints.py | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/dashboard/routes.py b/core/dashboard/routes.py index d86c9c4..b25a994 100644 --- a/core/dashboard/routes.py +++ b/core/dashboard/routes.py @@ -2946,7 +2946,7 @@ async def dashboard_user_oauth_clients_create(request: Request) -> Response: client_name = body.get("client_name", "").strip() redirect_uris_raw = body.get("redirect_uris", "") - scopes = body.get("scopes", ["read", "write"]) + scopes = body.get("scopes", ["read", "write", "admin"]) if not client_name: return JSONResponse({"error": "Client name required"}, status_code=400) diff --git a/core/oauth/token_manager.py b/core/oauth/token_manager.py index 4c4a89f..132a49a 100644 --- a/core/oauth/token_manager.py +++ b/core/oauth/token_manager.py @@ -139,6 +139,7 @@ class TokenManager: "verify_signature": True, "verify_exp": True, "verify_nbf": True, + "verify_aud": False, # Server is the resource server; no external aud check needed }, ) diff --git a/core/user_endpoints.py b/core/user_endpoints.py index c3910ff..b253f95 100644 --- a/core/user_endpoints.py +++ b/core/user_endpoints.py @@ -210,6 +210,9 @@ async def user_mcp_handler(request: Request) -> Response: 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 if api_key.startswith("mhu_"): try: @@ -234,6 +237,7 @@ async def user_mcp_handler(request: Request) -> Response: _jsonrpc_error(None, -32600, "API key does not match user"), status_code=403, ) + key_scopes = key_info.get("scopes", "read").split() else: # Try OAuth JWT token (issued after consent flow via GitHub/Google login) try: @@ -258,6 +262,7 @@ async def user_mcp_handler(request: Request) -> Response: _jsonrpc_error(None, -32600, "Token user mismatch"), status_code=403, ) + key_scopes = jwt_payload.get("scope", "read").split() except pyjwt.ExpiredSignatureError: return JSONResponse( _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")) 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} required_level = scope_hierarchy.get(required_scope, 0)