UniMail SSO 兼容边界
从理解,到动手实现。步骤、协议与可复制的示例,都在这份文档中。
2026-09-19 补充:顶层产品自动登录 已增加一次静默尝试与退出保护;它不等于本文的 UniMail iframe SSO 协议。普通登录不再默认 select_account。
日期:2026-09-07。依据:兄弟项目 unimail/docs/UNIDOC_UNIMAIL_SSO_V1.md 及实际宿主模块 static/vendor/unidoc-sso.js、static/index.html。
结论:正文嵌入可以继续兼容;unimail-sso-v1 免重复登录尚未实现、未部署、未通过真实账号联调。本轮只修改 UniDoc 的旧版正文桥,不宣告 SSO 能力,不修改 OmniDoc 或 UniMail。
本轮已经修改
正式嵌入地址保持:https://app.unidoc.top/?embed=1&embedView=flow。
- 仅真实 iframe 且显式
embed模式启用正文桥;普通被嵌入的应用页面不再自动提供正文读写协议。 - 接收验证当前
window.parent与精确 origin;发送绑定精确 origin,不再向*发送正文。优先采用浏览器提供的 referrer;无 referrer 时,通过真实父窗口的有效探测/正文命令绑定一次。不信任 URL 中的parent_origin。 - UniMail 的
unimail:auth:probe仅在正式 UniMail origin、正确 protocol 和 parentOrigin 下用于唤起旧版正文 ready;不发送unidoc:auth:ready或 authenticated。这是旧版兼容,不是身份认证。 - 空字符串/空白邮件正文归一为可编辑空段落,不再报 HTML 导入失败。
set/get/get-udoc有界串行处理,等待初始模板完成,避免异步 HTML 导出期间新草稿写入导致快照前后不一致;错误不会中断之后的请求。最多 32 个待处理请求,超限返回配对 error。- 保留
content-set/content/udoc/change/error消息名和请求 id、UDOC 严格校验及只读保护。ready 只发送一次,重复探测不重新触发宿主草稿初始化。
旧宿主如果使用 referrerpolicy="no-referrer",应在 iframe load 后发送:
frame.contentWindow.postMessage({ type: 'unidoc:probe' }, 'https://app.unidoc.top');UniMail 已发送其专用探测,因此无需为本轮正文兼容修改宿主。其他旧宿主需显式加 embed=1,不支持 origin 为 null 的 opaque/sandbox 父窗口。
不能直接接上的登录差异
| 项目 | UniDoc 当前实现 | UniMail v1 要求 / 后续工作 |
|---|---|---|
| 用户键 | base64url_no_pad(SHA256(issuer + NUL + sub));本地 userId 为 oidc_ 加该键 | 比对值为 lowercase_hex(SHA256(issuer + "\u007c" + sub)),分隔符是一个竖线 |
| 身份来源 | UniDoc 自己验证 OIDC ID Token 与 UserInfo 后建会话 | 从 UniDoc 验证后的身份另算兼容比对键,不能回显父页 expectedIdentity |
| 持久化 | oidcUsers/oidcSessions 保留内部 identityKey,不保留可供重新计算的原始 subject | 在以后合法 OIDC 回调时单独保存兼容键;旧会话缺失该键须重新验证,不能从旧哈希逆推 |
| 会话 Cookie | unidoc_session 和事务 Cookie 均为 SameSite=Lax | 跨站 iframe 需要独立、实测可用的会话机制;不能直接覆盖普通会话 Cookie |
| 默认 prompt | select_account;已有 none、空字符串选项 | 新的专用流程不强制选账号,保留必要 consent 与正常独立登录行为 |
| v1 流程 | 无 /auth/unimail/flows、/start、/complete | 需独立事务、verifier/challenge、明确第一方授权、到期/限流/一次性原子兑换及实际 iframe 会话复查 |
不能修改 UniDoc 的 opaque_identity_key 来“统一格式”。 它参与现有用户、会话和文件归属;直接换算法会让同一用户变成另一个本地身份。应新增仅用于跨产品比对的字段,保留既有资源所有权。
请 OmniDoc / UniMail 同事确认的一个前置事实
正式 UniMail client 与正式 UniDoc client 对同一账号签发的 sub 是否完全相同?若为 pairwise subject,请提供可信的服务端跨客户端映射方案。
本轮没有实际登录两个产品比较已验证 subject,也没有取得平台的客户端 subject 策略,不能声称已一致。提供商 Discovery 的 subject_types_supported 只能说明提供商支持哪些类型,不能证明这两个具体 client 的实际策略。
请提供配置结论或用专用测试账号在两端服务端完成相等性检查;不需要发送用户密码、Client Secret 或任何 token。邮箱、昵称、头像、父页 ownerKey 都不能作为自动合并依据。
后续实现边界(本轮未实施)
- 身份策略确认后,在合法 OIDC 回调中生成独立的 UniMail 比对键,保留原 userId 与云文件 scope。
- 设计第一方授权到 iframe 的独立会话桥:flowId 不是登录票据;父页永远拿不到 verifier;来源链无法证明时必须有一次明确授权确认;回调校验 state/nonce/PKCE,完成端校验接收者证明、同账号、期限与单次消费。
- 普通第一方会话与内嵌会话隔离;不自动换号,不退出任一产品。第一方登录成功不等于 iframe 登录成功。
- 子端仅在自己的服务器会话检查成功后按
authenticated → 带 auth 的 unidoc:ready回报;到期/换号冻结同步但不清空正文;恢复时不覆盖期间编辑。 - 默认关闭新能力,完成安全测试与真实浏览器测试后才能发
unidoc:auth:ready;不扩大 API CORS,不修改 OmniDoc 授权策略。 - 验证实际部署的 frame-ancestors 精确允许 UniMail;所有新
/auth/*流程 no-store。当前本地静态 Nginx 配置没有设置应用首页 frame-ancestors;不能据此推断线上外层代理的最终响应头。
Cookie 限制不能靠“同一个登录中心”消失。Lax 不覆盖跨站 iframe;None/Secure 也不保证第三方 Cookie 可用。CHIPS / Storage Access 若采用,必须分别验证目标浏览器和禁用第三方 Cookie 的情形。必要时一次第一方授权或独立窗口回退是正确结果,不能伪装成全浏览器无感成功。MDN Set-Cookie
跨客户端 subject 可能不同;应由平台确定是否采用公开 subject 或可信映射,而不是依赖邮箱相同。OpenID Connect Subject Identifier Types
消息来源与发送目标必须同时校验;origin 和 source 校验不等价于用户身份验证。MDN postMessage
本轮验证
node --test tests/unimail-embed-bridge.test.cjs:11/11 通过,执行从实际 app.js 提取的正文桥。覆盖明确嵌入开关、来源/窗口、无 referrer 探测、空草稿、请求顺序、错误恢复、只读检查、有界队列、变更通知及不误报 SSO。node tests/embed-mode-contract.test.js:通过。node --check js/app.js:通过。- 在 UniMail 工作目录只读运行原有测试:
node scripts/test-unidoc-sso.cjs9 suites 通过;node scripts/test-cloud-editor.cjs15 URL cases 与消息校验通过;node .deploy/test-inline.cjs通过。
以上是隔离消息测试和静态契约,不是完整编辑器渲染、真实提供商登录、跨站 Cookie 桥、生产部署或 Chrome/Edge/Firefox/Safari 免登录验收。新登录路由均未新增,不得让宿主当作线上可调用接口。