跳到主要内容

错误

错误遵循 RFC 6749 的结构 —— 一个 error 代码,通常还有一个 error_description:

{"error": "invalid_grant", "error_description": "授权码已过期"}

写任何错误处理代码之前,先知道两件事:

  • error_description 是中文,而且不是稳定接口。 请基于 error 和 HTTP 状态码 分支。描述只用于你自己的日志,永远不要展示给最终用户。
  • 描述与成因不是一一对应的。 多种不同的失败会共用同一个 error 代码。凡是会 造成影响的地方,下文都单独指出。

授权请求​

GET /oauth/authorize 会在浏览器里以 JSON 返回这些错误。它们不会被重定向到你的 回调地址,所以撞上的用户会看到一个原始的 JSON 页面 —— 也就是说,这些是你需要在上线 前修掉的对接 bug,而不是运行时需要处理的状态。

状态码errorerror_description成因
400invalid_request缺少 client_id 或 redirect_uri 参数client_id 或 redirect_uri 缺失或为空
400unsupported_response_type仅支持 response_type=coderesponse_type 存在且不是 code
400invalid_client应用不存在或已被禁用client_id 未知,或应用已被停用
400invalid_redirect_uri回调地址未注册redirect_uri 与任何已注册地址都不是逐字节一致
400invalid_scopescope 'X' 不被允许X 不在你的允许集合内。X 为空说明你的 scope 字符串里有连续空格、开头空格或结尾空格。

校验按上表顺序执行,所以你一次只会看到第一个失败。修好再试。

会送到回调地址的错误​

用户到达授权页之后,失败会以查询参数的形式回到你的回调地址,并带上 state。

error含义该怎么做
access_denied用户拒绝了把他带回你的应用;不要自动重试
login_required用了 prompt=none 但用户未登录去掉 prompt=none 重试
consent_required用了 prompt=none 但没有匹配的授权记录去掉 prompt=none 重试
interaction_required用了 prompt=none 但授权检查失败去掉 prompt=none 重试

Token 端点​

POST /oauth/token。

状态码errorerror_description成因
400unsupported_grant_type—grant_type 缺失或不是两种受支持的值之一。发 JSON 请求体时收到的也是这个 —— 字段根本没被读取。
401invalid_client—client_id 未知,或 client_secret 不对。两者不作区分。
400invalid_grant授权码无效或已使用授权码未知、属于别的客户端,或已经被换取过
400invalid_grant授权码已过期距签发超过 10 分钟
400invalid_grantredirect_uri 不匹配redirect_uri 与发给 /oauth/authorize 的不一致
400invalid_grant需要 code_verifier授权码是走 PKCE 签发的,但没传 code_verifier
400invalid_grantcode_verifier 验证失败verifier 与 challenge 不匹配 —— 常见原因是 code_challenge_method 拼错
400invalid_grant刷新令牌无效refresh token 未知,或它是签发给另一个客户端的
400invalid_grant刷新令牌已被使用,所有令牌已撤销提交了一个已经轮换过的 refresh token —— 见下文
400invalid_grant刷新令牌已过期超出了自首次授权起算的 30 天窗口

密钥明明是对的却收到 invalid_client​

几乎所有情况都出自这两个原因:

  1. = 没有做百分号编码。 client secret 和令牌都以 = 结尾。如果你是自己拼字符串 而不是把参数字典交给 HTTP 库,取值被截断了。
  2. 你用了 HTTP Basic。 凭证要放在表单请求体里。client_secret_basic 不受支持, 而且不会给出可区分的错误。

刷新令牌已被使用,所有令牌已撤销​

这是重放检测。提交一个已经轮换过的 refresh token 会被当作重放,响应会撤销该用户在该 应用下的所有 access token 和 refresh token。

有三条路会走到这里:

  • 你从响应里拿到了新的 refresh token,但进程在写入完成前就挂了,于是重放了旧的那个。
  • 两个 worker 并发刷新。请按用户串行化刷新。
  • 用户在 Nanako 账号设置里撤销了你的应用,令牌被标记为已撤销;你下一次诚实的刷新 于是看起来像一次重放。

三种情况的恢复方式相同:丢弃已存的令牌,让用户重新走一遍授权流程。不要重试。

UserInfo​

GET /oauth/userinfo。

状态码errorerror_description成因
401invalid_token—没有 Authorization 头,或它不是以 Bearer 开头
401invalid_token—令牌未知或已被撤销
401invalid_tokentoken 已过期超过 2 小时有效期
500server_error—令牌背后的账号已经不存在

对两种没有描述的 401,刷新后重试一次。如果刷新也失败,重新走授权。

撤销​

POST /oauth/revoke。

状态码error成因
400invalid_requestclient_id 或 token 缺失或为空 —— 包括请求体是 JSON 的情况
401invalid_clientclient_id 未知或 client_secret 不对
200—其余全部情况,包括令牌不存在、不属于你、或没带必需的 token_type_hint

200 不代表撤销发生了。见 POST /oauth/revoke。

不是来自 OAuth 的错误​

有两种响应来自周边基础设施,而不是 OAuth 处理逻辑:

状态码响应体含义
503{"error":"feature_disabled","feature":"developer"}运营方关闭了开发者子系统,响应带 Retry-After: 300。受影响的是应用管理和账号界面里的已授权应用列表;登录流程本身不受影响。
413—请求体超过 110 MB。在 OAuth 端点上你不应该见到它。

一份可用的重试策略​

响应是否重试
userinfo 返回 401 invalid_token是 —— 先刷新一次,再重试
刷新时返回 invalid_grant否 —— 重新授权
换取授权码时返回 invalid_grant否 —— 授权码已作废,从 /oauth/authorize 重新开始
invalid_client否 —— 这是配置错误
503 feature_disabled是 —— 等过 Retry-After
5xx是 —— 退避重试,且只对幂等的读操作

永远不要用同一个授权码重试换取。授权码只能用一次,无论第一次是否成功,第二次都会失败。