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
This commit is contained in:
1 parent
4abdc8aa4a
commit
cc0aa6a433
2 files changed
+40
-13
No files matched your search
+35
-12
@@ -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:<ephemeral>/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)}")
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
Reference in new issue
Block a user