This document was written by AI and has been manually reviewed.
Prism 是一个符合标准的 OAuth 2.0 授权服务器和 OpenID Connect 提供商。任何支持 OAuth 2.0 授权码流程的应用都可以使用 Prism 作为其身份提供商。
Discovery
OpenID Connect Discovery 文档位于:
https://your-prism-domain/.well-known/openid-configuration大多数 OAuth/OIDC 库可以从此 URL 自动完成配置。
注册应用程序
- 登录 Prism,前往 Apps → New Application
- 填写名称、描述和重定向 URI
- 复制 Client ID 和 Client Secret——密钥仅显示一次
如果你的应用完全运行在浏览器端(没有服务端来保密密钥),请启用公共客户端。公共客户端必须使用 PKCE,没有客户端密钥。
授权码流程(含 PKCE)
第一步 — 重定向用户
GET https://your-prism-domain/api/oauth/authorize
?response_type=code
&client_id=<CLIENT_ID>
&redirect_uri=https://yourapp.com/callback
&scope=openid profile email
&state=<RANDOM_STATE>
&code_challenge=<CODE_CHALLENGE>
&code_challenge_method=S256PKCE — 生成一个 code_verifier(43–128 个随机 URL 安全字符),然后:
code_challenge = BASE64URL(SHA-256(ASCII(code_verifier)))权限范围
| 范围 | 包含的声明 / 授权的访问 |
|---|---|
openid | sub、iss、aud、iat、exp(OIDC 必须) |
profile | name、preferred_username、picture |
profile:write | 更新用户的个人资料(名称、头像) |
email | email、email_verified |
apps:read | 用户拥有的应用列表 |
apps:write | 创建、更新和删除用户的应用 |
teams:read | 列出用户的团队 |
teams:write | 更新团队设置和管理成员 |
teams:create | 创建新团队 |
teams:delete | 删除团队 |
domains:read | 列出用户的自定义域名 |
domains:write | 添加和删除自定义域名 |
gpg:read | 列出用户已注册的 GPG 公钥 |
gpg:write | 添加或删除用户的 GPG 公钥 |
social:read | 列出用户已关联的社交提供商账号 |
social:write | 断开社交提供商账号关联 |
webhooks:read | 列出用户的 Webhook |
webhooks:write | 创建、更新和删除 Webhook |
admin:users:read | 读取所有用户账号(仅限管理员) |
admin:users:write | 修改用户账号(仅限管理员) |
admin:users:delete | 删除用户账号(仅限管理员) |
admin:config:read | 读取实例配置(仅限管理员) |
admin:config:write | 更新实例配置(仅限管理员) |
admin:invites:read | 列出邀请(仅限管理员) |
admin:invites:create | 创建邀请(仅限管理员) |
admin:invites:delete | 删除邀请(仅限管理员) |
admin:webhooks:read | 列出实例级别的 Webhook(仅限管理员) |
admin:webhooks:write | 创建和更新实例级别的 Webhook(仅限管理员) |
admin:webhooks:delete | 删除实例级别的 Webhook(仅限管理员) |
offline_access | 启用刷新令牌颁发 |
团队相关 scope —— 三个层级
涉及团队的 scope 分三类,作用范围差异巨大。请按需选最窄的那一类。
| 层级 | 示例 | 访问范围 | 授予方式 |
|---|---|---|---|
| 聚合(复数) | teams:read | 用户加入的所有团队 | 普通用户同意 |
| 单团队(单数) | team:read | 同意时选定的某一个团队 | 普通用户同意 + 团队选择器(用户须是该团队 admin 及以上) |
| 跨实例 | site:team:read | 实例上所有团队 | 仅管理员,且需通过 2FA + 输入确认短语 |
聚合 teams:*
teams:read teams:write teams:create teams:delete作用于用户的全部团队图谱。一次同意覆盖所有团队。适合需要反映或同步用户成员关系的场景 — 例如把 teams claim 注入到 ID Token 给 Cloudflare Access 用,或者展示一个「我所在的团队」切换器。
端点位于 /api/oauth/me/teams[/...]。
单团队 team:*
team:read team:member:read
team:write team:member:write
team:delete team:member:profile:read应用请求时使用上面这些字符串。同意时用户挑出一个具体团队,Prism 通过 bindTeamScopes() 把它们就地改写成 team:<team-id>:read 等 — 颁发的 token 里只剩绑定后的形式,从而只能作用于那一个团队。
同意时还有两条额外限制(worker/routes/oauth.ts:830-859):
- 用户必须是所选团队的
owner、co-owner或admin。有效角色同样适用 —— 通过上级团队继承得到的 admin 也可以在子团队上授予单团队 scope(与会话 API 一致)。同意页的团队选择器会列出该用户能管理的所有团队,无论是直接还是继承。 team:delete还要求owner或co-owner(admin 能授予读写,但只有真正能解散团队的人才能授予删除权)。继承的 owner 同样可以授予team:delete,递归的dissolveTeam级联 会完整执行。
team:member:write 同样不能越权:admin 用户授权后,应用提升成员的角色受到与该 admin 相同的上限保护,不会因 token 而获得超越授权人本身的能力 — 这条上限会在每次成员变更时校验。
每次授予都会在 team_scope_grants 表中独立审计(含团队 ID 与权限列表),与 OAuth 同意记录分开。
端点位于 /api/oauth/me/team/:teamId/...,通过 resolveTeamToken(c, teamId, "read"|"write"|"member:read"|...) 校验绑定关系;用绑定到 A 团队的 token 去访问 B 团队的端点会得到 403 insufficient_scope。
跨实例 site:team:*
site:team:read site:team:write site:team:delete无需逐团队同意的跨团队管理员权限。授予时同意者必须是站点管理员,并通过 站点 scope 确认流程 — 2FA + 完整输入 grant site access 这一确认短语。仅适合真的需要看 / 改全部团队的站点管理工具。
选哪一种 — 速记
- 「这个用户在哪些团队?」用
teams:*。绑定到单个团队的集成(如某 workspace 的部署机器人)用team:*。 - 不要同时请求
teams:*和team:*— 你会拿到两者的并集,但同意页里同时出现「团队选择器」和「全部团队提示」会让用户困惑。 site:team:*是给站点管理工具用的,不要给产品集成用。这层的授予会绕过团队所有者的同意。
站点 scope(仅管理员)
完整的跨实例 scope 列表;与 site:team:* 共用 admin-only / 2FA / 确认短语 的同一道关卡:
| Scope | 权限 |
|---|---|
site:user:read | 读取任意用户 |
site:user:write | 修改任意用户 |
site:user:delete | 删除任意用户 |
site:team:read | 读取任意团队 |
site:team:write | 修改任意团队 |
site:team:delete | 解散任意团队 |
site:config:read | 读取站点配置 |
site:config:write | 修改站点配置 |
site:token:revoke | 撤销任意用户的 OAuth token |
第二步 — 用户授权
Prism 显示授权页面,列出你的应用名称和请求的权限范围。如果用户已经对相同的权限范围授权过,则自动跳过授权页面。
第三步 — 接收授权码
Prism 重定向到你的 redirect_uri:
https://yourapp.com/callback?code=<AUTH_CODE>&state=<STATE>请务必验证 state 与你发送的值一致。
第四步 — 换取令牌
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<AUTH_CODE>
&redirect_uri=https://yourapp.com/callback
&client_id=<CLIENT_ID>
&client_secret=<CLIENT_SECRET>
&code_verifier=<CODE_VERIFIER>公共客户端省略 client_secret,必须包含 code_verifier。
响应
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "...",
"id_token": "...",
"scope": "openid profile email"
}第五步 — 调用 UserInfo
GET /api/oauth/userinfo
Authorization: Bearer <ACCESS_TOKEN>UserInfo 响应
{
"sub": "user-id",
"name": "Alice",
"preferred_username": "alice",
"email": "alice@example.com",
"email_verified": true,
"picture": "https://your-prism-domain/api/assets/avatars/..."
}刷新令牌
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=<REFRESH_TOKEN>
&client_id=<CLIENT_ID>
&client_secret=<CLIENT_SECRET>令牌内省(RFC 7662)
用于服务端间验证,无需解析 JWT:
POST /api/oauth/introspect
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <base64(client_id:client_secret)>
token=<ACCESS_TOKEN>响应(有效令牌)
{
"active": true,
"sub": "user-id",
"scope": "openid profile",
"client_id": "...",
"exp": 1234567890,
"iat": 1234564290
}令牌撤销(RFC 7009)
POST /api/oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=<ACCESS_OR_REFRESH_TOKEN>
&client_id=<CLIENT_ID>
&client_secret=<CLIENT_SECRET>ID 令牌
ID 令牌是一个签名的 JWT(RS256)。可通过 /.well-known/jwks.json 发布的公钥进行验证,或使用内省端点进行服务端验证。
标准声明(请求 openid 权限范围时始终包含):
| 声明 | 值 |
|---|---|
iss | 你的 Prism 实例 URL |
sub | 稳定的用户 ID |
aud | 你的 client_id |
iat | 颁发时间戳 |
exp | 过期时间戳 |
role | 用户角色(user 或 admin) |
nonce | 从授权请求中原样返回 |
范围关联声明 — profile 和 email 声明在授予对应权限范围时自动包含。下表中其余声明还需要应用在 oidc_fields 配置中声明对应的字段名:
| 权限范围 | 字段名 | 添加到 ID 令牌的声明 |
|---|---|---|
profile | (始终包含) | name、preferred_username、picture |
email | (始终包含) | email、email_verified |
teams:read | teams | teams — { id, name, role } 对象数组,表示用户的团队成员身份 |
apps:read | apps | apps — { id, name, client_id, is_verified } 对象数组,表示用户拥有的应用 |
domains:read | domains | domains — { id, domain, verified } 对象数组 |
gpg:read | gpg_keys | gpg_keys — { id, fingerprint, key_id, name } 对象数组 |
social:read | social_accounts | social_accounts — { id, provider, provider_user_id } 对象数组 |
通过 API 创建或更新应用时,在 oidc_fields 数组中声明所需字段,即可为该应用启用相应的自定义声明:
{ "oidc_fields": ["teams", "domains"] }加强 2FA(敏感操作再确认)
应用可以请求 Prism 让用户在执行敏感操作前用 TOTP 或通行密钥再确认一次——例如转账、删除资源、授予提权访问权限等。
流程是服务端发起的:你的服务器先通过 HTTPS 把这次操作注册到 Prism,然后才重定向用户。操作描述和回调 URI 都在服务端到服务端这一步固定下来——只控制 URL 的攻击者无法伪造一个写有任意内容的确认页。
用户必须已登录 Prism(未登录会被引导登录),并已启用 TOTP 认证器或通行密钥。这一过程不会授予任何新的账户权限——返回结果只是用户重新确认了一次的一次性凭证。
第一步 — 创建挑战(服务端到服务端)
POST /api/oauth/2fa/challenges
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/json
{
"redirect_uri": "https://app.example.com/2fa-callback",
"action": "确认转账 $1,000",
"nonce": "order_abc123",
"code_challenge": "PKCE_CHALLENGE",
"code_challenge_method": "S256"
}| 字段 | 是否必填 | 说明 |
|---|---|---|
client_id | 必填(Basic 或请求体) | OAuth 应用的 client ID |
client_secret | 机密客户端必填 | 通过 Basic 或请求体提供 |
redirect_uri | 必填 | 必须已在应用上注册 |
action | 推荐 | 用户要确认的操作的人类可读描述(≤ 200 字符)。在 Prism 页面原样展示,并在 verify 响应中回传 |
nonce | 可选 | 应用自定义的不透明值(≤ 256 字符),原样回传。建议绑定到具体操作(如订单 ID) |
code_challenge, code_challenge_method | 公开客户端必填 | PKCE — 见授权码流程 |
响应
{
"challenge_id": "f3a…opaque…",
"expires_at": 1761500900,
"url": "https://prism.example.com/oauth/2fa?challenge_id=f3a…"
}公开客户端(无 client_secret)依靠 PKCE 进行身份认证:在这里传 code_challenge,在校验时传 code_verifier。服务器对每个客户端限速创建挑战(每分钟 60 次),即便密钥泄露也无法用于骚扰用户。
第二步 — 重定向用户
https://prism.example.com/oauth/2fa?challenge_id=f3a…&state=RANDOMURL 里只有不透明的 challenge_id 和你设的 CSRF state。攻击者没有任何可篡改的内容。
第三步 — 用户确认
Prism 会展示应用图标、(如适用的)已验证域名徽章、来自挑战的 action 文案,并提示用户输入 TOTP 或使用通行密钥。用户还必须勾选一个回显操作内容的复选框("我已阅读并理解:…"),「确认」按钮才会启用。
用户点击 确认 或 拒绝。
第四步 — 接收 code
Prism 将用户重定向回挑战中固定的 redirect_uri:
https://app.example.com/2fa-callback?code=…&state=…或在拒绝/出错时:
https://app.example.com/2fa-callback?error=access_denied&state=…第五步 — 校验(服务端)
POST /api/oauth/2fa/verify
Content-Type: application/x-www-form-urlencoded
code=THE_CODE&client_id=YOUR_CLIENT_ID&redirect_uri=…&code_verifier=PKCE_VERIFIER机密客户端可用 client_secret(请求体)或 HTTP Basic 认证。公开客户端仅依靠 PKCE。
响应
{
"user_id": "u_abc",
"client_id": "YOUR_CLIENT_ID",
"verified_at": 1761500000,
"action": "确认转账 $1,000",
"nonce": "order_abc123",
"method": "totp"
}code 是单次使用的,签发后 5 分钟过期。校验成功后:
verified_at是用户完成 2FA 的 Unix 时间戳——超过你认可的窗口期就视为过期。- 将
nonce和action与你应用最初构造 URL 时存储的值比对——不一致就拒绝结果。 method是"totp"、"passkey"或"backup"。
验证码门槛
站点可以要求用户在批准 2FA 加强前先通过验证码。触发该门槛有两种方式:
- 站点默认 — 管理员开启
require_captcha_for_2fa,所有 2FA 加强都需要通过验证码。 - 应用按挑战开启 — 应用在调用
POST /api/oauth/2fa/challenges时传入require_captcha: true。适合在站点默认关闭时,某些应用仍希望对自己的高风险操作增加阻力。(应用无法关闭站点已强制启用的门槛。)
使用站点已配置好的验证码提供商(Turnstile、hCaptcha、reCAPTCHA 或 PoW)。如果 captcha_provider 为 "none",即便上述触发条件命中也不会有任何效果。
/api/oauth/2fa/info 响应中暴露了 captcha_required、captcha_provider、captcha_site_key,以便前端渲染对应的小部件。用户解决挑战后,把 captcha_token(或 pow_challenge + pow_nonce)连同 TOTP/通行密钥一并提交到 /api/oauth/2fa/authorize。
走 sudo 旁路时不会触发验证码:sudo 不检查任何因子,没有自动化攻击的面,强制挑战反而会让 sudo 宽限期失去意义。
Sudo 模式(宽限窗口)
用户在一次成功的 TOTP/通行密钥确认之后,可选择启用 sudo 宽限窗口:在此窗口内,同一会话同一应用的后续挑战会跳过 2FA 提示。但操作描述的确认复选框仍然必须勾选——用户始终能看到并确认自己批准的内容,只跳过 TOTP/通行密钥的重新输入。
TTL 由管理员通过 sudo_mode_ttl_minutes 站点设置控制。设为 0 即可完全禁用 sudo 模式。
授权绑定到 (user_id, session_id, client_id) 三元组——不会跨应用、跨会话、跨用户泄露。登出 Prism 会更换 session ID,该会话内所有 sudo 授权随即不可达。
用户启用 sudo 后,Prism 返回的重定向 code 的 method 字段为 "sudo"。进行极高风险操作(销户、大额转账)的应用应要求 method !== "sudo",使这些操作始终触发一次全新的 2FA 提示。
提前撤销 sudo 窗口
用户可在 TTL 到期前主动撤销:
POST /api/oauth/2fa/sudo/revoke
Authorization: Bearer <user-session-jwt>
Content-Type: application/json
{ "client_id": "YOUR_CLIENT_ID" }威胁模型
可防御的攻击:
- 仅靠 URL 的钓鱼。 仅能构造 URL 的攻击者(如钓鱼邮件中的链接)无法注入任意 action 文案或挑选任意 redirect URI——这两者都在第 1 步在服务端固定,攻击者拿不到应用的
client_secret(或公开客户端的应用本身)就够不到这一步。 - code 拦截。 PKCE 将 code 与 verifier 绑定;code 还与
(client_id, redirect_uri)绑定。即使 code 泄露(如通过 referrer),也无法被其他应用兑换或发送到其他 URI。 - TOTP 暴力破解。 每用户 5 分钟限速 8 次。一次失败也会消耗当前挑战——攻击者必须重新走一次服务端发起的 POST 才能重试。
- 重放 / 重复兑换。 挑战和生成的 code 都通过原子操作(
UPDATE … WHERE consumed_at IS NULL)单次消费。 - 盲目点击。 用户必须显式勾选回显 action 文案的复选框,「确认」按钮才会启用。
- UI 欺骗。
action、nonce、state都有长度上限,避免恶意应用通过 UI 投放超长内容欺骗用户。
无法防御:
- 设备完全失陷(恶意软件可读取屏幕上的 TOTP 验证码并窃取会话 Cookie——任何认证流程都救不了你)。
- 同时拥有应用
client_secret且被授权代表该应用行事的攻击者,他们可以发起合法挑战。如怀疑泄露请立即轮换密钥。
集成
Cloudflare Access
你可以将 Prism 作为 Cloudflare Access 的通用 OIDC 身份提供商,让用户使用 Prism 账号登录受 Cloudflare 保护的资源。
第一步 — 在 Prism 中创建 OAuth 应用
登录 Prism,前往 Apps → New Application
将重定向 URI 设置为:
texthttps://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callbackAllowed scopes 至少包含
openid和email,如需在 Access 策略中使用其他声明,可添加profile、teams:read等。OIDC fields 设置为需要嵌入 ID 令牌的自定义声明字段名,例如
["role", "teams"]。复制 Client ID 和 Client Secret。
第二步 — 在 Cloudflare 中添加 Prism 为身份提供商
在 Cloudflare Zero Trust 中,前往 Integrations → Identity providers → Add new → OpenID Connect,填写以下内容:
| 字段 | 值 |
|---|---|
| Name | Prism(或任意名称) |
| App ID | 你的 Prism Client ID |
| Client secret | 你的 Prism Client Secret |
| Auth URL | https://your-prism-domain/api/oauth/authorize |
| Token URL | https://your-prism-domain/api/oauth/token |
| Certificate URL | https://your-prism-domain/.well-known/jwks.json |
| PKCE | 启用(推荐) |
| Scopes | openid email(按需添加 profile teams:read 等) |
| OIDC Claims | 每行一个 — 需要在策略中使用的声明名 |
在 OIDC Claims 中填入 Prism 返回的自定义声明名,例如:
role
in_team_<team-id>
role_in_team_<team-id>保存后点击 Test 验证连接。成功后可在 oidc_fields 中看到声明:
{
"email": "alice@example.com",
"oidc_fields": {
"role": "admin",
"in_team_abc123": true,
"role_in_team_abc123": "owner"
}
}第三步 — 使用 Prism 声明构建 Access 策略
在 Access 应用策略中使用 OIDC Claim 选择器:
| 选择器 | Claim name | Claim value | 效果 |
|---|---|---|---|
| OIDC Claim | role | admin | 仅限 Prism 管理员 |
| OIDC Claim | in_team_<team-id> | true | 指定团队成员 |
| OIDC Claim | role_in_team_<team-id> | owner | 仅限团队所有者 |
注意: Cloudflare Access 从 ID 令牌(RS256 签名的 JWT)中读取自定义声明。Dashboard 中填写的声明名必须与 Prism 实际嵌入令牌的字段名完全一致,后者由应用的
oidc_fields配置决定。
错误响应
授权错误会重定向到你的 redirect_uri,附带:
?error=access_denied&error_description=User+denied+access令牌端点错误返回 HTTP 400:
{ "error": "invalid_grant", "error_description": "Code expired or invalid" }常见错误码:invalid_request、invalid_client、invalid_grant、unauthorized_client、unsupported_grant_type、access_denied。