From cc0aa6a43357b7ab9762a4a35e7ec9003829bf9f Mon Sep 17 00:00:00 2001 From: Chaos Date: Tue, 14 Jul 2026 14:30:38 +0800 Subject: [PATCH] Align OAuth mint with official Grok Build 0.2.101. - UA xai-grok-build/0.2.101 + x-grok-client-version/surface - Ephemeral loopback redirect_uri for authorize/consent/token - Drop grok-cli:access from scope to match discovery --- oidc_mint/oauth_code.py | 47 +++++++++++++++++++++++++++++---------- oidc_mint/oauth_device.py | 6 ++++- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/oidc_mint/oauth_code.py b/oidc_mint/oauth_code.py index 2199eb5..1441b3d 100644 --- a/oidc_mint/oauth_code.py +++ b/oidc_mint/oauth_code.py @@ -25,17 +25,20 @@ 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 +# 官方 CLI 0.2.101:loopback redirect,运行时随机端口(RFC 8252 端口无关) +# 兼容保留旧固定端口常量,仅作 fallback +REDIRECT_URI_LEGACY = "http://127.0.0.1:56121/callback" +REDIRECT_URI = REDIRECT_URI_LEGACY +# 对齐官方 docs + discovery:去掉二进制中已不出现的 grok-cli:access SCOPE = ( "openid profile email offline_access " - "grok-cli:access api:access conversations:read conversations:write" + "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)" -) +# 对齐官方 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_ACTION_ID = "4005315a1d7e426de592990bb54bb37471f39dd6d2" BROWSER_UA = ( @@ -60,6 +63,7 @@ class AuthCodeFlow: nonce: str code_verifier: str code_challenge: str + redirect_uri: str = REDIRECT_URI_LEGACY @dataclass @@ -77,6 +81,13 @@ def _b64url(data: bytes) -> str: return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") +def _new_loopback_redirect_uri() -> str: + """官方 CLI:http://127.0.0.1:/callback。""" + # 高位端口,避免与常见本机服务冲突 + port = 49152 + secrets.randbelow(16383) + return f"http://127.0.0.1:{port}/callback" + + def new_auth_code_flow() -> AuthCodeFlow: verifier = _b64url(secrets.token_bytes(32)) state = _b64url(secrets.token_bytes(16)) @@ -87,6 +98,7 @@ def new_auth_code_flow() -> AuthCodeFlow: nonce=nonce, code_verifier=verifier, code_challenge=challenge, + redirect_uri=_new_loopback_redirect_uri(), ) @@ -245,11 +257,15 @@ def _browser_headers(method: str, url: str, next_action: str = "") -> dict[str, def _token_headers() -> dict[str, str]: + # 对齐官方 0.2.101:小写 x-grok-* + surface=grok-build return { "User-Agent": GROK_TOKEN_UA, "Accept": "*/*", - "X-Grok-Client-Version": GROK_VERSION, "Content-Type": "application/x-www-form-urlencoded", + "x-grok-client-version": GROK_VERSION, + "x-grok-client-surface": GROK_CLIENT_SURFACE, + # 兼容旧中间件/代理仍读 Pascal 头 + "X-Grok-Client-Version": GROK_VERSION, } @@ -261,10 +277,12 @@ def _final_url(resp: Any) -> str: def open_authorize_page(session: Any, flow: AuthCodeFlow) -> str: + redirect_uri = flow.redirect_uri or _new_loopback_redirect_uri() + flow.redirect_uri = redirect_uri params = { "response_type": "code", "client_id": CLIENT_ID, - "redirect_uri": REDIRECT_URI, + "redirect_uri": redirect_uri, "scope": SCOPE, "code_challenge": flow.code_challenge, "code_challenge_method": "S256", @@ -334,11 +352,12 @@ def parse_consent_code(body: str) -> str: def approve_authorization(session: Any, consent_url: str, flow: AuthCodeFlow) -> str: + redirect_uri = flow.redirect_uri or REDIRECT_URI_LEGACY payload = [ { "action": "allow", "clientId": CLIENT_ID, - "redirectUri": REDIRECT_URI, + "redirectUri": redirect_uri, "scope": SCOPE, "state": flow.state, "codeChallenge": flow.code_challenge, @@ -373,10 +392,11 @@ def approve_authorization(session: Any, consent_url: str, flow: AuthCodeFlow) -> def exchange_auth_code(session: Any, code: str, flow: AuthCodeFlow) -> TokenResult: + redirect_uri = flow.redirect_uri or REDIRECT_URI_LEGACY form = { "grant_type": "authorization_code", "code": code, - "redirect_uri": REDIRECT_URI, + "redirect_uri": redirect_uri, "client_id": CLIENT_ID, "code_verifier": flow.code_verifier, } @@ -429,7 +449,10 @@ def _run_sso_flow( ) -> TokenResult: flow = new_auth_code_flow() backend = getattr(session, "_cpa_http_backend", "unknown") - log(f"Authorization Code Flow referrer={GROK_REFERRER} http={backend}") + log( + f"Authorization Code Flow ver={GROK_VERSION} ua={GROK_TOKEN_UA} " + f"referrer={GROK_REFERRER} redirect={flow.redirect_uri} http={backend}" + ) _set_sso_cookies(session, sso) consent_url = open_authorize_page(session, flow) log(f"authorize -> consent: {_short(consent_url, 120)}") diff --git a/oidc_mint/oauth_device.py b/oidc_mint/oauth_device.py index 4cc695d..dc4ce8f 100644 --- a/oidc_mint/oauth_device.py +++ b/oidc_mint/oauth_device.py @@ -20,7 +20,11 @@ CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828" ISSUER = "https://auth.x.ai" DEVICE_CODE_URL = "https://auth.x.ai/oauth2/device/code" TOKEN_URL = "https://auth.x.ai/oauth2/token" -SCOPE = "openid profile email offline_access grok-cli:access api:access" +# 对齐官方 0.2.101 discovery / docs(去掉二进制中已不出现的 grok-cli:access) +SCOPE = ( + "openid profile email offline_access " + "api:access conversations:read conversations:write" +) LogFn = Callable[[str], None]