接续 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
25 KiB
cdrop 账户与登录实施计划
内置账户层 · OAuth 为账户来源与凭证提供者 · cdrop 自维护薄账户数据与自签会话 · 扫码快登优先 状态:规划中。本文是折中架构定稿后的实施蓝本——把边界、数据模型、令牌架构、扫码流程、里程碑讲清楚,按里程碑分批落地。 关联:根
README.md;internal/jwtauth(认证中间件 / verify 链 / JWKS)、internal/httpapi/session.go(浏览器免重登 session)、internal/httpapi/shortcut.go(HS256 scoped token)、internal/db/migrations。记忆cdrop-state/cdrop-next-phase/cdrop-security-scan。
0. 定位、折中架构与关键决策
折中:OAuth 为体的凭证、cdrop 为体的会话
三方在 2026-06-21 收敛到一个折中位置,介于“OIDC 锚定身份”(原方案)与“内置账号系统取代 OIDC”(全反转)之间:
- 身份来源仍是 OAuth provider(当前自建 Casdoor)。
user_id继续等于 OIDCsub,cdrop 不引入内部 id 间接层。 - 凭证机制全部委派给 provider——账号密码、Passkey、2FA、密码重置、注册,cdrop 一律不实现。Casdoor 本就提供这些,cdrop 发起 OAuth 跳转即可复用。
- cdrop 只在其上自维护薄薄一层:一张
accounts表(cdrop 侧账户数据,不含任何凭证)+ 一套自签会话令牌(脱离 IdP refresh 流程,供扫码 / 访客 / 持久设备会话使用)。 - 产品化时加一个内建、可启用的 OAuth 后端。cdrop 消费侧保持“只认一个 OIDC provider”,该后端是 drop-in:今天 provider 是外部 Casdoor,将来翻开关变成内建的。账号密码 / Passkey / 2FA 那些机制届时才进 cdrop 代码库,但被隔离在 provider 模块里。
一句话:凭证是 provider 的事,会话是 cdrop 的事,账户数据两边各持一薄层。
决策记录
- 决策 A — 折中架构(已定):见上。否决“全反转”,因其令 cdrop 成为凭证保管者(argon2id / 防爆破 / 锁定 / 枚举防御 / 无邮件密码重置),攻击面与维护成本过高;这些 Casdoor 已成熟承担。
- 决策 B —
user_id继续 = OIDCsub,不引内部 id。新增薄accounts表(键在sub),其中存一个稳定匹配键(默认取 JWT 的emailclaim)。日常切换 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 登录页内完成 |
| 2FA(TOTP / 短信 / 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 token;IdP 拒绝则作废(保留 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_id,QR 与批准用
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
现有架构:cookie(web_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 token(typ缺省、scope:"clipboard"、走shortcut_tokens查吊销)区分,互不串验。 - 吊销 / 撤销(即时):长效凭证是
web_sessions行;access token 短命且绑定sid。verifySelfToken每请求按sid查 session 行——行被删除(吊销)即当场拒绝,故吊销在被吊销设备的下一次请求即生效,而非等一个 TTL。oidc会话 refresh 仍打 IdP,保留 IdP 侧登出 / 吊销传播。 - 被吊销设备的感知与回退:服务端拒绝之外,前端把“访问被拒且续期也被拒”视作会话不可恢复 → 就地清空登录态并回到登录页(
net/api.ts的forceLogout+__root反应式守卫)。离线设备于下一次使用(请求 401 或开机 cookie 续期失败)得知;在线设备经Hub.Kick关流 → 约 1s 后重连撞 401 得知。两路殊途同归到verifySelfToken的sid查行。
RS256(IdP access token)已退出浏览器每请求热路径:浏览器登录(含 OIDC)一律持自签 token,OIDC 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 流程
- 新设备发起:
POST /api/auth/qr/start { device_name, device_type }(公开、限流)→ 服务端建login_requests行,返回{ request_id, poll_secret, qr_payload }。poll_secret:新设备私有,仅此响应返回一次,QR 不含。qr_payload:request_id+approval_code,编码进二维码展示。
- 新设备轮询:
GET /api/auth/qr/status?request_id=…,HTTP header 带poll_secret(公开、限流)。pending时长轮询 / SSE 挂起等待。 - 手机扫码:扫得
qr_payload→ 打开/link?request_id=…&code=…。须已登录(否则先走正常 OAuth 登录)。页面显示“批准 <设备名>·<类型>·<IP 粗粒度> 登入你的账号?”,可选“信任此设备 7 天”或“仅此次 1 小时”,再点【批准】/【拒绝】。 - 手机批准:
POST /api/auth/qr/approve { request_id, approval_code, scope, persist }(完整会话鉴权 +requireFullSession,可选 step-up)。服务端校验请求pending且未过期、approval_code匹配 → 建web_sessions行(user_id=批准者、kind=self|guest、scope、expires_at按persist、granted_by=批准者 sid)→ 标login_requests为approved。 - 新设备领取:下一次
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_secret(X-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、扫码)并入原生客户端设备(桌面 / iOS,type ∈ {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_seenTTL 清陈旧设备外,新增清除“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_token,JWKS 验签 + 校验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=true,OIDC_*指向内建端点。 - 迁移标记(决策 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 / 注册 / 重置的开关在 provider(Casdoor)侧,非 cdrop env。
9. 认证中间件 verify() 扩展
现链:dev → HS256(scoped shortcut) → RS256(OIDC)。折中后:
verify(token):
dev 模式 → verifyDev
自签会话 HS256 → verifySelfToken ← 新增(typ=session,SESSION_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_sessions 加 kind / scope / granted_by 列,refresh 按 kind 分流;既有 oidc 会话零行为变化 |
阶段一基座 |
A1 accounts 薄表 |
建表;exchange 时 upsert(match_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每请求按sid查web_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 抽象,毫发无损。折中被牢牢圈在“身份 / 会话层”,应用主体不动。这是本计划改造面可控、可分里程碑安全落地的根本保证。