fix(skills): honor scope query from Google OAuth redirect URL

Parse scope from the raw callback URL before stripping the auth code so Flow.fetch_token matches user-granted scopes. Add regression test for dual-scope callbacks.

Made-with: Cursor
This commit is contained in:
xiahu88988
2026-04-17 07:24:19 +08:00
committed by Teknium
parent 6c87371815
commit 898ccfd667
2 changed files with 24 additions and 13 deletions

View File

@@ -289,6 +289,7 @@ def exchange_auth_code(code: str):
sys.exit(1)
pending_auth = _load_pending_auth()
raw_callback = code
code, returned_state = _extract_code_and_state(code)
if returned_state and returned_state != pending_auth["state"]:
print("ERROR: OAuth state mismatch. Run --auth-url again to start a fresh session.")
@@ -298,19 +299,13 @@ def exchange_auth_code(code: str):
from google_auth_oauthlib.flow import Flow
from urllib.parse import parse_qs, urlparse
# Extract granted scopes from the callback URL if present
if returned_state and "scope" in parse_qs(urlparse(code).query if isinstance(code, str) and code.startswith("http") else {}):
granted_scopes = parse_qs(urlparse(code).query)["scope"][0].split()
else:
# Try to extract from code_or_url parameter
if isinstance(code, str) and code.startswith("http"):
params = parse_qs(urlparse(code).query)
if "scope" in params:
granted_scopes = params["scope"][0].split()
else:
granted_scopes = SCOPES
else:
granted_scopes = SCOPES
# Extract granted scopes from the callback URL if the user pasted the full redirect URL.
granted_scopes = list(SCOPES)
if isinstance(raw_callback, str) and raw_callback.startswith("http"):
params = parse_qs(urlparse(raw_callback).query)
scope_val = (params.get("scope") or [""])[0].strip()
if scope_val:
granted_scopes = scope_val.split()
flow = Flow.from_client_secrets_file(
str(CLIENT_SECRET_PATH),

View File

@@ -177,6 +177,22 @@ class TestExchangeAuthCode:
flow = FakeFlow.created[-1]
assert flow.fetch_token_calls == [{"code": "4/extracted-code"}]
def test_passes_scopes_from_redirect_url_to_flow(self, setup_module):
"""Callback URL carries space-delimited scope list; Flow must receive it (not full SCOPES)."""
setup_module.PENDING_AUTH_PATH.write_text(
json.dumps({"state": "saved-state", "code_verifier": "saved-verifier"})
)
g1 = "https://www.googleapis.com/auth/gmail.readonly"
g2 = "https://www.googleapis.com/auth/calendar"
from urllib.parse import quote
scope_q = quote(f"{g1} {g2}", safe="")
setup_module.exchange_auth_code(
f"http://localhost:1/?code=4/extracted-code&state=saved-state&scope={scope_q}"
)
flow = FakeFlow.created[-1]
assert flow.scopes == [g1, g2]
def test_rejects_state_mismatch(self, setup_module, capsys):
setup_module.PENDING_AUTH_PATH.write_text(
json.dumps({"state": "saved-state", "code_verifier": "saved-verifier"})