passkey、MFA、TOTP、OIDC、JWT 到底有什么区别?给浏览器里的数据库编辑器加上 passkey 登录之后的总结

我是开源 SQL 编辑器 LibreDB Studio 的维护者之一。它是 MIT 协议,在浏览器里使用。这次不讲功能,只讲最近做 passkey 登录时理清楚的几个概念。

在浏览器里使用的数据库编辑器,登录的分量和大多数应用不一样。登录的人拿到的不只是一个账号,而是所有已保存的连接,连同已经配置好的凭据:生产环境的 Postgres、Redis、Mongo。这里泄露一个密码,泄露的不是个人资料,而是数据。

做的过程中,我们反复遇到同一个混淆:MFA、TOTP、passkey、OIDC 和 JWT 总是在讨论登录时一起出现,但它们回答的是不同的问题。

各自回答一个问题

  • 密码: "你知道这个秘密吗?"只有一个因素,可能被重复使用、泄露,或者被输入到钓鱼网站里。
  • MFA: 不是一种技术,而是一条规则:证明至少两种不同类型的因素(你知道的、你拥有的、你本身的特征)。密码 + TOTP 是 MFA,用 PIN 或生物识别解锁的 passkey 也是。
  • TOTP: 证明"手机在我手上"的一种方式。服务器和 App 共享一个密钥,验证码是用这个密钥对以 30 秒为单位的时间做 HMAC,再截成 6 位(RFC 6238)。
  • Passkey(WebAuthn): 一对密钥。私钥留在设备或密码管理器里,服务器只保存公钥。登录时,设备对服务器发来的挑战(challenge)签名。
  • OIDC: 不是登录方式,而是委托。由另一个系统(Keycloak、Entra ID、Okta、Google)按它自己的方式认证用户,再把 ID Token 交给应用。
  • JWT: 也不是登录,而是一种签名令牌的格式。用户通过上面任意一种方式证明身份之后,服务器签发一个会话,这个会话可以是 JWT。

所以"用 OIDC 还是用 passkey"不是同一层面的选择:用 OIDC 时,passkey 在 IdP 那边。团队已经有 IdP 的话就用它,MFA、passkey 和设备策略都集中在一个地方。应用自带的 passkey 是给没有 IdP 的人用的,也就是全部自托管的小团队这种常见情况。至于"我们用了 JWT",这句话对登录有多强什么也没说明。

为什么只有 TOTP 不够

TOTP 能防密码泄露,防不了钓鱼。用户读到 6 位数字,就会在要求输入的地方输入。像 Evilginx 这样的钓鱼代理会展示真实的登录页面,拿到密码和验证码,在 30 秒内一起转发给真实网站。第二个因素确实起作用了,只不过是为攻击者起作用。

passkey 靠机制而不是靠用户的警惕来解决这个问题。浏览器把页面的 origin 放进被签名的数据里,并且只在创建 passkey 的域名上使用它。在 studio-login.example.com 的页面上,studio.example.com 的 passkey 根本不会出现;在别的 origin 上做出的签名也通不过服务器的校验。

还有一个很少被提到的好处:服务器只保存公钥。账号库即使泄露,里面也没有任何能用来登录的东西。TOTP 密钥则不同,服务器为了计算验证码必须把它存下来。

实现过程中学到的三点

  1. 强制 user verification。 没有它,一把没有 PIN 的安全密钥只能证明"钥匙在我手上"。如果 passkey 登录跳过了 TOTP,就等于把两个因素换成了一个。注册和登录都强制 PIN 或生物识别之后,passkey 就是完整的 MFA(NIST SP 800-63B-4,AAL2),可以同时替代密码和验证码。
  2. origin 来自配置,而不是来自请求。 期望的 origin 和 RP ID 从环境变量(PASSKEY_ORIGIN)读取,从不取自 Host、X-Forwarded-Host 或请求 URL。如果取自请求,校验就变成拿签名去和请求自己的说法对比,等于什么也没校验。而且在反向代理后面,这个值往往是内部名称,而不是浏览器看到的地址。
  3. 注册 passkey 时重新要求密码。 拿着被盗的会话 Cookie,攻击者就可以注册自己的 passkey,这是一个会话过期之后仍然能打开的后门。所以添加或删除 passkey 都要求当前密码,账号开了 TOTP 的话还要验证码。

JWT 出现在最后。无论哪种登录(密码 + TOTP、passkey 或 OIDC),服务器签发的都是同一种会话:放在 HttpOnly Cookie 里的 JWT。JWT 的经典弱点是已签发的令牌无法撤销。所以我们的令牌带着一个会话版本号,服务器会和保存的账号核对:停用账号或删除 passkey 会改变这个版本号,其他令牌随之失效。

动手试试

passkey 登录从 [版本] 版本开始提供。

docker run -d --name libredb-studio -p 3000:3000 \
  -v libredb-data:/app/data \
  -e STORAGE_PROVIDER=sqlite \
  -e PASSKEY_ORIGIN=http://localhost:3000 \
  ghcr.io/libredb/libredb-studio:latest

请务必打开 http://localhost:3000,不要用 127.0.0.1:浏览器不会在 IP 地址上使用 passkey。用 docker logs libredb-studio 里打印的密码以 [email protected] 登录,打开用户菜单里的 Sign-in security,点 Add passkey。然后退出,在登录页用 Use a passkey 登录。

完整流程,以及它能防什么、不能防什么,都写在 docs/PASSKEYS.md 里:libredb-studio/docs/PASSKEYS.md at main · libredb/libredb-studio · GitHub

想听听在生产环境管理数据库的朋友:你们团队访问数据库编辑器是走 SSO,还是用本地账号?有没有人真的遇到过实时的 TOTP 钓鱼?

声明:本文内容由作者整理,写作和中文翻译借助了 AI 工具。

6 个赞

2 个答案

2

总结得很清楚,特别是"它们回答的是不同的问题"这个切入点,之前一直把 OIDC 和 passkey 放在同一层比较,看完终于理顺了。

1 个赞

passkey 是真好用,但国内基本没有什么讨论的人的。另外老外来中文论坛真的少见……

1 个赞