跳到正文
MuoCeon 文档中心
第三方 OAuth 接入接入准备

快速开始

十五分钟内跑通「跳转 → 同意 → 回调 → 换令牌 → 读用户信息」。

本页用一个最小可用的机密客户端(web_server)走完整个流程。所有示例都是标准 OAuth 2.0,你换成自己惯用的客户端库后参数含义不变。

0. 准备

需要从哪里来
client_id / client_secret见 申请客户端
redirect_uri已登记的回调地址,例如 https://your-app.example.com/callback
issuer例如 https://id.example.com/v1/oauth
authorization_endpoint从元数据读取,例如 https://id.example.com/authorize

本次申请的最小权限集:openid profile email。需要长期访问再加 offline_access。

1. 生成 PKCE 与 state

import { randomBytes, createHash } from 'node:crypto'

const verifier = randomBytes(32).toString('base64url')
const challenge = createHash('sha256').update(verifier, 'ascii').digest('base64url')
const state = randomBytes(16).toString('base64url')

verifier 与 state 必须和当前浏览器会话绑定(例如放进 HttpOnly Cookie 或服务端会话),回调时要取回来比对。

2. 让浏览器跳转到同意页

const url = new URL('https://id.example.com/authorize')
url.searchParams.set('client_id', CLIENT_ID)
url.searchParams.set('redirect_uri', REDIRECT_URI)
url.searchParams.set('response_type', 'code')
url.searchParams.set('scope', 'openid profile email')
url.searchParams.set('state', state)
url.searchParams.set('code_challenge', challenge)
url.searchParams.set('code_challenge_method', 'S256')

return Response.redirect(url, 302)

不要自己拼 /v1/oauth/authorize

服务器上的 /v1/oauth/authorize 是同意页提交决策用的内部 JSON 接口,只接受 POST。 浏览器跳转的目标必须是元数据里的 authorization_endpoint(同意页地址)。

3. 用户在同意页确认

同意页会展示应用名称与逐条权限。用户点击「同意」后,浏览器被 302 回你的 redirect_uri:

https://your-app.example.com/callback?code=...&state=...&iss=https%3A%2F%2Fid.example.com%2Fv1%2Foauth

回调处理的三件事,顺序不能换:

  1. 比对 state,不一致直接终止(防 CSRF)。
  2. 比对 iss 与你信任的 issuer,不一致直接终止(RFC 9207,防 mix-up)。
  3. 取出 code,立刻去换令牌(授权码是一次性的,且有分钟级寿命)。

用户拒绝授权时回调形如:

https://your-app.example.com/callback?error=access_denied&state=...&iss=...

4. 换令牌

curl -X POST 'https://id.example.com/v1/oauth/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d 'grant_type=authorization_code' \
  -d "code=$CODE" \
  -d "redirect_uri=$REDIRECT_URI" \
  -d "code_verifier=$VERIFIER"

成功响应:

{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJSUzI1NiIs...",
  "scope": "openid profile email offline_access",
  "id_token": "eyJhbGciOiJSUzI1NiIs..."
}

要点:

  • redirect_uri 必须与授权请求里的值完全一致,否则换令牌失败。
  • code_verifier 对应第 1 步的 challenge;plain 方式不被接受。
  • 响应带 Cache-Control: no-store,中间任何一层都不要缓存它。

5. 读用户信息

curl 'https://id.example.com/v1/auth/userinfo' \
  -H "Authorization: Bearer $ACCESS_TOKEN"
{
  "sub": "0192f3a4-...",
  "preferred_username": "muoceon_user",
  "name": "显示名",
  "email": "user@example.com",
  "email_verified": true
}

sub 是稳定用户标识,请用它做主键关联账号,不要用邮箱——邮箱可以变更,sub 不会。

检查点

现象常见原因
同意页报「客户端不存在」client_id 未登记或拼错
同意页报回调地址非法未登记该地址,或 redirect_uri 与登记值差一个末尾斜杠
换令牌返回 invalid_grant授权码已用过/已过期,或 code_verifier 与 challenge 不匹配
换令牌返回 invalid_client机密客户端没带密钥,或用了错误的认证方式
UserInfo 返回受众不匹配用了 oauth 受众的令牌,或 scope 里没有 openid/profile/email
拿到的 scope 比申请的少服务端按登记白名单裁剪,被裁掉的 scope 不报错,见 scope 与受众

跑通之后,建议接着读 授权码流程 补齐边界情况,再读 令牌 把校验做扎实。

本页目录