Device Authorization Flow(设备授权流程)
Device Authorization Grant(RFC 8628)让 CLI 工具、IoT 设备、以及无头环境(headless)可以在不打开本机浏览器的情况下完成用户认证 — 用户改用任何其他设备(手机、笔记本电脑等)完成浏览器端的授权步骤。
何时使用此流程
- 您在打造 CLI 工具(
my-tool login)
- 您的环境是 无头的 — SSH 远程服务器、Docker 容器、CI runner
- 程序化打开浏览器不可行或不便
客户端一律是 公开 的(无 client_secret)。若要做(较少见的)Device 版 PKCE,才会用到 code_challenge — Signet 不要求。
运作方式
步骤 1:请求 device code
curl -X POST https://your-signet/oauth/device/code \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=YOUR_CLIENT_ID" \
-d "scope=openid profile email offline_access"
| 参数 | 必填 | 备注 |
|---|---|---|
client_id |
是 | 已启用 Device Flow 的公开客户端 |
scope |
否 | 空格分隔;必须是客户端已注册 scope 的子集,省略时默认为客户端完整的已注册 scope 集合。包含 openid 才会拿到 ID token |
resource |
否 | RFC 8707 Resource Indicator。绝对 http(s) URI、无 fragment、≤ 1024 字符;可重复(最多 10 个)。带入后,签发的 access token aud 会绑定到这些值 — 但每个都必须在您客户端的 allowed-resources 白名单 内(默认全部拒绝)。格式不正确 或 不在白名单内的值会返回 invalid_target — 见 错误处理 |
附 Resource Indicator 的示例(MCP / 多 RS):
curl -X POST https://your-signet/oauth/device/code \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=YOUR_CLIENT_ID" \
-d "scope=read" \
-d "resource=https://api.example.com" \
-d "resource=https://mcp.example.com"
带 resource 时,用户会看到一个专属的 device 确认页 列出两个 resource,必须点击「Confirm and Authorize」后 device code 才会被标记为已授权。签发的 access token aud 也会绑定这些 resource。
在 /device/verify 记录的同意是按每个 resource 组合分别存储:对同一个应用批准不同的 resource 组合会创建独立的授权记录,不会覆盖先前的记录;每条授权都可在账户 → 已授权应用中单独撤销,撤销其中一条只会使该条授权签发的 token 失效。(旧版每个应用只保留一条授权。)
响应:
{
"device_code": "abc123...",
"user_code": "WXYZ-1234",
"verification_uri": "https://your-signet/device",
"expires_in": 1800,
"interval": 5
}
interval是 最短 轮询间隔。若遇到slow_down(见下)请再拉长。
步骤 2:向用户显示提示
请登录此网址:
https://your-signet/device
并输入验证码:
WXYZ-1234
等待授权中…
若本机有浏览器可打开,自动打开 verification_uri(但仍要打印出网址与 user code,以防自动打开失败):
// Go
_ = exec.Command("open", verificationURI).Start() // macOS
_ = exec.Command("xdg-open", verificationURI).Start() // Linux
_ = exec.Command("cmd", "/c", "start", verificationURI).Start() // Windows
附带显示 verification_uri 的 QR code(外加 user code)对手机用户是很友好的做法。
步骤 3:轮询换 token
curl -X POST https://your-signet/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "device_code=abc123..." \
-d "client_id=YOUR_CLIENT_ID"
收窄
resource(RFC 8707 §2.2):可选择在这里再传一次resource=...,将 access token 绑定到/oauth/device/code原授权集合的 子集。请求未在原授权的 resource(扩张)会返回400 invalid_target— 但 device code 不会 被消耗,CLI 可以修正后重试。
成功(用户已同意):
{
"access_token": "eyJhbG...",
"refresh_token": "def502...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email offline_access"
}
轮询中的错误(HTTP 400,格式 {"error": "...", "error_description": "..."}):
error |
HTTP | 含义 / 处理动作 |
|---|---|---|
authorization_pending |
400 | 用户还没同意 — 维持原 interval 继续轮询 |
slow_down |
400 | 轮询太快 — 将 interval 增加 ≥ 5 秒 |
access_denied |
400 | 用户拒绝了 — 停止轮询 |
expired_token |
400 | device_code 超过 expires_in — 从步骤 1 重新开始 |
invalid_grant |
400 | device_code 不存在或已被用过 — 从步骤 1 重新开始 |
invalid_target |
400 | 本次 token 请求带的 resource= 不在原 device-code 授权子集,或格式不对。Device code 不会 被消耗 — 修正后重试。见 错误处理 §Resource Indicator 错误 |
也可能遇到 429 Too Many Requests — 见 Token 与撤销。完整错误清单见 错误处理。
步骤 4:使用 access token
curl -H "Authorization: Bearer ACCESS_TOKEN" https://api.example.com/resource
步骤 5:刷新 access token
接近过期时用 refresh token 换 — 见 Token 与撤销。实现重试逻辑前请先阅读 轮换模式重用检测的陷阱。
步骤 6:登出
执行 my-tool logout 时 请撤销 refresh token — 只删本机 token 文件会让被盗走的 token 有效到过期为止。见 Token 与撤销。
本机存储 token
CLI 常见惯例:
- macOS:Keychain(例如
security add-generic-password)
- Linux:Secret Service(libsecret),或放
$XDG_CONFIG_HOME/<app>/token.json权限0600
- Windows:Credential Manager
绝对不要把 refresh token 写到 log 或调试输出。
接入检查清单
- [ ] 已由管理员启用 Device Flow 的
client_id
- [ ] 遵守
interval;遇到slow_down会退避
- [ ] 遇到
expired_token/access_denied会重启流程
- [ ] access 与 refresh token 放到 OS 级别安全存储
- [ ] 登出时撤销 refresh token
- [ ] 对 429 速率限制有退避处理
示例 CLI 客户端
github.com/go-signet/device-cli — Go 的完整 Device Flow 示例。