创建通行密钥以实现无密码登录

通行密钥可提高用户账号的安全性并简化用户账号的管理和使用。

使用通行密钥可增强安全性、简化登录流程并替代密码。与用户必须记住并手动输入的常规密码不同,通行密钥使用设备的屏幕锁定机制(例如生物识别或 PIN 码),可降低钓鱼式攻击风险和凭据盗用风险。

通行密钥会使用 Google 密码管理工具和 iCloud 钥匙串等通行密钥提供程序在设备间同步。

必须创建通行密钥,并将私钥安全地存储在通行密钥提供程序中,同时将必要的元数据和存储在服务器上用于身份验证的公钥存储在通行密钥中。私钥会在有效网域上完成用户验证后发出签名,从而使通行密钥能够防范钓鱼式攻击。公钥会验证签名,但不会存储敏感凭据,从而使通行密钥能够抵御凭据盗用。

通行密钥的创建方式

在用户能够使用通行密钥登录之前,您应先创建通行密钥,将其与用户账号相关联,并将其公钥存储在您的服务器上。

您可以在以下任一情况下要求用户创建通行密钥:

  • 在注册过程中或完成注册后。
  • 登录后。
  • 使用其他设备中的通行密钥登录后(即 [authenticatorAttachment](https://web.dev/articles/passkey-form-autofill#authenticator-attachment)cross-platform)。
  • 在专用页面上,用户可以管理通行密钥。

如需创建通行密钥,您可以使用 WebAuthn API

通行密钥注册流程的四个组成部分如下:

  • 后端:存储用户账号详细信息,包括公钥。
  • 前端:与浏览器通信并从后端提取必要数据。
  • 浏览器:运行 JavaScript 并与 WebAuthn API 交互。
  • 通行密钥提供程序:创建和存储通行密钥。这通常是密码管理工具(例如 Google 密码管理工具)或安全密钥。
创建和注册通行密钥的过程
创建和注册通行密钥的过程。

在创建通行密钥之前,请确保系统满足以下前提条件:

  • 系统会在短时间内通过安全的方法(例如电子邮件、电话验证或身份联合)验证用户账号。

  • 前端和后端可以安全地通信以交换凭据数据。

  • 浏览器支持 WebAuthn 和通行密钥创建。

我们将在后面的部分中向您展示如何检查其中的大多数问题。

当系统满足此条件后,系统会执行以下过程来创建通行密钥:

  1. 当用户发起操作(例如,在通行密钥管理页面中点击“创建通行密钥”按钮或完成注册后)时,系统会触发通行密钥创建流程。
  2. 前端从后端请求必要的凭据数据,包括用户信息、质询和凭据 ID,以防止重复。
  3. 前端调用 navigator.credentials.create() 以提示设备的通行密钥提供程序使用来自后端的信息生成通行密钥。请注意,此调用会返回一个 promise。
  4. 用户的设备使用生物识别方法、PIN 码或图案对用户进行身份验证,以创建通行密钥。
  5. 通行密钥提供程序会创建通行密钥,并将公钥凭据返回给前端,从而解析 Promise。
  6. 前端将生成的公钥凭据发送到后端。
  7. 后端会存储公钥和其他重要数据,以供日后进行身份验证,
  8. 后端会通知用户(例如,使用电子邮件)以确认通行密钥的创建,并检测潜在的未经授权的访问。

此流程可确保为用户提供安全、顺畅的通行密钥注册流程。

兼容性

大多数浏览器都支持 WebAuthn,但存在一些小问题。 如需了解浏览器和操作系统兼容性详情,请访问 passkeys.dev

创建新通行密钥

如需创建新的通行密钥,前端应遵循以下流程:

  1. 检查兼容性。
  2. 从后端提取信息。
  3. 调用 WebAuth API 以创建通行密钥。
  4. 将返回的公钥发送到后端。
  5. 保存凭据。

以下部分将介绍如何执行此操作。

检查兼容性

在显示“创建新通行密钥”按钮之前,前端应检查以下情况:

  • 浏览器支持使用 PublicKeyCredential 的 WebAuthn。

Browser Support

  • Chrome: 67.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

  • 设备支持使用 PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() 的平台身份验证器(可以创建通行密钥并使用通行密钥进行身份验证)。

Browser Support

  • Chrome: 67.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

  • 浏览器支持使用 PublicKeyCredenital.isConditionalMediationAvailable()WebAuthn 条件界面

Browser Support

  • Chrome: 108.
  • Edge: 108.
  • Firefox: 119.
  • Safari: 16.

Source

以下代码段展示了如何在显示与通行密钥相关的选项之前检查兼容性。

// Availability of `window.PublicKeyCredential` means WebAuthn is usable.  
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.  
// `isConditionalMediationAvailable` means the feature detection is usable.  
if (window.PublicKeyCredential &&  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&  
    PublicKeyCredential.isConditionalMediationAvailable) {  
  // Check if user verifying platform authenticator is available.  
  Promise.all([  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),  
    PublicKeyCredential.isConditionalMediationAvailable(),  
  ]).then(results => {  
    if (results.every(r => r === true)) {  
      // Display "Create a new passkey" button  
    }  
  });  
}  

在此示例中,只有在满足所有条件时,系统才应显示创建新通行密钥按钮。

从后端提取信息

当用户点击该按钮时,从后端提取所需信息以调用 navigator.credentials.create()

以下代码段显示了一个 JSON 对象,其中包含调用 navigator.credentials.create() 所需的信息:

// Example `PublicKeyCredentialCreationOptions` contents
{
  challenge: *****,
  rp: {
    name: "Example",
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },
  pubKeyCredParams: [{
    alg: -7, type: "public-key"
  },{
    alg: -257, type: "public-key"
  }],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

对象中的键值对包含以下信息:

  • challenge:服务器在 ArrayBuffer 中为此项注册生成的质询。
  • rp.id:RP ID(依赖方 ID)、网域和网站可以指定自己的网域,也可以指定一个可注册后缀。例如,如果 RP 的来源为 https://login.example.com:1337,则 RP ID 可以是 login.example.comexample.com。如果将 RP ID 指定为 example.com,则用户可以在 login.example.com 上或是在 example.com 的任何子网域上进行身份验证。如需详细了解,请参阅允许在您的网站中使用相关来源请求重复使用通行密钥
  • rp.name:RP(依赖方)的名称。此属性在 WebAuthn L3 中已废弃,但出于兼容性原因而包含。
  • user.id:ArrayBuffer 中的唯一用户 ID,在创建账号时生成。它应是永久性的,而用户名则可以修改。用户 ID 用于标识账号,但不应包含任何个人身份信息 (PII)。您的系统中可能已经有用户 ID,但如果需要,请专门为通行密钥创建一个,以确保其中不包含任何个人身份信息 (PII)。
  • user.name:用户能够识别的账号的唯一标识符,例如电子邮件地址或用户名。此 ID 将显示在账号选择器中。
  • user.displayName:账号的名称(必须提供),应更易于用户理解。该名称不必是唯一的,可以是用户选择的名称。如果您的网站没有适合此参数的值,传递一个空字符串即可。此参数可能会显示在账号选择器中,具体取决于浏览器。
  • pubKeyCredParams:指定 RP(依赖方)支持的公钥算法。我们建议将其设置为 [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]。因为这样设置后,即可支持采用 P-256 和 RSA PKCS#1 的 ECDSA,继而实现全面覆盖。
  • excludeCredentials:已注册凭据 ID 的列表。通过提供已注册凭据 ID 的列表,防止同一设备重复注册两次transports 成员(如果提供)应包含每个凭据注册期间调用 getTransports() 的结果。
  • authenticatorSelection.authenticatorAttachment:如果此通行密钥创建是从密码升级而来(例如,在登录后进行促销活动时),请将此值与 hint: ['client-device'] 一起设置为 "platform""platform" 表示 RP 希望使用平台身份验证器(嵌入到平台设备中的身份验证器),例如,不会提示用户插入 USB 安全密钥。用户可以通过更简单的方式创建通行密钥。
  • authenticatorSelection.requireResidentKey:将其设置为布尔值 true可检测到的凭据(常驻密钥)会将用户信息存储到通行密钥中,并允许用户在身份验证后选择账号。
  • authenticatorSelection.userVerification:用于指示是必须 ("required")、首选 ("preferred") 还是不建议 ("discouraged") 使用设备屏幕锁定功能进行用户验证。默认值为 "preferred",这意味着身份验证器可能会跳过用户验证。将其设置为 "preferred" 或省略该属性。

我们建议在服务器上构建对象,使用 Base64网址 对 ArrayBuffer 进行编码,然后从前端提取该对象。这样,您就可以使用 PublicKeyCredential.parseCreationOptionsFromJSON() 解码载荷,并将其直接传递给 navigator.credentials.create()

以下代码段展示了如何提取和解码创建通行密钥所需的信息。

// Fetch an encoded `PubicKeyCredentialCreationOptions` from the server.
const _options = await fetch('/webauthn/registerRequest');

// Deserialize and decode the `PublicKeyCredentialCreationOptions`.
const decoded_options = JSON.parse(_options);
const options = PublicKeyCredential.parseCreationOptionsFromJSON(decoded_options);
...

调用 WebAuthn API 以创建通行密钥

调用 navigator.credentials.create() 创建新的通行密钥。API 会返回一个 Promise,等待用户与显示模态对话框进行互动。

Browser Support

  • Chrome: 60.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

// Invoke WebAuthn to create a passkey.
const credential = await navigator.credentials.create({
  publicKey: options
});

将返回的公钥凭据发送到后端

使用设备的屏幕锁定功能验证用户后,系统会创建通行密钥并解析 promise,将 PublicKeyCredential 对象返回给前端。

承诺可能会因各种原因而被拒绝。您可以通过检查 Error 对象的 name 属性来处理这些错误:

  • InvalidStateError:设备上已存在通行密钥。系统不会向用户显示任何错误对话框。网站不应将其视为错误。用户希望注册本地设备,并且已成功注册。
  • NotAllowedError:用户已取消操作。
  • AbortError:操作已中止。
  • 其他异常:发生了意外错误。浏览器会向用户显示一个错误对话框。

公钥凭据对象包含以下属性:

  • id:所创建通行密钥的 Base64网址 编码 ID。此 ID 有助于浏览器在进行身份验证时确定设备中是否存在匹配的通行密钥。此值必须存储在后端的数据库中。
  • rawId:凭据 ID 的 ArrayBuffer 版本。
  • response.clientDataJSON:ArrayBuffer 编码的客户端数据。
  • response.attestationObject:ArrayBuffer 编码的证明对象。其中包含一些重要信息,例如 RP ID、标志和公钥。
  • authenticatorAttachment:如果是在支持通行密钥的设备上创建此凭据,此参数的值会是 "platform"
  • type:此字段始终设置为 "public-key"

使用 .toJSON() 方法对对象进行编码,使用 JSON.stringify() 对其进行序列化,然后将其发送到服务器。

...

// Encode and serialize the `PublicKeyCredential`.
const _result = credential.toJSON();
const result = JSON.stringify(_result);

// Encode and send the credential to the server for verification.  
const response = await fetch('/webauthn/registerResponse', {
  method: 'post',
  credentials: 'same-origin',
  body: result
});
...

保存凭据

在后端收到公钥凭据后,我们建议您使用服务器端库或解决方案,而不是编写自己的代码来处理公钥凭据。

然后,您可以将从凭据检索到的信息存储到数据库中,以备日后使用。

以下列表包含建议保存的媒体资源:

  • 凭据 ID:随公钥凭据一起返回的凭据 ID。
  • 凭据名称:凭据的名称。根据创建通行密钥的通行密钥提供程序(可通过 AAGUID 进行识别)为其命名
  • 用户 ID:用于创建通行密钥的用户 ID。
  • 公钥:随公钥凭据一起返回的公钥。这是验证通行密钥断言所必需的。
  • 创建日期和时间:记录通行密钥的创建日期和时间。这有助于识别通行密钥。
  • 上次使用日期和时间:记录用户上次使用通行密钥登录的日期和时间。这有助于确定用户使用过(或未使用过)哪些通行密钥。
  • AAGUID:通行密钥提供程序的唯一标识符。
  • 备份资格标志:如果设备符合通行密钥同步条件,则为 true。这些信息有助于用户在通行密钥管理页面上识别可同步的通行密钥和绑定到设备(不可同步)的通行密钥。

如需更详细的说明,请参阅服务器端通行密钥注册

如果注册失败,发出信号

如果通行密钥注册失败,可能会让用户感到困惑。如果通行密钥提供程序中存在可供用户使用的通行密钥,但关联的公钥未存储在服务器端,则使用通行密钥的登录尝试永远不会成功,并且很难进行问题排查。如果是这种情况,请务必告知用户。

为防止出现这种情况,您可以使用 Signal API 向通行密钥提供程序发送未知通行密钥信号。通过使用 RP ID 和凭据 ID 调用 PublicKeyCredential.signalUnknownCredential(),RP 可以告知通行密钥提供程序指定的凭据已被移除或不存在。通行密钥提供程序可以自行决定如何处理此信号,但如果受支持,则预计会移除关联的通行密钥。

// Detect authentication failure due to lack of the credential
if (response.status === 404) {
  // Feature detection
  if (PublicKeyCredential.signalUnknownCredential) {
    await PublicKeyCredential.signalUnknownCredential({
      rpId: "example.com",
      credentialId: "vI0qOggiE3OT01ZRWBYz5l4MEgU0c7PmAA" // base64url encoded credential ID
    });
  } else {
    // Encourage the user to delete the passkey from the password manager nevertheless.
    ...
  }
}

如需详细了解 Signal API,请参阅使用 Signal API 确保通行密钥与服务器上的凭据保持一致

向用户发送通知

在注册通行密钥时发送通知(例如电子邮件)有助于用户检测未经授权的账号访问。如果攻击者在用户不知情的情况下创建了通行密钥,那么即使密码已更改,通行密钥仍可供日后滥用。该通知会提醒用户并帮助防止这种情况。

核对清单

  • 请先验证用户(最好使用电子邮件或安全方法),然后再允许他们创建通行密钥。
  • 使用 excludeCredentials 防止为同一通行密钥提供程序创建重复的通行密钥。
  • 保存 AAGUID 以标识通行密钥提供程序并为用户命名凭据。
  • 如果尝试注册通行密钥失败,则发送 PublicKeyCredential.signalUnknownCredential() 信号。
  • 为用户的账号创建并注册通行密钥后,向用户发送通知。

资源

后续步骤:通过表单自动填充功能实现通行密钥登录