
Civitai OAuth 授权服务器与 Scoped Token 位掩码权限体系设计原理与源码实现【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai本篇基于仓库中的规划文档 oauth-scoped-tokens.md系统讲解 Civitai 如何把原本从未真正执行的 API Key 权限升级为一套位掩码bitwiseScoped Token 体系并在此之上从零构建支持 PKCE 授权码流程、设备码流程、OIDCid_token签发的 OAuth 2.1 授权服务器。读完你可以掌握TokenScope位标志的完整定义与Full冻结策略、tRPC 层的 scope 强制链路、OauthClient/OauthConsent/ApiKey三张表的协作关系以及 access token 与 orchestrator生成编排器之间的免改造集成方式。一、背景KeyScope 已定义但从未执行Civitai 早先就有一个 API Key 系统密钥以 SHA-512 哈希存于ApiKey表用户可通过账户设置创建带名称与 scope 选择Read/Write/Generate的密钥验证入口是getSessionFromBearerToken()→getSessionUser()。问题在于 schema 里定义的KeyScope枚举从未被任何中间件或 tRPC procedure 校验——任何一把 API key 都能做用户能做的一切。与此同时2 年前启动的 PR #1313 曾尝试引入 OAuth但分支落后于主线无法合并且存在多个严重安全缺陷。当前仓库中的 bearer-token.ts 展示了该系统的现状getSessionFromBearerToken(key)先用generateSecretHash对传入密钥求哈希再查询ApiKey行一次性取出tokenScope、buzzLimit、lastUsedAt、clientId等字段最终把tokenScope: number附着到 session 对象上——这正是 scope 强制链路的第一环。规划文档对 PR #1313 列出了 8 条重建时必须规避的教训这些教训在后来的实现中逐条被验证修复对照 oauth-scoped-tokens-checklist.mdclient secret 从不校验——getClient()忽略了clientSecret参数没有 PKCE——易受授权码截获攻击OAuth 2.1 强制要求没有 scope 校验——validateScope被注释掉Redis 过期时间 bug——TTL 设在原始 code 字段上但实际存储用的是哈希后的 key无 CSRF 防护——allowEmptyState: true同意页直接展示裸 client_id而非应用名Token 响应中夹带 PII——应改用独立的 userinfo 端点ApiKey.key唯一索引在迁移与 schema 之间不一致地被删除。对应的实现结论checklist 中已勾选项PKCE S256 在/authorize颁发码前强制校验、allowEmptyState: false、过期时间设在哈希键上、secret 采用crypto.timingSafeEqual常量时间比较、consent 的approvedtrue只接受 POST阻断 consent 绕过等。二、TokenScope25 个资源级位标志设计原则在文档中非常明确层级性models:write隐含models:read资源导向scope 绑定你访问什么而非怎么访问每个 tRPC procedure 标注必需 scope且第一天就追求全覆盖不做渐进式补标API key 与 OAuth token 共用同一套 scope 体系向后兼容存量 API key 与内部创建的 key 一律授予Fullscope 以单个Int列的位掩码存储——契合仓库既有的Flags位运算工具该工具已用于 NSFW 等级等场景/api/v1/me的 handler 顶部就import { Flags } from ~/shared/utils/flags。位标志的权威定义现在位于共享包 token-scope.ts主应用通过 token-scope.constants.ts 的 re-export shim 保持旧 import 路径不变避免主应用与 auth hub 出现两份位掩码定义。核心定义如下值与文档完全一致export const TokenScope { None: 0, // Account Profile UserRead: 1 0, // 1 UserWrite: 1 1, // 2 // Models Resources ModelsRead: 1 2, // 4 ModelsWrite: 1 3, // 8 ModelsDelete: 1 4, // 16 // Media Posts图片、视频、帖子强耦合共用一组 MediaRead: 1 5, // 32 MediaWrite: 1 6, // 64 MediaDelete: 1 7, // 128 // Articles ArticlesRead: 1 8, // 256 ArticlesWrite: 1 9, // 512 ArticlesDelete: 1 10, // 1024 // Bountieswrite 隐含允许建赏金时的 buzz 消费 BountiesRead: 1 11, // 2048 BountiesWrite: 1 12, // 4096 BountiesDelete: 1 13, // 8192 // AI Services生成、训练、扫描——所有 orchestrator 请求 AIServicesRead: 1 14, // 16384 AIServicesWrite: 1 15, // 32768 // Buzz平台币 BuzzRead: 1 16, // 65536 // Collections Interactions CollectionsRead: 1 17, // 131072 CollectionsWrite: 1 18, // 262144 SocialWrite: 1 19, // 524288 — 关注、反应、评论、评价 SocialTip: 1 20, // 1048576 — 打赏buzz 消费 // Notifications NotificationsRead: 1 21, // 2097152 NotificationsWrite: 1 22, // 4194304 // Vault VaultRead: 1 23, // 8388608 VaultWrite: 1 24, // 16777216 // ……后续演进新增的 opt-in 位见下文 Full: (1 25) - 1, // 33554431 } as const;文档原文为 25 scopes, fits in 32-bit integer with 7 spare bits。三个关键设计点Full是单一值所有位全置 1即 33554431用于存量 key、浏览器 session 认证和内部创建 key 的默认值免去逐一枚举。Buzz 消费是隐含能力不设独立的BuzzSpendscopeAIServicesWrite隐含生成/训练/扫描的 buzz 消费BountiesWrite隐含建赏金消费打赏由专用位SocialTip覆盖。新增 scope 的成本极低加一个位位置 一次迁移即可。Full 的冻结策略与 opt-in 位演进从源码可以看到一个文档撰写之后的重要演进位 25 起出现了刻意排除在Full之外的 opt-in 能力位token-scope.tsAppBlocksSubmit: 1 25, // 33554432 — 提交 App Block 供审核 AppBlocksDevTunnel: 1 26, // 67108864 — 打开站内开发隧道 LinkConnect: 1 27, // 134217728 — 配对 Civitai Link 桌面应用 Full: (1 25) - 1, // 33554431 —— 刻意冻结只含 bit 0..24注释解释了冻结原因存量个人 API key 在数据库中持久化tokenScope 33554431若让Full吸收新位会静默改变所有已存 key 的语义。因此另提供ALL_SCOPES由枚举计算得出、永不落后于新增位作为 OAuth 流程中校验请求/存储值的上界。这是一个值得借鉴的位掩码系统演进技巧默认值语义与全部已定义位必须解耦。此外 block-scope.constants.ts 展示了位掩码体系如何向下支撑 App Blocks 生态块清单manifest声明的字符串 scope 通过BLOCK_SCOPE_TO_OAUTH_BIT映射到 OAuth 位如models:read:self → TokenScope.ModelsRead、posts:write:self → TokenScope.MediaWrite注册时校验manifest scope 必须是OauthClient.allowedScopes的逐位子集发 token 时二次校验以防审批后偷换 manifest无对应位的能力如apps:storage:*使用显式SKIP_OAUTH_CHECK哨兵而非 0 值防止维护者误填 0 造成 allowlist 绕过。Scope 预设与权限表格API Key 创建 UI 使用预设下拉 权限表格预设填充 Read/Write/Delete 三列的复选框用户可继续微调。预设常量同样定义在 token-scope.tsTokenScopePresets并配套tokenScopeGrid描述资源类别到 read/write/delete 位的映射供权限表格渲染预设位标志组合适用场景Read OnlyUserRead \| ModelsRead \| MediaRead \| ArticlesRead \| BountiesRead \| BuzzRead \| CollectionsRead \| AIServicesRead \| NotificationsRead \| VaultRead分析、仪表盘CreatorRead Only ModelsWrite \| MediaWrite \| ArticlesWrite \| BountiesWrite \| CollectionsWrite \| SocialWrite发布工具AI ServicesAIServicesWrite \| AIServicesRead \| BuzzRead源码实现中并入UserRead生成/训练 agentFull AccessFull个人自动化注意这些预设是代码常量而非数据库记录。每密钥 Buzz 消费限额对携带消费类 scope 的 keyAIServicesWrite、BountiesWrite、SocialTip用户可设置 daily/weekly/monthly buzz 上限存储于ApiKey行的buzzLimit JSON?字段。强制点有两层Civitai 中间件层tRPC 处理 buzz 交易前与 Orchestrator对照/api/v1/me的响应见第五节。对 agent 委托令牌尤其重要——文档中agent delegation tokens with spend caps正是靠这个机制落地而非单独的 token 类型checklist 4.3 中特殊 agent token 类型被推迟当前标准 OAuth token 消费限额即为方案。三、Scope 强制架构从 Bearer Token 到 tRPC 中间件强制聚焦 tRPC 层——v1 REST API 是只读/公开的无需 scope。文档给出的调用链与仓库实现一一对应Request (Bearer token) → getSessionFromBearerToken() ← src/server/auth/bearer-token.ts → 查 ApiKey取 tokenScope 位掩码 → 附着 scope 到 session 上下文 → tRPC procedure 中间件 → Flags.hasFlag(ctx.tokenScope, procedure.requiredScope) → 缺失则 403实现要点结合 oauth-scoped-tokens-checklist.md 的 Phase 1 已勾选项tRPC meta 类型扩展了requiredScope: number83 个 router、约 767 个 procedure全部以.meta({ requiredScope: TokenScope.X })标注中间件读取ctx.tokenScope用Flags.hasFlag()判定缺失时返回 403浏览器/NextAuth session 认证一律视为Full无限制Fail-safe 而非 fail-open早期版本未标注端点对 scoped token 放行的漏洞被识别并修复——现在 scoped token 访问未标注端点会被拒绝user.getToken被提升到需要Fullscope防止用一把受限 key 去铸造更高权限的 tokentoken minting 提权路径。从 bearer-token.ts 的源码还能看到另一处演进subject与buzzLimit的解析按 token 来源分叉——OAuth 签发的 tokenapiKey.clientId非空以OauthConsent(userId, clientId)作为跨 access-token 轮转的稳定标识消费限额从consent 记录上读取普通 API key 则用自身行上的buzzLimit。这保证了 access token 每小时轮换不影响限额语义。另外被 ban 的用户在 bearer 路径被集中拒绝user.bannedAt检查确保任何 OAuth token 或个人 API key 无法绕过AuthedEndpoint逐端点补查。四、OAuth 服务器设计表结构、端点与流程支持的流程与优先级流程用例优先级Authorization Code PKCEWeb 应用、开源前端Log in with CivitaiP0Refresh Token长时会话P0Client Credentials服务端互信受信合作方P1Device AuthorizationCLI 工具、agentP2核心用例是开源/社区前端用户经 OAuth 授权后 token 存于浏览器 localStorage前端直接拿 token 调 Civitai 的 tRPC/API也可以像内部隐藏 API key 那样直连 orchestrator 生成。这带来一个硬约束OAuth token 必须在我们现有 API key 生效的每一处都生效——由于 token 落库为ApiKey行这一约束天然成立。纯前端公共客户端无服务端按 OAuth 2.1 标准走PKCE-only授权码流程、无 client secret客户端注册为isConfidential: false配合短时效 access token1 小时 refresh token30 天localStorage 中的 token 不会永久有效。数据模型OauthClient表开发者的 OAuth 应用登记id TEXT PK (UUID) secret TEXT哈希存储与 API key 同法公共客户端为 null name TEXT description TEXT logoUrl TEXT? redirectUris TEXT[] grants TEXT[]authorization_code, refresh_token, client_credentials 等 allowedScopes INT位掩码——该客户端可请求的 scope 上限 isConfidential BOOLEAN userId INT FK → User注册的开发者 isVerified BOOLEANCivitai 审核过的应用在同意页显示徽章 createdAt / updatedAt TIMESTAMPOauthConsent表用户授权记录跳过重复同意userIdclientId唯一约束scope INT记录已同意的位掩码另有buzzLimit源码实现中追加作为 OAuth token 的消费限额载体见上一节。ApiKey表增量tokenScope Int默认Full取代旧KeyScope[]、clientId TEXT?哪枚 OAuth client 签发、lastUsedAt、buzzLimit JSON?。迁移策略存量 key 全部Full列默认值即Full保证灰度期间对生产库新建的 key 语义正确旧列延迟到生产验证后再删除。OAuth 令牌的落库形态是ApiKey行type取Access/Refresh两个新增枚举值令牌字符串带civitai_前缀便于区分access token 生命周期 1 小时、refresh 30 天、授权码 10 分钟。端点与安全检查单端点集/authorize校验 client_id/redirect_uri/response_type/scope/state强制 S256 PKCEstate 必填、/login/oauth/authorize同意页展示应用名/logo/描述而非裸 client_idhuman-readable 列出请求的 scopeisVerified徽章记住此决定复选框all-or-nothing 语义、/token校验 client secret 与 PKCE verifier响应不含 PII、/userinfo需UserRead、/revokeRFC 7009未命中也返回 200按 IP 限速防绕过校验调用者所有权。在安全要求上实现比文档更进了一步checklist Security Hardening 章节缺失/非法的 scope 默认值取0 而非 Full、拒绝负数与越界值、consent 绕过被 POST-only 阻断、user.getToken锁定Full、公共客户端可刷新 tokenOAuth 2.1 合规、createOAuthTokenPair共享 helper 防止逻辑漂移、userinfo对缺失 scope fail-safe 归 0。OAuth 事件审计由 audit-log.ts 承担logOAuthEvent以结构化 JSON 打日志供 Axiom 等聚合事件类型覆盖client.created/updated/deleted/secret_rotated、authorization.granted/denied、token.issued/refreshed/revoked等fire-and-forget 不阻塞请求。端点在仓库中的实际位置规划文档中写的src/pages/api/auth/oauth/*.ts在仓库演进后已迁移至集中式 auth 应用 apps/auth/src/routes/api/auth/oauth/实际目录包含authorize、token、userinfo、revoke、introspect、device、device-token、device-approve、device-deny、device-info、session、legacy-exchange等主应用侧 src/pages/api/auth/oauth/[...path].ts 以代理形式把旧路径转发过去相关迁移背景见 oauth-provider-implementation-checklist.md。设备码流程的用户码格式由 user-code.ts 单一来源定义8 位字符XXXX-XXXX分组、字符集剔除 I/O/0/1 防误读——这套细节文档没有展开但源码中它是服务端生成器与客户端输入页共享的常量。五、Orchestrator 集成/api/v1/me 的增量字段Orchestrator 不做任何密钥类型特判它只把 API key 作为 bearer token 调/api/v1/mesrc/pages/api/v1/me.ts用响应决定用户能做什么。当前响应形态{ id: 123, username: user, tier: member, status: active, isMember: true, subscriptions: [gold] }当请求经 API key而非 session认证时追加{ tokenScope: 33554431, buzzLimit: { daily: 5000, weekly: null, monthly: 50000 } }AuthedEndpointhelper 本就支持从 cookie 或 bearer token 解析 session实现只需三件事bearer 认证时把ApiKey记录不只 user透传给 handler、在响应中带上tokenScope与buzzLimit、orchestrator 侧用Flags.hasFlag(tokenScope, ...)判定并按buzzLimit自行执行限额。结果是OAuth access token 对 orchestrator 完全透明——因为它们本来就是ApiKey行。六、Addendum基于 civitai/auth 的 OIDC id_token文档 2026-06-09 的附录补齐了Sign in with Civitai与Sign in with Google体验之间的最后一块签名的id_tokenRP 可经 JWKS 本地验签免去 userinfo 往返。集中式 auth hubcentralized-auth-app.md、auth-verification-strategy.md提供了同一把 RS256 hub 密钥与/api/auth/jwks端点同时服务第一方 session JWT 与第三方 id_token。落地的要点mintIdToken({ sub, aud, nonce, authTime, claims?, expiresIn? })加在civitai/auth签名器上sign.tsRS256iss AUTH_JWT_ISSUER须与 discovery 的issuer相等nonce/auth_time 侧信道捕获由于node-oauth的 grant 对象不携带 nonce服务端在/authorize时把它们暂存到 Redis按 code 关联/token时按 code 消费当授予UserRead身份scope 且签名器已配置时/token的authorization_codegrant 返回签名id_tokenprofile/email claims 刻意省略——RP 仍从/userinfo取Discovery 只在签名启用时才声明jwks_uri与id_token_signing_alg_values_supported: [RS256]——否则 JWKS 会 404声明 RS256 就是对 RP 撒谎。两条设计决策值得注意Access token 保持不透明/DB 支持ApiKey行模型换来对不受信客户端的即时吊销只有id_token是 JWT。这是 Google 式的拆分与第一方 session无状态 JWT 换性能的取向刻意相反触发条件 UserReadscope在/authorize强制打开而非字面openidscope——位掩码系统里没有openid位。若未来希望不向纯 API code grant 发 id_token再引入专用openid标记位。附录还列了未决项.well-known/jwks.json路径重写当前服务在/api/auth/jwks部分 RP 库假设.well-known路径、可选地把 profile/email claims 折进 id_token省一次 userinfo 往返、at_hashclaimOIDC §3.1.3.6若有 RP 校验、以及专用openidscope。七、Token 管理体验与发布 APIAPI Key UI/user/account#api-keys从旧的名称 Read/Write/Generate 多选演进为名称 预设下拉 权限表格 可选过期 可选 buzz 限额key 列表展示 scope 摘要徽章与lastUsedAt。lastUsedAt采用每小时去抖的 fire-and-forget 更新bearer-token.ts 中LAST_USED_DEBOUNCE_MS 60 * 60 * 1000。Connected Apps 页/user/account#connected-apps列出用户经 OAuth 授权过的全部第三方应用应用名、徽章、已授予 scope、授权日期、活跃 token 数支持按应用整体吊销删除该 client 全部 token 与 consent 记录。开发者门户/user/account/developers语义的 tRPCoauth-clientrouter提供应用 CRUD、secret 轮换与按应用统计活跃 token 数、总授权数。Publishing API的策略是tRPC 先行v2 服务层 API 在后外部客户端在 OAuth scope 落地后直接调既有 tRPC 端点完成发图、传模型等发布动作未来 v2 会是更干净的 REST 面直打服务层甚至独立成 API server避免继续往这个 Next.js 项目里堆端点。八、实施阶段与关键决策一览分四期交付Phase 1 为 scope 强制基础设施位枚举、tokenScope列、迁移、中间件、全量标注、UI、lastUsedAt、内部 key 审计Phase 2 为 OAuth 核心两张表、授权/令牌/userinfo/吊销端点、同意页、开发者门户、限速、审计、消费限额Phase 3 为 Connected Apps 与管理Phase 4 为高级流程Client Credentials、Device Authorization、agent 委托 消费上限、OIDC Discovery。从 checklist 看四期中除少数低危遗留项ScopeSelector 组件抽取、限速 TOCTOU 改 Lua/SET NX EX、消费限额 UI 与 Redis 计数器等外主体已完成。文档Decisions Made表沉淀的关键决策是理解整套体系取舍的索引问题决策scope 粒度media图视频帖合并、articles 独立、bounties 独立Buzz 消费隐含式——AIServicesWrite/BountiesWrite/SocialTip不设独立BuzzSpend存量 API key以Full语义继承grandfatheredOAuth 应用注册开放注册人人可注册scope 存储位掩码单 Int 列——匹配既有Flags模式高效易扩展默认 scope 值Full——生产库联调安全、全开单值公共客户端PKCE-only 授权码流程无 client secretToken 前缀OAuth token 加civitai_前缀同意 UXall-or-nothing——用户接受全部请求 scope 或整体拒绝CORStoken 端点宽松 CORS按 client 限制 origin 留作后续长期方向第一方应用mobile、扩展、子域最终也走 OAuth逐步退役 NextAuth九、小结这套体系可复用的工程决策Civitai 的 Scoped Token OAuth 方案给中大型 Next.js 站点的授权改造提供了几条可迁移经验位掩码 单一 Int 列让权限成为一次位与运算Flags.hasFlag迁移、校验、UI 渲染共享同一份常量tokenScopeGrid、tokenScopeLabelsFull语义冻结、ALL_SCOPES动态计算解决了位掩码系统最棘手的新增位如何不改变存量数据语义问题OAuth token 复用ApiKey表使新 token 在 orchestrator 等既有集成点零改造生效同时保留即时吊销能力fail-safe 中间件 首日全量标注用scoped token 访问未标注端点即拒绝把覆盖率变成安全属性而非负担access token 不透明、id_token 有状态拆分兼顾吊销安全与 OIDC 体验。如需继续深入可直接阅读 docs/auth/oauth-scoped-tokens-review.md评审记录、docs/auth/oauth-resume-state.md登录中断恢复与 docs/auth/thin-session-token-design.md瘦 session token 设计以及 apps/auth 中 OAuth 端点的实际路由实现与 packages/civitai-auth 中的签名/验证库。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考