开源 CLI 工具里的 OAuth Device Flow 实现
---
title: 开源 CLI 工具里的 OAuth Device Flow 实现
published_at: 2026-07-10
language: zh
---
# 开源 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_code`、`user_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()`:
```text
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` 包里。源码注释直接说明:
```text
DetectFlow tries to perform Device flow first and falls back to Web application flow.
```
对应位置:
```text
github.com/cli/oauth: oauth.go:97-101
```
Device Flow 的字段也和标准模型一致:
```text
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:
```text
urn:ietf:params:oauth:grant-type:device_code
```
对应位置:
```text
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 的时钟漂移做特殊提示。
对应位置:
```text
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` 直接把命令描述为:
```text
Device Flow authorization login
```
对应位置:
```text
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。
主流程位置:
```text
github.com/larksuite/cli: cmd/auth/login.go:263-329
github.com/larksuite/cli: cmd/auth/login.go:349-388
```
底层实现里,返回字段和标准 Device Flow 对齐:
```text
github.com/larksuite/cli: internal/auth/device_flow.go:20-28
```
请求设备授权时,它会提交 `client_id` 和 `scope`,并额外用 Basic Auth 带上 `app_id:app_secret`:
```text
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:
```text
github.com/larksuite/cli: internal/auth/device_flow.go:171-176
```
并处理这些典型状态:
```text
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_url` 和 `device_code`,让 Agent 把链接或二维码发给用户;用户授权后,再用 `--device-code` 继续轮询完成登录。
对应位置:
```text
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` 两个互斥选项:
```text
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 环境:
```text
gitlab.com/gitlab-org/cli: internal/commands/auth/login/login.go:105-111
```
真正选择流程的位置在这里:
```text
gitlab.com/gitlab-org/cli: internal/commands/auth/login/login.go:527-536
```
如果用户选择 Device Flow,调用:
```text
oauth2.StartDeviceFlow(...)
```
`StartDeviceFlow` 的注释直接写明它实现的是 RFC 8628:
```text
gitlab.com/gitlab-org/cli: internal/oauth2/device.go:17-20
```
它做的事也符合标准 Device Flow:
```text
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,而是优先走浏览器交互授权。
源码里可以看到判断逻辑:
```text
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:
```text
github.com/Azure/azure-cli: src/azure-cli-core/azure/cli/core/auth/identity.py:174-182
```
它调用:
```text
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 服务模型里有:
```text
StartDeviceAuthorization
CreateToken
```
对应 API 模型:
```text
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:
```text
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 的核心判断标准不是“界面上有没有验证码”,而是完整链路:
```text
申请 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 场景的一条路径。