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