fix(oidc): consent allow 改走 auth.x.ai form POST,修复 404

硬编码 Next-Action 在 accounts.x.ai consent 页轮换后返回
"Server action not found"。对齐当前 HTML form:POST
https://auth.x.ai/oauth2/authorize (action=allow),旧
Next-Action 路径仅作 fallback。
This commit is contained in:
chaos committed 2026-07-16 06:43:30 +08:00
1 parent bc5ad63755
commit 986bc36f04
1 file changed
+116 -14
+116 -14
View File
@@ -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 ""
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]
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 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 = {