Skip to content

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/assertion:Bearer Token(插件类 App 的 OAuth access_token,须含 profile:read
  • /.well-known/jwks.json:无需认证(公开 JWKS)
  • /oauth/apps/:clientId/public:无需认证(公开信息)
  • /oauth/apps/:clientId/development:需要 JWT(仅应用作者本人,开发自测元数据)

CORS 说明

/oauth/token/oauth/revoke/oauth/userinfo/oauth/assertion/.well-known/jwks.json 允许所有 origin 跨域访问,其他端点使用默认 CORS。


端点列表

GET /oauth/authorize

发起 OAuth 授权流程。校验参数后 302 重定向到前端 consent 页。

查询参数

参数类型必填说明
response_typestring固定为 code
client_idstringOAuth App 的 client_id
redirect_uristring回调地址(必须在 App 注册的白名单中)
scopestring请求的权限(空格分隔),默认使用 App 配置
statestring客户端随机值(CSRF 防护;长度 8-256,仅 unreserved 字符 A-Z / a-z / 0-9 / - / . / _ / ~
code_challengestringPKCE code_challenge(BASE64URL 格式,43-128 字符)
code_challenge_methodstring固定为 S256
promptstringOIDC prompt 参数(空格分隔)。支持:login(强制重新登录)、consentselect_account(同 login)、none(无交互)
login_hintstring预填邮箱,传到前端登录表单
displaystring显示模式,仅支持 popup
developmentstring自助应用开发自测标记,仅接受字面量 1(其他值返回 invalid_request)。详见下方「开发自测模式」

开发自测模式(development=1,仅自助应用作者)

自助应用作者在上架前自测时,在标准授权 URL 上追加 development=1

  • management_mode = self_service 的应用可用;官方应用返回 invalid_request
  • scope 必须显式非空(开发态没有「回落应用注册 scopes」的语义);
  • 不可与 prompt=none 组合(开发自测必须交互,组合返回 invalid_request);
  • 本步跳过按已注册白名单的 redirect_uri / scope 校验(授权基准是作者的草稿版本, 强校验在 consent 页作者态元数据端点与 POST /oauth/authorize 完成),仅要求 redirect_uri 是合法绝对 URL;
  • development=1 会透传到 consent 页;consent 页要求登录且登录者必须是应用作者本人, 确认授权时自动签发一次性开发票据并随 POST /oauth/authorize 提交。

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

json
{
  "error": "invalid_request",
  "error_description": "Missing required parameter: client_id"
}

GET /oauth/apps/:clientId/public

获取 OAuth 应用的公开信息(供 consent 页面展示)。

路径参数

参数类型说明
clientIdstringOAuth App 的 client_id

成功响应 200

json
{
  "name": "Page Agent",
  "description": "AI 网页操控助手",
  "logo_url": "https://example.com/logo.png",
  "homepage_url": "https://example.com",
  "redirect_uris": ["http://localhost:4100/callback"],
  "scopes": ["profile:read", "chat:completions"],
  "app_definition_version": 4,
  "redirect_uri_allowed": true,
  "scope_descriptions": [
    {
      "scope": "profile:read",
      "description": {
        "zh-CN": "读取用户基本信息",
        "en-US": "Read user profile information"
      },
      "display_names": {
        "zh-CN": "基本资料",
        "en-US": "Profile"
      },
      "descriptions": {
        "zh-CN": "读取用户基本信息",
        "en-US": "Read user profile information"
      },
      "definition_version": 1
    }
  ]
}

display_namesdescriptionsdefinition_version 来自数据库实时 Scope registry;description 是兼容旧 consent 客户端的双语别名,新客户端应读取前两项。app_definition_version 是应用授权配置 (Scope 集合、回调白名单、capability 等)的单调版本。不存在、未发布、禁用或永久撤销的 Scope 不会作为可授权定义返回。Consent 页面必须把实际展示的 Scope 版本和 App 版本一起冻结,不能在登录、 切换账号或点击授权时静默重取。

Consent 请求应把原始 redirect_uri 作为同名 query 参数传给本端点,并只信 redirect_uri_allowed=true。该结论由服务端复用 authorize matcher 计算,覆盖 native_loopback App 的 RFC 8252 临时端口语义;前端不得用 redirect_uris.includes() 自行做精确匹配。


GET /oauth/apps/:clientId/development

获取自助应用的作者态授权元数据(开发自测专用,供 consent 页在 development=1 时渲染与 版本冻结)。需要 JWT 认证,且必须是应用作者本人。

与公开端点的分工:公开端点只暴露当前公众版本,对未过审应用一律 404;本端点返回的是 未冻结草稿版本的快照——未过审应用没有公众版本、已过审应用草稿新增的 Scope 公开端点 拿不到版本号,开发自测一律从这里取数。

路径参数

参数类型说明
clientIdstringOAuth App 的 client_id

查询参数

参数类型必填说明
redirect_uristring本次授权将使用的回调地址,服务端按草稿白名单给出 redirect_uri_allowed 结论

成功响应 200

响应形状与公开端点一致(name / redirect_uris / scopes / scope_descriptions / app_definition_version / redirect_uri_allowed,数据来自草稿版本),并附加:

字段类型说明
developmentboolean恒为 true,标记作者态响应
app_idstring应用内部 id,consent 页用它调用开发票据签发端点
draft_revision_numberinteger草稿版本号

错误响应

状态码error说明
404oauth_app_not_found应用不存在 / 非自助应用 / 已禁用 / 当前登录者不是作者(刻意同码,不泄漏存在性与归属)
409oauth_app_draft_not_available没有可用草稿(不存在,或已冻结进审核)

POST /oauth/authorize

用户确认授权,生成 authorization code。需要 JWT 认证

请求体(JSON)

参数类型必填说明
client_idstringOAuth App 的 client_id
redirect_uristring回调地址
scopestring请求的权限(省略时按 OAuth App 配置默认下发)
statestring客户端随机值(CSRF 防护;长度 8-256,仅 unreserved 字符 A-Z / a-z / 0-9 / - / . / _ / ~
code_challengestringPKCE code_challenge(BASE64URL 格式,43-128 字符)
code_challenge_methodstring固定为 S256
expected_scope_versionsobjectConsent 页面实际展示的全部 Scope 及其正整数 definition_version;键集合必须与本次请求的 Scope 完全一致
expected_app_definition_versionintegerConsent 页面加载 App 信息时冻结的正整数 app_definition_version
development_ticketstring自助应用开发自测的一次性票据明文(仅应用作者本人;由 consent 页在开发态自动签发并提交)。带票据时授权码绑定草稿版本,回调地址与 Scope 也按草稿判定

两项版本字段始终必填。非滚动维护切换不提供旧 consent 兼容窗口;后端绝不以当前数据库值静默补齐 用户没有实际展示并确认的版本。oauth_scope_writes_enabled 只控制 Scope 管理写,也不会改变此规则。

成功响应 200

json
{
  "redirectTo": "http://localhost:4100/callback?code=wno-code-...&state=..."
}

前端收到后执行 window.location.assign(redirectTo) 完成跳转。

服务端在数据库事务内锁定并复核 Scope 生命周期与 definition_version 后创建授权码;管理员并发修改 API 集合时不会生成可兑换旧同意的新授权码。

定义并发变化 409

如果用户看到权限说明后,Scope 定义或 OAuth App 授权配置发生变化,服务端不会签发授权码。客户端应 刷新公开信息,并要求用户基于新内容重新确认:

json
{
  "error": "scope_definition_changed",
  "error_description": "Authorization scope definitions changed; reload and confirm the permissions again"
}

App 配置并发变化时 errorapp_definition_changed。两者 HTTP 状态都为 409,不得自动重试、 复用旧确认或把它们降级为普通 500

非滚动维护切换合同

数据库 migration 本身可先在旧应用版本运行时 additive apply;但旧后端与本版本后端不能在 OAuth 流量下混跑。oauth_scope_writes_enabled=false 只阻断 Scope 生命周期/routes 管理,不是 OAuth 签发、refresh、App 或 peer 写入的维护开关,不能拿它冒充流量冻结。

  1. 在入口层冻结旧 revision 流量;本版本还会实时读取受保护的 oauth_runtime_hardening_writes_enabled,在值不是布尔 true(包括缺行、读失败)时,对 POST authorize、 code exchange、direct grant、refresh,以及 OAuth App、peer key、Scope 的全部配置写稳定返回 503 + Retry-After: 30。GET authorize 等只读入口、运维探针、RFC 7009 与用户主动安全撤销入口保持可用。应用门禁不能替代 对旧 revision 的入口冻结,因为旧代码不知道该 setting。
  2. 等待入口和所有旧 backend 实例的在途请求归零,并等待默认 600 秒 OAUTH_CODE_TTL(若环境覆盖则 按实际值)或查询确认 scope_definition_versions/app_definition_version IS NULL 的未消费、未过期 authorization code 为 0;旧 code 不做静默升级,客户端需重新授权。随后确认 runtime-hardening migration 已 apply/verify; 若该环境尚未预先完成 additive schema 落地,只能在此冻结窗口内 apply/verify。
  3. 一次性替换全部 backend 实例;用实际部署清单逐实例核对目标 Git revision,旧 revision 数必须为 0。
  4. 通过 migration 提供的 service-only 单向 activation RPC 把 oauth_runtime_hardening_writes_enabledfalse 切到 true。这是一次明确的破坏性重授权操作: RPC 在同一事务内消费全部未兑换 authorization code、撤销全部 OAuth token family/活跃 token,并 清理已经不满足 App/Scope/peer 合同的 gateway key;所有现有 OAuth 用户都必须重新授权。执行前先 调 service-only impact preview,记录 apps、codes、families、tokens、peer keys 的权威计数与有限样本, 由操作者显式确认;activation 自身把实际计数写入审计事件。随后才执行 authorize/exchange/direct/ refresh、App mutation、peer key、impact 与 verifier smoke。不得用通用 site_settings CRUD 改写或 反向关闭。
  5. 部署 consent 前端并核对公开信息含 Scope/App 两类版本、POST 原样回传;随后才解除旧 revision 的 OAuth 入口冻结。此时 runtime setting 已是不可逆的 true,新 revision 的 admission 正常放行。
  6. 所有前后端节点和静态资产都确认新合同后,才可把 oauth_scope_writes_enabled 设为 true;无论 该开关状态如何,缺任一版本字段始终返回 400

runtime activation 前,任一实例版本无法证明、在途请求或 legacy code 未清零时保持冻结并修复。activation 或首次受控写后,usage sequence 使 schema rollback 按设计 fail closed;此后 smoke 失败只能保持入口冻结 并 forward-fix,不能把 gate 改回 false 或恢复旧节点。Scope 永久撤权也不会因应用回退自动恢复。


POST /oauth/delegated/authorize

父子代签铸码端点(issue #1119,A 即主站模型):官方桌面壳 App 持自己的 access token(须含 oauth:delegate scope),替已过审的插件类 OAuth App 铸一条普通授权码。插件随后用 PKCE 走 POST /oauth/token(authorization_code)换码。插件类应用无法走浏览器 GET/POST /oauth/authorizeunauthorized_client)。

请求格式application/json + Authorization: Bearer <壳的 access_token>

参数类型必填说明
target_client_idstring插件类 App 的 client_id(须 plugin + self_service + 已过审)
scopesstring[]申请的 scope(四维交集:插件过审快照 ∩ 插件当前 ∩ 壳 token ∩ 壳当前;后端计算,超额即 400)
redirect_uristring插件注册的回调 URI 之一,换码时须传相同值
code_challengestringPKCE S256 challenge(由插件生成,经壳传入)
code_challenge_methodstring固定 S256

响应{ "code": "...", "expires_in": 600, "scopes": [...], "scope_definition_versions": {...} }

壳白名单由 OAUTH_DELEGATION_HOST_CLIENT_IDS 控制(留空 = fail-closed 全拒)。详见 docs/features/oauth/delegated-authorization.md


POST /oauth/assertion

受众断言签发端点(issue #1136):插件类 App 持 access token 换取 aud=目标站点 的短时 JWT,供第三方站点经 JWKS 验签建立登录身份。适用于 Host 内 Bearer 注入只覆盖同源 API、第三方站点收不到插件 token 的场景(受众断言桥)。

请求格式application/json + Authorization: Bearer <插件的 access_token>

参数类型必填说明
audstring目标受众,须为绝对 HTTPS URL;服务端规范化为 origin(拒绝 userinfo/query/fragment)后精确匹配

门禁(全部 fail-closed,认证优先——伪造/撤销 token 先于请求体解析被拒):

  1. Bearer 须为有效 OAuth access token 且含 profile:read scope
  2. token 所属 App 须为 app_type='plugin'(查询失败/不存在一律拒绝)
  3. 规范化 aud 须命中该 client 的受众绑定 OAUTH_ASSERTION_AUDIENCE_BINDINGS(未登记即拒)
  4. 签发配置(iss / 签名 JWKS / 绑定)任一缺失或非法 → 503 temporarily_unavailable

响应{ "assertion": "<JWT>" }Cache-Control: no-store

JWT(ES256)claimsiss(env 定值)、aud(规范化 origin)、sub(与 /oauth/userinfo 同值)、iatexp = iat + 300sjti(唯一标识,非防重放机制)、azp(调用方 client_id,归因用,消费方可忽略)、name/email(可选)。头部含 kid 供 JWKS 轮换定位。

限流:30 req/min/IP(本端点独立限流,豁免全局限流);429 返回 OAuth 错误契约(temporarily_unavailable + no-store + Retry-After,带 CORS 头)。

密钥轮换三阶段与配置说明详见 docs/features/oauth/audience-assertion.md


GET /.well-known/jwks.json

受众断言验签公钥发布(匿名 JWKS,Cache-Control: public, max-age=300)。发布 OAUTH_ASSERTION_SIGNING_JWKS 中全部密钥的公钥部分(无 d);首个为当前签名钥。配置缺失/非法时返回 503 + no-store(防负缓存)。

POST /oauth/token

换取 access_token、刷新 token,或在已开通能力的 OAuth App 中用验证码直连换取 OAuth token。

请求格式application/x-www-form-urlencoded

grant_type=authorization_code

参数类型必填说明
grant_typestringauthorization_code
codestring授权码
client_idstringclient_id
redirect_uristring与授权请求时一致
code_verifierstringPKCE code_verifier(长度 43-128,仅 unreserved 字符 A-Z / a-z / 0-9 / - / . / _ / ~

grant_type=refresh_token

参数类型必填说明
grant_typestringrefresh_token
refresh_tokenstringrefresh_token
client_idstringclient_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_typestringurn:wainao:params:oauth:grant-type:otp
client_idstringOAuth App 的 client_id
otp_typestringemailphone
identifierstring邮箱地址或手机号
codestring6 位验证码
scopestring请求的权限,省略时按 App 配置默认下发
langstring初始化默认 Team/Project 时使用的语言

警告

标准 OAuth 仍推荐走官网授权页。验证码直连不会暴露用户密码,但会把登录入口嵌入第三方 App,因此必须由平台为对应 App 单独开通 direct_otp_grant

成功响应 200

json
{
  "access_token": "wno-...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "wno-rt-...",
  "scope": "chat:completions"
}

响应头:Cache-Control: no-storePragma: no-cache

三种签发路径(授权码兑换、refresh、验证码直签)均以数据库 App-control RPC 返回的实际 scope 为准。 授权码兑换还会复核发码时冻结的 App 定义版本;验证码直签会在最终签发事务内重新复核 App capability; refresh 与 App 配置变更使用同一串行化边界。Scope 停止续签、临时禁用、永久撤销,或 Scope/App 定义 版本变化时均 fail closed,不会回显未真正写入 token 的 scope。

错误响应

json
{
  "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

参数类型必填说明
tokenstring要撤销的 token(access 或 refresh)
token_type_hintstringaccess_tokenrefresh_token
client_idstringclient_id

响应 200

json
{}

GET /oauth/userinfo

获取当前 OAuth 用户的基本信息。

请求头

Authorization: Bearer wno-...

成功响应 200

json
{
  "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

json
{
  "error": "invalid_token",
  "error_description": "Token expired or revoked"
}

Admin OAuth Apps API

OAuth 应用管理端点。需要 JWT + site:admin 权限。

基础路径:/admin/oauth-apps

GET /admin/oauth-apps

列表(分页、搜索)。

查询参数

参数类型说明
pagenumber页码(默认 1)
pageSizenumber每页数量(默认 20)
searchstring搜索关键词(名称或 client_id)

POST /admin/oauth-apps

创建应用。

请求体

json
{
  "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

更新应用。请求必须携带列表/详情响应中的 expected_app_definition_versionexpected_updated_at。展示字段更新与安全字段更新都经同一原子 RPC;当 scopesredirect_uriscapabilities、禁用/删除状态或 peer contract 发生授权语义变化时, 数据库会在同一事务递增 App 版本、消费未兑换授权码并撤销所有 token family。旧版本返回 409,不会静默覆盖另一位管理员的新配置。

capabilities 当前支持:

Capability说明
direct_otp_grant允许该 App 使用邮箱/手机号验证码直连 grant 获取 OAuth token

PATCH /admin/oauth-apps/:id/app-type

应用分类(issue #1119)。请求体 { "app_type": "standard" | "plugin" }。标记为 plugin 时要求该 App 为 self_service 管理模式且已通过审核(存在过审公开版本),并在同一事务撤销其全部既有 token(插件只能持有代签凭证);数据库 guard 触发器保证插件类应用无法持有非代签 code/token,且 oauth:delegate scope 只能授予官方应用。另外 scopesoauth:delegate 时仅 OAUTH_DELEGATION_HOST_CLIENT_IDS 白名单内的 client_id 可被授予。


DELETE /admin/oauth-apps/:id

请求体为 { expected_app_definition_version, expected_updated_at }。软删除、消费授权码、撤销 token 与该 App 签发的 peer gateway key 在同一事务完成。

POST /admin/oauth-apps/:id/disable

请求体为 { expected_app_definition_version, expected_updated_at }。禁用与全部安全清理原子完成。

POST /admin/oauth-apps/:id/enable

请求体为 { expected_app_definition_version, expected_updated_at }。启用同样走 CAS,避免覆盖并发变更。

Mobile / Native App 接入

ReAI Mobile 客户端走自定义 URL scheme + PKCE 接入:

Redirect URIreai-mobile://callback
Scopemobile:full(vendor-locked,仅生产 / 沙箱 client_id 允许申请)
浏览器组件iOS ASWebAuthenticationSession / Android Custom Tabs(不要使用 WebView)

mobile:full scope 覆盖 profile、devices、个人默认项目、项目存储上传、ROMP、推送、模型网关与云端 ASR 等移动端业务接口,详细接入步骤见仓库内 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_grant Refresh 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 异步写入。仅供审计,不参与认证决策。

AI Workflow Editor