扫码登录:cdrop 自签会话原语 + 薄账户层 + 受限访客 + 2FA step-up
身份仍源自 OAuth provider(user_id = OIDC sub),cdrop 在其上自维护一薄层: 自签会话令牌 + accounts 表,并新增扫码快速登录。实施蓝本见 AUTH.md。 后端 · 自签会话原语 - web_sessions 加 kind/scope/granted_by 列(bootstrap 幂等迁移);既有 oidc 会话行为不变,新增 self(滑动续期)/ guest(受限·不续)两类 - cdrop 自签 HS256 会话 access token,密钥派生自 SESSION_SECRET,与落盘 AES、 shortcut HS256 三密钥域隔离;jwtauth.verifySelfToken 无状态校验、靠 typ 区分 - requireFullSession 守卫:guest 会话不得改账号 / 再批准设备 / 签长效 token - /auth/refresh 按 kind 分流:self/guest 纯自签、不触 IdP 后端 · 扫码登录(internal/httpapi/qr.go) - qr/start・status・request・approve・deny 五端点 + login_requests 表 + reaper - 三密钥分离:QR 仅含批准信息,会话只投递给持私有 poll_secret 的原设备 (偷拍 QR 者无 poll_secret 领不到会话、未登录批不了准) - 会话在新设备侧领取(cookie 不经手机)、单次消费、短 TTL 后端 · 薄账户层与 2FA step-up - accounts 表(键 sub,不含任何凭证):exchange/refresh upsert match_key / 显示名 / 头像 / roles,供显示与未来管理员开启迁移标记时跨源关联 - step-up(默认关):开启后 qr/approve 要求新鲜 prompt=login 授权码,后端就地 换 id_token、JWKS 验签 + auth_time 窗口 + sub 匹配,provider 2FA 于此往返强制 前端(web/) - 显码页 /link/new + 批准页 /link + net/qr.ts,对齐 Theme B、复用 AuthShell 与聚珍排版管线、零新全局样式 - step-up 再认证流:批准前跑 prompt=login PKCE,回调分叉(不消费一次性 code、 独立 state key)后带 step_up_code/verifier 调 approve - 三语 i18n qr.*;新增 qrcode 依赖 修复 · clipboard sweeper(早已提交的损坏) - ClearExpiredClipboards 因 clipboard.sql 全角注释触发 sqlc 1.31 多字节偏移 bug,生成 SQL 被截断为「UPDATE clipboard_state SET content =」,sweeper 运行 期报「incomplete input」、过期剪贴板内容从未清除(短 TTL 暴露保护失效) - 注释改纯 ASCII 并加 bug 警告,重生成得完整 SQL;prod 已验证 sweeper 由 ERROR 转为正常清理(cleared count=1) 构建 - .dockerignore:排除本地 node_modules 等,避免宿主原生二进制污染镜像内 vite 构建 - Dockerfile.base:GOPROXY 改为可经 --build-arg 覆盖(默认仍官方代理,受限网络 构建时传区域镜像即可,仓库不固化区域值) 文档 - 新增 AUTH.md(账户与登录实施蓝本);README 特性;.env.example / compose.snippet 增配置项(QR / 自签会话 TTL / step-up / match_claim) 测试 - 自签 token 密钥域隔离、扫码端到端(领取 / 单次 / poll_secret 校验 / deny / step-up 门)、extractIdentity、web_sessions 升级迁移
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
# 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”(全反转)之间:
|
||||
|
||||
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 — 扫码会话 = 受限访客会话,持久性可选**。批准时选“信任此设备(7 天滑动)”或“仅此次(1 小时)”;访客会话能收发文件,但不能改账号、不能再批准别的设备、不能签发长效 token。
|
||||
|
||||
> 本文取代 `.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`(薄账户数据层)
|
||||
|
||||
```sql
|
||||
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'`):
|
||||
|
||||
```sql
|
||||
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`(扫码)
|
||||
|
||||
```sql
|
||||
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` 查吊销)区分,互不串验。
|
||||
- **吊销 / 撤销**:access token 无状态、短命;长效凭证是 `web_sessions` 行。`/auth/refresh` 每次(≤TTL 周期)查 session 行存活与未吊销,故吊销在一个 TTL 内生效。`oidc` 会话 refresh 仍打 IdP,保留 IdP 侧登出 / 吊销传播。
|
||||
|
||||
> RS256(IdP access token)退出每请求热路径,仅在 OIDC exchange 时验 `id_token` 签名一次(§5)。中间件保留 RS256 分支作兼容(桌面端等仍直接带 IdP token 的客户端),待后续统一迁移到自签后再清理——避免 flag day。
|
||||
|
||||
### 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_payload`:`request_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|guest`、`scope`、`expires_at` 按 `persist`、`granted_by=批准者 sid`)→ 标 `login_requests` 为 `approved`。
|
||||
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_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` 借用设备、可吊销)。
|
||||
|
||||
---
|
||||
|
||||
## 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` 等于调用者 + `auth_time` 在窗口内,方才放行(provider 若开了 2FA,即在这次 prompt=login 往返中强制)。失败返回 `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 信任收敛到这唯一一刻。
|
||||
- **自签 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` 抽象,毫发无损**。折中被牢牢圈在“身份 / 会话层”,应用主体不动。这是本计划改造面可控、可分里程碑安全落地的根本保证。
|
||||
Reference in New Issue
Block a user