开源 CLI 工具里的 OAuth Device Flow 实现

OAuth Device Flow 在真实 CLI 工具里通常不是孤立存在的。常见做法是:能打开浏览器时优先走浏览器授权;不能打开浏览器、运行在 SSH / Codespaces / Agent 环境里时,才走 Device Flow;有些工具则把 Device Flow 做成默认登录方式。

下面用几个开源 CLI 源码验证这个判断。

结论先看

工具 是否使用 Device Flow 默认选择 主要差异
GitHub CLI gh 先尝试 Device Flow,失败再退回浏览器 Web App Flow slow_down 做了额外保护,考虑 WSL/VM 时钟漂移
Lark/Feishu lark-cli auth login 明确就是 Device Flow 要求 client_secret,并为 Agent 场景提供 --no-wait / --device-code 两段式流程
GitLab CLI glab 默认可选浏览器、PAT 或 Device Flow;--device 强制使用 Device Flow 与普通 OAuth Web Flow 并存,适合无浏览器环境
Azure CLI az 默认浏览器交互;无浏览器、Codespaces 或 --use-device-code 时切到 Device Flow Device Flow 由 MSAL 封装,CLI 只负责选择入口和展示提示
AWS CLI / Botocore SSO 接近 Device Flow / OIDC 设备授权 IAM Identity Center SSO 使用 OIDC 设备授权接口 API 名称是 StartDeviceAuthorization / CreateToken,属于云厂商包装后的设备授权模型

所以真实市场里的规律是:

Device Flow 是 CLI 的重要登录方式,但不总是唯一方式。成熟 CLI 往往同时支持浏览器回调、Device Flow、个人令牌、服务账号等多种路径。

标准 Device Flow 的骨架

标准 Device Flow 可以压缩成 6 步:

  1. CLI 向授权服务器申请 device_code
  2. 授权服务器返回 device_codeuser_code、授权 URL、过期时间和轮询间隔;
  3. CLI 把 URL 和短码展示给用户;
  4. 用户在另一台有浏览器的设备上登录并授权;
  5. CLI 按间隔轮询 token endpoint;
  6. 授权成功后,CLI 拿到 access token / refresh token 并保存。

源码里的实现基本都围绕这几个动作展开。

GitHub CLI:Device Flow 优先,失败再回退浏览器

GitHub CLI 的 gh auth login 最接近标准 Device Flow。

cli/cli 里,AuthFlow 构造了 OAuth flow,设置 DisplayCode 展示 one-time code,设置 BrowseURL 打开授权 URL,然后调用 flow.DetectFlow()

github.com/cli/cli: internal/authflow/flow.go:42-48
github.com/cli/cli: internal/authflow/flow.go:61-83
github.com/cli/cli: internal/authflow/flow.go:95

真正的 Device Flow 逻辑在 github.com/cli/oauth 包里。源码注释直接说明:

DetectFlow tries to perform Device flow first and falls back to Web application flow.

对应位置:

github.com/cli/oauth: oauth.go:97-101

Device Flow 的字段也和标准模型一致:

github.com/cli/oauth: device/device_flow.go:39-54
github.com/cli/oauth: device/device_flow.go:69-120

它请求到:

  • device_code
  • user_code
  • verification_uri
  • verification_uri_complete
  • expires_in
  • interval

轮询 token 时,使用标准 grant type:

urn:ietf:params:oauth:grant-type:device_code

对应位置:

github.com/cli/oauth: device/device_flow.go:123
github.com/cli/oauth: device/device_flow.go:213-217

GitHub CLI 比标准流程多做了一层工程化处理:它不是简单按 interval 等待,而是默认加 20% 安全余量;遇到 slow_down 后增加间隔,并对 WSL / VM 的时钟漂移做特殊提示。

对应位置:

github.com/cli/oauth: device/device_flow.go:150-181
github.com/cli/oauth: device/device_flow.go:240-276

这说明真实 CLI 实现不只照抄 RFC,还要处理运行环境的时间误差和轮询限速问题。

Lark/Feishu CLI:明确以 Device Flow 登录

官方 larksuite/cliauth login 直接把命令描述为:

Device Flow authorization login

对应位置:

github.com/larksuite/cli: cmd/auth/login.go:47-56

它的流程也非常标准:

  1. RequestDeviceAuthorization 请求设备授权;
  2. 输出 verification_uri_complete 或 JSON 结构;
  3. 调用 PollDeviceToken 轮询 token;
  4. 成功后获取用户信息;
  5. 保存 access token / refresh token。

主流程位置:

github.com/larksuite/cli: cmd/auth/login.go:263-329
github.com/larksuite/cli: cmd/auth/login.go:349-388

底层实现里,返回字段和标准 Device Flow 对齐:

github.com/larksuite/cli: internal/auth/device_flow.go:20-28

请求设备授权时,它会提交 client_idscope,并额外用 Basic Auth 带上 app_id:app_secret

github.com/larksuite/cli: internal/auth/device_flow.go:64-91

这是它和许多 public client CLI 的一个区别:Lark/Feishu CLI 绑定的是用户自己创建或配置的飞书应用,登录时需要应用的 app_secret。换句话说,它不是“只有公开 client_id 的纯 public client”,而是“CLI 代表某个已配置的飞书应用去申请用户授权”。

轮询 token 时,它使用标准 device code grant:

github.com/larksuite/cli: internal/auth/device_flow.go:171-176

并处理这些典型状态:

github.com/larksuite/cli: internal/auth/device_flow.go:229-248

包括:

  • authorization_pending
  • slow_down
  • access_denied
  • expired_token
  • invalid_grant

它还有一个很现实的 Agent 场景扩展:--no-wait 先返回 verification_urldevice_code,让 Agent 把链接或二维码发给用户;用户授权后,再用 --device-code 继续轮询完成登录。

对应位置:

github.com/larksuite/cli: cmd/auth/login.go:273-295
github.com/larksuite/cli: cmd/auth/login.go:392-419

这不是 RFC 的新流程,而是把同一个 Device Flow 拆成两段,适配“当前对话回合不能一直阻塞等待用户扫码”的 Agent 运行环境。

GitLab CLI:浏览器 OAuth 和 Device Flow 并存

GitLab CLI glab 支持多种登录方式:PAT、浏览器 OAuth、Device Flow。

登录命令里有 --web--device 两个互斥选项:

gitlab.com/gitlab-org/cli: internal/commands/auth/login/login.go:50-52
gitlab.com/gitlab-org/cli: internal/commands/auth/login/login.go:173-186

文档示例明确说明 --device 用于无本地浏览器的 headless 环境:

gitlab.com/gitlab-org/cli: internal/commands/auth/login/login.go:105-111

真正选择流程的位置在这里:

gitlab.com/gitlab-org/cli: internal/commands/auth/login/login.go:527-536

如果用户选择 Device Flow,调用:

oauth2.StartDeviceFlow(...)

StartDeviceFlow 的注释直接写明它实现的是 RFC 8628:

gitlab.com/gitlab-org/cli: internal/oauth2/device.go:17-20

它做的事也符合标准 Device Flow:

gitlab.com/gitlab-org/cli: internal/oauth2/device.go:30-47
  • 无 redirect URI;
  • 请求 DeviceAuth
  • 打印 UserCodeVerificationURI
  • 调用 DeviceAccessToken 等待授权完成;
  • 保存 token。

相比 ghglab 的设计更像“多登录方式菜单”:浏览器 OAuth 是常规路径,Device Flow 是 headless 场景下的专门选项。

Azure CLI:默认浏览器,必要时切到 Device Flow

Azure CLI 的 az login 默认不是直接 Device Flow,而是优先走浏览器交互授权。

源码里可以看到判断逻辑:

github.com/Azure/azure-cli: src/azure-cli-core/azure/cli/core/_profile.py:166-179

规则是:

  • 如果不能打开浏览器,切到 device code;
  • 如果检测到 GitHub Codespaces,切到 device code;
  • 如果用户显式指定 use_device_code,也走 device code;
  • 否则走 login_with_auth_code

Device Flow 的细节交给 MSAL:

github.com/Azure/azure-cli: src/azure-cli-core/azure/cli/core/auth/identity.py:174-182

它调用:

initiate_device_flow(...)
acquire_token_by_device_flow(...)

这说明 Azure CLI 自己不手写 authorization_pending / slow_down 轮询逻辑,而是把标准流程封装在 MSAL 库里。CLI 负责做环境判断、展示 flow["message"],以及把登录结果纳入 Azure 账号和订阅管理。

AWS CLI / Botocore:云厂商包装后的设备授权

AWS IAM Identity Center SSO 使用 OIDC 设备授权模型,但在源码和 API 名称上不是直接叫 /oauth/device/code

Botocore 的 SSO OIDC 服务模型里有:

StartDeviceAuthorization
CreateToken

对应 API 模型:

github.com/boto/botocore: botocore/data/sso-oidc/2019-06-10/service-2.json:18-24
github.com/boto/botocore: botocore/data/sso-oidc/2019-06-10/service-2.json:88-95

这和标准 Device Flow 的关系是:

标准 OAuth 名字 AWS SSO OIDC 名字
device authorization endpoint StartDeviceAuthorization
token endpoint CreateToken
device/user code deviceCode / user-facing verification fields

Botocore 里也能看到后续 token 刷新使用 create_tokenrefresh_token grant:

github.com/boto/botocore: botocore/tokens.py:327-347

AWS 这类实现说明:有些 CLI 看起来不像“标准 OAuth URL + 表单参数”,但本质仍是在设备授权、轮询换 token、缓存刷新 token 这一套模型上做云厂商包装。

实现差异总结

1. 默认入口不同

模式 代表工具 特点
Device Flow 优先 GitHub CLI gh 先试设备流,服务端不支持再退回 Web Flow
Device Flow 明确登录 Lark/Feishu lark-cli 登录命令本身就是设备授权,尤其适合 Agent 转发 URL/二维码
浏览器优先,Device Flow 兜底 Azure CLI az 本地浏览器可用时体验更顺滑,无浏览器时切换
多登录方式并列 GitLab CLI glab PAT、Web OAuth、Device Flow 都保留
云厂商包装 AWS SSO API 名称不同,但模型接近 Device Flow

2. public client 不一定真的“无密钥”

标准讲解里常说 Device Flow 适合 public client,因为 CLI 很难安全保存 client_secret

但真实工具里分两类:

  • ghglab:更接近 public client,使用公开的 OAuth client id;
  • lark-cli:用户先配置飞书应用,CLI 带 app_secret 去请求和换 token。

所以判断一个 CLI 是否是 Device Flow,不能只看有没有 client_secret;应该看有没有:

  • 设备授权请求;
  • 用户短码或完整验证 URL;
  • token 轮询;
  • urn:ietf:params:oauth:grant-type:device_code 或等价云厂商接口。

3. Agent 场景会把流程拆开

标准 Device Flow 假设 CLI 可以一直阻塞等待用户授权。

但 Agent / 聊天机器人场景里,当前回合可能要先把 URL 或二维码发给用户,然后结束;用户授权后,再由 Agent 执行下一条命令完成轮询。

lark-cli--no-wait / --device-code 就是这个差异:

  • 第一段:生成授权 URL 和 device_code
  • 第二段:用户授权后,用 device_code 继续轮询。

底层仍是 Device Flow,只是交互形态更适合异步聊天环境。

4. 轮询策略会被工程化

标准流程只说按 interval 轮询,遇到 slow_down 放慢。

真实实现会更细:

  • gh 给初始间隔加安全余量,并处理 WSL / VM 时钟漂移;
  • lark-cli 遇到网络错误会逐步增加间隔,最多到 60 秒;
  • Azure CLI 把轮询交给 MSAL;
  • AWS 把流程包装进 SSO OIDC API。

这说明轮询不是“while true 每 5 秒请求一次”那么简单。成熟实现要处理限速、超时、取消、网络失败、系统时钟异常。

小结

从这些开源 CLI 看,Device Flow 的核心判断标准不是“界面上有没有验证码”,而是完整链路:

申请 device code → 展示用户授权 URL/短码 → 用户浏览器授权 → CLI 轮询 token → 保存 token

ghlark-cliglabaz 都能在源码中找到这条链路,只是默认入口不同:

  • gh:Device Flow 优先;
  • lark-cli:Device Flow 是主登录方式,并扩展了 Agent 两段式;
  • glab:Device Flow 是无浏览器环境的可选登录方式;
  • az:浏览器优先,不能打开浏览器时切换到 Device Flow;
  • AWS SSO:接口名不同,但模型是设备授权 + token 换取。

因此,用 OAuth Device Flow 理解 CLI 登录是对的,但需要补上一层真实工程判断:CLI 登录通常是一个“认证策略选择器”,Device Flow 是其中最适合 headless、SSH、IoT、Agent 场景的一条路径。