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:
1 parent
bc5ad63755
commit
986bc36f04
1 file changed
+117
-15
+117
-15
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user