认证 · 令牌刷新为什么这么难
grok login 背后是一套比想象中复杂得多的鉴权:OIDC/OAuth2/device-code/外部命令四条路,令牌刷新的单点变更与双花防护,以及一个依赖倒置的 seam。这一章看它为什么难、又怎么做对——实现如今已拆成独立的 xai-grok-login crate。
认证 · 令牌刷新为什么这么难
grok login 看着简单,背后是这个仓库里最讲究的子系统之一。为什么难?因为令牌会过期、要刷新、多进程还会抢着刷,一步错就把用户踢下线。
一个依赖倒置的 seam
xai-grok-auth 是个接缝 crate:数据收集器(xai-file-utils)需要「刷新感知的令牌解析」来驱动 HTTP,但又不该反向依赖 shell 的类型。于是抽两个 trait:HttpAuth(盖鉴权头)、AuthCredentialProvider(snapshot() + refresh_after_unauthorized())。AuthRetryMiddleware 就靠它做 401 自动刷新重试。如今 sampler、otel、telemetry、tools、workspace 等十个 crate 都依赖这层接缝。用一个 seam crate 打断循环依赖,是大型 Rust 工程常见手法。
写作之后,trait 背后的实现也搬了家:登录与刷新整块从 xai-grok-shell::auth 拆成了独立 crate xai-grok-login(AuthManager、OIDC、device code、外部命令、凭据存储都在这里),shell 再以 xai_grok_shell::auth 重新导出,老路径照常解析。
四条登录路
AuthMode(xai-grok-login/src/model.rs):Oidc(企业 SSO)、ApiKey、External(外部命令)、WebLogin(旧版,已弃,只为读旧 auth.json,缓存里遇到会迁到 OIDC)。登录流程(flow.rs)按序尝试:缓存 → 静默刷新 → 外部鉴权命令(sh -c,stdout=token/stderr=UX)与 devbox 自动迁移 → 交互式(企业 OIDC 或 xAI OAuth2)。/login 这类强制重登只是跳过缓存,并不清掉它。
外部命令现在明确分两种契约:设了 GROK_AUTH_EXPIRED=1 是无人值守的静默刷新(stdin 关闭、stderr 被吞,几秒就超时,拿不到就交回登录流程);不设则是有人在场的登录(stderr 直达用户,上限 300 秒)。
交互式 OAuth 用 PKCE,还同时跑两条路竞速:Path A 起本地 loopback HTTP server 收回调,Path B 让你从浏览器粘贴——因为远程 VM 上回调回不到 CLI,粘贴是刚需。CI/远程还有 device code 流(RFC 8628,轮询换 token)。
刷新:单点变更 + 双花防护
令牌刷新是命门。AuthManager 把所有变更收在一个 refresh_chain(落盘与判定只在 apply_refresh_outcome 一处发生),配一把 async refresh_lock。为什么这么严?
RT 双花会触发 IdP 令牌族撤销——两个进程/线程拿同一个 refresh token 各刷一次,IdP 认为令牌被盗,把整族令牌全撤了,用户直接掉线。所以刷新必须:① 全局串行(进程内 mutex + 跨进程文件锁);② 拿到锁先看磁盘:兄弟进程已经轮换过,就直接收养它的新 token(try_adopt_disk_token),绝不拿旧 RT 再去换(否则 invalid_grant);③ 文件锁拿到后还要校验 inode,防止锁文件被并发删掉重建后两边各持一把。还有系统睡眠这一关:刷新跨过一次挂起,可能丢掉轮换后的新 RT、磁盘上只剩被撤销的旧 RT。现在的 sleep gate(manager/sleep_gate.rs)在睡眠将至时推迟还没开始的刷新,绝不中途丢弃进行中的交换,并暂扣系统的睡眠确认,直到它跑完或超时;闸门的时限用 DualClock(单调 + 墙钟)计——挂起时单调时钟暂停、墙钟照走,两者之差才量得出睡了多久。
这些坑单看每一个都隐蔽,合起来就是「为什么令牌刷新这么难」的答案。
接回采样器
鉴权最终要喂给 Part 1 的采样器——那个 bearer_resolver 就是 GrokAuthCredentials(现在也在 xai-grok-login):deployment key → 裸 Bearer(两者都有时它优先);user token → Bearer + X-XAI-Token-Auth(头名和值都用 obfstr 混淆)。凭据的 snapshot().token 必须和实际发的 bearer 一致,否则 401 归因的前缀对不上。
秘密卫生贯穿始终:嵌入字面量 obfstr 混淆、Debug 只露 token 末 12 位、auth.json 权限 0o600 且写后 fsync(文件坏了先备份成 auth.json.corrupt.<毫秒> 再重写)。
和《从 LLM 到 Coding Agent》对照
教学最小实现里鉴权就是「读个 API key 塞进头」。grok 的增量是一整套生产级鉴权:四条登录路、PKCE 竞速、device flow、以及令牌刷新那一堆「多进程 + 挂起 + RT 撤销」的血泪防护。这些代码不产生智能,却决定用户会不会莫名其妙掉线。下一章看它收集(与不收集)哪些遥测。