Files
Commilitia-Drop/AUTH.md
T
admin 018a77b13a 登录子系统:OIDC 统一自签 token + 吊销即时生效 + 设备并入会话列表 + 应用内扫码 + 禁则修复
接续 90a3790(扫码 A0+A1+B + step-up 雏形)。蓝本见 AUTH.md。

OIDC 统一到 cdrop 自签 token
- /auth/exchange 先 JWKS 验 id_token 签名(自签 token 的唯一信任锚)再签 cdrop
  自签 HS256 access token(绑 sid),不再把 IdP RS256 下发浏览器;验签失败仅告警
  并回退 RS256(会话仍建、下次 refresh 自愈),登录不中断
- /auth/refresh oidc 路径仍打 IdP 轮换 refresh_token + 探测 IdP 侧吊销,但同样回
  自签 token;createWebSession 改返回行 id
- verify 链保留 RS256 分支仅供桌面端 loopback(其即时吊销留后续)

吊销即时生效 + 被吊销设备自知回退
- verifySelfToken 每请求按 sid 查 web_sessions 行,行删即拒 → 吊销在被吊销设备
  下一次请求即生效(不等 TTL),oidc / self / guest 一致
- 前端仅在「服务端以 401 确证会话失效」时 forceLogout 清登录态、回登录页;短期连接
  失败(5xx / 网络)一律保留登录态(refreshTokens 改三态 refreshed/invalid/transient,
  cookieRefresh 改 discriminated 结果)
- __root 反应式守卫:prod 下失登录态且非认证路由即跳登录页;新增 auth.sessionLost.* 三语

设备列表并入会话列表 + 孤儿自动清除
- GET /api/auth/sessions 统一 web_sessions 与原生客户端设备(桌面 / iOS,自 devices
  登记表呈现,native=true,离线也列);同名不重复列出,排除 shortcut 与无会话 browser
- 吊销网页会话连带删其设备登记(无同名在用会话时);原生「登出」走
  DELETE /api/devices/{name}(同要求 step-up)
- 新增 DeleteOrphanBrowserDevices(browser 型且无存活 web_session),接入设备清扫器(1h)

应用内扫码 + 聚珍崩溃 / CJK 禁则修复
- 登录页内置 getUserMedia + jsqr 扫码,不再外跳系统应用(避免开错浏览器)
- 修聚珍(cjk-autospace)MutationObserver 与 React 重渲染同子树冲突致的 insertBefore
  崩溃:扫码 / 显码 / 批准三状态机页整页跳过聚珍(data-jz-skip)
- 修流动正文未跑 CJK 禁则(jinze 为段落级、须 opt-in):设置 / 快捷指令 / 桌面页一批
  hint 补 data-jz-level="paragraph" + justify,短标签 / 状态仍留文本级

step-up 完善
- auth_time 改可选(Casdoor 不下发,原强制致死循环);stepped_up_at 按会话绑定、
  StepUpMaxAgeSeconds 窗口内不复要求;新增 POST /api/auth/stepup
- 批准页 persist 编进 step-up returnTo 防整页跳转丢失;scope 随档绑定(信任=full /
  仅此次=guest),默认偏安全的「仅此次」

文档与测试
- AUTH.md:折中架构、accounts 薄表、令牌架构、扫码流程、step-up、§4.4 统一会话列表
- 新增 Go 测试:OIDC 自签验证、verifySelfToken 吊销即拒、会话吊销连带删设备、
  统一会话列表(含原生 / 不含 shortcut / 不重复)、孤儿清扫、删设备需 step-up
2026-06-22 11:55:02 +08:00

25 KiB
Raw Blame History

cdrop 账户与登录实施计划

内置账户层 · OAuth 为账户来源与凭证提供者 · cdrop 自维护薄账户数据与自签会话 · 扫码快登优先 状态:规划中。本文是折中架构定稿后的实施蓝本——把边界、数据模型、令牌架构、扫码流程、里程碑讲清楚,按里程碑分批落地。 关联:根 README.mdinternal/jwtauth(认证中间件 / verify 链 / JWKS)、internal/httpapi/session.go(浏览器免重登 session)、internal/httpapi/shortcut.goHS256 scoped token)、internal/db/migrations。记忆 cdrop-state / cdrop-next-phase / cdrop-security-scan


0. 定位、折中架构与关键决策

折中:OAuth 为体的凭证、cdrop 为体的会话

三方在 2026-06-21 收敛到一个折中位置,介于“OIDC 锚定身份”(原方案)与“内置账号系统取代 OIDC”(全反转)之间:

  1. 身份来源仍是 OAuth provider(当前自建 Casdoor)。user_id 继续等于 OIDC sub,cdrop 不引入内部 id 间接层。
  2. 凭证机制全部委派给 provider——账号密码、Passkey、2FA、密码重置、注册,cdrop 一律不实现。Casdoor 本就提供这些,cdrop 发起 OAuth 跳转即可复用。
  3. cdrop 只在其上自维护薄薄一层:一张 accounts 表(cdrop 侧账户数据,不含任何凭证)+ 一套自签会话令牌(脱离 IdP refresh 流程,供扫码 / 访客 / 持久设备会话使用)。
  4. 产品化时加一个内建、可启用的 OAuth 后端。cdrop 消费侧保持“只认一个 OIDC provider”,该后端是 drop-in:今天 provider 是外部 Casdoor,将来翻开关变成内建的。账号密码 / Passkey / 2FA 那些机制届时才进 cdrop 代码库,但被隔离在 provider 模块里。

一句话:凭证是 provider 的事,会话是 cdrop 的事,账户数据两边各持一薄层。

决策记录

  • 决策 A — 折中架构(已定):见上。否决“全反转”,因其令 cdrop 成为凭证保管者(argon2id / 防爆破 / 锁定 / 枚举防御 / 无邮件密码重置),攻击面与维护成本过高;这些 Casdoor 已成熟承担。
  • 决策 B — user_id 继续 = OIDC sub,不引内部 id。新增薄 accounts 表(键在 sub),其中存一个稳定匹配键(默认取 JWT 的 email claim)。日常切换 OAuth provider 即账号失效——运维须知此前提。仅当管理员显式启用“后端迁移标记”时,cdrop 才在新源登录时按该匹配键自动关联回同一账号(受控窗口,非日常自动 link,规避账号接管)。
  • 决策 C — 2FA 委派给 provider。cdrop 不自存任何第二因子密钥,仅保留敏感动作的 step-up 钩子(要求 provider 再认证),默认关闭。
  • 决策 D — 扫码方向 = 陌生设备显码、已登录手机扫码批准(微信 / WhatsApp 网页版模型)。陌生设备常无摄像头但有屏,最贴合“临时借设备传文件”。
  • 决策 E — 扫码会话两档,scope 与持久性绑定。批准时二选一:“信任此设备(完整会话,7 天滑动)”——授予 scope=full、kind=self,可完整使用本账号(自有设备用);“仅此次(受限访客,1 小时)”——授予 scope=guest、kind=guest,能收发文件与消息,但不能改账号、不能再批准别的设备、不能签发长效 token(陌生/借用设备用)。默认偏向更安全的“仅此次”。(原设计两档皆 guest、仅持久性不同;2026-06-21 实测后改为 scope 随档绑定,消除“信任却受限”的语义矛盾。)

本文取代 .scratch/auth-inversion-diff.html 中按“全反转”写的 §四 大半——那五个“凭证保管者”决策(无邮件重置 / 开放注册 / argon2id 等)在折中下随委派 provider 而消散。


1. 设计边界:cdrop 建什么 / 委派什么

能力 归属 说明
账号密码登录 provider Casdoor 登录页提供;cdrop 只发起 OIDC PKCE 跳转
Passkey / WebAuthn 登录 provider 同上,provider 登录页内完成
2FATOTP / 短信 / passkey-as-2FA provider cdrop 仅保留 step-up 钩子(决策 C)
密码重置 / 找回 provider Casdoor 邮件流;cdrop 无 SMTP 设施,不介入
注册 / 开放注册策略 provider Casdoor 注册配置
账号枚举 / 防爆破 / 锁定 provider 凭证侧防护全在 provider
扫码快速登录 cdrop 唯一的纯新增功能(§4
自签会话令牌 cdrop 脱离 IdP refresh,供扫码 / 访客 / 持久会话(§3)
cdrop 侧账户数据 cdrop accounts 薄表:匹配键 / 显示名 / 头像 / 角色缓存(§2)
设备列表 / 在线状态 cdrop 已有,沿用,键在 user_id
会话 / 设备管理与吊销 cdrop 管理 cdrop 自签会话(§3、§4)

Provider 抽象边界cdrop 认证消费侧(/api/auth/config、PKCE exchange、JWKS 验签)只依赖“一个 OIDC provider”的标准面,不内嵌任何 provider 专有逻辑。这是内建后端(§7)能非破坏性接入的前提。


2. 数据模型

2.1 新增 accounts(薄账户数据层)

CREATE TABLE IF NOT EXISTS accounts (
    user_id      TEXT PRIMARY KEY,         -- = OIDC sub,与既有各表的 user_id 一致
    match_key    TEXT NOT NULL DEFAULT '', -- 稳定匹配键(默认 email claim),供迁移标记关联
    display_name TEXT NOT NULL DEFAULT '',
    avatar_url   TEXT NOT NULL DEFAULT '',
    roles        TEXT NOT NULL DEFAULT '', -- OIDC groups claim 缓存(逗号分隔),admin 判定用
    provider     TEXT NOT NULL DEFAULT '', -- 来源标识(如 "casdoor"),迁移时区分新旧源
    created_at   INTEGER NOT NULL,
    last_login_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_accounts_match_key ON accounts (match_key);
  • 写入时机OIDC exchange 成功后 upsert(捕获 match_key / 显示名 / 头像 / roles,刷新 last_login_at)。
  • 不含任何凭证——密码 / 第二因子全在 provider。
  • match_key 仅在“迁移标记”开启时被读作 join key(§7),日常不参与登录判定。

2.2 泛化 web_sessions → 支持自签会话

既有 web_sessions 只承载“绑定 IdP refresh_token 的浏览器会话”。扫码 / 访客会话拿不到 IdP refresh_token,需泛化。新增列(bootstrap.go 幂等 ALTER 补列,既有行视为 kind='oidc'scope='full'):

ALTER TABLE web_sessions ADD COLUMN kind       TEXT NOT NULL DEFAULT 'oidc';  -- oidc | self | guest
ALTER TABLE web_sessions ADD COLUMN scope      TEXT NOT NULL DEFAULT 'full';  -- full | guest
ALTER TABLE web_sessions ADD COLUMN granted_by TEXT NOT NULL DEFAULT '';      -- 扫码批准者的 session id(审计 / 连带吊销)
-- refresh_token 改为“可空语义”:self / guest 会话无 IdP refresh_token,存空串
kind refresh_token refresh 行为 用途
oidc AES-GCM 密文 向 IdP 续 + 自签 access tokenIdP 拒绝则作废(保留 IdP 吊销传播) 正常浏览器登录
self 仅按 session 行自签 access token,不触 IdP 桌面 / 持久设备(未来)、扫码“信任”
guest self,但 scope='guest' 受限 扫码“仅此次” / 受限借用设备

2.3 新增 login_requests(扫码)

CREATE TABLE IF NOT EXISTS login_requests (
    id            TEXT PRIMARY KEY,         -- request_idQR 与批准用
    poll_secret   TEXT NOT NULL,            -- SHA-256(新设备私有轮询密钥),QR 不含,仅新设备持有
    approval_code TEXT NOT NULL,            -- 短码,随 QR 给批准方
    status        TEXT NOT NULL,            -- pending | approved | denied | consumed | expired
    new_device_name TEXT NOT NULL,
    new_device_type TEXT NOT NULL,
    user_agent    TEXT NOT NULL DEFAULT '',
    request_ip    TEXT NOT NULL DEFAULT '',
    approver_user_id TEXT NOT NULL DEFAULT '',  -- 批准后填
    grant_scope   TEXT NOT NULL DEFAULT '',     -- full | guest
    grant_persist TEXT NOT NULL DEFAULT '',     -- once | persist
    created_at    INTEGER NOT NULL,
    expires_at    INTEGER NOT NULL,             -- 短 TTL(默认 120s
    approved_at   INTEGER
);
CREATE INDEX IF NOT EXISTS idx_login_requests_expires ON login_requests (expires_at);

后台 reaper 清 expires_at < now(复用 RunWebSessionReaper 同款 goroutine)。

2.4 不动的表

devices / shortcut_tokens / transfer_sessions / clipboard_state / push_subscriptions 全部只键在 user_id一字不改。这是折中改造面可控的根本原因——应用主体被 user_id 抽象隔离。


3. 令牌与会话架构

3.1 cdrop 自签 access token

现有架构:cookieweb_sessions,窄 Path=/api/auth)→ /auth/refresh 签发 access token → JS 内存持有 → 作 Bearer 打 /api/*。折中改动只在“access token 由谁签”:从“IdP 签的 RS256”改为“cdrop 自签的 HS256”。

  • 签名密钥:从 SESSION_SECRET 派生独立 HMAC 密钥(DeriveSessionTokenKey(SESSION_SECRET),与既有 DeriveHS256Key(HS256Secret) 区分密钥域)。SESSION_SECRET 在 prod 本就强制非空,无新增必填密钥。
  • 载荷{ sub: user_id, sid: session_id, typ: "session", scope: "full"|"guest", iat, exp }。短 TTL(默认 900s)。
  • 校验:中间件新增 verifySelfToken 分支(§9)。靠 typ:"session" + 独立签名密钥与 scoped shortcut tokentyp 缺省、scope:"clipboard"、走 shortcut_tokens 查吊销)区分,互不串验。
  • 吊销 / 撤销(即时):长效凭证是 web_sessions 行;access token 短命且绑定 sidverifySelfToken 每请求按 sid 查 session 行——行被删除(吊销)即当场拒绝,故吊销在被吊销设备的下一次请求即生效,而非等一个 TTL。oidc 会话 refresh 仍打 IdP,保留 IdP 侧登出 / 吊销传播。
  • 被吊销设备的感知与回退:服务端拒绝之外,前端把“访问被拒且续期也被拒”视作会话不可恢复 → 就地清空登录态并回到登录页(net/api.tsforceLogout + __root 反应式守卫)。离线设备于下一次使用(请求 401 或开机 cookie 续期失败)得知;在线设备Hub.Kick 关流 → 约 1s 后重连撞 401 得知。两路殊途同归到 verifySelfTokensid 查行。

RS256IdP access token)已退出浏览器每请求热路径:浏览器登录(含 OIDC)一律持自签 tokenOIDC exchange 时验 id_token 签名一次(§5)作唯一信任锚,refresh 仍打 IdP 轮换 refresh_token 但同样回自签 token。中间件保留 RS256 分支仅供桌面端(自跑 loopback PKCE、直带 IdP token);桌面端即时吊销留后续。

3.2 受限能力门(guest 会话)

复用既有 rejectScoped(挡 scoped shortcut token 进非 clipboard 路由)的同款思路,新增 requireFullSession 守卫。scope:"guest" 的会话:

  • 放行/clipboard/transfer/*/relay/*/message/hub/*/devices(看列表)、/me/push/*
  • 拒绝/auth/qr/approve(再批准设备)、/shortcut/*(签长效 token)、/devices/{name} DELETE 别的设备、未来账号设置 / 会话管理。

能力矩阵随路由分组实现,集中在 server.go 中间件层(§9)。


4. 扫码登录(优先里程碑 B

陌生设备显码、已登录手机扫码批准。三方密钥分离是安全核心:QR 可被偷拍,故 QR 里只放“能发起批准请求”的信息,而会话只投递给持有私有轮询密钥的原始新设备。

4.1 流程

  1. 新设备发起POST /api/auth/qr/start { device_name, device_type }(公开、限流)→ 服务端建 login_requests 行,返回 { request_id, poll_secret, qr_payload }
    • poll_secret:新设备私有,仅此响应返回一次,QR 不含。
    • qr_payloadrequest_id + approval_code,编码进二维码展示。
  2. 新设备轮询GET /api/auth/qr/status?request_id=…HTTP header 带 poll_secret(公开、限流)。pending 时长轮询 / SSE 挂起等待。
  3. 手机扫码:扫得 qr_payload → 打开 /link?request_id=…&code=…须已登录(否则先走正常 OAuth 登录)。页面显示“批准 <设备名>·<类型>·<IP 粗粒度> 登入你的账号?”,可选“信任此设备 7 天”或“仅此次 1 小时”,再点【批准】/【拒绝】。
  4. 手机批准POST /api/auth/qr/approve { request_id, approval_code, scope, persist }完整会话鉴权 + requireFullSession,可选 step-up)。服务端校验请求 pending 且未过期、approval_code 匹配 → 建 web_sessions 行(user_id=批准者kind=self|guestscopeexpires_atpersistgranted_by=批准者 sid)→ 标 login_requestsapproved
  5. 新设备领取:下一次 qr/status 轮询(凭 poll_secret)见 approved → 响应 Set-Cookie(新设备自己的 TLS 连接)下发 session cookie + 返回首枚 access token → 标 consumed(单次)。

手机全程只发送批准、从不接触新设备的 session 密钥;新设备的 cookie 只经其自身轮询连接下发。

4.2 安全要点

  • approval_code / poll_secret 高熵随机;login_requests 短 TTL(默认 120s),consumed / expired 后不可复用。
  • 偷拍 QR 者:无 poll_secret(领不到会话)、未登录(批不了准),双重失败。
  • 访客会话受 §3.2 能力门约束;granted_by 支持“吊销我批准过的借用设备”与连带吊销。
  • 批准动作可挂 step-up(§6)。

4.3 端点小结

方法 路径 鉴权 作用
POST /api/auth/qr/start 公开 + 限流 新设备建请求,返 request_id / poll_secret / qr_payload
GET /api/auth/qr/status poll_secretX-Poll-Secret 头)+ 限流 新设备长轮询,批准后在本响应 Set-Cookie + 返回 access token,并标 consumed(单次)
GET /api/auth/qr/request 完整会话 + approval_code 批准方读新设备信息(名 / 类型 / 来源 IP)以渲染确认页
POST /api/auth/qr/approve 完整会话 + requireFullSession 手机批准,记录 approver / scope / persist(会话在新设备领取时才建)
POST /api/auth/qr/deny 完整会话 手机拒绝

前端:新设备“显码页”+ 手机“批准页”+ 设备 / 会话管理(列出 granted_by 借用设备、可吊销)。

4.4 登录会话列表 = 全部设备(统一视图 + 自动清除)

「登录会话」面板收敛到“一台设备 = 一条会话”:列表即当前登录此账号的全部设备。既往“手动清除设备”入口已撤,导致被吊销设备长期滞留——本节即为此消除。

  • 统一列表GET /api/auth/sessions):web_sessions(浏览器 oidc / self / guest、扫码)并入原生客户端设备(桌面 / iOStype ∈ {macos, windows, linux, ios})。原生客户端用 IdP 令牌、不入 web_sessions,故从设备登记表 devices(即决策里说的“等价表”)呈现:id 形如 device:<设备名>kind 取设备类型、native=true。同名已有会话的设备不重复列出;无会话的 browser 型(孤儿)与 shortcut 型(自有面板)不在此列;过期会话已隐藏。这满足“原生设备也在会话列表登记”,且离线原生设备照常显示(devices 留有 last_seen)。
  • 吊销即清设备:吊销网页会话(DELETE /api/auth/sessions/{id})在删会话行后连带删除其设备登记(除非另有同名在用会话),再 Kick + 广播 presence → 被吊销设备即时从所有列表消失。原生客户端的“登出”改走 DELETE /api/devices/{name}(同样要求 step-up),删设备 + Kick;该设备若仍在线,会在下次请求经中间件重新登记(原生即时吊销需客户端配合,留后续)。
  • 孤儿自动清除:设备清扫器(每 1h)除按 last_seen TTL 清陈旧设备外,新增清除“browser 型且无存活 web_session”的孤儿(会话已撤 / 过期但设备行滞留);DeleteOrphanBrowserDevices 一条 SQL 完成,原生 / shortcut 设备无 web_session 属正常、仅受 TTL 清扫。即时路径(吊销)+ 周期路径(清扫)合起来让设备列表与会话列表持续对齐。

5. 账号密码 / Passkey 登录(委派 provider

几乎零 cdrop 新增——复用既有 OIDC PKCE:

  • cdrop /api/auth/config 已暴露 authorize_url 等;前端“登录”按钮发起跳转,Casdoor 登录页呈现账号密码 / Passkey / 2FA 表单。
  • 回调 → /api/auth/exchange(既有)→ 解析 token。折中下的唯一安全强化id_token 必须在此验签JWKS)——这是 cdrop 信任 OIDC 的唯一时刻(原方案故意不验、靠后续每请求 JWKS 兜底;自签 access token 后那层兜底消失)。
  • exchange 成功 → upsert accounts(§2.1,捕获 match_key 等)→ 建 kind='oidc' 会话 → 自签首枚 access token。

“确保 Casdoor 启用了账号密码 / Passkey / 2FA”是部署配置项,非 cdrop 代码。


6. 2FA(委派 + step-up 钩子,已实装)

  • 默认2FA 在 provider 完成(Casdoor TOTP / passkey-as-2FA),cdrop 不感知、不存密钥。
  • step-up 钩子(决策 C,默认关,CDROP_STEP_UP_ENABLED;新鲜度窗口 CDROP_STEP_UP_MAX_AGE_SECONDS,默认 300):开启后,批准扫码设备(qr/approve)这类敏感动作要求一次现场再认证。机制:前端先跑一次带 prompt=login / max_age=0 的新鲜 PKCE,把拿到的授权码 + verifier 交给 qr/approve;后端就地用它向 IdP 换 id_tokenJWKS 验签 + 校验 sub 等于调用者,方才放行(provider 若开了 2FA,即在这次 prompt=login 往返中强制)。auth_time可选——OIDC 仅在 IdP 选择遵循 max_age 时才要求它,Casdoor 不下发;有则强制 StepUpMaxAgeSeconds 窗口,无则以「单次短寿授权码刚换出」作为新鲜度界(否则会因缺 auth_time 而恒拒、陷死循环,2026-06-21 实测修正)。失败返回 403 step_up_required。授权码一次性、不可重放;cdrop 不存任何第二因子,也不留 step-up 服务端状态——证明随这一次批准请求过期。
  • qr/request 响应回带 step_up 标志,供批准页判断是否需先再认证。
  • passkey 作为“快速登录”的补充:陌生设备场景下扫码优于 passkey(passkey 设备绑定,陌生设备上没有),故 passkey 由 provider 承担、不进 cdrop 自建范围。

7. 内建 OAuth 后端(产品化,远期)

目标:自托管者不必跑外部 Casdoor。设计为 drop-in provider,不改 cdrop 消费侧。

  • 形态:cdrop 内嵌(或同容器旁挂)一个最小 OIDC provider 模块,提供账号密码 / Passkey / 2FA / 注册 / 重置。凭证机制届时才进 cdrop 代码库,但隔离在 provider 模块,主体仍只认标准 OIDC 面。
  • 启用CDROP_BUILTIN_OAUTH_ENABLED=trueOIDC_* 指向内建端点。
  • 迁移标记(决策 B):管理员置 CDROP_OIDC_MIGRATION_MODE=true 后,新源登录时按 accounts.match_key(默认 email)匹配既有账号并自动改键关联(把旧 sub 的数据迁到新 sub,或登记别名)。受控、可关闭,非日常自动 link。
  • 本里程碑不在此展开内建后端内部设计——仅锁定“provider 抽象边界”这一前提,确保现在的实现不堵死将来。

8. 配置项(“均可配置关闭”)

默认 作用
CDROP_QR_LOGIN_ENABLED true 扫码登录总开关(关闭则 qr/* 返 404 / 503
CDROP_QR_REQUEST_TTL_SECONDS 120 二维码 / 登录请求有效期
CDROP_QR_GUEST_TTL_SECONDS 3600 “仅此次”访客会话 TTL
CDROP_QR_PERSIST_TTL_HOURS 168 “信任此设备”持久会话 TTL(与设备 TTL 一致)
CDROP_SESSION_TOKEN_TTL_SECONDS 900 cdrop 自签 access token TTL
CDROP_SESSION_SECRET prod 必填) 既有;现额外派生自签会话签名密钥
CDROP_STEP_UP_ENABLED false 批准扫码设备等敏感动作要求 provider 再认证
CDROP_STEP_UP_MAX_AGE_SECONDS 300 step-up 的 auth_time 新鲜度窗口(秒)
CDROP_ACCOUNT_MATCH_CLAIM email accounts.match_key 取哪个 JWT claim
CDROP_BUILTIN_OAUTH_ENABLED false 远期:启用内建 OAuth 后端
CDROP_OIDC_MIGRATION_MODE false 远期:按 match_key 关联跨源账号

账号密码 / Passkey / 2FA / 注册 / 重置的开关在 providerCasdoor)侧,非 cdrop env。


9. 认证中间件 verify() 扩展

现链:dev → HS256(scoped shortcut) → RS256(OIDC)。折中后:

verify(token):
    dev 模式            → verifyDev
    自签会话 HS256       → verifySelfToken   ← 新增(typ=sessionSESSION_SECRET 派生密钥)
    scoped shortcut HS256 → verifyHS256      ← 既有(typ 缺省,HS256Secret 派生,查 shortcut_tokens
    OIDC RS256          → verifyRS256        ← 既有,保留作兼容(桌面直带 IdP token)
  • 凭据来源新增:cookie(既有,/auth/refresh 用)之外,自签 access token 作 Bearer 走 verifySelfToken
  • 路由守卫两层叠加:requireScope("clipboard")(既有,scoped token);requireFullSession(新增,挡 scope=guest 进敏感路由,§3.2)。
  • accounts.roles 缓存供 admin 判定(既有“admin 仅 groups claim”语义保留,读 accounts.roles 而非每次解 token)。

10. 里程碑

里程碑 内容 阶段
A0 自签会话原语 SESSION_SECRET 派生会话签名密钥;自签 access token 签发 / 校验(verifySelfToken);web_sessionskind / scope / granted_by 列,refresh 按 kind 分流;既有 oidc 会话零行为变化 阶段一基座
A1 accounts 薄表 建表;exchange 时 upsertmatch_key / 显示名 / 头像 / roles);id_token exchange 时验签;/me、admin 判定改读 accounts 阶段一
B 扫码登录(优先) login_requests 表;qr/start qr/status qr/approve qr/deny;访客 / 持久会话;requireFullSession 能力门;前端显码页 + 批准页 + 设备 / 会话吊销 阶段一(端到端,drop.commilitia.net 即可上线)
C 2FA step-up 钩子 敏感动作要求 provider 再认证(prompt=login / max_age);默认关 阶段三
D 内建 OAuth 后端 provider 抽象 drop-in;凭证机制隔离模块;迁移标记按 match_key 关联 远期 / 产品化

折中下“阶段二(账号密码登录)”几乎无 cdrop 工作量——账号密码 / Passkey 本就是 provider 提供,A1 的 accounts 落地即覆盖其 cdrop 侧需求。重心在阶段一的 A0 + A1 + B:自签会话原语 + 薄账户表 + 扫码,先在现有 Casdoor 部署上端到端可用,且对老用户零行为变化。


11. 安全要点

  • id_token 必须 exchange 时验签——自签 access token 后,原“每请求 JWKS 兜底”消失,OIDC 信任收敛到这唯一一刻。
  • 吊销即时verifySelfToken 每请求按 sidweb_sessions 行,删行即当场拒绝,被吊销设备下一次请求即失效(不依赖 TTL);前端据 401 + 续期失败回退登录页(§3.1)。自签 access token 短 TTL(默认 900s)只是续期节律,长效凭证只在 web_sessions 行。
  • 扫码三密钥分离QR 仅含批准信息,会话只投私有 poll_secret 持有者;偷拍 QR 双重失败。
  • 访客会话能力门scope=guest 服务端强制受限(§3.2),不依赖前端自律。
  • 迁移标记受控:跨源 match_key 关联仅在管理员显式开启时生效,非日常自动 link,规避 email 接管。
  • 密钥域分离:自签会话密钥(SESSION_SECRET 派生)与 scoped shortcut 密钥(HS256Secret 派生)不同域,互不串验。
  • 限流qr/start / qr/status 纳入既有 /api/auth 限流桶(60/min per-IP)或单独更严桶。

12. 不变的部分

设备模型、SSE Hub、传输状态机、剪贴板、Web Push、cookie 机制、AES-256-GCM 落盘、striped refresh 锁、HS256 scoped shortcut 基建——全部只认 user_id 抽象,毫发无损。折中被牢牢圈在“身份 / 会话层”,应用主体不动。这是本计划改造面可控、可分里程碑安全落地的根本保证。