第三方 OAuth 接入授权流程
授权码流程
从浏览器跳转到拿到授权码:参数、同意页内部动作、回调校验。
第三方应用取令牌只有一条路:授权码 + PKCE。本页只讲流程的前半段——怎么把用户送到同意页,以及怎么把授权码安全地接回来。
时序
用户浏览器 同意页 (id) 授权服务器 (server)
│ GET /authorize │ │
├─────────────────────>│ 登录态检查(未登录先登录) │
│ │ 签发一次性 id_assertion │
│ │ POST /oauth/preauthorize │
│ ├─────────────────────────────>│
│ │<── 应用信息 + consent_ticket ──┤
│ 展示应用名与权限清单 │ │
│ 点击「同意」 │ 现签新 id_assertion │
│ │ POST /oauth/authorize │
│ ├─────────────────────────────>│
│ │<── 重定向地址(JSON)──────────┤
│ 302 redirect_uri │ │
│<─────────────────────┤ code + state + iss │第 2 步到第 5 步全部在 MuoCeon 自己的页面与服务里完成,第三方无法干预、也不需要复刻。下面列出它们的真实行为,是为了让你理解「为什么某些参数会失效」以及「错误到底从哪一步来」。
授权请求参数
浏览器跳转到 authorization_endpoint(同意页),携带:
| 参数 | 必填 | 说明 |
|---|---|---|
client_id | ✅ | 登记得到的客户端标识 |
redirect_uri | 建议总是带 | 与登记值精确匹配(回环地址忽略端口)。未带时仅在客户端只有一个可用地址时回退 |
response_type | ✅ | 固定 code,其他值一律拒绝 |
scope | 可选 | 空格分隔;不传则按客户端登记白名单的默认集合 |
state | 强烈建议 | 你会原样收到,用于防 CSRF |
code_challenge | ✅ | PKCE 挑战值(S256) |
code_challenge_method | ✅ | 固定 S256,plain 被拒 |
acr_values | 可选 | 要求的认证强度,多个值按优先级排列,见 逐步认证与提权 |
max_age | 可选 | 允许的最大认证年龄(秒);超出则要求用户重新认证 |
authorization_details | 可选(第一方) | RFC 9396 的 JSON 字符串,见 细粒度授权 |
示例:
https://id.example.com/authorize
?client_id=app_9f3c
&redirect_uri=https%3A%2F%2Fyour-app.example.com%2Fcallback
&response_type=code
&scope=openid%20profile%20email%20offline_access
&state=k3Jd9x
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256同意页内部发生了什么
了解这四步能解释大多数「莫名其妙」的报错。
- 登录态检查:未登录时先跳登录页,登录后带着全部原始参数回到同意页,参数不会丢。
- 前置校验:同意页向
/v1/oauth/preauthorize提交client_id、redirect_uri、scope等,服务端校验客户端与回调地址后返回展示素材(应用名、权限清单)和一张不透明票据consent_ticket。票据有效期 30 分钟,内容对客户端不可见。 - 提交决策:用户点「同意」或「拒绝」时,同意页会现签一枚新的身份断言,连同票据一起提交给
/v1/oauth/authorize。服务端同时消费票据与断言(断言只能使用一次,重放会被拒绝)。 - 返回重定向地址:
/authorize返回的是 JSON(含目标地址),由前端执行跳转,而不是 302。这是给自家前端用的内部接口,第三方不要直接调用。
认证强度不足时票据不会被消费
如果用户当前认证强度低于请求的 acr_values(或超过 max_age),/authorize 返回
401 INSUFFICIENT_USER_AUTHENTICATION,并且不消费票据。同意页据此把用户送去提权页,
完成后带着原参数回来重试。对第三方而言,这一过程完全透明——你只会看到回调晚一些到达。
回调参数
成功:
https://your-app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=k3Jd9x&iss=https%3A%2F%2Fid.example.com%2Fv1%2Foauth拒绝:
https://your-app.example.com/callback?error=access_denied&state=k3Jd9x&iss=https%3A%2F%2Fid.example.com%2Fv1%2Foauth必须做的三件校验:
state与发起时存的值一致。iss与可信 issuer 一致(RFC 9207)。服务器声明了authorization_response_iss_parameter_supported: true,成功与拒绝响应都会带iss;缺失即视为异常。code立即用于换令牌——授权码一次性、寿命以分钟计,不要先存库再异步处理。
常见拒绝原因
| 现象 | 原因 |
|---|---|
| 同意页直接报错,未回调 | client_id 不存在、redirect_uri 与登记值不匹配、response_type 非 code、PKCE 缺失或非 S256 |
回调带 error=access_denied | 用户点了拒绝 |
换令牌时 invalid_grant | 授权码已被使用、已过期,或 redirect_uri/code_verifier 不一致 |
| 反复被送去提权页 | acr_values 要求高于用户可达强度,或 max_age 设得过小 |
不要做的事
- 不要实现隐式流程(
response_type=token):服务端不支持。 - 不要在浏览器里保存
client_secret;浏览器端应登记为web_client(公开客户端,无密钥)。 - 不要用
state承载业务数据——它是防 CSRF 的一次性随机值,回调后即作废。