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"
|
ISSUER = "https://auth.x.ai"
|
||||||
TOKEN_URL = f"{ISSUER}/oauth2/token"
|
TOKEN_URL = f"{ISSUER}/oauth2/token"
|
||||||
AUTHORIZE_URL = f"{ISSUER}/oauth2/authorize"
|
AUTHORIZE_URL = f"{ISSUER}/oauth2/authorize"
|
||||||
REDIRECT_URI = "http://127.0.0.1:56121/callback"
|
# 官方 CLI 0.2.101:loopback redirect,运行时随机端口(RFC 8252 端口无关)
|
||||||
# 比旧 device-code scope 多 conversations:*,对齐 grok-build
|
# 兼容保留旧固定端口常量,仅作 fallback
|
||||||
|
REDIRECT_URI_LEGACY = "http://127.0.0.1:56121/callback"
|
||||||
|
REDIRECT_URI = REDIRECT_URI_LEGACY
|
||||||
|
# 对齐官方 docs + discovery:去掉二进制中已不出现的 grok-cli:access
|
||||||
SCOPE = (
|
SCOPE = (
|
||||||
"openid profile email offline_access "
|
"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_REFERRER = "grok-build"
|
||||||
GROK_VERSION = "0.2.93"
|
# 对齐官方 stable CLI(2026-07-14:0.2.101)
|
||||||
GROK_TOKEN_UA = (
|
GROK_VERSION = "0.2.101"
|
||||||
f"grok-pager/{GROK_VERSION} grok-shell/{GROK_VERSION} (linux; x86_64)"
|
GROK_TOKEN_UA = f"xai-grok-build/{GROK_VERSION}"
|
||||||
)
|
GROK_CLIENT_SURFACE = "grok-build"
|
||||||
# Next.js Server Action id(consent 页 POST 需要)
|
# Next.js Server Action id(consent 页 POST 需要)
|
||||||
NEXT_ACTION_ID = "4005315a1d7e426de592990bb54bb37471f39dd6d2"
|
NEXT_ACTION_ID = "4005315a1d7e426de592990bb54bb37471f39dd6d2"
|
||||||
BROWSER_UA = (
|
BROWSER_UA = (
|
||||||
@@ -60,6 +63,7 @@ class AuthCodeFlow:
|
|||||||
nonce: str
|
nonce: str
|
||||||
code_verifier: str
|
code_verifier: str
|
||||||
code_challenge: str
|
code_challenge: str
|
||||||
|
redirect_uri: str = REDIRECT_URI_LEGACY
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -77,6 +81,13 @@ def _b64url(data: bytes) -> str:
|
|||||||
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
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:
|
def new_auth_code_flow() -> AuthCodeFlow:
|
||||||
verifier = _b64url(secrets.token_bytes(32))
|
verifier = _b64url(secrets.token_bytes(32))
|
||||||
state = _b64url(secrets.token_bytes(16))
|
state = _b64url(secrets.token_bytes(16))
|
||||||
@@ -87,6 +98,7 @@ def new_auth_code_flow() -> AuthCodeFlow:
|
|||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
code_verifier=verifier,
|
code_verifier=verifier,
|
||||||
code_challenge=challenge,
|
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]:
|
def _token_headers() -> dict[str, str]:
|
||||||
|
# 对齐官方 0.2.101:小写 x-grok-* + surface=grok-build
|
||||||
return {
|
return {
|
||||||
"User-Agent": GROK_TOKEN_UA,
|
"User-Agent": GROK_TOKEN_UA,
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"X-Grok-Client-Version": GROK_VERSION,
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"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:
|
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 = {
|
params = {
|
||||||
"response_type": "code",
|
"response_type": "code",
|
||||||
"client_id": CLIENT_ID,
|
"client_id": CLIENT_ID,
|
||||||
"redirect_uri": REDIRECT_URI,
|
"redirect_uri": redirect_uri,
|
||||||
"scope": SCOPE,
|
"scope": SCOPE,
|
||||||
"code_challenge": flow.code_challenge,
|
"code_challenge": flow.code_challenge,
|
||||||
"code_challenge_method": "S256",
|
"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:
|
def approve_authorization(session: Any, consent_url: str, flow: AuthCodeFlow) -> str:
|
||||||
|
redirect_uri = flow.redirect_uri or REDIRECT_URI_LEGACY
|
||||||
payload = [
|
payload = [
|
||||||
{
|
{
|
||||||
"action": "allow",
|
"action": "allow",
|
||||||
"clientId": CLIENT_ID,
|
"clientId": CLIENT_ID,
|
||||||
"redirectUri": REDIRECT_URI,
|
"redirectUri": redirect_uri,
|
||||||
"scope": SCOPE,
|
"scope": SCOPE,
|
||||||
"state": flow.state,
|
"state": flow.state,
|
||||||
"codeChallenge": flow.code_challenge,
|
"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:
|
def exchange_auth_code(session: Any, code: str, flow: AuthCodeFlow) -> TokenResult:
|
||||||
|
redirect_uri = flow.redirect_uri or REDIRECT_URI_LEGACY
|
||||||
form = {
|
form = {
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
"code": code,
|
"code": code,
|
||||||
"redirect_uri": REDIRECT_URI,
|
"redirect_uri": redirect_uri,
|
||||||
"client_id": CLIENT_ID,
|
"client_id": CLIENT_ID,
|
||||||
"code_verifier": flow.code_verifier,
|
"code_verifier": flow.code_verifier,
|
||||||
}
|
}
|
||||||
@@ -429,7 +449,10 @@ def _run_sso_flow(
|
|||||||
) -> TokenResult:
|
) -> TokenResult:
|
||||||
flow = new_auth_code_flow()
|
flow = new_auth_code_flow()
|
||||||
backend = getattr(session, "_cpa_http_backend", "unknown")
|
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)
|
_set_sso_cookies(session, sso)
|
||||||
consent_url = open_authorize_page(session, flow)
|
consent_url = open_authorize_page(session, flow)
|
||||||
log(f"authorize -> consent: {_short(consent_url, 120)}")
|
log(f"authorize -> consent: {_short(consent_url, 120)}")
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828"
|
|||||||
ISSUER = "https://auth.x.ai"
|
ISSUER = "https://auth.x.ai"
|
||||||
DEVICE_CODE_URL = "https://auth.x.ai/oauth2/device/code"
|
DEVICE_CODE_URL = "https://auth.x.ai/oauth2/device/code"
|
||||||
TOKEN_URL = "https://auth.x.ai/oauth2/token"
|
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]
|
LogFn = Callable[[str], None]
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user