UniDoc说明中心下载 .udoc
UNIDOC / KNOWLEDGE IN MOTION

UniMail SSO 兼容边界

从理解,到动手实现。步骤、协议与可复制的示例,都在这份文档中。

2026-09-19 补充:顶层产品自动登录 已增加一次静默尝试与退出保护;它不等于本文的 UniMail iframe SSO 协议。普通登录不再默认 select_account

日期:2026-09-07。依据:兄弟项目 unimail/docs/UNIDOC_UNIMAIL_SSO_V1.md 及实际宿主模块 static/vendor/unidoc-sso.jsstatic/index.html

结论:正文嵌入可以继续兼容;unimail-sso-v1 免重复登录尚未实现、未部署、未通过真实账号联调。本轮只修改 UniDoc 的旧版正文桥,不宣告 SSO 能力,不修改 OmniDoc 或 UniMail。

本轮已经修改

正式嵌入地址保持:https://app.unidoc.top/?embed=1&embedView=flow

  1. 仅真实 iframe 且显式 embed 模式启用正文桥;普通被嵌入的应用页面不再自动提供正文读写协议。
  2. 接收验证当前 window.parent 与精确 origin;发送绑定精确 origin,不再向 * 发送正文。优先采用浏览器提供的 referrer;无 referrer 时,通过真实父窗口的有效探测/正文命令绑定一次。不信任 URL 中的 parent_origin
  3. UniMail 的 unimail:auth:probe 仅在正式 UniMail origin、正确 protocol 和 parentOrigin 下用于唤起旧版正文 ready;不发送 unidoc:auth:ready 或 authenticated。这是旧版兼容,不是身份认证
  4. 空字符串/空白邮件正文归一为可编辑空段落,不再报 HTML 导入失败。
  5. set/get/get-udoc 有界串行处理,等待初始模板完成,避免异步 HTML 导出期间新草稿写入导致快照前后不一致;错误不会中断之后的请求。最多 32 个待处理请求,超限返回配对 error。
  6. 保留 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 回调时单独保存兼容键;旧会话缺失该键须重新验证,不能从旧哈希逆推
会话 Cookieunidoc_session 和事务 Cookie 均为 SameSite=Lax跨站 iframe 需要独立、实测可用的会话机制;不能直接覆盖普通会话 Cookie
默认 promptselect_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 都不能作为自动合并依据。

后续实现边界(本轮未实施)

  1. 身份策略确认后,在合法 OIDC 回调中生成独立的 UniMail 比对键,保留原 userId 与云文件 scope。
  2. 设计第一方授权到 iframe 的独立会话桥:flowId 不是登录票据;父页永远拿不到 verifier;来源链无法证明时必须有一次明确授权确认;回调校验 state/nonce/PKCE,完成端校验接收者证明、同账号、期限与单次消费。
  3. 普通第一方会话与内嵌会话隔离;不自动换号,不退出任一产品。第一方登录成功不等于 iframe 登录成功。
  4. 子端仅在自己的服务器会话检查成功后按 authenticated → 带 auth 的 unidoc:ready 回报;到期/换号冻结同步但不清空正文;恢复时不覆盖期间编辑。
  5. 默认关闭新能力,完成安全测试与真实浏览器测试后才能发 unidoc:auth:ready;不扩大 API CORS,不修改 OmniDoc 授权策略。
  6. 验证实际部署的 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.cjs 9 suites 通过;node scripts/test-cloud-editor.cjs 15 URL cases 与消息校验通过;node .deploy/test-inline.cjs 通过。

以上是隔离消息测试和静态契约,不是完整编辑器渲染、真实提供商登录、跨站 Cookie 桥、生产部署或 Chrome/Edge/Firefox/Safari 免登录验收。新登录路由均未新增,不得让宿主当作线上可调用接口。