自建 Device Flow 授权服务实现规范
适用场景:企业用户希望自行控制 CLI 授权过程,通过搭建私有授权服务并在客户端修改 endpoint 来实现自定义授权。
1. 概述
CloudBase CLI 使用 RFC 8628 — OAuth 2.0 Device Authorization Grant 协议完成设备授权。整个流程涉及三个接口:
| 接口 | 方法 | 作用 | 规范要求 |
|---|---|---|---|
/device/code | POST | 客户端发起授权请求,获取 device_code 和 user_code | 必须遵循 |
/device/verify | POST | 用户在浏览器侧确认授权 | 自行实现,本文仅提供参考示例 |
/token | POST | 客户端轮询获取授权凭证 | 必须遵循 |
1.1 授权时序
2. 接口规范
2.1 申请设备授权码
POST /device/code
请求
| Content-Type | application/json |
|---|
请求体:
| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
client_id | string | 是 | 1–128 字符 | 客户端标识 |
请求示例:
{
"client_id": "cloudbase-cli"
}
响应
成功响应(HTTP 200):
| 字段 | 类型 | 说明 |
|---|---|---|
device_code | string | 设备码,客户端后续用于轮询换取凭证,建议不少于 32 字节随机值 |
user_code | string | 用户码,格式为 XXXX-XXXX(大写字母 + 数字,排除易混淆字符 0, 1, I, O),用户在浏览器端输入以确认授权。服务端应确保活跃的 user_code 唯一,生成时需检测碰撞并重试 |
verification_uri | string | 用户确认授权的页面 URL |
expires_in | number | 设备码有效期(秒),推荐值 600 |
interval | number | 客户端轮询间隔(秒),推荐值 3 |
响应示例:
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS8A",
"user_code": "QWER-ASDF",
"verification_uri": "https://example.com/cli-auth",
"expires_in": 600,
"interval": 3
}
错误响应
| 字段 | 类型 | 说明 |
|---|---|---|
error | string | 错误码 |
error_description | string | 错误描述 |
可能的错误码:
| 错误码 | 说明 |
|---|---|
invalid_client | client_id 格式非法或不在允许列表内 |
2.2 用户确认授权(参考示例,非强制规范)
ℹ️ 此接口由用户在浏览器端的授权页调用。由于用户会自行搭建授权页面(即
verification_uri指向的页面),因此 该接口的路径、请求参数、鉴权方式均由实现方自行设计。本节仅提供一个参考示例,帮助理解整体流程。
核心职责
无论接口如何设计,该步骤需要完成以下事项:
- 验证用户身份 — 确认当前操作者已通过身份认证
- 接收
user_code— 用户在授权页中输入从 CLI 获取的用户码 - 关联授权信息 — 将
user_code对应的device_code状态从pending更新为authorized,并关联当前用户的身份信息
参考示例
POST /device/verify
请求体:
{
"user_code": "QWER-ASDF"
}
成功响应:
{
"status": "USER_VERIFIED"
}
💡 实现方可根据业务需求自由扩展该接口,例如增加二次确认、权限范围选择、多因素认证等能力。
2.3 轮询获取凭证
POST /token
请求
| Content-Type | application/json |
|---|
请求体:
| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
grant_type | string | 是 | 固定值 | 必须为 urn:ietf:params:oauth:grant-type:device_code |
device_code | string | 是 | 最长 256 字符 | 通过 /device/code 获取的设备码 |
client_id | string | 是 | 1–128 字符 | 客户端标识,需与申请设备码时一致 |
device_info | object | 是 | — | 设备信息对象 |
device_info.os | string | 是 | 最长 64 字符 | 操作系统标识 |
device_info.mac | string | 是 | 最长 128 字符 | 设备 MAC 地址 |
device_info.hash | string | 是 | 最长 256 字符 | 设备唯一标识哈希 |
请求示例:
{
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS8A",
"client_id": "cloudbase-cli",
"device_info": {
"os": "darwin",
"mac": "AA:BB:CC:DD:EE:FF",
"hash": "a1b2c3d4e5f6..."
}
}
响应
授权成功(HTTP 200)
⚠️ 与 RFC 8628 标准的差异说明:标准 OAuth 2.0 Token Response 使用
access_token、token_type、refresh_token、expires_in、scope等字段。本接口的成功响应为 CloudBase 业务扩展格式,直接返回refreshToken、临时密钥等业务字段,以适配 CloudBase CLI 的凭证体系。实现方应按下表字段返回,而非标准 OAuth Token Response。
ℹ️ 自建授权服务能力说明:在企业自建授权服务场景下,CloudBase CLI 主要依赖临时密钥完成后续操作。
refreshToken和expired为兼容保留字段,通常依赖中心化账号体系生成,企业自建服务暂时无需实现其真实语义。
用户已确认授权且凭证生成成功时,返回以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
refreshToken | string | 兼容保留字段。企业自建授权服务无需实现,固定返回空字符串 "" |
uin | string | 授权用户的主账号 UIN |
mac | string | 回传设备 MAC 地址 |
os | string | 回传操作系统标识 |
tokenId | string | 令牌 ID,用于标识和管理登录会话 |
expired | number | 兼容保留字段。企业自建授权服务无需实现真实过期语义,建议固定返回 0 |
tmpToken | string | 临时安全令牌(STS Token) |
tmpSecretId | string | 临时密钥 SecretId |
tmpSecretKey | string | 临时密钥 SecretKey |
tmpExpired | number | 临时密钥过期时间戳(毫秒) |
成功响应示例:
{
"refreshToken": "",
"uin": "100000001",
"mac": "AA:BB:CC:DD:EE:FF",
"os": "darwin",
"tokenId": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"expired": 0,
"tmpToken": "xxxxxxxxxxxxxxxx",
"tmpSecretId": "AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"tmpSecretKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"tmpExpired": 1750557600000
}
💡 在自建授权服务场景中,
refreshToken为空属于预期行为。CLI 通过临时密钥完成后续访问,不要求实现可续期的长期令牌。
授权未完成 / 错误(HTTP 400)
当授权流程尚未完成或发生错误时,返回 HTTP 400 及错误对象:
| 字段 | 类型 | 说明 |
|---|---|---|
error | string | 错误码 |
error_description | string | 错误描述 |
错误码一览:
| 错误码 | 说明 | 客户端行为 |
|---|---|---|
authorization_pending | 用户尚未完成授权 | 继续轮询,按 interval 间隔重试 |
slow_down | 轮询过于频繁 | 增大轮询间隔后继续轮询 |
expired_token | 设备码已过期 | 停止轮询,需重新发起 /device/code |
invalid_client | client_id 不匹配 | 停止轮询,检查客户端配置 |
invalid_grant | 授权数据异常 | 停止轮询,需重新发起流程 |
unsupported_grant_type | 不支持的 grant_type | 停止轮询,检查请求参数 |
already_consumed | 设备码已被使用 | 停止轮询,避免重放 |
错误响应示例:
{
"error": "authorization_pending",
"error_description": "The authorization request is still pending"
}
3. 设备码状态机
设备码在整个生命周期中经历以下状态流转:
| 状态 | 值 | 说明 |
|---|---|---|
| 待授权 | pending | 初始状态,等待用户确认 |
| 已授权 | authorized | 用户已确认,等待客户端换取凭证 |
| 已过期 | expired | 设备码超过有效期 |
| 已消费 | consumed | 凭证已成功下发,设备码不可重用 |
4. 实现要求
4.1 安全性要求
| 要求 | 说明 |
|---|---|
| 传输安全 | 所有接口必须通过 HTTPS(TLS 1.2+)提供服务,禁止明文 HTTP 传输 |
| 设备码随机性 | device_code 应使用密码学安全的随机数生成,建议不少于 32 字节 |
| 用户码字符集 | 使用字符集 23456789ABCDEFGHJKLMNPQRSTUVWXYZ(排除 0, 1, I, O),降低用户输入错误 |
| 一次性消费 | 每个 device_code 仅允许成功换取一次凭证,换取后应立即标记为 consumed |
| 防重放 | consumed 状态需保留一定时间(建议 ≥ 60 秒),防止重放攻击 |
| 并发安全 | 对同一 device_code 的并发 token 请求需做互斥控制,避免重复下发凭证 |
| 验证接口鉴权 | /device/verify 必须在已认证的上下文中调用,确保操作者身份可信 |
4.2 限流建议
| 接口 | 建议限制 |
|---|---|
/device/code | 同一 IP 每分钟不超过 20 次 |
/token | 同一 IP 每分钟不超过 120 次 |
/device/verify | 根据业务需求自定义 |
4.3 时间参数建议
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 设备码有效期 | 600 秒(10 分钟) | 用户需在此时间内完成授权 |
| 轮询间隔 | 3 秒 | 客户端轮询的最小间隔 |
| 临时密钥有效期 | 按业务需求设定 | 应覆盖 CLI 完成目标操作所需时长 |
consumed 状态保留 | ≥ 60 秒 | 防重放窗口 |
5. 客户端轮询逻辑参考
客户端在获取到 device_code 后,按以下逻辑轮询 /token:
6. 完整流程示例
Step 1:客户端请求设备码
curl -X POST https://your-auth-server.example.com/device/code \
-H "Content-Type: application/json" \
-d '{"client_id": "cloudbase-cli"}'
响应:
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS8A",
"user_code": "QWER-ASDF",
"verification_uri": "https://your-auth-server.example.com/cli-auth",
"expires_in": 600,
"interval": 3
}
Step 2:CLI 展示信息
请在浏览器中打开 https://your-auth-server.example.com/cli-auth
并输入授权码: QWER-ASDF
Step 3:用户在浏览器确认(实现方自行设计)
用户在浏览器中打开 verification_uri,完成身份认证后输入 user_code 确认授权。以下为参考示例:
curl -X POST https://your-auth-server.example.com/device/verify \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <user-session-token>" \
-d '{"user_code": "QWER-ASDF"}'
参考响应:
{
"status": "USER_VERIFIED"
}
Step 4:客户端轮询获取凭证
curl -X POST https://your-auth-server.example.com/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS8A",
"client_id": "cloudbase-cli",
"device_info": {
"os": "darwin",
"mac": "AA:BB:CC:DD:EE:FF",
"hash": "a1b2c3d4e5f6..."
}
}'
轮询中(用户尚未确认):
{
"error": "authorization_pending",
"error_description": "The authorization request is still pending"
}
授权成功:
{
"refreshToken": "",
"uin": "100000001",
"mac": "AA:BB:CC:DD:EE:FF",
"os": "darwin",
"tokenId": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"expired": 0,
"tmpToken": "xxxxxxxxxxxxxxxx",
"tmpSecretId": "AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"tmpSecretKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"tmpExpired": 1750557600000
}
附录 A:错误码速查表
| 错误码 | 值 | 出现接口 |
|---|---|---|
authorization_pending | authorization_pending | /token |
expired_token | expired_token | /token |
slow_down | slow_down | /token |
invalid_client | invalid_client | /device/code, /token |
invalid_grant | invalid_grant | /token |
unsupported_grant_type | unsupported_grant_type | /token |
already_consumed | already_consumed | /token |
💡
/device/verify接口由实现方自行设计,其错误码不在本规范约束范围内。