Files
grok-keygen-new/oidc_mint/oauth_code.py
T
chaos 8b399277e7 Fallback SSO OAuth mint to browser PKCE when Python TLS fails.
HTTP mint often dies on auth.x.ai (curl OpenSSL / SSLEOFError); reuse the
registration browser to set SSO, complete authorize+consent, and fetch tokens
so CPA export/push to cpa.nopj.cn can continue.
2026-07-14 10:04:55 +08:00

924 lines
29 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 ""
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=30,
)
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=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]
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=30,
)
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。"""
log = log or _noop_log
sso = normalize_sso_cookie(sso_cookie)
if not sso:
raise OAuthCodeError("sso cookie 为空")
# 先 curl_cffi;若遇到 OpenSSL invalid library / curl(35),自动回退 std requests
session = _make_session(proxy, prefer="curl")
try:
return _run_sso_flow(
sso, session=session, log=log, require_referrer=require_referrer
)
except Exception as exc: # noqa: BLE001
backend = str(getattr(session, "_cpa_http_backend", "") or "")
can_fallback = _is_curl_tls_broken(exc) or (
backend.startswith("curl_cffi") and "curl: (35)" in str(exc).lower()
)
if not can_fallback:
raise
log(f"curl TLS 异常,回退标准 requests: {_short(str(exc), 160)}")
try:
session.close()
except Exception:
pass
session = _make_session(proxy, prefer="requests")
try:
return _run_sso_flow(
sso, session=session, log=log, require_referrer=require_referrer
)
finally:
try:
session.close()
except Exception:
pass
session = None # type: ignore[assignment]
finally:
if session is not None:
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)",
"failed to perform, curl: (35)",
"ssl_error_syscall",
"connection reset",
"connection aborted",
"name resolution",
"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 失败
try:
page.get("https://accounts.x.ai/")
time.sleep(0.4)
except Exception as exc: # noqa: BLE001
log(f"open accounts.x.ai warn: {exc}")
set_js = r"""
(sso) => {
try {
const maxAge = 60 * 60 * 24 * 30;
const base = `; path=/; max-age=${maxAge}; SameSite=Lax`;
document.cookie = `sso=${sso}${base}`;
document.cookie = `sso-rw=${sso}${base}`;
// 兼容 secure 场景
document.cookie = `sso=${sso}${base}; Secure`;
document.cookie = `sso-rw=${sso}${base}; Secure`;
return document.cookie.includes('sso=');
} catch (e) {
return String(e);
}
}
"""
try:
ok = _page_eval(page, set_js, sso)
log(f"browser sso cookie set: {ok!r}")
except Exception:
# 退而求其次:DrissionPage set.cookies
try:
setter = getattr(page, "set", None)
cookies = getattr(setter, "cookies", None) if setter is not None else None
if callable(cookies):
for domain in ("accounts.x.ai", "auth.x.ai", ".x.ai"):
cookies({"name": "sso", "value": sso, "domain": domain, "path": "/"})
cookies({"name": "sso-rw", "value": sso, "domain": domain, "path": "/"})
log("browser sso cookie set via page.set.cookies")
else:
raise OAuthCodeError("无法写入 sso cookie")
except Exception as exc: # noqa: BLE001
raise OAuthCodeError(f"写入 sso cookie 失败: {exc}") from exc
def _browser_click_allow(page: Any, log: LogFn) -> bool:
js = r"""
() => {
function isVisible(node) {
if (!node) return false;
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function textOf(node) {
return [node.innerText, node.textContent, node.getAttribute('aria-label'), node.getAttribute('value')]
.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
}
const nodes = Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"], a'));
const prefer = [];
const weak = [];
for (const n of nodes) {
if (!isVisible(n) || n.disabled || n.getAttribute('aria-disabled') === 'true') continue;
const t = textOf(n);
const compact = t.replace(/\s+/g, '');
const lower = compact.toLowerCase();
if (!compact) continue;
// 精确允许,排除“全部允许”
if (compact === '允许' || lower === 'allow' || lower === 'authorize' || compact === '授权') {
prefer.push(n);
continue;
}
if ((compact.includes('允许') || lower.includes('allow') || lower.includes('authorize'))
&& !compact.includes('全部') && !lower.includes('all')) {
weak.push(n);
}
}
const target = prefer[0] || weak[0];
if (!target) return {clicked:false, texts: nodes.slice(0,8).map(textOf)};
target.focus();
target.click();
return {clicked:true, text: textOf(target)};
}
"""
try:
ret = _page_eval(page, js)
except Exception as exc: # noqa: BLE001
log(f"click allow js failed: {exc}")
return False
if isinstance(ret, dict) and ret.get("clicked"):
log(f"browser clicked allow: {ret.get('text')!r}")
return True
log(f"browser allow button not found: {ret!r}")
return False
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(20.0, float(timeout_sec))
last_url = ""
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=
if "code=" in url and ("127.0.0.1" in url or "callback" in url or "localhost" in url):
qs = parse_qs(urlparse(url).query)
code = (qs.get("code") or [""])[0].strip()
if code:
log("browser got code from redirect")
break
# consent 页
if "/oauth2/consent" in url or "consent" in url:
_browser_click_allow(page, log)
time.sleep(0.8)
# 有时 consent 响应是 RSC,不跳转;尝试从 HTML 抽 code
try:
html = ""
try:
html = str(getattr(page, "html", "") or "")
except Exception:
html = str(_page_eval(page, "() => document.documentElement.outerHTML") or "")
if html:
try:
code = parse_consent_code(html)
if code:
log("browser got code from consent html")
break
except OAuthCodeError:
pass
except Exception:
pass
continue
if "sign-in" in url or "sign-up" in url:
# cookie 可能没带上,重写一次
_ensure_sso_on_page(page, sso, log)
try:
page.get(auth_url)
except Exception:
pass
time.sleep(0.8)
continue
time.sleep(0.5)
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,
}