身份与授权 · 授权码流程

授权码流程

本服务唯一支持的用户登录流程是 OAuth 2.0 授权码(Authorization Code)+ PKCE,符合 OIDC Core 1.0 规范。本页给出协议细节、参数定义与响应字段。协议依据:RFC 6749(OAuth 2.0)、RFC 7636(PKCE)、OIDC Core 1.0。

流程选型#

流程是否支持说明
授权码 + PKCE支持用户登录的标准流程。token 只在服务器之间交换,浏览器不接触 token
client_credentials支持服务器对服务器凭据,无用户参与,仅用于应用成员组管理 API,不能登录用户
隐式流程(implicit)不支持不安全,不提供

时序#

用户                应用                          平台
 │                   │                             │
 │  点击"用平台账号登录" │                             │
 │──────────────────▶│                             │
 │                   │  302 → /oauth/authorize      │
 │                   │  (client_id, redirect_uri,   │
 │                   │   scope, state, PKCE)        │
 │                   │────────────────────────────▶│
 │                   │  302 → 登录页(未登录时)       │
 │  ◀──────────────────────────────────────────────│
 │  登录 + 同意页                        │
 │──────────────────────────────────────────────▶│
 │                   │  同意 → 302 回 redirect_uri   │
 │                   │  ?code=…&state=…            │
 │  ◀──────────────────────────────────────────────│
 │                   │  后端用 code 换 token (POST)  │
 │                   │────────────────────────────▶│
 │                   │  ← access + refresh + id_token
 │                   │  调 userinfo 拿身份与应用成员组      │
 │                   │────────────────────────────▶│
 │  ◀── 按应用成员组渲染的内容 ──────────────────           │

端点#

端点方法调用方作用
/oauth/authorizeGET / POST浏览器授权页与同意 / 拒绝
/oauth/tokenPOST应用后端换 token、刷新、client_credentials
/oauth/userinfoGET应用后端获取用户身份、群与应用成员组
/oauth/jwksGET应用后端RS256 公钥,用于验 id_token
/.well-known/openid-configurationGET应用后端发现端点,列出以上全部地址

协议端点前缀为 https://mooc.pneumas.org,不带 /api/v1。

发现端点#

GET /.well-known/openid-configuration 返回 OIDC 发现文档,标准客户端库可据此自动配置:

{
  "issuer": "https://mooc.pneumas.org",
  "authorization_endpoint": "https://mooc.pneumas.org/oauth/authorize",
  "token_endpoint": "https://mooc.pneumas.org/oauth/token",
  "userinfo_endpoint": "https://mooc.pneumas.org/oauth/userinfo",
  "jwks_uri": "https://mooc.pneumas.org/oauth/jwks",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported": ["email", "group", "member_groups", "member_groups:write", "members:read", "openid", "profile"],
  "claims_supported": ["sub", "name", "preferred_username", "email", "email_verified", "picture"],
  "code_challenge_methods_supported": ["S256"]
}

authorize#

GET /oauth/authorize,参数:

参数必填说明
client_id是应用创建时获得
redirect_uri是必须与注册值 完全一致,平台逐字符比对
response_type是固定 code
scope否空格分隔;留空默认 openid profile email。请求未获批的 scope 会被拒绝
state推荐应用生成的随机串,回调时原样带回,用于防 CSRF
nonce可选回显到 id_token,用于防 id_token 重放
code_challenge是PKCE 挑战值
code_challenge_method固定 S256仅支持 S256

PKCE 算法(RFC 7636):

code_verifier  = 43–128 位随机串,仅含 A-Za-z0-9-._~
code_challenge = base64url( SHA256( code_verifier ) )   # 去掉末尾的 =

校验发生在 token 阶段:应用换 token 时必须携带 code_verifier,平台重新计算 challenge 并比对。即使授权码在传输中被截获,没有 verifier 也无法换 token。

未登录用户会被 302 跳到平台登录页,登录完成后自动回到授权流程,参数不丢失。非群成员不被拦截,正常展示同意页并签发 code,userinfo 返回 is_member=false。

同意页#

同意页向用户展示应用名(含 Logo)、要访问的群名,以及按申请 scope 翻译成中文的信息清单。用户选择"同意"或"拒绝"。拒绝时平台回跳:

redirect_uri?error=access_denied&state=STATE

每次发起授权都会展示同意页,即使用户此前已授权过同一应用。用户撤销授权后再次登录时同样会看到同意页。

回调与 code#

用户同意后:

redirect_uri?code=<32字节随机串>&state=STATE

code 的约束:

  • 随机 32 字节,URL 安全编码后约 64 字符;
  • 10 分钟 内有效;
  • 只能使用一次,用后即作废;
  • 必须与同一 client_id、同一 redirect_uri 配对;
  • 平台只存其 SHA-256 哈希,数据库泄露无法还原明文。

token#

POST /oauth/token,请求体为表单编码。客户端认证两种方式任选其一:

# 方式一:HTTP Basic
Authorization: Basic base64(client_id:client_secret)

# 方式二:表单字段
client_id=…&client_secret=…

授权码换 token 的参数:

参数说明
grant_typeauthorization_code
code回调获得的 code
redirect_uri与授权时一致
code_verifier发起授权时给了 challenge 就必须携带

成功响应字段:

字段说明
access_token不透明串,1 小时有效。调用 userinfo 与管理 API 时携带
token_typeBearer
expires_in3600(秒)
scope实际授予的 scope,等于"申请 ∩ 获批"
refresh_token30 天有效,可轮换刷新;仅授权码流程签发,client_credentials 不签发
id_token请求含 openid 时签发;JWT,RS256 签名

id_token 只含标准身份 claims,不含群与应用成员组——群与应用成员组必须从 userinfo 获取。验签使用 GET /oauth/jwks 的公钥,按 kid 匹配。payload 示例:

{
  "iss": "https://mooc.pneumas.org",
  "sub": "10042",
  "aud": "app_9f8e7d6c5b4a3f2e1d0c",
  "iat": 1750000000,
  "exp": 1750003600,
  "nonce": "客户端在 authorize 传入的 nonce(未传则无此字段)",
  "name": "用户 10042",
  "preferred_username": "user_10042",
  "email": "user10042@example.com",
  "email_verified": true,
  "picture": "https://cdn.example.com/avatar/10042.webp"
}

校验时需确认:iss 等于 https://mooc.pneumas.org、aud 等于你的 client_id、exp 未过期;若 authorize 时传了 nonce,需与本地记录一致。

userinfo#

GET /oauth/userinfo,Authorization: Bearer <access_token>。返回字段按 token 的 scope 过滤:

scopeuserinfo 字段
profilename、preferred_username、picture
emailemail、email_verified
groupgroup 对象:is_member、role(creator / admin / member / null)、群 id / slug / name
member_groupsmember_groups 数组:用户在该群的全部应用成员组

完整示例见接入指南。群与应用成员组只从 userinfo 获取,id_token 不含。

refresh#

POST /oauth/token,grant_type=refresh_token,携带旧 refresh_token。成功返回新的 access token 与轮换后的新 refresh_token,旧 refresh_token 即时作废。

每次刷新平台都会重新校验以下条件,任一不满足即返回 invalid_grant:

  1. 应用仍处于激活状态(被禁用则失败);
  2. 绑定的群仍然存在;
  3. 用户仍是该群的成员(被移出则失败)。

因此"移出群即失去访问"的最迟生效时间为下一次刷新或 access token 过期(1 小时)。

错误响应#

token 与 userinfo 的错误统一为 RFC 6749 形状:

{ "error": "invalid_grant", "error_description": "授权码无效、已使用或已过期" }

完整错误码表见API 参考 · 错误码。401 invalid_token 响应携带 WWW-Authenticate: Bearer 头,标准客户端库会自动处理。