diff --git a/oidc_mint/oauth_code.py b/oidc_mint/oauth_code.py index 8d110b1..520256b 100644 --- a/oidc_mint/oauth_code.py +++ b/oidc_mint/oauth_code.py @@ -3,8 +3,14 @@ 对齐最新可用流程:authorize / consent 必须带 referrer=grok-build, 否则 access_token JWT 缺少 referrer 字段,cli-chat-proxy / grok-build 不可用。 -参考实现:sso -> oauth2/authorize(referrer=grok-build) -> consent allow +参考实现:sso -> oauth2/authorize(referrer=grok-build) -> consent page + -> POST auth.x.ai/oauth2/authorize (form action=allow) -> oauth2/token (authorization_code + PKCE) + +2026-07-16:accounts.x.ai consent 页 Next.js Server Action 频繁轮换, +硬编码 Next-Action 会 404 "Server action not found"。 +官方 HTML form 的 action 指向 auth.x.ai/oauth2/authorize(非 consent URL), +form-urlencoded + action=allow 可稳定拿到 code。 """ from __future__ import annotations @@ -38,11 +44,11 @@ SCOPE = ( ) GROK_CLI_SCOPE = "grok-cli:access" GROK_REFERRER = "grok-build" -# 对齐官方 stable CLI(2026-07-14:0.2.101) +# 对齐官方 stable CLI(2026-07-14 二进制:0.2.101) GROK_VERSION = "0.2.101" GROK_TOKEN_UA = f"xai-grok-build/{GROK_VERSION}" GROK_CLIENT_SURFACE = "grok-build" -# Next.js Server Action id(consent 页 POST 需要) +# 旧 Next.js Server Action id(已失效,仅作 fallback 尝试) NEXT_ACTION_ID = "4005315a1d7e426de592990bb54bb37471f39dd6d2" BROWSER_UA = ( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " @@ -391,7 +397,85 @@ def parse_consent_code(body: str) -> str: raise OAuthCodeError(f"consent 响应缺少 code: {_short(text, 300)}") -def approve_authorization(session: Any, consent_url: str, flow: AuthCodeFlow) -> str: +def _code_from_location(url: str) -> str: + if not url or "code=" not in url: + return "" + qs = parse_qs(urlparse(url).query) + return str((qs.get("code") or [""])[0] or "").strip() + + +def _approve_via_form_post( + session: Any, consent_url: str, flow: AuthCodeFlow +) -> str: + """POST form to auth.x.ai/oauth2/authorize (matches consent page HTML).""" + redirect_uri = flow.redirect_uri or REDIRECT_URI_LEGACY + fields = { + "client_id": CLIENT_ID, + "redirect_uri": redirect_uri, + "scope": SCOPE, + "state": flow.state, + "code_challenge": flow.code_challenge, + "code_challenge_method": "S256", + "nonce": flow.nonce, + "principal_type": "User", + "principal_id": "", + "referrer": GROK_REFERRER, + "action": "allow", + } + headers = { + "User-Agent": BROWSER_UA, + "Accept": ( + "text/html,application/xhtml+xml,application/xml;q=0.9," + "*/*;q=0.8" + ), + "Content-Type": "application/x-www-form-urlencoded", + "Origin": "https://accounts.x.ai", + "Referer": consent_url, + "Sec-Fetch-Site": "same-site", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Dest": "document", + "Upgrade-Insecure-Requests": "1", + "Accept-Language": "en-US,en;q=0.9", + } + # 不自动 follow 到 127.0.0.1 loopback(本机无 listener) + resp = session.post( + AUTHORIZE_URL, + data=urlencode(fields), + headers=headers, + allow_redirects=False, + timeout=30, + ) + loc = ( + resp.headers.get("Location") + or resp.headers.get("location") + or "" + ) + code = _code_from_location(loc) + if code: + return code + + # 少数库会吞 Location 到 resp.url / 已 follow + final = _final_url(resp) + code = _code_from_location(final) + if code: + return code + + text = resp.text or "" + if 200 <= resp.status_code < 300: + try: + return parse_consent_code(text) + except OAuthCodeError: + pass + raise OAuthCodeError( + f"consent form POST HTTP {resp.status_code}: " + f"loc={_short(loc, 120)} body={_short(text, 200)}" + ) + + +def _approve_via_next_action( + session: Any, consent_url: str, flow: AuthCodeFlow +) -> str: + """旧路径:Next.js Server Action POST consent URL(action id 常失效)。""" redirect_uri = flow.redirect_uri or REDIRECT_URI_LEGACY payload = [ { @@ -413,24 +497,42 @@ def approve_authorization(session: Any, consent_url: str, flow: AuthCodeFlow) -> consent_url, data=body.encode("utf-8"), headers=_browser_headers("POST", consent_url, NEXT_ACTION_ID), - allow_redirects=True, + allow_redirects=False, timeout=30, ) text = resp.text or "" + loc = ( + resp.headers.get("Location") + or resp.headers.get("location") + or "" + ) + code = _code_from_location(loc) or _code_from_location(_final_url(resp)) + if code: + return code if resp.status_code < 200 or resp.status_code >= 300: - raise OAuthCodeError(f"consent HTTP {resp.status_code}: {_short(text, 300)}") - - # 有时 302 到 redirect_uri?code= - final = _final_url(resp) - if "code=" in final: - qs = parse_qs(urlparse(final).query) - code = (qs.get("code") or [""])[0] - if code: - return code - + raise OAuthCodeError( + f"consent Next-Action HTTP {resp.status_code}: {_short(text, 300)}" + ) return parse_consent_code(text) +def approve_authorization(session: Any, consent_url: str, flow: AuthCodeFlow) -> str: + """Approve consent and return authorization code. + + 优先:HTML form POST → https://auth.x.ai/oauth2/authorize + 回退:旧 Next-Action POST consent URL(易 404)。 + """ + try: + return _approve_via_form_post(session, consent_url, flow) + except OAuthCodeError as form_exc: + try: + return _approve_via_next_action(session, consent_url, flow) + except OAuthCodeError as next_exc: + raise OAuthCodeError( + f"consent allow failed: form={form_exc}; next_action={next_exc}" + ) from next_exc + + def exchange_auth_code(session: Any, code: str, flow: AuthCodeFlow) -> TokenResult: redirect_uri = flow.redirect_uri or REDIRECT_URI_LEGACY form = {