Practical FIDO: Implementing Passwordless Login

实战 FIDO 验证实现无密码登录

前言

不同网站的密码需要管理,不仅容易忘记,也存在被窃取或重复使用带来的风险。如果只需要扫描指纹或使用脸部识别,就能立即完成注册与登入——这正是 FIDO(Fast IDentity Online)所推动的「无密码身分验证」愿景。

透过 FIDO 能享受以下优势:

  • 伺服器没有储存密码就不怕被偷
  • 攻击者无法透过网路拦截,因为私钥从头到尾都不会被传输
  • 绑定网站的来源(Origin)并强制使用 HTTPS 建立安全连线。就算用户误入以假乱真的钓鱼网站,浏览器也不会放行。
  • 可搭配生物辨识提升「解锁私钥」的安全性

FIDO 大概念

利用非对称式加密来验证身份,彻底抛弃帐号密码
  • 挑战(challenge):每次请求都不同且只能用一次的随机字串,用于让私钥进行签章
  • 签章(signature):拿私钥与挑战进行运算的动作或结果
  • FIDO 注册:用户产生一组「公钥 / 私钥」,网站保存公钥,用户保存私钥
  • FIDO 登入:验证用户持有私钥、用户回应是针对挑战即时产生的(防重放攻击)
  • 伺服器产生挑战
  • 使用者的装置用私钥对挑战进行签章
  • 伺服器使用公钥来验证签章是否正确
  • 生物识别:用户解锁私钥的一种方式,例如指纹。
  • Passkey:一种管理私钥的机制。建构在「私钥」技术之上的一套数位钥匙圈系统,让私钥变得好懂好管且不怕弄丢。

FIDO 实践重点

FIDO 之所以可行是因为浏览器提供了 Web Authentication API🔗 能安全与私钥互动、用户验证、Origin 绑定、装置绑定。

  • 用户 → navigator.credentials.create()

  • OS / 硬体帮你产 key

  • 私钥锁在装置

  • 公钥给伺服器

  • 用户 → navigator.credentials.get()

  • 用私钥签挑战

  • 回传签章

  • 伺服器用公钥验证

FIDO 流程图

注册

伺服器 (RP Server)FIDO 客户端 (Client)(如浏览器、App)验证器 (Authenticator)(如手机、安全金钥)使用者 (User)伺服器 (RP Server)FIDO 客户端 (Client)(如浏览器、App)验证器 (Authenticator)(如手机、安全金钥)使用者 (User)验证器针对该伺服器网域 (RP ID)随机产生一对全新的「公私钥 (Key Pair)」验证器将「私钥」安全地封装在硬体晶片内并用私钥对挑战码等资料建立「认证签章 (Attestation)」伺服器验证签章是否有效、挑战码是否吻合验证通过后,将该使用者的「公钥」与帐号绑定存入资料库alt[注册成功][注册失败]点击注册 / 绑定安全装置 (Passkey)1发送注册初始化请求2产生并回传「挑战码 (Challenge)」、伺服器网域 (RP ID) 及使用者资讯3呼叫 WebAuthn API,传递挑战码与注册参数4弹出视窗,要求授权建立凭证5进行本地验证 (如指纹、脸部辨识、PIN 码)6回传认证物件 (包含「公钥」与签章)7将认证物件送往伺服器完成注册8注册完成9显示装置绑定成功讯息10拒绝注册 (如验证错误或超时)11显示注册失败讯息12

登入

伺服器 (RP Server)FIDO 客户端 (Client)(如浏览器、App)验证器 (Authenticator)(如手机指纹、安全金钥)使用者 (User)伺服器 (RP Server)FIDO 客户端 (Client)(如浏览器、App)验证器 (Authenticator)(如手机指纹、安全金钥)使用者 (User)验证成功后,验证器找出当初注册的私钥并用该私钥对「挑战码」进行数位签章伺服器找出该帐号绑定的公钥验证签章是否正确,并确认挑战码未被窜改alt[验证成功][验证失败]点击登入 (输入帐号或无密码登入)1发送登入请求2产生并回传「挑战」3将挑战码与伺服器网域 (RP ID) 交给验证器4弹出视窗,要求进行本地验证5进行生物辨识 (如指纹/脸部) 或输入 PIN 码6回传签章结果 (Assertion)7将签章结果与验证资料送回伺服器8允许登入,核发 Session/Token9登入成功,进入系统10拒绝登入11显示登入失败讯息12

实践

注册

  • 阶段一:生成 (Begin)
  1. 使用者登入或刚注册后,申请绑定 Passkey。
  2. 前端呼叫:POST /auth/passkey/register/begin
  3. 后端回传资讯:
  • challenge:一次性随机字串。
  • user 资讯:包含 id, name, displayName
  • relying party (RP):网站 Domain 或 rpId
  • pubKeyCredParams:支援的加密演算法。
  1. 前端执行 navigator.credentials.create()
  • 产生公钥与私钥、对挑战进行 Attestation(证明金钥来源与合法性)

  • 阶段二:验证 (Finish)

  1. 前端将结果送回后端:POST /auth/passkey/register/finish
  2. 后端进行多重验证:
  • 一致性:挑战是否与先前发出的一致(防重放攻击)。
  • 合法性:Attestation 是否合法(确认装置与金钥可信)。
  • 来源:Origin / rpId 是否正确(防止钓鱼网站)。
  1. 验证成功后存档:
  • public keycredential idcounter (计数器) 与 user 关联。
  1. 完成注册:该使用者已成功绑定一组 Passkey。

登入

  • 阶段一:生成 (Begin)
  1. 使用 FIDO 登入
  2. 前端呼叫:POST /auth/passkey/login/begin
  3. 后端回传资讯:
  • challenge:挑战字串。
  • allowCredentials:该使用者已注册过的 Credential IDs(若留空则代表使用 Discoverable Credentials 模式)。
  1. 前端执行 navigator.credentials.get()
  • 使用者认证:透过指纹、FaceID 或 PIN 码解锁私钥。

  • 签署:使用私钥对挑战进行签章。

  • 阶段二:验证 (Finish)

  1. 前端将签章结果送回后端:POST /auth/passkey/login/finish
  2. 后端验证逻辑:
  • 挑战码:挑战是否一致。
  • 签章:使用资料库中的公钥验证签章是否正确。
  • 计数器:检查 counter 是否大于前次(防止重放或复制装置)。
  • 来源:Origin / rpId 是否正确。
  1. 验证成功后处理:
  • 更新资料库中的 counter
  • 建立登入状态(签发 Session 或 JWT)。
  1. 完成登入:实现无密码安全登入。

可发现凭证(驻留金钥)

传统「不可发现凭证 Non-Discoverable」的验证方式伺服器必须先知道你是谁(先输入帐号),伺服器才能从资料库找出对应的 Credential ID 送给认证器。认证器解密后才能用私钥签章。

而现代认证器空间够大。注册时,它不仅产生公私钥,还会把私钥、使用者 ID、帐号名称、以及网站的网域 (RP ID) 全部存在认证器。

登入时伺服器不需要知道你是谁,只要问认证器:「你有这个网站(RP ID)的凭证吗?」认证器就会在本地「发现」这些凭证,并跳出选单问你要用哪一个帐号登入,完全不需要输入帐号。

注册

navigator.credentials.create({
publicKey: {
// ... 其他设定
authenticatorSelection: {
authenticatorAttachment: "platform",
// 要求建立可发现凭证
residentKey: "required",
userVerification: "required"
}
}
});
  • residentKey
  • required:必须建立可供搜寻的凭证。如果无法建立,系统会传回 NotSupportedError
  • preferred:RP 偏好建立可供探索的凭证,但也接受不可探索的凭证。
  • discouraged:RP 偏好建立不可侦测的凭证,但也接受可侦测的凭证。
  • requireResidentKey: 为了与旧版规格的 WebAuthn Level 1 保持回溯相容性,这个属性会保留。如果 residentKey 是 required,请将此值设为 true,否则请设为 false。

登入

在发起验证请求时,你不需要在 allowCredentials 中带入特定的 Credential ID。

navigator.credentials.get({
publicKey: {
challenge: serverGeneratedChallenge,
rpId: "yourdomain.com",
// 留空或不提供 allowCredentials 阵列
// 认证器会自动根据 rpId 去本地「发现」所有可用的帐号
allowCredentials: [],
userVerification: "required"
}
});

总结

FIDO 规格文件🔗有明确的验证步骤确保不产生安全漏洞,以及不同浏览器与平台的差异,实战上最好依靠现成套件来处理,这也是为什么会有这么多 FIDO 套件,把所有复杂情境包装成 verifyRegistrationResponse()verifyAuthenticationResponse() 能够直接使用。

延伸閱讀