OAuth Device Flow 简明指南:CLI 如何通过浏览器完成授权

---
title: OAuth Device Flow 简明指南:CLI 如何通过浏览器完成授权
published_at: 2026-07-10
language: zh
---

# OAuth Device Flow 简明指南:CLI 如何通过浏览器完成授权

随着 AI Agent 和开发者工具的发展,CLI 正重新成为常见的交互与集成入口。GitHub CLI `gh`、Lark/Feishu `lark-cli`、Codex CLI 等工具在调用云端 API 时,都需要完成用户授权,而 OAuth Device Flow 正是这类场景中常见的一种方式。

OAuth Device Flow,也叫 Device Authorization Flow,主要用于那些不方便打开浏览器、不适合输入密码,或者无法接收 redirect URI 回调的设备和工具,例如智能电视、游戏机、IoT 设备、服务器终端和 CLI 工具。

它的核心思路可以概括为:

> 设备不直接让用户输入密码,而是显示一个短码;用户用手机或电脑打开浏览器完成登录授权;设备在后台轮询,直到拿到 access token。

## 它解决什么问题

普通 OAuth Authorization Code Flow 通常依赖浏览器跳转和 redirect URI。桌面应用可以在本机启动一个回调服务,Web 应用可以接收服务器回调,但很多设备做不到:

- 电视、游戏机输入账号密码很麻烦;
- CLI 工具可能运行在没有浏览器的 SSH 环境里;
- IoT 设备通常没有完整浏览器,也没有稳定公网回调地址;
- 用户密码和 MFA 不应该输入到不可信或输入体验差的设备上。

Device Flow 的作用,就是把登录和授权动作转移到用户更方便操作的手机或电脑上完成。

## 参与角色

- **Device Client**:受限设备或 CLI 工具,负责显示验证码并轮询 token。
- **Authorization Server**:OAuth 授权服务器,负责生成验证码、处理登录授权、发放 token。
- **User**:用户本人,用手机或电脑完成授权。
- **Resource Server**:真正提供 API 的服务,例如 GitHub API、Google Drive API。

## 完整流程

### 1. 设备申请 device code

设备先向授权服务器请求一组用于设备授权的码:

```http
POST /oauth/device/code
Content-Type: application/x-www-form-urlencoded

client_id=abc123&scope=repo read:user
```

这里通常包含:

- `client_id`:客户端标识;
- `scope`:希望申请的权限范围。

Device Flow 的客户端通常是 public client,不能安全保存 `client_secret`。

### 2. 授权服务器返回验证码信息

授权服务器返回类似结果:

```json
{
  "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://example.com/device",
  "verification_uri_complete": "https://example.com/device?user_code=WDJB-MJHT",
  "expires_in": 900,
  "interval": 5
}
```

字段含义:

| 字段 | 作用 |
|---|---|
| `device_code` | 给设备使用的长码,后续轮询 token 时提交 |
| `user_code` | 给用户看的短码,用户在网页上输入 |
| `verification_uri` | 用户需要打开的授权页面 |
| `verification_uri_complete` | 已带上 user code 的完整授权链接,用户打开后可能不必手动输入 |
| `expires_in` | 这组码的有效期,单位通常是秒 |
| `interval` | 设备轮询 token 接口的最小间隔 |

### 3. 设备提示用户授权

设备或 CLI 显示:

```text
请打开下面的链接完成授权:
https://example.com/device

然后输入验证码:
WDJB-MJHT
```

如果有 `verification_uri_complete`,也可以直接显示完整链接:

```text
https://example.com/device?user_code=WDJB-MJHT
```

### 4. 用户在浏览器里登录并确认

用户用手机或电脑打开授权页面,输入验证码,登录账号,并确认是否允许这个应用访问指定权限。

授权服务器会更新这次设备授权请求的状态:已授权、被拒绝,或者已过期。

### 5. 设备轮询 token 接口

设备从拿到 `device_code` 开始,按 `interval` 指定的间隔请求 token 接口:

```http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

client_id=abc123&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS&grant_type=urn:ietf:params:oauth:grant-type:device_code
```

关键参数是:

```text
grant_type=urn:ietf:params:oauth:grant-type:device_code
```

它表示客户端正在用 Device Flow 的授权结果换取 token。

### 6. 授权成功后返回 access token

用户完成授权后,设备下一次轮询会收到:

```json
{
  "access_token": "access_token_here",
  "token_type": "Bearer",
  "scope": "repo read:user",
  "expires_in": 3600,
  "refresh_token": "refresh_token_here"
}
```

之后设备就可以带着 access token 调用资源服务器:

```http
GET /user
Authorization: Bearer access_token_here
```

## 流程图

```text
设备 / CLI                    授权服务器                    用户浏览器
   |                              |                              |
   | 申请 device_code             |                              |
   |----------------------------->|                              |
   | 返回 device_code/user_code    |                              |
   |<-----------------------------|                              |
   | 显示 URL 和 user_code          |                              |
   |                              | 用户打开页面、输入 code、登录  |
   |                              |<----------------------------->|
   | 轮询 token 接口                |                              |
   |----------------------------->|                              |
   | authorization_pending         |                              |
   |<-----------------------------|                              |
   | 继续轮询                      |                              |
   |----------------------------->|                              |
   | 返回 access_token             |                              |
   |<-----------------------------|                              |
```

## 轮询时的几种状态

设备轮询 token 接口时,授权不一定已经完成。常见返回状态如下:

| 错误码 | 含义 | 客户端应该怎么做 |
|---|---|---|
| `authorization_pending` | 用户还没有完成授权 | 继续等待,按 `interval` 轮询 |
| `slow_down` | 轮询太频繁 | 增加轮询间隔 |
| `access_denied` | 用户拒绝授权 | 停止流程,提示授权被拒绝 |
| `expired_token` | device code 已过期 | 停止流程,让用户重新发起登录 |
| `invalid_client` | client_id 不合法 | 检查客户端配置 |
| `invalid_grant` | device_code 无效或状态异常 | 停止流程,重新开始 |

客户端不应该把 `authorization_pending` 当成失败。它只是表示用户还没完成网页端授权。

## 为什么 Device Flow 适合 CLI、电视和 IoT 设备

Device Flow 的优势在于:

- 设备不需要打开浏览器;
- 设备不需要接收 redirect URI 回调;
- 用户不用在设备上输入密码和 MFA;
- 登录和授权发生在授权服务器的正规网页上;
- CLI 或设备只需要显示短码,并在后台轮询。

所以它尤其适合输入能力弱、没有浏览器、没有公网回调地址的场景。

## 安全要点

Device Flow 的安全性依赖几个关键设计:

1. **用户密码不进入设备**  
   用户只在授权服务器网页上登录,设备只拿到 token。

2. **user code 短但有效期也短**  
   短码方便输入,但也更容易被猜,所以必须设置过期时间、错误次数限制和风控。

3. **轮询必须限速**  
   客户端要遵守 `interval`,遇到 `slow_down` 要放慢请求,避免压垮授权服务器。

4. **授权页面必须展示应用和权限**  
   用户需要清楚看到哪个应用在请求哪些权限,避免误授权。

5. **适合 public client**  
   电视、CLI、IoT 设备通常不能安全保存密钥,因此 Device Flow 不依赖客户端安全保存 `client_secret`。

## 小结

Device Flow 可以压缩成 6 步:

1. 设备向授权服务器申请 `device_code`;
2. 授权服务器返回 `device_code`、`user_code` 和授权 URL;
3. 设备显示 URL 和 `user_code`;
4. 用户在另一台设备上打开网页,登录并授权;
5. 设备按间隔轮询 token 接口;
6. 授权完成后,设备拿到 access token 并调用 API。

一句话总结:

> Device Flow 是 OAuth 为受限设备设计的授权方式:设备显示短码,用户在浏览器里授权,设备轮询直到拿到 token。