OAuth 2.0 API
OAuth 2.0 Authorization Code + PKCE 授权服务器端点。第三方应用通过此 API 获取用户授权的 access_token。
基础路径
/oauth认证方式
/oauth/authorize(GET):无需认证(参数校验后重定向)/oauth/authorize(POST):需要 JWT(用户确认授权)/oauth/token:无需认证(公开端点)/oauth/revoke:无需认证(公开端点)/oauth/userinfo:Bearer Token(OAuth access_token)/oauth/apps/:clientId/public:无需认证(公开信息)
CORS 说明
/oauth/token、/oauth/revoke、/oauth/userinfo 允许所有 origin 跨域访问,其他端点使用默认 CORS。
端点列表
GET /oauth/authorize
发起 OAuth 授权流程。校验参数后 302 重定向到前端 consent 页。
查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
response_type | string | 是 | 固定为 code |
client_id | string | 是 | OAuth App 的 client_id |
redirect_uri | string | 是 | 回调地址(必须在 App 注册的白名单中) |
scope | string | 否 | 请求的权限(空格分隔),默认使用 App 配置 |
state | string | 是 | 客户端随机值(CSRF 防护;长度 8-256,仅 unreserved 字符 A-Z / a-z / 0-9 / - / . / _ / ~) |
code_challenge | string | 是 | PKCE code_challenge(BASE64URL 格式,43-128 字符) |
code_challenge_method | string | 是 | 固定为 S256 |
prompt | string | 否 | OIDC prompt 参数(空格分隔)。支持:login(强制重新登录)、consent、select_account(同 login)、none(无交互) |
login_hint | string | 否 | 预填邮箱,传到前端登录表单 |
display | string | 否 | 显示模式,仅支持 popup |
prompt 参数行为
| 值 | 行为 |
|---|---|
login / select_account | 即使已登录也强制重新登录 |
consent | 要求用户重新确认授权(当前每次都需确认,等效于默认行为) |
none | 不显示任何 UI。redirect 模式:302 到 redirect_uri 附带 error=interaction_required;popup 模式:由前端 postMessage 回传错误 |
注意:
none不可与其他值组合使用,否则返回invalid_request;未知值也返回invalid_request。
成功响应 302
重定向到前端 consent 页面:
{FRONTEND_BASE_URL}/oauth/consent?client_id=...&scope=...&state=...&prompt=...&login_hint=...错误响应 400
{
"error": "invalid_request",
"error_description": "Missing required parameter: client_id"
}GET /oauth/apps/:clientId/public
获取 OAuth 应用的公开信息(供 consent 页面展示)。
路径参数
| 参数 | 类型 | 说明 |
|---|---|---|
clientId | string | OAuth App 的 client_id |
成功响应 200
{
"name": "Page Agent",
"description": "AI 网页操控助手",
"logo_url": "https://example.com/logo.png",
"homepage_url": "https://example.com",
"redirect_uris": ["http://localhost:4100/callback"],
"scopes": ["chat:completions"],
"scope_descriptions": [
{ "scope": "profile:read", "description": "读取用户基本信息" },
{ "scope": "chat:completions", "description": "调用 AI 对话接口" }
]
}POST /oauth/authorize
用户确认授权,生成 authorization code。需要 JWT 认证。
请求体(JSON)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
client_id | string | 是 | OAuth App 的 client_id |
redirect_uri | string | 是 | 回调地址 |
scope | string | 否 | 请求的权限(省略时按 OAuth App 配置默认下发) |
state | string | 是 | 客户端随机值(CSRF 防护;长度 8-256,仅 unreserved 字符 A-Z / a-z / 0-9 / - / . / _ / ~) |
code_challenge | string | 是 | PKCE code_challenge(BASE64URL 格式,43-128 字符) |
code_challenge_method | string | 是 | 固定为 S256 |
成功响应 200
{
"redirectTo": "http://localhost:4100/callback?code=wno-code-...&state=..."
}前端收到后执行 window.location.assign(redirectTo) 完成跳转。
POST /oauth/token
换取 access_token、刷新 token,或在已开通能力的 OAuth App 中用验证码直连换取 OAuth token。
请求格式:application/x-www-form-urlencoded
grant_type=authorization_code
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
grant_type | string | 是 | authorization_code |
code | string | 是 | 授权码 |
client_id | string | 是 | client_id |
redirect_uri | string | 是 | 与授权请求时一致 |
code_verifier | string | 是 | PKCE code_verifier(长度 43-128,仅 unreserved 字符 A-Z / a-z / 0-9 / - / . / _ / ~) |
grant_type=refresh_token
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
grant_type | string | 是 | refresh_token |
refresh_token | string | 是 | refresh_token |
client_id | string | 是 | client_id |
grant_type=urn:wainao:params:oauth:grant-type:otp
验证码直连 extension grant。仅当 OAuth App 显式开通 direct_otp_grant capability 时可用;未开通会返回 unauthorized_client。成功响应与标准授权码流程一致,返回 wno- access token 与 wno-rt- refresh token。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
grant_type | string | 是 | urn:wainao:params:oauth:grant-type:otp |
client_id | string | 是 | OAuth App 的 client_id |
otp_type | string | 是 | email 或 phone |
identifier | string | 是 | 邮箱地址或手机号 |
code | string | 是 | 6 位验证码 |
scope | string | 否 | 请求的权限,省略时按 App 配置默认下发 |
lang | string | 否 | 初始化默认 Team/Project 时使用的语言 |
警告
标准 OAuth 仍推荐走官网授权页。验证码直连不会暴露用户密码,但会把登录入口嵌入第三方 App,因此必须由平台为对应 App 单独开通 direct_otp_grant。
成功响应 200
{
"access_token": "wno-...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "wno-rt-...",
"scope": "chat:completions"
}响应头:Cache-Control: no-store、Pragma: no-cache
错误响应
{
"error": "invalid_grant",
"error_description": "Authorization code expired or already used"
}POST /oauth/revoke
撤销 token(RFC 7009)。始终返回 200。
以 refresh token 撤销时终止整个登录会话:同 token_family 的所有活跃 token(含 access token、宽限轮换刚签发的 token)一并撤销;以 access token 撤销时仅撤销对应 token 对。
请求格式:application/x-www-form-urlencoded
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
token | string | 是 | 要撤销的 token(access 或 refresh) |
token_type_hint | string | 否 | access_token 或 refresh_token |
client_id | string | 否 | client_id |
响应 200
{}GET /oauth/userinfo
获取当前 OAuth 用户的基本信息。
请求头
Authorization: Bearer wno-...成功响应 200
{
"sub": "3be0c712-480d-43bf-971b-77e82ebbc428",
"email": "user@example.com",
"name": "用户名",
"avatar_url": "https://example.com/avatar.jpg",
"scope": "profile:read",
"is_site_admin": false,
"is_real_team_admin": true
}is_real_team_admin 表示用户是否至少管理一个不少于 2 人的真实团队。个人默认 1 人 Team 不计入该判断;查询失败时后端会安全降级为 false。
错误响应 401
{
"error": "invalid_token",
"error_description": "Token expired or revoked"
}Admin OAuth Apps API
OAuth 应用管理端点。需要 JWT + site:admin 权限。
基础路径:/admin/oauth-apps
GET /admin/oauth-apps
列表(分页、搜索)。
查询参数
| 参数 | 类型 | 说明 |
|---|---|---|
page | number | 页码(默认 1) |
pageSize | number | 每页数量(默认 20) |
search | string | 搜索关键词(名称或 client_id) |
POST /admin/oauth-apps
创建应用。
请求体
{
"name": "My App",
"description": "描述",
"redirect_uris": ["https://example.com/callback"],
"scopes": ["chat:completions"],
"capabilities": [],
"homepage_url": "https://example.com",
"logo_url": "https://example.com/logo.png"
}PATCH /admin/oauth-apps/:id
更新应用。scopes、redirect_uris 或 capabilities 变更会撤销所有已发放的 token。
capabilities 当前支持:
| Capability | 说明 |
|---|---|
direct_otp_grant | 允许该 App 使用邮箱/手机号验证码直连 grant 获取 OAuth token |
DELETE /admin/oauth-apps/:id
软删除 + 撤销所有关联 token。
POST /admin/oauth-apps/:id/disable
禁用应用 + 撤销所有 token。
POST /admin/oauth-apps/:id/enable
启用应用。
Mobile / Native App 接入
ReAI Mobile 客户端走自定义 URL scheme + PKCE 接入:
| 项 | 值 |
|---|---|
| Redirect URI | reai-mobile://callback |
| Scope | mobile:full(vendor-locked,仅生产 / 沙箱 client_id 允许申请) |
| 浏览器组件 | iOS ASWebAuthenticationSession / Android Custom Tabs(不要使用 WebView) |
mobile:full scope 覆盖 profile、devices、个人默认项目、项目存储上传、ROMP、推送与 /services/passthrough/* 等移动端业务接口,详细接入步骤见仓库内 docs/features/oauth/client-guide.md(注:该文档在仓库根 docs/ 目录,未纳入 docs-site 文档站;本页直接外链 GitHub 文件)。
Refresh Token Reuse Detection
后端按 RFC 6819 §5.2.2.3 实现 family-based reuse detection:
- 每次成功 rotate 后,旧 refresh token 立即失效(
revoked_at写入)。 - 同一登录会话所有 token 共享
token_family;rotation 时新 token 沿用 family。 - 旧 refresh 二次复用 → 整个 family 全部撤销,下次访问 access token 也 401。
- 客户端拿到
invalid_grantRefresh token reuse detected必须重新登录,禁止重试。 - 2 分钟宽限轮换(grace rotation):rotate 的响应可能在弱网 / 切网 / App 挂起时丢失,此时客户端手里仍是旧 refresh token。旧 token 在 rotate 后 2 分钟内的第一次重放会完成一次正常轮换并返回全新的 token 对(客户端无感续期,不掉登录);每个旧 token 的宽限资格仅一次(
grace_used_at标记)。宽限仅在标准丢响应形态下生效(轮换出的新 token 仍活跃):登出、管理员撤销或 family revoke 之后,旧 token 重放不会复活会话。宽限轮换会同时撤销 family 内其他活跃 token(即客户端从未收到的原新 token),保证 family 内始终只有一条活跃链——被撤销的 token 事后被重放(token 被盗场景)会立即触发 family revoke 自愈。同一旧 token 第二次重放、或超过 2 分钟后重放,走完整 family revoke 路径。客户端始终应使用最新一次响应返回的 refresh token。
Token 审计字段
oauth_tokens 表新增 last_used_at / last_used_ip / last_used_user_agent 字段,由 oauthAuth 中间件 fire-and-forget 异步写入。仅供审计,不参与认证决策。