錯誤處理

Signet 回傳的 OAuth 錯誤碼,以及串接方該怎麼處理。所有錯誤都遵循 RFC 6749 §5.2:

{
  "error": "invalid_grant",
  "error_description": "Human-readable description of what went wrong"
}

error_description 是給您看的(記 log、除錯)— 不要 顯示給終端使用者。

GitHub 登入 issuer 驗證

當 OAUTH_ISS_VALIDATION_ENABLED=true(預設值)時,Signet 會將 GitHub 回呼的 iss 與 https://github.com/login/oauth 進行完整字串比對。此 issuer 已內建,不需新增環境變數或在 Callback URL 加上參數。其他值(包含多一個尾端 /)會在交換 token 前被拒絕;缺少 iss 時仍允許通過,以維持相容性。設為 false 會略過所有第三方登入 provider 的 issuer 驗證。

依情境分類的錯誤

授權端點的導回錯誤

當 /oauth/authorize 在使用者已被導回 redirect_uri 後失敗,錯誤會透過 query string 傳回:

https://yourapp.example/callback?error=access_denied&error_description=...&state=RANDOM_STATE&iss=https://your-signet

錯誤導回(與成功導回相同)依 RFC 9207 帶有標識 Signet 的 iss 參數。處理錯誤前,每個用戶端都必須確認 iss 存在,並以簡單字串完全比對,驗證它等於該次授權請求所記錄的 issuer;缺少或不相符時必須拒絕,即使只使用一個授權伺服器也同樣適用。

error 原因 您該怎麼做
access_denied 使用者拒絕授權,或管理員撤銷了使用者的存取權 顯示「登入已取消」;讓使用者重試
invalid_request 缺少 / 畸形的參數,或 此客戶端要求 PKCE 但沒帶 code_challenge 修正請求 — 這是客戶端的 bug
invalid_scope 要求的 scope 不在此客戶端允許範圍 拿掉該 scope;與管理員確認
unauthorized_client 此客戶端沒開 Authorization Code Flow 請管理員為此客戶端開啟 Auth Code Flow
invalid_client 僅在啟用 CIMD 時:URL 形式 client_id 的中繼資料文件抓取失敗或驗證不通過(以本機 400 頁面呈現,不會重新導向 — 此時尚未驗證任何 redirect_uri) 確認文件可存取、不超過 64 KB,且其 client_id 與所在 URL 完全一致
unsupported_response_type response_type 不是 code 改用 response_type=code
invalid_target 某個 resource= 參數不在此客戶端的允許資源清單(CIMD 客戶端則是全伺服器的 CIMD_ALLOWED_RESOURCES 清單;預設全部拒絕),或未通過 RFC 8707 格式驗證(非 http(s)、含 fragment、空 host、超過 10 個、超過 1024 字元) 修正請求 — 見下方 Resource Indicator 錯誤
server_error Signet 暫時性錯誤 退避重試

Token 端點錯誤(/oauth/token)

回傳 HTTP 400 JSON(除了 invalid_client 是 401):

error HTTP 常見原因 您該怎麼做
invalid_request 400 缺必填 form 欄位 修正請求
invalid_client 401 client_id / client_secret 錯,或沒提供客戶端認證 核對憑證;HTTP Basic vs. body 要一致
invalid_grant 400 code / refresh token / device code 無效、過期、已用過、或被撤銷(含 rotation 重用偵測);或 PKCE code_verifier 與原 code_challenge 不符 停止重試。重啟流程 / 要求使用者重新登入
invalid_scope 400 Scope 超過客戶端或原授權的範圍 去掉或縮小 scope
unauthorized_client 400 此 grant type 在此客戶端未啟用 請管理員開啟
unsupported_grant_type 400 不認識的 grant_type 用 authorization_code、refresh_token、urn:ietf:params:oauth:grant-type:device_code 或 client_credentials 之一
invalid_target 400 resource= 格式不對、不在此客戶端的允許資源清單(預設全部拒絕),或(refresh_token / authorization_code / device_code 文法下)要求的 resource 不在原授權子集(RFC 8707 §2.2 narrowing rule) 修正請求 — 見下方 Resource Indicator 錯誤。同一值不要重試
server_error 500 Signet 內部錯誤 退避重試;持續異常請上報

Device Flow 輪詢錯誤

對 /oauth/token 做 grant_type=urn:ietf:params:oauth:grant-type:device_code 輪詢時:

error 意義 您該怎麼做
authorization_pending 使用者還沒同意 維持 interval 繼續輪詢
slow_down 輪詢太快 將 interval 增加 ≥ 5 秒
access_denied 使用者拒絕 停止;告訴使用者
expired_token device_code 超過 expires_in 從 POST /oauth/device/code 重跑
invalid_grant device_code 不存在或已被用過 重跑流程

細節見 Device Flow。

Token Introspection 與驗證

端點 失敗情境 回應
GET /oauth/tokeninfo 缺 Bearer header 401 {"error": "missing_token"} + WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource"
GET /oauth/tokeninfo Token 無效或過期 401 {"error": "invalid_token", ...} + WWW-Authenticate: Bearer error="invalid_token", resource_metadata="…/.well-known/oauth-protected-resource"
GET /oauth/userinfo 缺 / 無效 Bearer 401 + WWW-Authenticate: Bearer error="invalid_token", resource_metadata="…/.well-known/oauth-protected-resource"
POST /oauth/introspect 缺 / 無效的客戶端認證 401 + WWW-Authenticate: Basic realm="signet"
POST /oauth/introspect Token 無效 / 過期 / 被撤銷 200 {"active": false}(依 RFC 7662 — 永遠不是 4xx)
POST /oauth/revoke 任何情況 200(依 RFC 7009 — 不帶錯誤訊號)
GET /oauth/tokeninfo sgk_ 金鑰不存在、格式錯誤、已撤銷、已過期、綁定的應用被停用,或功能被關閉 401 {"error": "invalid_token", ...} — 這六種刻意做成無法區分
POST /oauth/introspect sgk_ 金鑰因上述任一原因失效 200 {"active": false}

個人 API 金鑰(sgk_)在 /oauth/token 的每一種 grant 都會被拒(invalid_grant — 金鑰不是 code、refresh token,也不是 device code;device_code 回的是 access_denied),在 /oauth/userinfo 也會被拒(401 invalid_token — 該面只吃 JWT access token)。由於金鑰的所有失敗都收斂成同一個 401,您無法從回應診斷原因 — 請改到 /account/api-keys 看該金鑰的狀態。見 API 金鑰。

Introspection 擁有權:預設情況下(INTROSPECTION_REQUIRE_OWNERSHIP=true)/oauth/introspect 只 對您自己的客戶端所簽發的 token 回傳完整 metadata。內省一個屬於 其他 客戶端的有效 token,只會拿到 {"active": true} — 沒有 sub、scope、username、aud 等。這是精簡過的成功回應,不是錯誤。見 Token 與撤銷。

速率限制錯誤 — HTTP 429

超過速率限制會拿到 429 Too Many Requests。大多數限制按 IP 計算;/oauth/introspect 按 client_id 計算(背後另有每 IP 上限),所以把同一個 client 分散到多個 IP 不會得到更多額度。

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1735689600
Content-Type: application/json

{"error": "rate_limit_exceeded", "error_description": "..."}

處理方式:

  • 等到 X-RateLimit-Reset(Unix epoch 秒)之後再重試。
  • 連續 429 就指數退避 + jitter。
  • 做 Device Flow 輪詢的話,interval 本該讓您遠低於限制。看到 429 代表您輪詢節奏不對 — 修客戶端,不是加快重試。
  • 多服務共用同一出口 IP 時,可以請管理員調高每 IP 限制;introspect 則讓每個服務用自己的 client_id,各自有獨立額度。

預設值見 Token 與撤銷。

特例:Refresh Token 重用 → Family 撤銷

rotation 模式下,使用已被輪轉掉的舊 refresh token 會回 invalid_grant,同時 整個 token family 在伺服器端被撤銷。這是 終態,不要重試。

{
  "error": "invalid_grant",
  "error_description": "Refresh token is invalid or expired"
}

原因可能是:

  • 兩個分頁 / 行程用同一份儲存的 token 並發刷新
  • 部分失敗後重試,但沒持久化新 token
  • 被偷走的 token 被別人先用了

回應:強制使用者重新登入。預防方式見 Token 與撤銷。

Resource Indicator 錯誤 (RFC 8707)

任何 resource= 參數驗證失敗都會回 invalid_target。分三類:

允許清單關卡(適用 每一個 接受 resource 的 grant — client_credentials、authorization_code、device_code、refresh_token):

每個客戶端都有一份由管理員維護的 允許資源清單。客戶端傳入的 resource= 只有在與清單某一項 完全字串相符 時才會被接受。這份清單 預設全部拒絕 — 若為空,任何 resource= 值都會被拒,即使是格式完全正確的也一樣。完全 不 傳 resource 一律沒問題(token 的 aud 會回退到全域的 JWT_AUDIENCE)。

原因 修正方式
客戶端沒有設定允許清單卻送了 resource= 請管理員把該 resource 加入此客戶端的允許清單
resource= 的值不與清單任一項完全相符 使用管理員登記過的完整 URI 之一,或請他把您的加入

error_description 會指出您送出的違規值(例如 requested resource "https://x" is not in this client's allowed resources)。破壞性變更:先前可自由傳 resource 的客戶端,現在在管理員填入允許清單前都會拿到 invalid_target。

格式驗證(適用所有接受 resource 的端點):

原因 修正方式
不是絕對 URI(例如 resource=/api) 改用完整形式 https://api.example.com
Scheme 不是 http 或 https(例如 javascript:、urn:、data:) RFC 8707 要求是網路定位形式的 URI
含 fragment(#...) 拿掉 fragment — aud 不能帶 fragment
空 host 補上正確的 host
超過 10 個 resource=,或單一值超過 1024 字元 減少數量 / 縮短 URI

Subset rule(RFC 8707 §2.2 — token endpoint 上,對曾經做過 resource binding 的 grant,位於 上述允許清單關卡 _之後_):

Grant 規則
authorization_code /oauth/token 的 resource= 必須是 /oauth/authorize 所送集合的子集
urn:ietf:params:oauth:grant-type:device_code /oauth/token 的 resource= 必須是 /oauth/device/code 所送集合的子集
refresh_token resource= 必須是原授權的子集 — 拒絕擴張、允許縮小
client_credentials 沒有先前的授權可比對子集 — 只套用允許清單關卡與格式驗證

device_code 與 authorization_code 都會在 code 被消耗之前先驗證所請求的 resource,因此 invalid_target 不會燒掉 code — 客戶端可用修正後的 resource 重送 token 請求。(成功兌換仍會依 RFC 6749 消耗一次性的 authorization_code。)

各 flow 範例見:Authorization Code Flow、Device Authorization Flow、Client Credentials Flow。

錯誤處理檢查清單

  • [ ] 刷新時遇到 invalid_grant 視為終態 — 觸發重新登入,不要重試
  • [ ] access_denied 是使用者主動 — 客氣地提示,不要自動重試
  • [ ] server_error 與網路錯誤指數退避重試
  • [ ] 遇 429 尊重 Retry-After
  • [ ] 伺服器端記 error_description;絕對不要 顯示給終端使用者
  • [ ] invalid_request / invalid_scope / unsupported_grant_type / unsupported_response_type / invalid_target 是客戶端 bug — 修,不要重試
  • [ ] 關注 invalid_client 飆高 — 可能有人在探測憑證,或發生了輪替 / 外洩

相關文件