Files
chaos d10009d639 Initial commit: grok-free-register-oss
Open-source Grok free registration CLI, xai_enroller auth pipeline,
local auth service, tests and docs.
2026-07-16 21:04:05 +08:00

182 lines
6.9 KiB
Python

import asyncio
import base64
import binascii
import json
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Awaitable, Callable
from urllib.parse import urlencode
import httpx
from .models import DeviceFlow, OAuthCredential
@dataclass(frozen=True)
class XAIProfile:
client_id: str
scope: str
version: str = "xai-device-v1"
@classmethod
def default(cls):
return cls(
client_id="b1a00492-073a-47ea-816f-4c329264a828",
scope="openid profile email offline_access grok-cli:access api:access",
version="grok-cli-device-v1",
)
class XAIProtocol:
DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration"
MAX_BODY = 64 * 1024
def __init__(
self,
client: httpx.AsyncClient,
profile: XAIProfile,
sleep=None,
default_poll_interval=5.0,
):
self.client = client
self.profile = profile
self.sleep = sleep or asyncio.sleep
self.default_poll_interval = float(default_poll_interval)
self._discovered = None
@staticmethod
def _allowed_url(value):
from urllib.parse import urlparse
parsed = urlparse(value)
hostname = parsed.hostname
return bool(hostname) and parsed.scheme == "https" and (
hostname == "x.ai" or hostname.endswith(".x.ai")
)
async def _json(self, response):
body = await response.aread()
if len(body) > self.MAX_BODY:
raise RuntimeError("response body exceeds limit")
try:
return json.loads(body)
except (TypeError, ValueError) as exc:
raise RuntimeError("invalid JSON response") from exc
async def discover(self):
response = await self.client.get(self.DISCOVERY_URL, follow_redirects=False)
if response.status_code // 100 != 2:
raise RuntimeError("discovery rejected")
document = await self._json(response)
device_endpoint = document.get("device_authorization_endpoint")
token_endpoint = document.get("token_endpoint")
if not device_endpoint or not token_endpoint or not all(
self._allowed_url(url) for url in (device_endpoint, token_endpoint)
):
raise ValueError("discovery endpoint failed x.ai HTTPS allowlist")
self._discovered = (device_endpoint, token_endpoint)
return self._discovered
async def start_device_flow(self):
device_endpoint, token_endpoint = await self.discover()
response = await self.client.post(
device_endpoint,
data={"client_id": self.profile.client_id, "scope": self.profile.scope},
follow_redirects=False,
)
if response.status_code // 100 != 2:
raise RuntimeError("device authorization rejected")
document = await self._json(response)
try:
device_code = document["device_code"]
user_code = document["user_code"]
base_url = document.get("verification_uri") or document["verification_url"]
expires_in = int(document["expires_in"])
except (KeyError, TypeError, ValueError) as exc:
raise RuntimeError("invalid device authorization response") from exc
if not self._allowed_url(base_url):
raise ValueError("verification URL failed x.ai HTTPS allowlist")
interval = float(document.get("interval", self.default_poll_interval))
verification_url = document.get("verification_uri_complete")
if verification_url is None:
verification_url = (
f"{base_url}{'&' if '?' in base_url else '?'}"
f"{urlencode({'user_code': user_code})}"
)
if not self._allowed_url(verification_url):
raise ValueError("verification URL failed x.ai HTTPS allowlist")
return DeviceFlow(device_code, user_code, verification_url, expires_in, interval, token_endpoint)
async def poll_token(self, *, endpoint, flow, timeout):
deadline = time.monotonic() + timeout
interval = max(0.0, float(flow.interval))
while time.monotonic() < deadline:
response = await self.client.post(
endpoint,
data={
"client_id": self.profile.client_id,
"device_code": flow.device_code,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
},
follow_redirects=False,
)
document = await self._json(response)
if response.status_code // 100 == 2:
return self._credential(document, endpoint)
error = document.get("error")
if error in {"authorization_pending", "slow_down"}:
interval += 1 if error == "slow_down" else 0
await self.sleep(interval)
continue
if error == "access_denied":
raise RuntimeError("oauth_denied")
if error == "expired_token":
raise RuntimeError("oauth_expired")
raise RuntimeError("oauth_rejected")
raise RuntimeError("oauth_expired")
@staticmethod
def _jwt_subject(token):
if not isinstance(token, str):
return None
parts = token.split(".")
if len(parts) != 3 or not parts[1]:
return None
payload = parts[1] + "=" * (-len(parts[1]) % 4)
try:
claims = json.loads(base64.urlsafe_b64decode(payload).decode("utf-8"))
except (binascii.Error, UnicodeDecodeError, ValueError):
return None
if not isinstance(claims, dict):
return None
for claim in ("sub", "principal_id"):
value = claims.get(claim)
if isinstance(value, str) and value:
return value
return None
@staticmethod
def _credential(document, endpoint):
if not document.get("access_token") or not document.get("refresh_token"):
raise RuntimeError("oauth_rejected")
now = datetime.now(timezone.utc)
expires_in = int(document.get("expires_in", 3600))
expires_at = datetime.fromtimestamp(now.timestamp() + expires_in, timezone.utc).isoformat().replace("+00:00", "Z")
subject = (
XAIProtocol._jwt_subject(document.get("id_token"))
or XAIProtocol._jwt_subject(document["access_token"])
or document.get("sub")
)
return OAuthCredential(
access_token=document["access_token"],
refresh_token=document["refresh_token"],
id_token=document.get("id_token"),
token_type=document.get("token_type"),
expires_in=expires_in,
expires_at=expires_at,
last_refresh=now.isoformat().replace("+00:00", "Z"),
subject=subject,
token_endpoint=endpoint,
)