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 不要求。

运作方式

sequenceDiagram participant CLI participant Signet participant Browser CLI->>Signet: POST /oauth/device/code(client_id、scope) Signet-->>CLI: device_code、user_code、verification_uri、interval note over CLI: 向用户显示 verification_uri 与 user_code loop 每 `interval` 秒轮询 CLI->>Signet: POST /oauth/token(device_code) Signet-->>CLI: authorization_pending end Browser->>Signet: 访问 verification_uri Browser->>Signet: 输入 user_code 并同意 CLI->>Signet: POST /oauth/token(device_code) Signet-->>CLI: access_token + refresh_token

步骤 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 示例。

相关文档