跳到主要内容

授权码流程

本页从你应用里的那个按钮开始,一直走到拿到用户资料,完整过一遍。这里的每个请求都是 真实的,拿到凭证后可以直接运行。

全文使用的示例应用:

client_id8f14e45fceea167a5a36dedd4bea2543
client_secretkZ8mQ2vX7pL4nR9tY6wA3sD1fG5hJ0cV8bN2mK4xQ7E=
回调地址https://app.example.com/callback
已注册 scopeprofile email

第 1 步 —— 把用户带到 Nanako​

把浏览器重定向到授权端点:

https://api.nanako.org/oauth/authorize
?client_id=8f14e45fceea167a5a36dedd4bea2543
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&response_type=code
&scope=profile%20email
&state=f8c3de3d1b0e4a1
参数必填说明
client_id是
redirect_uri是必须与某个已注册地址逐字节一致
response_type否默认 code;也只接受 code
scope否默认 openid profile —— 请显式传
state否服务端不强制,但你必须传 —— 见下文
code_challenge否PKCE;见第 1a 步
code_challenge_method否默认 plain

Nanako 按这个顺序校验 —— client_id 与 redirect_uri 是否存在、response_type、 应用是否存在且启用、回调地址是否已注册,最后逐个检查 scope 是否被允许。只会返回第一个 失败,并且以 JSON 形式返回,不会重定向到你的回调地址。见错误。

请求合法时,用户的浏览器会落在 Nanako 的登录与授权页面。

state 要保持 URL 安全

state 会被原样拼回回调地址,不做转义。取值里含 &、#、= 或空格会破坏回调: & 会注入一个多余参数,# 会把后面的一切截断进 fragment,于是你完全拿不到 state。

用十六进制或无填充 base64url 的随机串。需要结构化状态就存在服务端,只传一个不透明的 查询键。

从工程角度讲 state 不是可选项。它是你的 CSRF 防线:每次尝试都新生成一个,绑定到 用户会话,回调时 state 对不上就直接拒绝。

第 1a 步 —— PKCE(可选)​

PKCE 保护传输中的授权码。生成一个随机的 code_verifier,推导出 challenge 后一起发出:

code_verifier=$(openssl rand -base64 60 | tr -d '\n=' | tr '/+' '_-')
code_challenge=$(printf '%s' "$code_verifier" \
| openssl dgst -binary -sha256 \
| openssl base64 | tr -d '\n=' | tr '/+' '_-')

在授权 URL 上追加 &code_challenge=$code_challenge&code_challenge_method=S256, 并保留 code_verifier 供第 3 步使用。

S256 区分大小写

只有完全一致的 S256 才会启用 SHA-256。其他任何值 —— 包括 s256 —— 都按 plain 处理,也就是服务端拿你的 verifier 和 challenge 做字面比较。这个错配要到换取 token 那一步才会暴露,报的是 code_verifier 验证失败 —— 指向 verifier,而不是指向真正 的病根:那个拼错的方法名。

第 2 步 —— 接收授权码​

用户同意后,Nanako 重定向到你的回调地址:

https://app.example.com/callback?code=Xk9dQm2vR7pL4nT6wY3sA1fG5hJ0cV8bN2mK4xQ=&state=f8c3de3d1b0e4a1

先核对 state 与你保存的一致,再读取 code。授权码 44 个字符,以 = 结尾 —— 标准的查询字符串解析器能正确处理。

用户拒绝时,你收到的是 ?error=access_denied,同样带回你的 state。

授权码有效期 10 分钟,只能使用一次。立即换取;不要排队处理,也不要用同一个 码重试一次失败的换取。

第 3 步 —— 用授权码换取令牌​

curl -X POST https://api.nanako.org/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=Xk9dQm2vR7pL4nT6wY3sA1fG5hJ0cV8bN2mK4xQ=' \
--data-urlencode 'client_id=8f14e45fceea167a5a36dedd4bea2543' \
--data-urlencode 'client_secret=kZ8mQ2vX7pL4nR9tY6wA3sD1fG5hJ0cV8bN2mK4xQ7E=' \
--data-urlencode 'redirect_uri=https://app.example.com/callback'

PKCE 客户端把 client_secret 换成 code_verifier。

表单编码,不是 JSON

token 端点读的是表单字段。JSON 请求体不会被解析 —— 所有字段都是空的,于是你收到 {"error":"unsupported_grant_type"},它描述的是症状而不是病因。

HTTP Basic 认证(client_secret_basic)同样不支持。凭证放在请求体里。

redirect_uri 必须在这里再传一次,与第 1 步逐字节一致。它会和授权码上存的那个值 做比对。

响应:

{
"access_token": "aB3dE5fG7hJ9kL1mN3pQ5rS7tU9vW1xY3zA5bC7dE9=",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "zY9xW7vU5tS3rQ1pN9mL7kJ5hG3fE1dC9bA7zY5xW3=",
"scope": "profile email"
}

scope 是实际授予的 scope。请检查它 —— 这是用户真正批准的范围,可能比你申请的窄。

第 4 步 —— 调用接口​

curl https://api.nanako.org/oauth/userinfo \
-H 'Authorization: Bearer aB3dE5fG7hJ9kL1mN3pQ5rS7tU9vW1xY3zA5bC7dE9='
{
"sub": "0198c4a1-7f3e-7b21-9d04-1a2b3c4d5e6f",
"name": "hana",
"picture": "https://cdn.nanako.org/avatars/…",
"email": "hana@example.com",
"email_verified": true
}

sub 是这个用户稳定且永久的标识。用它作为你自己记录的主键。不要用 email —— 用户可以更换邮箱。

/oauth/userinfo 是目前唯一能用 OAuth access token 调用的接口。见 Scope。

第 5 步 —— 到期前刷新​

Access token 有效期两小时。刷新方式:

curl -X POST https://api.nanako.org/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=zY9xW7vU5tS3rQ1pN9mL7kJ5hG3fE1dC9bA7zY5xW3=' \
--data-urlencode 'client_id=8f14e45fceea167a5a36dedd4bea2543' \
--data-urlencode 'client_secret=kZ8mQ2vX7pL4nR9tY6wA3sD1fG5hJ0cV8bN2mK4xQ7E='

这里 client_secret 是必需的 —— 这个 grant 没有 PKCE 分支。

响应结构与第 3 步相同,包括一个新的 refresh_token。refresh token 每次使用都会轮换。

先把新的 refresh token 落盘,再去用新的 access token

新的一对令牌一旦签发,旧的 refresh token 立即作废。如果响应已经返回、但你的进程在 写入替换值之前崩溃,下一次刷新提交的就是一个已经轮换过的 token —— Nanako 会把它当作 令牌被盗后的重放,响应方式是撤销这个用户在这个应用下的所有 access token 和 refresh token。

用户会被踢出你的应用,必须重新授权。请先把新的 refresh token 写入持久化存储,再继续。

并发也是同一种失败模式:两个 worker 同时刷新同一个 token 就会触发。请按用户串行化刷新。

轮换不会延长链条。新的 refresh token 继承原来的过期时间,所以无论你刷新多少次,整条链 都在用户首次授权后满 30 天时结束。遇到 invalid_grant / 刷新令牌已过期,把用户送回 第 1 步。

第 6 步 —— 退出登录​

要提前结束会话,撤销令牌:

curl -X POST https://api.nanako.org/oauth/revoke \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=8f14e45fceea167a5a36dedd4bea2543' \
--data-urlencode 'client_secret=kZ8mQ2vX7pL4nR9tY6wA3sD1fG5hJ0cV8bN2mK4xQ7E=' \
--data-urlencode 'token=zY9xW7vU5tS3rQ1pN9mL7kJ5hG3fE1dC9bA7zY5xW3=' \
--data-urlencode 'token_type_hint=refresh_token'
这里的 token_type_hint 不是可选的

RFC 7009 把这个 hint 当作参考信息。Nanako 不是:不带 token_type_hint=refresh_token 时,只会在 access token 里查找。提交一个 refresh token 而不带 hint,会返回 200 {} 但什么都没撤销 —— 一次看起来像成功的静默空操作。

请分两次调用撤销两种令牌,每次都显式带上对应的 hint。

用户也可以在 Nanako 账号设置里撤销你的应用。发生时,你的下一次刷新会以 invalid_grant 失败。把它当作"授权已被收回":清掉存储的令牌,重新引导登录,不要重试。