错误处理

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 飙高 — 可能有人在探测凭证,或发生了轮替 / 泄漏

相关文档