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)
|
sys.exit(1)
|
||||||
|
|
||||||
pending_auth = _load_pending_auth()
|
pending_auth = _load_pending_auth()
|
||||||
|
raw_callback = code
|
||||||
code, returned_state = _extract_code_and_state(code)
|
code, returned_state = _extract_code_and_state(code)
|
||||||
if returned_state and returned_state != pending_auth["state"]:
|
if returned_state and returned_state != pending_auth["state"]:
|
||||||
print("ERROR: OAuth state mismatch. Run --auth-url again to start a fresh session.")
|
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 google_auth_oauthlib.flow import Flow
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
# Extract granted scopes from the callback URL if present
|
# Extract granted scopes from the callback URL if the user pasted the full redirect URL.
|
||||||
if returned_state and "scope" in parse_qs(urlparse(code).query if isinstance(code, str) and code.startswith("http") else {}):
|
granted_scopes = list(SCOPES)
|
||||||
granted_scopes = parse_qs(urlparse(code).query)["scope"][0].split()
|
if isinstance(raw_callback, str) and raw_callback.startswith("http"):
|
||||||
else:
|
params = parse_qs(urlparse(raw_callback).query)
|
||||||
# Try to extract from code_or_url parameter
|
scope_val = (params.get("scope") or [""])[0].strip()
|
||||||
if isinstance(code, str) and code.startswith("http"):
|
if scope_val:
|
||||||
params = parse_qs(urlparse(code).query)
|
granted_scopes = scope_val.split()
|
||||||
if "scope" in params:
|
|
||||||
granted_scopes = params["scope"][0].split()
|
|
||||||
else:
|
|
||||||
granted_scopes = SCOPES
|
|
||||||
else:
|
|
||||||
granted_scopes = SCOPES
|
|
||||||
|
|
||||||
flow = Flow.from_client_secrets_file(
|
flow = Flow.from_client_secrets_file(
|
||||||
str(CLIENT_SECRET_PATH),
|
str(CLIENT_SECRET_PATH),
|
||||||
|
|||||||
@@ -177,6 +177,22 @@ class TestExchangeAuthCode:
|
|||||||
flow = FakeFlow.created[-1]
|
flow = FakeFlow.created[-1]
|
||||||
assert flow.fetch_token_calls == [{"code": "4/extracted-code"}]
|
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):
|
def test_rejects_state_mismatch(self, setup_module, capsys):
|
||||||
setup_module.PENDING_AUTH_PATH.write_text(
|
setup_module.PENDING_AUTH_PATH.write_text(
|
||||||
json.dumps({"state": "saved-state", "code_verifier": "saved-verifier"})
|
json.dumps({"state": "saved-state", "code_verifier": "saved-verifier"})
|
||||||
|
|||||||
Reference in New Issue
Block a user