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
+116
-14
+116
-14
@@ -3,8 +3,14 @@
|
|||||||
对齐最新可用流程:authorize / consent 必须带 referrer=grok-build,
|
对齐最新可用流程:authorize / consent 必须带 referrer=grok-build,
|
||||||
否则 access_token JWT 缺少 referrer 字段,cli-chat-proxy / 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)
|
-> 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
|
from __future__ import annotations
|
||||||
@@ -38,11 +44,11 @@ SCOPE = (
|
|||||||
)
|
)
|
||||||
GROK_CLI_SCOPE = "grok-cli:access"
|
GROK_CLI_SCOPE = "grok-cli:access"
|
||||||
GROK_REFERRER = "grok-build"
|
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_VERSION = "0.2.101"
|
||||||
GROK_TOKEN_UA = f"xai-grok-build/{GROK_VERSION}"
|
GROK_TOKEN_UA = f"xai-grok-build/{GROK_VERSION}"
|
||||||
GROK_CLIENT_SURFACE = "grok-build"
|
GROK_CLIENT_SURFACE = "grok-build"
|
||||||
# Next.js Server Action id(consent 页 POST 需要)
|
# 旧 Next.js Server Action id(已失效,仅作 fallback 尝试)
|
||||||
NEXT_ACTION_ID = "4005315a1d7e426de592990bb54bb37471f39dd6d2"
|
NEXT_ACTION_ID = "4005315a1d7e426de592990bb54bb37471f39dd6d2"
|
||||||
BROWSER_UA = (
|
BROWSER_UA = (
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
"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)}")
|
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
|
redirect_uri = flow.redirect_uri or REDIRECT_URI_LEGACY
|
||||||
payload = [
|
payload = [
|
||||||
{
|
{
|
||||||
@@ -413,24 +497,42 @@ def approve_authorization(session: Any, consent_url: str, flow: AuthCodeFlow) ->
|
|||||||
consent_url,
|
consent_url,
|
||||||
data=body.encode("utf-8"),
|
data=body.encode("utf-8"),
|
||||||
headers=_browser_headers("POST", consent_url, NEXT_ACTION_ID),
|
headers=_browser_headers("POST", consent_url, NEXT_ACTION_ID),
|
||||||
allow_redirects=True,
|
allow_redirects=False,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
text = resp.text or ""
|
text = resp.text or ""
|
||||||
if resp.status_code < 200 or resp.status_code >= 300:
|
loc = (
|
||||||
raise OAuthCodeError(f"consent HTTP {resp.status_code}: {_short(text, 300)}")
|
resp.headers.get("Location")
|
||||||
|
or resp.headers.get("location")
|
||||||
# 有时 302 到 redirect_uri?code=
|
or ""
|
||||||
final = _final_url(resp)
|
)
|
||||||
if "code=" in final:
|
code = _code_from_location(loc) or _code_from_location(_final_url(resp))
|
||||||
qs = parse_qs(urlparse(final).query)
|
|
||||||
code = (qs.get("code") or [""])[0]
|
|
||||||
if code:
|
if code:
|
||||||
return 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)
|
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:
|
def exchange_auth_code(session: Any, code: str, flow: AuthCodeFlow) -> TokenResult:
|
||||||
redirect_uri = flow.redirect_uri or REDIRECT_URI_LEGACY
|
redirect_uri = flow.redirect_uri or REDIRECT_URI_LEGACY
|
||||||
form = {
|
form = {
|
||||||
|
|||||||
Reference in new issue
Block a user