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 會綁到這些值 — 但每個值都必須在您客戶端的 允許資源清單 上(預設全部拒絕)。格式不正確 或 未在允許清單的值會回 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 範例。

相關文件