mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
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:
@@ -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),
|
||||
|
||||
@@ -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"})
|
||||
|
||||
Reference in New Issue
Block a user