錯誤處理
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飆高 — 可能有人在探測憑證,或發生了輪替 / 外洩