OAUTH

OAuth 文档

本文档介绍如何通过 OAuth 2.0 协议授权第三方 App 获取本网站上的信息。

INTRO

什么是 OAuth 2.0

OAuth 2.0 可以理解为一种“授权第三方应用访问部分信息”的方式。它的重点不是把你的账号密码交给第三方 App,而是让你先在本站完成登录和确认,再决定是否允许对方读取你同意开放的信息。

在本网站的实际应用过程中,`第三方 App` 指的是接入本网站账号体系的外部应用、网站或程序;`授权页面` 则是本站展示给你的确认页面,用来让你决定是否同意授权,以及允许它访问哪些内容。

`scope` 可以理解为“权限范围”。它的作用是告诉本站,这个第三方 App 想读取哪些信息;只有你同意、并且本站允许开放的范围,才会真的授权出去,这样可以避免一旦登录就把所有信息都交给对方。

`access_token` 可以理解为授权成功后发给第三方 App 的访问凭证。它的作用是让 App 在后续请求接口时证明“这位用户已经同意授权了”,因此它不等于你的账号密码,而是一份带有限制条件的授权结果。

当你确认授权后,本站会先返回一个一次性的 `authorization code` 给第三方 App。这个临时码不能直接长期使用,它还需要配合 `PKCE` 这个安全机制一起完成校验,避免授权码在中途被别人拿去冒用。这里的 `redirect_uri` 则是授权完成后浏览器要跳回去的回调地址。

在后续结果里,你还会看到 `id_token` 和 `nonce` 这类名词。`id_token` 主要用于告诉第三方 App 当前授权对应的是哪个登录身份;`nonce` 可以理解为这次登录请求附带的另一串随机标记。第三方 App 发起请求时先保存它,本站再把它写回 `id_token`,这样第三方 App 就能检查拿到的身份结果是不是确实对应当前这一次登录授权流程。

补充阅读: 《理解 OAuth 2.0》

overview

这份授权是怎么工作的

本站当前使用授权码模式完成登录授权,并用 PKCE 保护授权过程。

当前能力

  • 授权模式固定为 Authorization Code,response_type 只能使用 code。
  • PKCE 为必需项,code_challenge_method 当前仅支持 S256。
  • 令牌交换成功后返回 access_token、id_token、expires_in 和 scope。
  • 当前没有 refresh_token;access_token 过期后需要重新发起授权流程。

核心端点

/oauth/authorizeGET 必填

浏览器跳转入口。用户会在这里完成登录、授权确认,并最终被重定向到注册过的 redirect_uri。

/oauth/tokenPOST 必填

授权码交换端点。客户端提交 authorization code 与 code_verifier 后在这里换取 access_token 和 id_token。

/oauth/userinfoGET 必填

用户信息端点。应用拿到 access_token 后可在这里读取 OIDC sub,若 scope 允许还会返回 preferred_username。

client

先创建 OAuth 客户端

先登录站内账号,然后点击右上角用户名进入设置页,选择开发选项卡,切换面板到 OAuth 页,填写信息创建 OAuth 客户端。

创建时要准备的信息

namestring 必填

客户端显示名称,会出现在授权确认页。

redirectUrisstring[] 必填

允许回调的地址列表。授权请求里的 redirect_uri 必须与这里的某一项完全一致。

requestedScopesstring[] 必填

客户端希望申请的 scope 列表。只有审核通过的 scope 才能在授权请求中使用。

descriptionstring | null

可选描述,便于用户理解该应用用途。

homepageUrlstring | null

可选主页地址,用于开发者识别和后续管理。

客户端创建成功后,普通用户需要等待管理员审核 scope;客户端创建者可在自己客户端的 scope 仍为待审核时继续完成 OAuth 登录与授权测试。

authorize

发起授权请求

第三方应用应在浏览器里发起授权。

授权请求参数

response_typestring 必填

固定填写 code。

client_idstring 必填

创建客户端后分配的 client_id。

redirect_uristring 必填

授权完成后的回调地址,必须与注册列表里的某一项完全匹配。

scopespace-delimited string 必填

空格分隔的 scope 列表。

statestring 必填

建议填写一个随机字符串。发起授权时先把它保存起来,等回调回来后再对比返回值是否一致,用来确认这次回调确实是你自己刚才发起的那次请求。

code_challengestring 必填

由 code_verifier 经过 SHA-256 和 Base64URL 计算得到的 PKCE challenge。

code_challenge_methodstring 必填

固定填写 S256。当前实现不接受 plain。

noncestring

可选。建议填写一个随机字符串并在本地保存;如果这次登录返回了 id_token,再检查里面带回来的 nonce 是否和你最初保存的一致,用来确认这份身份结果确实对应当前这次请求。

授权 URL 示例

/oauth/authorize
    ?response_type=code
    &client_id=client_123
    &redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
    &scope=api.auth.me.read
    &state=9f4f1c8c4d5a4f43
    &code_challenge=7w7Tt39Qn1m6M5Lx9c1xQ1kBrmR8J4t9wL4mUt5M2pU
    &code_challenge_method=S256
    &nonce=b2d0f7a1c81e41d7

token

处理回调并交换令牌

应用在回调地址拿到 code 后,应先检查返回的 state 是否和发起授权前保存的一致,再用同一个 redirect_uri 和原始 PKCE verifier 换取令牌。

回调处理要点

  • 先检查回调里是否带有 error;常见拒绝值为 access_denied。
  • 把回调里的 state 和你发起授权前保存的随机值逐字对比;只有完全一致,才能继续处理这次登录结果。
  • 确保 token 交换时使用的 redirect_uri 与授权阶段完全一致。
  • code 是一次性的,过期或已消费后再次提交会收到 invalid_grant。

成功回调 URL 示例

https://app.example.com/oauth/callback
    ?code=SplxlOBeZQQYbYS6WxSbIA
    &state=9f4f1c8c4d5a4f43

失败回调 URL 示例

https://app.example.com/oauth/callback
    ?error=access_denied
    &state=9f4f1c8c4d5a4f43

POST /oauth/token 请求体

当前服务端会从请求体读取这些字段。建议使用 application/x-www-form-urlencoded 提交,这是 OAuth 2.0 中 token 交换最常见的提交方式。

grant_typestring 必填

固定填写 authorization_code。

codestring 必填

回调参数中的 authorization code。

client_idstring 必填

与你的 OAuth 客户端对应的 client_id。

redirect_uristring 必填

必须与授权阶段提交的 redirect_uri 完全一致。

code_verifierstring 必填

这是第三方 App 在发起授权前随机生成并保存在本地的一串字符串。服务端会用它与之前提交的 code_challenge 做校验。

cURL 交换令牌示例

curl -X POST /oauth/token \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode 'grant_type=authorization_code' \
    --data-urlencode 'client_id=client_123' \
    --data-urlencode 'code=<回调中的 code>' \
    --data-urlencode 'redirect_uri=https://app.example.com/oauth/callback' \
    --data-urlencode 'code_verifier=<浏览器里保存的 code_verifier>'

成功响应示例

{
    "access_token": "ocrh_u_xxxxxxxxxxxxxxxxxxxx",
    "token_type": "Bearer",
    "expires_in": 7200,
    "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9jcmgta2V5LTEifQ...",
    "scope": "api.auth.me.read"
}

userinfo

使用 access_token

拿到 access_token 后,第三方应用可以按 Bearer Token 访问当前用户相关接口。

更多 API 请见 API 文档

请求 userinfo

curl /oauth/userinfo \
    -H 'Authorization: Bearer <access_token>'

userinfo 至少会返回 sub。只有当授权 scope 中包含 api.auth.me.read 时,服务端才会额外返回 preferred_username。id_token 里的身份信息也遵循相同约束,因此不要假设用户名字段一定存在。

userinfo 响应示例

{
    "sub": "dGhpcy1pcy1hLXN0YWJsZS1vaWRjLXN1YmplY3Q"
}

{
    "sub": "dGhpcy1pcy1hLXN0YWJsZS1vaWRjLXN1YmplY3Q",
    "preferred_username": "demo-user"
}

id_token 说明

  • id_token 由服务端使用 RS256 签名,包含 iss、sub、aud、azp、exp、iat、auth_time 和 at_hash。
  • 如果授权请求里提供了 nonce,服务端会把它写回 id_token;第三方 App 收到后应把它和自己发起请求前保存的值做对比,确认这份身份结果确实属于当前这一次登录。
  • preferred_username 只有在 scope 允许读取当前身份信息时才会出现。

troubleshooting

限制与排障

多数接入失败都来自 scope 审核、redirect_uri 不匹配或 PKCE 参数错误。

当前限制

  • 仅支持 Authorization Code,不支持 implicit、device code 或 client credentials。
  • 仅支持 PKCE S256,不支持 plain。
  • 当前没有 refresh_token,过期后需要重新授权。
  • 请求里的所有 scope 必须全部审核通过,否则授权阶段直接失败。

常见失败原因

  • invalid_request:缺失必填字段、response_type 不是 code,或 code_challenge_method 不是 S256。
  • access_denied:用户拒绝授权,或者当前登录用户本身没有被请求的 scope。
  • invalid_grant:authorization code 已过期、已被消费、redirect_uri 不一致,或 code_verifier 校验失败。
  • 回调后直接失败:通常是 state 对比不一致,说明这次回调不是当前这次授权请求的结果,或者浏览器本地保存的值已经丢失。

安全建议

  • 始终生成高熵 state 和 code_verifier,并把它们只保存在短期会话存储中。
  • 只通过 HTTPS 传输 access_token,避免把令牌写进 URL、日志或前端埋点。
  • 公共客户端不要尝试长期缓存 access_token;过期后直接重新走授权码流程。
  • 如果你的应用只需要识别登录用户身份,优先申请最小 scope,避免过度请求接口权限。