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 步:
- CLI 向授权服务器申请
device_code; - 授权服务器返回
device_code、user_code、授权 URL、过期时间和轮询间隔; - CLI 把 URL 和短码展示给用户;
- 用户在另一台有浏览器的设备上登录并授权;
- CLI 按间隔轮询 token endpoint;
- 授权成功后,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_codeuser_codeverification_uriverification_uri_completeexpires_ininterval
轮询 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/cli 的 auth login 直接把命令描述为:
Device Flow authorization login
对应位置:
github.com/larksuite/cli: cmd/auth/login.go:47-56
它的流程也非常标准:
RequestDeviceAuthorization请求设备授权;- 输出
verification_uri_complete或 JSON 结构; - 调用
PollDeviceToken轮询 token; - 成功后获取用户信息;
- 保存 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_id 和 scope,并额外用 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_pendingslow_downaccess_deniedexpired_tokeninvalid_grant
它还有一个很现实的 Agent 场景扩展:--no-wait 先返回 verification_url 和 device_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; - 打印
UserCode和VerificationURI; - 调用
DeviceAccessToken等待授权完成; - 保存 token。
相比 gh,glab 的设计更像“多登录方式菜单”:浏览器 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_token 和 refresh_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。
但真实工具里分两类:
gh、glab:更接近 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
gh、lark-cli、glab、az 都能在源码中找到这条链路,只是默认入口不同:
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 场景的一条路径。