先说结论
auth_passkey_portal 不是“把后台的 Passkey 弹窗搬到 /my/security”这么简单。
从 views/templates.xml、static/src/js/passkeys_portal_create.js、static/src/js/passkeys_portal.js,再顺着 auth_passkey/models/auth_passkey_key.py 和 res_users.py 往下看,官方真正处理的是三层问题:
- 门户用户是否真的还是“当前这个人”。
- 浏览器刚注册出来的凭据能不能安全挂到当前账号。
- 用户删掉或新增 Passkey 之后,旧 session 还能不能继续被信任。
所以这套能力的本质不是“多一个登录方式”,而是:
让低权限的 portal 用户也能安全地自助管理认证凭据,同时不把账号接管风险带进来。
Portal 侧为什么不直接开放 CRUD
templates.xml 只是在 portal.portal_my_security 里插入了一块 Passkeys 区域:
- 展示名称、创建时间、最后使用时间
- 提供 Rename / Delete
- 提供 Add Passkey
表面上看,这很像普通资料页的列表管理。但真正关键的是:每个危险动作都没有直接裸调 ORM。
原因很简单:门户用户虽然有自己的账号,但并不代表他当前这个会话仍然可信。一个常见误解是:
- 既然已经登录了 portal
- 那我当然可以顺手增删认证器
源码明显不这么想。Odoo 把“改认证凭据”视为高风险动作,必须重新校验身份。
这就是为什么前端无论创建还是删除,都要经过 handleCheckIdentity(...)。它不是多余的 UX,而是把“普通已登录态”和“允许修改认证要素的高信任态”分开。
新增 Passkey:先复核身份,再启动 WebAuthn 注册
passkeys_portal_create.js 的主链路很清楚:
- 用户点击 Add Passkey。
- 前端先调用
res.users.action_create_passkey。 - 这个调用被
handleCheckIdentity()包住。 - 后端返回
registration options后,浏览器才执行startRegistration(serverOptions)。 - 前端创建一个临时
auth.passkey.key.create记录。 - 再调用
make_key(registration)完成落库。
这里最容易被忽略的点,是WebAuthn 注册并不是第一步。
第一步其实是 check_identity。
这说明 Odoo 设计者非常清楚:如果当前 portal 会话只是被别人借用了电脑、共享了浏览器、或者 session 泄漏了,那么“允许它直接新增 passkey”就等于让攻击者为自己绑定一个长期登录入口。
因此真正的顺序不是:
- 点击按钮
- 注册设备
- 成功
而是:
- 先证明你还是账号本人
- 再允许浏览器发起注册挑战
- 再把新 credential 挂到你的账号上
这个顺序,就是 portal 场景和普通 WebAuthn demo 最大的区别。
make_key() 为什么要重新计算 session token
auth_passkey_key.py 里的 make_key() 很值得细看。
它做了几件事:
- 从 session 里拿 challenge
- 校验浏览器回传的 registration response
- 通过
res.users.auth_passkey_key_ids创建新 key - 直接写入
credential_identifier和public_key - 最后重新计算
request.session.session_token
最后这一步很多人第一次看会觉得“有点绕”。
但它其实是在解决一个非常现实的问题:
会话签发时看到的用户认证要素集合,和现在数据库里的认证要素集合,已经不一样了。
如果 session token 不刷新,那么服务端就可能继续拿着“旧认证状态”看待这个会话。Odoo 通过把 auth_passkey_key_ids 纳入 session token 字段,确保新增或删除 passkey 后,当前会话的信任快照会同步更新。
这不是性能细节,而是安全边界。
删除 Passkey:重点不是删掉记录,而是避免“删错对象”
passkeys_portal.js 里删除动作同样走 handleCheckIdentity(),后端对应的是 action_delete_passkey()。
这个方法的关键点有两个:
- 只允许删除
create_uid == self.env.user.id的 key - 删除后再次重算 session token
也就是说,Portal 场景下真正危险的不是“不会删”,而是:
- 是否可能删到别人的凭据
- 删完后当前会话的可信状态有没有同步变化
源码专门通过 create_uid 做所有权边界,不是依赖前端传来的用户 ID,也不是仅靠 portal 页面上只展示自己的数据。因为前端展示永远不等于后端授权。
配套测试 test_portal_permissions 更直接:portal 用户尝试改管理员 passkey,会抛 AccessError。这就是官方在测试层明确锁住的越权场景。
Rename 看起来最轻,为什么也值得关注
重命名似乎只是体验问题,但 passkeys_portal.js 仍然通过 ORM 写 auth.passkey.key,而不是把它当成前端本地标签。
这意味着 passkey name 在 Odoo 里不是纯浏览器侧备注,而是账号凭据资产的一部分。它要满足两个目标:
- 用户自己能认出“这是公司电脑上的 Windows Hello,还是手机上的 iCloud Keychain”
- 平台还能维持记录级权限约束
所以 rename 虽然没有 delete 那么危险,但它依然被纳入同一条“用户只能改自己的 key”边界内。认证资产一旦进入数据库,就必须服从对象权限,而不能因为“只是名字”就降低规则。
门户 Passkey 真正解决的是“弱密码之后”的长期治理
很多团队把 portal 安全只理解成:
- 登录页加密码复杂度
- 忘记密码发邮件
- 高风险页面加二次确认
但 passkey portal 的意义在于,它把认证治理能力下放到了终端用户自助层。
一旦用户可以在 /my/security:
- 新增自己的 passkey
- 给不同设备命名
- 删除丢失设备的凭据
那安全团队就不需要每次都介入“换手机了怎么办”“旧电脑丢了怎么办”这类运营动作。
可与此同时,Odoo 也没有把这个自助流程做成“只要你还在线就能改”。它坚持:
- 高风险动作要复核身份
- 只能动自己的凭据
- 凭据集合变化后要刷新 session 信任态
这三点一起,才构成真正可上线的 portal self-service passkey。
实战里最容易踩的 4 个误区
误区 1:把已登录态当成高信任态
Portal 用户已经登录,不代表可以直接增删认证器。共享设备、被盗 cookie、旁路登录都可能让“已登录态”不再可靠。
误区 2:只做前端过滤,不做后端所有权校验
页面上只展示自己的 key 不够,后端必须检查 create_uid,否则直接改请求仍可能越权。
误区 3:新增 / 删除凭据后不刷新 session token
否则你以为系统看到了“认证面已变化”,其实旧 session 还沿用旧快照。
误区 4:把 portal passkey 当成纯前端功能
真正难点不在按钮和弹窗,而在 challenge、identity check、对象权限和 session invalidation 是不是打通。
我会怎么向业务团队解释这套设计
如果你要用一句最通俗的话解释给非技术同事听,我会这么说:
Portal Passkey 不是让用户“更方便登录”这么简单,而是让用户能安全地自己管理长期登录凭据,同时确保错的人不能趁机把账号据为己有。
所以它的价值既是体验,也是治理;既是无密码趋势,也是账号接管防线。
结语
看完这套源码后,一个判断会变得很清楚:auth_passkey_portal 真正升级的不是页面,而是信任模型。
它把门户用户从“只能被动使用登录方式”推进到“可以自助治理认证资产”,但每一步都用 check_identity、对象所有权和 session token 重算把风险重新圈住。
这也是 Odoo 平台层一贯的风格:
- UI 可以轻
- 交互可以顺
- 但安全边界绝不靠“大家应该会正常使用”来赌
这,才是 Portal Passkey 值得写一篇文章的原因。ey 值得写一篇文章的原因。
DISCUSSION
评论区