错误处理
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= 参数不在此客户端的 allowed-resources 白名单内(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= 格式不对、不在此客户端的 allowed-resources 白名单内(默认全部拒绝),或(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 返回完整元数据。对属于 其他 客户端的有效 token 做 introspect 只会得到{"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):
每个客户端都有一份由运营方管理的 allowed-resources 白名单。客户端送来的 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飙高 — 可能有人在探测凭证,或发生了轮替 / 泄漏