Files
grok-keygen-new/oidc_mint/oauth_code.py
T
chaos bf30d40c8b Fix browser PKCE stuck on consent without code.
Use real Allow clicks (JS click breaks React action), then browser-side
Next.js Server Action POST as fallback; harden SSO cookie injection.
2026-07-14 10:23:24 +08:00

1163 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""xAI OAuth Authorization Code + PKCE (SSO cookie → CPA token).
对齐最新可用流程:authorize / consent 必须带 referrer=grok-build,
否则 access_token JWT 缺少 referrer 字段,cli-chat-proxy / grok-build 不可用。
参考实现:sso -> oauth2/authorize(referrer=grok-build) -> consent allow
-> oauth2/token (authorization_code + PKCE)
"""
from __future__ import annotations
import base64
import hashlib
import json
import re
import secrets
import time
from dataclasses import dataclass
from typing import Any, Callable
from urllib.parse import parse_qs, urlencode, urljoin, urlparse
from .proxyutil import resolve_proxy
CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828"
ISSUER = "https://auth.x.ai"
TOKEN_URL = f"{ISSUER}/oauth2/token"
AUTHORIZE_URL = f"{ISSUER}/oauth2/authorize"
REDIRECT_URI = "http://127.0.0.1:56121/callback"
# 比旧 device-code scope 多 conversations:*,对齐 grok-build
SCOPE = (
"openid profile email offline_access "
"grok-cli:access api:access conversations:read conversations:write"
)
GROK_REFERRER = "grok-build"
GROK_VERSION = "0.2.93"
GROK_TOKEN_UA = (
f"grok-pager/{GROK_VERSION} grok-shell/{GROK_VERSION} (linux; x86_64)"
)
# Next.js Server Action id(consent 页 POST 需要)
NEXT_ACTION_ID = "4005315a1d7e426de592990bb54bb37471f39dd6d2"
BROWSER_UA = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
)
LogFn = Callable[[str], None]
def _noop_log(_: str) -> None:
return None
class OAuthCodeError(RuntimeError):
pass
@dataclass
class AuthCodeFlow:
state: str
nonce: str
code_verifier: str
code_challenge: str
@dataclass
class TokenResult:
access_token: str
refresh_token: str
id_token: str | None
token_type: str
expires_in: int
raw: dict[str, Any]
referrer: str = ""
def _b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def new_auth_code_flow() -> AuthCodeFlow:
verifier = _b64url(secrets.token_bytes(32))
state = _b64url(secrets.token_bytes(16))
nonce = _b64url(secrets.token_bytes(16))
challenge = _b64url(hashlib.sha256(verifier.encode("ascii")).digest())
return AuthCodeFlow(
state=state,
nonce=nonce,
code_verifier=verifier,
code_challenge=challenge,
)
def normalize_sso_cookie(raw: str) -> str:
token = (raw or "").strip()
if token.lower().startswith("sso="):
token = token[4:].strip()
return token
def jwt_payload(token: str) -> dict[str, Any]:
parts = (token or "").split(".")
if len(parts) < 2:
raise ValueError("invalid JWT")
seg = parts[1]
seg += "=" * (-len(seg) % 4)
return json.loads(base64.urlsafe_b64decode(seg.encode("ascii")))
def _short(value: str, limit: int = 240) -> str:
value = value or ""
return value if len(value) <= limit else value[:limit]
def _is_curl_tls_broken(exc: BaseException | str) -> bool:
"""识别 curl_cffi / libcurl OpenSSL 损坏类错误(常见于部分 Windows 环境)。"""
text = str(exc or "").lower()
needles = (
"openssl_internal:invalid library",
"tls connect error",
"curl: (35)",
"failed to perform, curl: (35)",
"ssl_error_syscall",
"ssl connect error",
"wrong version number",
)
return any(n in text for n in needles)
def _make_std_session(proxy: str | None = None):
try:
import requests as std_requests
except ImportError as e: # pragma: no cover
raise OAuthCodeError(
"需要 curl_cffi 或 requests 才能执行 SSO→OAuth 转换"
) from e
resolved = resolve_proxy(proxy)
proxies = {"http": resolved, "https": resolved} if resolved else None
sess = std_requests.Session()
if proxies:
sess.proxies.update(proxies)
try:
sess._cpa_http_backend = "requests" # type: ignore[attr-defined]
except Exception:
pass
return sess
def _make_curl_session(proxy: str | None = None):
from curl_cffi import requests as crequests
resolved = resolve_proxy(proxy)
proxies = {"http": resolved, "https": resolved} if resolved else None
last_err: Exception | None = None
for impersonate in ("chrome131", "chrome124", "chrome120", "chrome110", None):
try:
if impersonate:
sess = crequests.Session(impersonate=impersonate, proxies=proxies)
else:
sess = crequests.Session(proxies=proxies)
try:
sess._cpa_http_backend = f"curl_cffi:{impersonate or 'default'}" # type: ignore[attr-defined]
except Exception:
pass
return sess
except Exception as exc: # noqa: BLE001
last_err = exc
continue
if last_err:
raise last_err
raise OAuthCodeError("curl_cffi Session 创建失败")
def _make_session(proxy: str | None = None, *, prefer: str = "curl"):
"""优先 curl_cffi(Chrome TLS);prefer=requests 时直接标准库。"""
prefer = (prefer or "curl").strip().lower()
errors: list[str] = []
if prefer != "requests":
try:
return _make_curl_session(proxy)
except ImportError:
errors.append("curl_cffi 未安装")
except Exception as exc: # noqa: BLE001
errors.append(f"curl_cffi: {exc}")
try:
return _make_std_session(proxy)
except Exception as exc: # noqa: BLE001
errors.append(f"requests: {exc}")
raise OAuthCodeError(
"无法创建 HTTP Session: " + " | ".join(errors)
) from exc
def _set_sso_cookies(session: Any, sso: str) -> None:
sso = normalize_sso_cookie(sso)
if not sso:
raise OAuthCodeError("sso cookie 为空")
# curl_cffi / requests cookie jar
for domain in ("accounts.x.ai", "auth.x.ai", ".x.ai"):
for name in ("sso", "sso-rw"):
try:
session.cookies.set(name, sso, domain=domain, path="/")
except Exception:
try:
session.cookies.set(name, sso)
except Exception:
pass
def _browser_headers(method: str, url: str, next_action: str = "") -> dict[str, str]:
headers = {
"User-Agent": BROWSER_UA,
"Sec-CH-UA": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"Linux"',
"Accept-Language": "en-US,en;q=0.9",
}
if method.upper() == "POST":
headers.update(
{
"Accept": "text/x-component",
"Content-Type": "text/plain;charset=UTF-8",
"Origin": "https://accounts.x.ai",
"Referer": url,
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
}
)
if next_action:
headers["Next-Action"] = next_action
else:
headers.update(
{
"Accept": (
"text/html,application/xhtml+xml,application/xml;q=0.9,"
"application/json;q=0.8,*/*;q=0.7"
),
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Site": "none",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Dest": "document",
}
)
return headers
def _token_headers() -> dict[str, str]:
return {
"User-Agent": GROK_TOKEN_UA,
"Accept": "*/*",
"X-Grok-Client-Version": GROK_VERSION,
"Content-Type": "application/x-www-form-urlencoded",
}
def _final_url(resp: Any) -> str:
try:
return str(getattr(resp, "url", "") or "")
except Exception:
return ""
# HTTP 铸造单步超时:直连/跨境链路差时不要卡 30s,尽快让上层走浏览器
HTTP_STEP_TIMEOUT = 12
def open_authorize_page(session: Any, flow: AuthCodeFlow) -> str:
params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": SCOPE,
"code_challenge": flow.code_challenge,
"code_challenge_method": "S256",
"state": flow.state,
"nonce": flow.nonce,
"referrer": GROK_REFERRER,
}
url = f"{AUTHORIZE_URL}?{urlencode(params)}"
resp = session.get(
url,
headers=_browser_headers("GET", url),
allow_redirects=True,
timeout=HTTP_STEP_TIMEOUT,
)
body = resp.text or ""
final = _final_url(resp)
if resp.status_code < 200 or resp.status_code >= 300:
raise OAuthCodeError(
f"authorize HTTP {resp.status_code}: {_short(body)}"
)
if "sign-in" in final or "sign-up" in final:
raise OAuthCodeError("sso 无效(authorize 跳转登录页)")
if "/oauth2/consent" not in final:
# 少数情况 consent 在 body 的 redirect 里
m = re.search(r'https?://[^"\']+/oauth2/consent[^"\']*', body)
if m:
final = m.group(0)
else:
raise OAuthCodeError(f"authorize 未进入 consent: {final or _short(body)}")
return final
def parse_consent_code(body: str) -> str:
"""从 Next.js RSC / text-x-component 响应中解析 code。"""
text = body or ""
# 1) 逐行 JSON(Go 实现路径)
for line in text.splitlines():
idx = line.find("{")
if idx < 0:
continue
try:
obj = json.loads(line[idx:])
except Exception:
continue
if isinstance(obj, dict) and obj.get("code"):
if obj.get("success") is False:
raise OAuthCodeError(
f"consent 失败: {obj.get('error') or obj.get('action')}"
)
return str(obj["code"]).strip()
if isinstance(obj, list):
for item in obj:
if isinstance(item, dict) and item.get("code"):
return str(item["code"]).strip()
# 2) 宽松正则
m = re.search(r'"code"\s*:\s*"([A-Za-z0-9._~\-]+)"', text)
if m:
return m.group(1)
# 3) redirect 里带 code=
m = re.search(r"[?&]code=([A-Za-z0-9._~\-]+)", text)
if m:
return m.group(1)
raise OAuthCodeError(f"consent 响应缺少 code: {_short(text, 300)}")
def approve_authorization(session: Any, consent_url: str, flow: AuthCodeFlow) -> str:
payload = [
{
"action": "allow",
"clientId": CLIENT_ID,
"redirectUri": REDIRECT_URI,
"scope": SCOPE,
"state": flow.state,
"codeChallenge": flow.code_challenge,
"codeChallengeMethod": "S256",
"nonce": flow.nonce,
"principalType": "User",
"principalId": "",
"referrer": GROK_REFERRER,
}
]
body = json.dumps(payload, separators=(",", ":"))
resp = session.post(
consent_url,
data=body.encode("utf-8"),
headers=_browser_headers("POST", consent_url, NEXT_ACTION_ID),
allow_redirects=True,
timeout=HTTP_STEP_TIMEOUT,
)
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]
if code:
return code
return parse_consent_code(text)
def exchange_auth_code(session: Any, code: str, flow: AuthCodeFlow) -> TokenResult:
form = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"code_verifier": flow.code_verifier,
}
resp = session.post(
TOKEN_URL,
data=urlencode(form),
headers=_token_headers(),
timeout=HTTP_STEP_TIMEOUT,
)
text = resp.text or ""
if resp.status_code < 200 or resp.status_code >= 300:
raise OAuthCodeError(f"token HTTP {resp.status_code}: {_short(text, 300)}")
try:
data = resp.json()
except Exception as e:
raise OAuthCodeError(f"token 响应非 JSON: {_short(text)}") from e
if not isinstance(data, dict) or not data.get("access_token"):
raise OAuthCodeError(f"token 响应缺少 access_token: {data!r}")
access = str(data["access_token"]).strip()
refresh = str(data.get("refresh_token") or "").strip()
if not refresh:
raise OAuthCodeError("token 响应缺少 refresh_token")
referrer = ""
try:
pl = jwt_payload(access)
referrer = str(pl.get("referrer") or "")
except Exception:
pl = {}
expires_in = int(data.get("expires_in") or 21600)
return TokenResult(
access_token=access,
refresh_token=refresh,
id_token=(str(data["id_token"]).strip() if data.get("id_token") else None),
token_type=str(data.get("token_type") or "Bearer"),
expires_in=expires_in,
raw=data,
referrer=referrer,
)
def _run_sso_flow(
sso: str,
*,
session: Any,
log: LogFn,
require_referrer: bool,
) -> TokenResult:
flow = new_auth_code_flow()
backend = getattr(session, "_cpa_http_backend", "unknown")
log(f"Authorization Code Flow referrer={GROK_REFERRER} http={backend}")
_set_sso_cookies(session, sso)
consent_url = open_authorize_page(session, flow)
log(f"authorize -> consent: {_short(consent_url, 120)}")
code = approve_authorization(session, consent_url, flow)
log("consent allow ok")
token = exchange_auth_code(session, code, flow)
if token.referrer != GROK_REFERRER:
msg = f"access_token 未包含预期 referrer(got={token.referrer!r})"
if require_referrer:
raise OAuthCodeError(msg)
log(f"WARN {msg}")
else:
log("access_token referrer=grok-build ok")
log(f"token ok expires_in={token.expires_in} refresh=yes")
return token
def sso_to_token(
sso_cookie: str,
*,
proxy: str | None = None,
log: LogFn | None = None,
require_referrer: bool = True,
) -> TokenResult:
"""SSO cookie → 带 referrer=grok-build 的 OAuth token。
仅用 curl_cffi(Chrome TLS)。不再回退 std requests:
实测 requests 访问 auth.x.ai / accounts.x.ai 常 ReadTimeout,比 curl 更差。
连接/TLS/超时由上层切到浏览器 PKCE。
"""
log = log or _noop_log
sso = normalize_sso_cookie(sso_cookie)
if not sso:
raise OAuthCodeError("sso cookie 为空")
session = _make_session(proxy, prefer="curl")
try:
return _run_sso_flow(
sso, session=session, log=log, require_referrer=require_referrer
)
finally:
try:
session.close()
except Exception:
pass
def mint_from_sso(
sso_cookie: str,
*,
proxy: str | None = None,
log: LogFn | None = None,
require_referrer: bool = True,
) -> dict[str, Any]:
"""上层统一返回 dict,兼容 cpa_export。"""
tr = sso_to_token(
sso_cookie,
proxy=proxy,
log=log,
require_referrer=require_referrer,
)
return {
"access_token": tr.access_token,
"refresh_token": tr.refresh_token,
"id_token": tr.id_token,
"token_type": tr.token_type,
"expires_in": tr.expires_in,
"referrer": tr.referrer,
"sso": normalize_sso_cookie(sso_cookie),
}
def _is_http_tls_failure(exc: BaseException | str) -> bool:
"""HTTP 层 TLS/连接/超时失败:适合改走浏览器铸造。"""
text = str(exc or "").lower()
needles = (
"unexpected_eof_while_reading",
"sslerror",
"ssleoferror",
"max retries exceeded",
"openssl_internal:invalid library",
"tls connect error",
"curl: (35)",
"curl: (28)",
"failed to perform, curl: (35)",
"failed to perform, curl: (28)",
"connection timed out",
"ssl_error_syscall",
"connection reset",
"connection aborted",
"name resolution",
"readtimeout",
"connecttimeout",
"timed out",
"timeout",
)
return any(n in text for n in needles)
def _page_eval(page: Any, js: str, *args: Any) -> Any:
"""兼容 DrissionPage page.run_js / page.run_js_loaded。"""
if page is None:
raise OAuthCodeError("page 为空,无法浏览器铸造")
last_err: Exception | None = None
for name in ("run_js", "run_js_loaded", "run_async_js"):
fn = getattr(page, name, None)
if not callable(fn):
continue
try:
if args:
return fn(js, *args)
return fn(js)
except TypeError:
# 某些签名不接受额外参数
try:
return fn(js)
except Exception as exc: # noqa: BLE001
last_err = exc
except Exception as exc: # noqa: BLE001
last_err = exc
continue
raise OAuthCodeError(f"page 无法执行 JS: {last_err or 'no run_js'}")
def _ensure_sso_on_page(page: Any, sso: str, log: LogFn) -> None:
sso = normalize_sso_cookie(sso)
if not sso:
raise OAuthCodeError("sso cookie 为空")
# 先落到 accounts 域,再写 cookie,避免 set 失败
for url in ("https://accounts.x.ai/", "https://auth.x.ai/"):
try:
page.get(url)
time.sleep(0.25)
except Exception as exc: # noqa: BLE001
log(f"open {url} warn: {exc}")
items = []
for domain in (".x.ai", "accounts.x.ai", ".accounts.x.ai", "auth.x.ai", ".auth.x.ai"):
for name in ("sso", "sso-rw"):
items.append(
{
"name": name,
"value": sso,
"domain": domain,
"path": "/",
"secure": True,
"sameSite": "None",
}
)
set_ok = False
# 优先 CDP/DrissionPage set.cookies(可写 httpOnly 域 cookie)
for target in (page, getattr(page, "browser", None)):
if target is None:
continue
try:
setter = getattr(target, "set", None)
cookies_fn = getattr(setter, "cookies", None) if setter is not None else None
if not callable(cookies_fn):
continue
try:
cookies_fn(items)
set_ok = True
log(f"browser sso cookie set bulk via {type(target).__name__}")
break
except Exception:
n = 0
for it in items:
try:
cookies_fn(it)
n += 1
except Exception:
pass
if n:
set_ok = True
log(f"browser sso cookie set one-by-one={n}")
break
except Exception:
continue
# document.cookie 兜底(非 httpOnly)
set_js = r"""
(sso) => {
try {
const maxAge = 60 * 60 * 24 * 30;
const base = `; path=/; max-age=${maxAge}; SameSite=None; Secure`;
document.cookie = `sso=${sso}${base}; domain=.x.ai`;
document.cookie = `sso-rw=${sso}${base}; domain=.x.ai`;
document.cookie = `sso=${sso}${base}`;
document.cookie = `sso-rw=${sso}${base}`;
return document.cookie.includes('sso=');
} catch (e) {
return String(e);
}
}
"""
try:
ok = _page_eval(page, set_js, sso)
log(f"browser sso document.cookie: {ok!r}")
set_ok = set_ok or (ok is True) or (ok == True) or (str(ok).lower() == "true")
except Exception as exc: # noqa: BLE001
log(f"browser sso document.cookie fail: {exc}")
if not set_ok:
raise OAuthCodeError("写入 sso cookie 失败")
def _page_html(page: Any) -> str:
try:
html = str(getattr(page, "html", "") or "")
if html:
return html
except Exception:
pass
try:
return str(_page_eval(page, "() => document.documentElement.outerHTML") or "")
except Exception:
return ""
def _extract_next_action_id(html: str) -> str:
"""从 consent 页 HTML 抽 Next-Action / Server Action id。"""
text = html or ""
patterns = (
r'"next-action"\s*:\s*"([a-f0-9]{20,})"',
r'"actionId"\s*:\s*"([a-f0-9]{20,})"',
r'\$ACTION_ID_([a-f0-9]{20,})',
r'next-action["\']?\s*[:=]\s*["\']([a-f0-9]{20,})',
)
for pat in patterns:
m = re.search(pat, text, re.I)
if m:
return m.group(1)
return NEXT_ACTION_ID
def _browser_click_allow(page: Any, log: LogFn) -> bool:
"""Consent「允许」必须真实点击(JS click 会导致 Invalid action)。"""
labels = ("允许", "Allow", "Authorize", "Approve", "授权")
# 1) DrissionPage 真实点击(与 device consent 同一套经验)
try:
candidates = []
for sel in ("tag:button", "css:button", "css:[role='button']", "css:input[type='submit']"):
try:
found = page.eles(sel, timeout=0.3) or []
except Exception:
found = []
for el in found:
try:
t = (getattr(el, "text", None) or el.raw_text or "").strip()
except Exception:
t = ""
if not t:
continue
compact = re.sub(r"\s+", "", t)
if compact in labels or t in labels:
candidates.append((0, el, t))
elif any(x in compact for x in labels) and "全部" not in compact and "All" not in compact:
candidates.append((1, el, t))
candidates.sort(key=lambda x: x[0])
for _, el, t in candidates:
try:
el.click() # real click
log(f"browser REAL click allow: {t!r}")
return True
except Exception as exc: # noqa: BLE001
log(f"real click {t!r} failed: {exc}")
try:
el.click(by_js=True)
log(f"browser JS click allow: {t!r}")
return True
except Exception:
continue
except Exception as exc: # noqa: BLE001
log(f"ele allow click failed: {exc}")
# 2) 表单 action=allow 后 submit(device consent 兜底同款)
try:
ret = _page_eval(
page,
r"""
() => {
const labels = new Set(['允许','Allow','Authorize','Approve','授权']);
const f = document.querySelector('form');
if (f) {
let a = f.querySelector('input[name=action]');
if (!a) {
a = document.createElement('input');
a.type = 'hidden';
a.name = 'action';
f.appendChild(a);
}
a.value = 'allow';
const btn = [...f.querySelectorAll('button,[role=button],input[type=submit]')]
.find(b => labels.has(((b.innerText||b.value||'').trim())));
if (btn) { btn.click(); return {ok:true, via:'form-btn'}; }
f.submit();
return {ok:true, via:'form-submit'};
}
// 无 form:真实派发 pointer/mouse 事件
const nodes = [...document.querySelectorAll('button,[role=button],input[type=submit],a')];
const target = nodes.find(n => {
const t = ((n.innerText||n.textContent||n.value||'').replace(/\s+/g,'')).trim();
return labels.has(t) || (t.includes('允许') && !t.includes('全部'));
});
if (!target) {
return {ok:false, texts: nodes.slice(0,10).map(n => (n.innerText||n.value||'').trim()).filter(Boolean)};
}
target.scrollIntoView({block:'center'});
target.focus();
for (const type of ['pointerdown','mousedown','pointerup','mouseup','click']) {
target.dispatchEvent(new MouseEvent(type, {bubbles:true, cancelable:true, view:window}));
}
return {ok:true, via:'mouse-events', text:(target.innerText||target.value||'').trim()};
}
""",
)
if isinstance(ret, dict) and ret.get("ok"):
log(f"browser allow fallback: {ret}")
return True
log(f"browser allow button not found: {ret!r}")
except Exception as exc: # noqa: BLE001
log(f"click allow js failed: {exc}")
return False
def _browser_consent_server_action(
page: Any,
consent_url: str,
flow: AuthCodeFlow,
log: LogFn,
) -> str:
"""在浏览器内 POST Next.js Server Action 批准 consent,直接拿 code。"""
html = _page_html(page)
action_id = _extract_next_action_id(html)
payload = [
{
"action": "allow",
"clientId": CLIENT_ID,
"redirectUri": REDIRECT_URI,
"scope": SCOPE,
"state": flow.state,
"codeChallenge": flow.code_challenge,
"codeChallengeMethod": "S256",
"nonce": flow.nonce,
"principalType": "User",
"principalId": "",
"referrer": GROK_REFERRER,
}
]
body = json.dumps(payload, separators=(",", ":"))
# 用 window key 轮询,兼容 DrissionPage 对 Promise 支持不一
wrap = f"""
(() => {{
const key = '__cpa_consent_' + Date.now();
window[key] = null;
fetch({consent_url!r}, {{
method: 'POST',
headers: {{
'Accept': 'text/x-component',
'Content-Type': 'text/plain;charset=UTF-8',
'Next-Action': {action_id!r},
'Origin': 'https://accounts.x.ai',
'Referer': {consent_url!r},
}},
body: {body!r},
credentials: 'include',
redirect: 'follow',
}}).then(async (r) => {{
const text = await r.text();
window[key] = {{status: r.status, url: r.url, text: text}};
}}).catch((e) => {{
window[key] = {{status: 0, url: '', text: String(e)}};
}});
return key;
}})()
"""
try:
key = _page_eval(page, wrap)
except Exception as exc: # noqa: BLE001
raise OAuthCodeError(f"browser consent fetch 启动失败: {exc}") from exc
log(f"browser consent server-action post action_id={action_id[:12]}…")
deadline = time.time() + 20
ret = None
while time.time() < deadline:
try:
ret = _page_eval(page, f"() => window[{key!r}]")
except Exception:
try:
ret = _page_eval(page, f"window[{key!r}]")
except Exception:
ret = None
if ret:
break
time.sleep(0.2)
if not isinstance(ret, dict):
raise OAuthCodeError(f"browser consent 无响应: {ret!r}")
status = int(ret.get("status") or 0)
text = str(ret.get("text") or "")
final = str(ret.get("url") or "")
if status and (status < 200 or status >= 300):
raise OAuthCodeError(f"browser consent HTTP {status}: {_short(text, 240)}")
if "code=" in final:
qs = parse_qs(urlparse(final).query)
code = (qs.get("code") or [""])[0].strip()
if code:
log("browser got code from consent redirect url")
return code
# 响应体 / RSC
try:
code = parse_consent_code(text)
if code:
log("browser got code from consent server-action body")
return code
except OAuthCodeError:
pass
# 有时 code 在 text 的 redirect 串里
m = re.search(r"[?&]code=([A-Za-z0-9._~\-]+)", text)
if m:
log("browser got code from consent body regex")
return m.group(1)
raise OAuthCodeError(f"browser consent 未返回 code: {_short(text, 240)}")
def _browser_fetch_token(page: Any, code: str, flow: AuthCodeFlow, log: LogFn) -> TokenResult:
"""在浏览器上下文用 fetch 换 token,绕开 Python TLS 对 auth.x.ai 的 EOF。"""
form = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"code_verifier": flow.code_verifier,
}
body = urlencode(form)
js = r"""
(tokenUrl, body, ua, ver) => {
return fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': '*/*',
'User-Agent': ua,
'X-Grok-Client-Version': ver,
},
body: body,
credentials: 'include',
}).then(async (r) => {
const text = await r.text();
return {status: r.status, text: text};
}).catch((e) => ({status: 0, text: String(e)}));
}
"""
# 先到 auth 域,减少跨站限制
try:
page.get(ISSUER + "/")
time.sleep(0.3)
except Exception:
pass
ret = None
# DrissionPage 对 Promise 支持不一,做短轮询包装
wrap = r"""
(tokenUrl, body, ua, ver) => {
const key = '__cpa_token_result_' + Date.now();
window[key] = null;
fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': '*/*',
'User-Agent': ua,
'X-Grok-Client-Version': ver,
},
body: body,
credentials: 'include',
}).then(async (r) => {
const text = await r.text();
window[key] = {status: r.status, text: text};
}).catch((e) => {
window[key] = {status: 0, text: String(e)};
});
return key;
}
"""
try:
key = _page_eval(page, wrap, TOKEN_URL, body, GROK_TOKEN_UA, GROK_VERSION)
except Exception:
# 无参回退:把参数内联
key = _page_eval(
page,
f"""
(() => {{
const key = '__cpa_token_result_' + Date.now();
window[key] = null;
fetch({TOKEN_URL!r}, {{
method: 'POST',
headers: {{
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': '*/*',
'User-Agent': {GROK_TOKEN_UA!r},
'X-Grok-Client-Version': {GROK_VERSION!r},
}},
body: {body!r},
credentials: 'include',
}}).then(async (r) => {{
const text = await r.text();
window[key] = {{status: r.status, text: text}};
}}).catch((e) => {{
window[key] = {{status: 0, text: String(e)}};
}});
return key;
}})()
""",
)
deadline = time.time() + 30
while time.time() < deadline:
try:
ret = _page_eval(page, f"() => window[{key!r}]")
except Exception:
try:
ret = _page_eval(page, f"window[{key!r}]")
except Exception as exc:
raise OAuthCodeError(f"读取 browser token 结果失败: {exc}") from exc
if ret:
break
time.sleep(0.2)
if not isinstance(ret, dict):
raise OAuthCodeError(f"browser token 无响应: {ret!r}")
status = int(ret.get("status") or 0)
text = str(ret.get("text") or "")
if status < 200 or status >= 300:
raise OAuthCodeError(f"browser token HTTP {status}: {_short(text, 300)}")
try:
data = json.loads(text)
except Exception as e:
raise OAuthCodeError(f"browser token 非 JSON: {_short(text)}") from e
if not isinstance(data, dict) or not data.get("access_token"):
raise OAuthCodeError(f"browser token 缺少 access_token: {data!r}")
access = str(data["access_token"]).strip()
refresh = str(data.get("refresh_token") or "").strip()
if not refresh:
raise OAuthCodeError("browser token 缺少 refresh_token")
referrer = ""
try:
referrer = str(jwt_payload(access).get("referrer") or "")
except Exception:
pass
return TokenResult(
access_token=access,
refresh_token=refresh,
id_token=(str(data["id_token"]).strip() if data.get("id_token") else None),
token_type=str(data.get("token_type") or "Bearer"),
expires_in=int(data.get("expires_in") or 21600),
raw=data,
referrer=referrer,
)
def mint_from_sso_browser(
sso_cookie: str,
page: Any,
*,
log: LogFn | None = None,
require_referrer: bool = True,
timeout_sec: float = 90.0,
) -> dict[str, Any]:
"""用注册浏览器完成 SSO→PKCE(绕开 Python TLS 访问 auth.x.ai 失败)。
流程:
1. 写入 sso cookie
2. 打开 authorize(referrer=grok-build)
3. 在 consent 页点击允许 / 或解析 callback code
4. 浏览器 fetch oauth2/token
"""
log = log or _noop_log
sso = normalize_sso_cookie(sso_cookie)
if not sso:
raise OAuthCodeError("sso cookie 为空")
if page is None:
raise OAuthCodeError("page 为空")
flow = new_auth_code_flow()
params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": SCOPE,
"code_challenge": flow.code_challenge,
"code_challenge_method": "S256",
"state": flow.state,
"nonce": flow.nonce,
"referrer": GROK_REFERRER,
}
auth_url = f"{AUTHORIZE_URL}?{urlencode(params)}"
log(f"browser PKCE authorize referrer={GROK_REFERRER}")
_ensure_sso_on_page(page, sso, log)
try:
page.get(auth_url)
except Exception as exc: # noqa: BLE001
raise OAuthCodeError(f"browser 打开 authorize 失败: {exc}") from exc
code = ""
deadline = time.time() + max(30.0, float(timeout_sec))
last_url = ""
consent_attempts = 0
server_action_tried = False
wait_after_click_until = 0.0
def _code_from_url(u: str) -> str:
if not u or "code=" not in u:
return ""
if not any(x in u for x in ("127.0.0.1", "localhost", "callback", "redirect")):
# 仍允许任意带 code 的 redirect
if "code=" not in u:
return ""
qs = parse_qs(urlparse(u).query)
return (qs.get("code") or [""])[0].strip()
while time.time() < deadline:
try:
url = str(getattr(page, "url", "") or "")
except Exception:
url = ""
if url and url != last_url:
log(f"browser url: {_short(url, 140)}")
last_url = url
# callback 已跳到 redirect_uri?code=
code = _code_from_url(url)
if code:
log("browser got code from redirect")
break
# consent 页:真实点击 Allow;点后短暂等待跳转;仍不行再 Server Action POST
if "/oauth2/consent" in url or "/consent" in url:
# 刚点过,先等 redirect
if wait_after_click_until and time.time() < wait_after_click_until:
time.sleep(0.3)
continue
consent_attempts += 1
if consent_attempts <= 4:
clicked = _browser_click_allow(page, log)
if clicked:
wait_after_click_until = time.time() + 4.0
time.sleep(0.6)
# 点后立刻看 url / html
try:
url2 = str(getattr(page, "url", "") or "")
except Exception:
url2 = url
code = _code_from_url(url2)
if code:
log("browser got code after allow click")
break
html = _page_html(page)
if html:
try:
code = parse_consent_code(html)
if code:
log("browser got code from consent html after click")
break
except OAuthCodeError:
pass
continue
# 真实点击无效:浏览器内 Server Action POST(与 HTTP approve 同 payload)
if not server_action_tried:
server_action_tried = True
try:
code = _browser_consent_server_action(page, url, flow, log)
if code:
break
except Exception as exc: # noqa: BLE001
log(f"browser consent server-action failed: {exc}")
time.sleep(0.5)
continue
if "sign-in" in url or "sign-up" in url:
# cookie 可能没带上,重写一次
log("browser landed sign-in, re-inject sso")
_ensure_sso_on_page(page, sso, log)
try:
page.get(auth_url)
except Exception:
pass
time.sleep(0.8)
continue
# 中间跳转页:稍等
time.sleep(0.4)
if not code:
# 最后再扫一次页面 HTML
try:
html = _page_html(page)
if html:
try:
code = parse_consent_code(html)
except OAuthCodeError:
code = ""
except Exception:
code = ""
if not code:
raise OAuthCodeError(
f"browser PKCE 超时未拿到 code(last_url={_short(last_url, 160)})"
)
token = _browser_fetch_token(page, code, flow, log)
if token.referrer != GROK_REFERRER:
msg = f"access_token 未包含预期 referrer(got={token.referrer!r})"
if require_referrer:
raise OAuthCodeError(msg)
log(f"WARN {msg}")
else:
log("browser access_token referrer=grok-build ok")
log(f"browser token ok expires_in={token.expires_in}")
return {
"access_token": token.access_token,
"refresh_token": token.refresh_token,
"id_token": token.id_token,
"token_type": token.token_type,
"expires_in": token.expires_in,
"referrer": token.referrer,
"sso": sso,
}