启用 Web 身份验证 API (WebAuthn) 密钥

Passkeys 基于 Web 身份验证 API(WebAuthn)FIDO2 标准提供了一种具有钓鱼能力的新式身份验证方法。 它们是使用公钥加密和设备身份验证的密码的安全替代方法。 本文介绍如何将 ASP.NET Core 应用配置为使用密钥对用户进行身份验证。

有关特定于新和现有 Blazor Web Apps 的指导,请参阅阅读本文后 ,在 ASP.NET Core Blazor Web App中实现密钥

什么是密钥?

密码是使用加密密钥对的密码的替代项。 私钥安全地存储在用户的设备上,例如在硬件安全模块、平台验证器(例如:Windows Hello、Touch ID、Face ID)或密码管理器中,而公钥由 Web 应用存储。 在身份验证期间,用户证明拥有私钥,而无需离开其设备。

passkey 的主要优点包括:

  • 网络钓鱼抵抗:密码绑定到特定网站,不能在虚假网站上使用。
  • 无共享机密:服务器仅存储公钥,消除了密码数据库泄露的风险。
  • 用户便利:简单的生物识别或 PIN 验证取代了复杂的密码要求。
  • 跨设备同步:许多密钥提供程序跨用户的设备同步凭据。

有关详细信息,请参阅 Web 身份验证 API (MDN 文档)。

ASP.NET Core 中的密码 Identity

ASP.NET Core Identity 包括对密钥注册和身份验证的内置支持:

  • 与 Identity 基础结构无缝集成。
  • 对最常见的 WebAuthn 方案的用户身份验证支持。
  • 内置于项目模板中 Blazor Web App ,因此只需要开发人员配置。

重要

ASP.NET Core Identity 中的密钥实现故意限定为身份验证方案。 它不用作常规用途 WebAuthn 库。 需要完整 WebAuthn 功能的开发人员应考虑提供全面协议支持的社区库。

支持的方案

ASP.NET 核心 Identity 密钥实现支持以下主要方案:

  • 将密钥添加到现有帐户:具有基于密码的帐户的用户可以将密码注册为其他身份验证方法。
  • 无密码帐户创建:用户可以通过在帐户创建时注册密码来创建没有密码的帐户。
  • 无密码登录:用户只需使用其密码即可进行身份验证,而无需输入密码。

局限性

当前实现具有以下限制:

  • 范围限定为 ASP.NET 核心 Identity:API 专为 Identity 身份验证方案而设计。
  • 无默认证明验证:默认情况下,实现不会验证证明语句。
  • 模板支持:仅 Blazor Web App 模板包含密钥支持。
  • 没有内置 2FA 支持:密码被视为主要身份验证因素,而不是第二个因素。

核心概念

两个基本过程支撑了密钥作:证明和断言。

证明(注册)

证明 是创建和注册新密钥的过程。 在证明期间,服务器将生成身份验证器必须包含在返回的凭据中的唯一质询。 验证器会创建一个新的密钥对,并返回公钥以及证明密钥的来源证明数据。 然后,服务器会验证此证明,并存储公钥以用于将来的身份验证尝试。

断言(身份验证)

断言 是使用现有密钥进行身份验证的过程。 服务器生成一个唯一的挑战,验证器使用私钥进行签名。 验证器将此签名断言返回到服务器,该断言使用以前存储的公钥验证签名。 如果签名有效,则会对用户进行身份验证。

先决条件

  • .NET 10 SDK
  • 支持 WebAuthn 的新式 Web 浏览器。
  • 具有平台验证器的设备,例如 Windows Hello 或 Apple 安全 enclave 或安全密钥。

安全注意事项

在 ASP.NET Core Identity中实现通行密钥时,请确保应用满足本节中所述的安全要求。

主机标头验证

如果未显式配置,则实现会从主机标头 ServerDomain 推断信赖方 ID。 托管环境必须验证主机标头,以防止凭据范围攻击,这些攻击涉及使用泄露或被盗的用户凭据(用户名、密码、令牌)来获得未经授权的访问。

缓解:显式配置 ServerDomainIdentityPasskeyOptions 确保托管环境(KestrelIIS,反向代理)验证主机标头。 有关配置详细信息,请参阅托管平台的文档。

子域安全性

ASP.NET Core 的密钥实现通过 ServerDomain 配置选项处理子域安全性。 ServerDomain如果未显式指定,则实现将使用主机标头来确定域。 这意味着 已注册密钥的页面控制 该凭据的域。

例如:

  • 如果注册 app.contoso.com了密钥,则它还适用于 *.app.contoso.com该密钥。
  • 如果已注册 contoso.com,它也适用于 *.contoso.com
  • 浏览器强制要求,密钥只能在注册的域(和子域)上使用。

要求:需要严格域控制的应用应显式设置 ServerDomain ,而不是依赖于主机标头。 不要在范围内的任何子域 ServerDomain 上提供不受信任的内容。 如果无法保证这一点,请实施 自定义源验证 ,以将密钥用法限制为特定源。

HTTPS 要求

所有密钥作都需要 HTTPS。 该实现将身份验证数据存储在加密和签名的 Cookie 中,这些 Cookie 可以通过未加密的连接截获。

要求:始终在生产环境中使用 HTTPS。 配置 HTTP 严格传输安全协议(HSTS), 以防止协议降级攻击。

帐户恢复

帐户恢复主要涉及允许将密钥作为唯一身份验证机制的应用。 默认 Blazor Web App 项目模板已在创建帐户时要求用户设置备份身份验证方法(密码或外部提供程序),因此帐户恢复通过这些现有机制进行处理。

建议

对于实现仅限密钥身份验证的应用程序,请考虑:

  • 创建帐户期间生成的恢复代码。
  • 基于电子邮件的恢复流。
  • 强制注册多个密钥。
  • IsBackedUp监视标志UserPasskeyInfo以提示用户添加其他凭据。

管理控制

发现验证器模型存在安全漏洞时,可能需要撤销受影响的凭据。 该实现使用每个凭据存储完整的证明对象,包括 Authenticator 证明 GUID (AAGUID),这是一个指示密钥类型的 128 位标识符。

实现:从存储的证明对象中提取 AAGUID,与已知泄露的模型进行比较,并撤销受影响的凭据。 AAGUID 可靠性取决于应用是否验证证明语句。 若要在自定义证明语句验证逻辑中挂钩,请参阅 自定义证明语句验证。 第三方库可用于证明验证,例如 Passkeys - FIDO2 .NET 库 (WebAuthn) (passwordless-lib/fido2-net-lib GitHub 存储库)†。

警告

†第三方库(包括 passwordless-lib/fido2-net-lib)不由Microsoft拥有或维护,并且不受任何Microsoft支持协议或许可证保护。 采用第三方库时,请谨慎使用,尤其是对于安全功能。 确认该库遵循官方规范并采用安全最佳做法。 使库的版本保持最新状态,以获取最新的 bug 修复。

资源限制

为了防止数据库耗尽攻击,应用应对密钥注册强制实施限制,例如:

  • 每个用户帐户的最大密钥数。
  • 传递密钥显示名称的最大长度。

默认情况下,模板 Blazor Web App 会在应用程序级别强制实施这些限制。 有关示例,请参阅项目模板中的Razor以下Blazor Web App组件:

注释

指向 .NET 引用源的文档链接通常会加载存储库的默认分支,该分支代表正在进行的 .NET 下一版本的开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉菜单。 有关详细信息,请参阅如何选择 ASP.NET Core 源代码的版本标记 (dotnet/AspNetCore.Docs #26205)

配置 passkey 选项

ASP.NET Core Identity 提供了通过类配置密钥行为 IdentityPasskeyOptions 的各种选项,其中包括:

  • AuthenticatorTimeout:获取或设置浏览器应等待验证器提供通行密钥的时间 TimeSpan。 此选项适用于创建新密钥并请求现有密钥。 此选项被视为浏览器提示,浏览器可能会忽略该选项。 默认值为 5 分钟。
  • ChallengeSize:获取或设置在证明和断言期间发送到客户端的质询的大小(以字节为单位)。 此选项适用于创建新密钥并请求现有密钥。 默认值为 32 字节。
  • ServerDomain:获取或设置服务器的有效信赖方 ID(域)。 这应是唯一的,将用作服务器的标识。 此选项适用于创建新密钥并请求现有密钥。 如果是 null默认值,则使用服务器的源。 有关详细信息,请参阅 信赖方标识符 RP ID

示例配置:

builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.ServerDomain = "contoso.com";
    options.AuthenticatorTimeout = TimeSpan.FromMinutes(3);
    options.ChallengeSize = 64;
});

有关 .NET 10 预览版发布期间配置选项的完整列表,请参阅IdentityPasskeyOptions参考源(dotnet/aspnetcoreGitHub 存储库)。

注释

指向 .NET 引用源的文档链接通常加载存储库的默认分支,该分支表示下一个 .NET 预览版的当前开发。 若要为特定版本选择标记,请使用“切换分支或标记”下拉菜单。 有关详细信息,请参阅如何选择 ASP.NET 核心源代码的版本标记(dotnet/AspNetCore.Docs#26205)。

注释

截至 2025 年 8 月,API 文档中提到的浏览器默认值有效。 有关最 up-to日期默认值,请参阅 W3C WebAuthn 规范

自定义证明语句验证

默认情况下,ASP.NET Core Identity 不会验证证明语句。 这适用于大多数使用者身份验证方案。 如果你的应用需要验证验证器属性,或者如果你希望禁止使用特定的验证器,例如,在需要更高级别的安全性的企业环境中,则可以实现自定义证明验证:

builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.VerifyAttestationStatement = async (context) =>
    {
        // Custom attestation validation logic
        // Return 'true' if the attestation is valid
        // Return 'false' if the attestation is invalid
        return true;
    };
});

警告

证明验证很复杂,需要维护验证器证书的信任存储。 仅当应用需要验证特定的验证器属性时,才实现自定义验证。

自定义源验证

默认源验证允许来自子域的请求,并禁止跨域 iframe。 若要自定义此行为,请执行以下作:

builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.ValidateOrigin = async (context) =>
    {
        // Custom origin validation logic
        //   Access the origin via 'context.Origin'
        //   Access the HTTP context via 'context.HttpContext'
        // Return 'true' if the origin is valid
        // Return 'false' if the origin is invalid
        return true;
    };
});

注册流

本部分逐步讲解密钥注册过程的每个步骤,说明 ASP.NET Core Identity 如何促进密钥凭据的创建和存储。

sequenceDiagram
    participant Authenticator
    participant User
    participant Browser
    participant Server

    User->>Browser: Click "Add passkey"
    Browser->>Server: Request creation options
    Server->>Browser: Return creation options
    Browser->>Authenticator: Request new credential
    Authenticator->>User: Verify identity (biometric/PIN)
    User->>Authenticator: Approve
    Authenticator->>Browser: Return credential
    Browser->>Server: Submit credential
    Server->>Server: Verify and store
    Server->>Browser: Registration complete
    Browser->>User: Success message

步骤 1:启动注册

当用户决定将密钥添加到其帐户时,注册过程将开始。 这通常通过应用的用户界面中的按钮或链接进行。 选中后,此元素将触发 JavaScript 代码来协调注册流。

客户端实现在应用之间差异很大。 在 Blazor Web App 模板中 PasskeySubmit.razor.js,可以找到一个完整的示例,其中显示了自定义 Web 组件如何处理注册启动和管理后续的 WebAuthn API 调用。

步骤 2:请求创建选项

启动注册后,浏览器必须从服务器获取创建选项。 这些选项告知浏览器要创建和包含重要安全参数的凭据类型,例如必须签名的挑战。

从浏览器的角度来看,此步骤涉及向服务器发出 HTTP 请求:

async function createCredential(headers, signal) {
  // Step 2: Request creation options from the server
  const optionsResponse = 
    await fetchWithErrorHandling('/Account/PasskeyCreationOptions', 
    {
      method: 'POST',
      headers,
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
  return await navigator.credentials.create({ publicKey: options, signal });
}

应用程序应定义生成以下选项的终结点:

app.MapPost("/Account/PasskeyCreationOptions", async (
    HttpContext context,
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager) =>
{
    var user = await userManager.GetUserAsync(context.User);

    if (user is null)
    {
        return Results.NotFound();
    }

    var userId = await userManager.GetUserIdAsync(user);
    var userName = await userManager.GetUserNameAsync(user) ?? "User";
    
    var optionsJson = await signInManager.MakePasskeyCreationOptionsAsync(new()
    {
        Id = userId,
        Name = userName,
        DisplayName = userName
    });
    
    return TypedResults.Content(optionsJson, contentType: "application/json");
});

该方法 MakePasskeyCreationOptionsAsync 是此过程的核心。 该方法接受描述 PasskeyUserEntity 为其创建密钥的用户。 此实体包含用户的 ID、用户名(通常是电子邮件地址)和人类可读的显示名称。 该方法返回符合 WebAuthn PublicKeyCredentialCreationOptions 架构的 JSON 字符串,浏览器在下一步中使用该架构。 在后台,此方法还会将临时状态存储在身份验证 cookie 中,以确保来自浏览器的响应对应于这些特定选项。

步骤 3:服务器生成选项

执行时 MakePasskeyCreationOptionsAsync ,它使用应用的 IdentityPasskeyOptions 配置来确定用于创建凭据的特定参数。 这些选项控制密钥创建过程的各个方面。

可以在应用程序启动期间自定义这些选项。 例如:

builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.ServerDomain = "contoso.com";
    options.AuthenticatorTimeout = TimeSpan.FromMinutes(3);
    options.UserVerificationRequirement = "required";
    options.ResidentKeyRequirement = "preferred";
});

UserVerificationRequirement 选项确定验证器是否必须验证用户的标识(通过生物识别或 PIN 方法),同时 ResidentKeyRequirement 指示凭据是否应可发现,从而允许身份验证,而无需首先提供用户名。 有关 .NET 10 预览版发布期间的详细信息,请参阅IdentityPasskeyOptions参考源(dotnet/aspnetcoreGitHub 存储库)。

步骤 4:客户端请求凭据

使用可用的创建选项时,客户端 JavaScript 会将选项传递给 WebAuthn API 以创建新凭据:

async function createCredential(headers, signal) {
  // Step 4: Parse the options and request a new credential from the authenticator
  const optionsResponse = 
    await fetchWithErrorHandling('/Account/PasskeyCreationOptions', 
    {
      method: 'POST',
      headers,
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
  return await navigator.credentials.create({ publicKey: options, signal });
}

parseCreationOptionsFromJSON 函数将 JSON 响应转换为 WebAuthn API 预期的格式,并使用 navigator.credentials.create() 验证器启动凭据创建过程。

步骤 5:验证器交互

此时,浏览器将与验证器通信以创建凭据。 验证器会提示用户进行验证,这可能涉及扫描指纹、输入 PIN 或使用面部识别。 此交互完全由浏览器和验证器处理,无需应用代码。 用户体验因验证器和平台功能类型而异。

步骤 6:凭据提交

验证器创建凭据后,浏览器必须将凭据发送回服务器进行验证和存储。 在提交之前,凭据必须序列化为 JSON:

async function createCredential(headers, signal) {
  // Step 6: The credential is returned from navigator.credentials.create()
  // and is serialized to JSON for submission to the server
  const optionsResponse = 
    await fetchWithErrorHandling('/Account/PasskeyCreationOptions', 
    {
      method: 'POST',
      headers,
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
  return await navigator.credentials.create({ publicKey: options, signal });
}

在模板中,返回的 Blazor Web App 凭据通过表单自动序列化和提交,但具体提交机制因应用程序而异。

步骤 7:服务器验证和存储

当服务器收到凭据时,它必须验证其有效性,并存储公钥以供将来进行身份验证。 这就是 ASP.NET Core Identity的密钥 API 变得至关重要的地方。

该方法 PerformPasskeyAttestationAsync 验证来自客户端的证明响应。 此全面的验证过程:

  • 验证凭据类型是否符合预期。
  • 验证客户端数据 JSON,包括源和质询。
  • 检查验证器数据标志是否存在用户状态和验证
  • 提取并验证公钥。

如果所有检查都通过,该方法将 PasskeyAttestationResult 返回包含已验证的密钥信息。

验证证明后,应用将用于 AddOrUpdatePasskeyAsync 将 passkey 存储在数据库中:

var attestationResult = 
    await signInManager.PerformPasskeyAttestationAsync(credentialJson);

if (!attestationResult.Succeeded)
{
    return Results.BadRequest($"Error: {attestationResult.Failure.Message}");
}

var addResult = 
    await userManager.AddOrUpdatePasskeyAsync(user, attestationResult.Passkey);

if (!addResult.Succeeded)
{
    return Results.BadRequest("Failed to store passkey");
}

存储 UserPasskeyInfo 包含将来身份验证所需的所有信息,包括凭据 ID、公钥、用于重播保护的签名计数器,以及指示密钥是备份还是有资格备份的标志。

步骤 8:注册后任务

成功注册通行密钥后,应用通常会执行其他任务来改善用户体验。 常见的模式是提示用户为其密码提供友好名称,以便更轻松地在多个凭据之间识别。 该 UserPasskeyInfo.Name 属性存储此用户友好名称,可以使用同一 AddOrUpdatePasskeyAsync 方法进行更新:

passkey.Name = "My iPhone";
await userManager.AddOrUpdatePasskeyAsync(user, passkey);

身份验证流

本部分介绍用户如何使用其密钥进行身份验证,从启动登录过程到建立经过身份验证的会话。

sequenceDiagram
    participant Authenticator
    participant User
    participant Browser
    participant Server

    User->>Browser: Click "Sign in with passkey"
    Browser->>Server: Request authentication options
    Server->>Browser: Return authentication options
    Browser->>Authenticator: Request assertion
    Authenticator->>User: Verify identity
    User->>Authenticator: Approve
    Authenticator->>Browser: Return signed assertion
    Browser->>Server: Submit assertion
    Server->>Server: Verify signature
    Server->>Browser: Authentication complete
    Browser->>User: Redirect to app

步骤 1:启动身份验证

用户通常通过登录页上的专用按钮或链接启动密钥身份验证。 某些应用还支持条件 UI,其中密钥在用户名字段中显示为自动填充建议。 初始方法触发 JavaScript 代码,用于管理身份验证流,类似于注册过程。

步骤 2:请求身份验证选项

浏览器从服务器请求身份验证选项以开始身份验证过程。 这些选项包括可接受的凭据列表和要签名的新质询:

async function requestCredential(email, mediation, headers, signal) {
  // Step 2: Request authentication options from the server
  const optionsResponse = 
    await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${email}`, 
    {
      method: 'POST',
      headers,
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
  return await navigator.credentials.get({ publicKey: options, mediation, signal });
}

该方法 MakePasskeyRequestOptionsAsync 生成这些选项。 提供特定用户时,它仅包含允许列表中的该用户的凭据。 在没有用户的情况下调用时,它会生成适合条件 UI 或无用户名身份验证的选项:

app.MapPost("/Account/PasskeyRequestOptions", async (
    SignInManager<ApplicationUser> signInManager,
    string? username) =>
{
    var user = string.IsNullOrEmpty(username) 
        ? null 
        : await userManager.FindByNameAsync(username);

    var optionsJson = await signInManager.MakePasskeyRequestOptionsAsync(user);

    return TypedResults.Content(optionsJson, contentType: "application/json");
});

步骤 3:服务器生成选项

服务器使用注册期间所用的相同 IdentityPasskeyOptions 配置生成身份验证选项。 必须与 ServerDomain 最初注册密钥的域匹配,否则身份验证失败。 确定 UserVerificationRequirement 身份验证器在身份验证过程中是否必须验证用户的标识。

步骤 4:客户端请求断言

客户端 JavaScript 将身份验证选项传递给 WebAuthn API,以从验证器请求断言:

async function requestCredential(email, mediation, headers, signal) {
  // Step 4: Parse the options and request an assertion from the authenticator
  const optionsResponse = 
    await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${email}`, 
    {
      method: 'POST',
      headers,
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
  return await navigator.credentials.get({ publicKey: options, mediation, signal });
}

navigator.credentials.get() 调用使用验证器启动身份验证过程,这提示用户进行验证。

步骤 5:验证器验证

验证器验证用户的标识,并使用私钥对质询进行签名。 此过程完全由浏览器和验证器处理,类似于注册过程中的验证步骤。 用户体验取决于验证器类型,并且可能涉及生物识别验证或 PIN 条目。

步骤 6:断言提交

验证器创建签名断言后,浏览器将其序列化为 JSON 并将其提交到服务器:

async function requestCredential(email, mediation, headers, signal) {
  // Step 6: The assertion is returned from navigator.credentials.get()
  // and is serialized to JSON for submission to the server
  const optionsResponse = 
    await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${email}`, 
    {
      method: 'POST',
      headers,
      signal,
    });
  const optionsJson = await optionsResponse.json();
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
  return await navigator.credentials.get({ publicKey: options, mediation, signal });
}

提交机制因应用而异,但通常涉及表单提交或 API 调用。

步骤 7:服务器验证

服务器验证用于对用户进行身份验证的断言。 ASP.NET Core Identity 提供 PasskeySignInAsync 的方法,该方法在单个调用中执行完整的身份验证流:

var result = await signInManager.PasskeySignInAsync(credentialJson);

if (result.Succeeded)
{
    return Results.Ok("Authentication successful");
}

return Results.Unauthorized();

该方法 PasskeySignInAsync 在内部调用 PerformPasskeyAssertionAsync

  • 使用存储的公钥验证断言签名。
  • 验证质询是否与最初发送的质询匹配。
  • 检查验证器标志是否存在用户状态和验证。
  • 更新签名计数器以防止重播攻击。

如果所有检查都通过,该方法将登录用户并返回指示 SignInResult 成功。

对于需要更多控制的方案,可以使用 PerformPasskeyAssertionAsync 直接验证断言,而无需立即登录用户:

  • PerformPasskeyAssertionAsync 返回 PasskeyAssertionResult<TUser> 包含经过身份验证的用户和更新的密钥信息。
  • 由于自上次断言以来,传递密钥的登录计数和验证器标志可能已更改,并且更新的密钥在调用PerformPasskeyAssertionAsync时不会自动存储,因此使用返回userManager.AddOrUpdatePasskeyAsync的调用PasskeyAssertionResult<TUser>

步骤 8:会议建立

身份验证成功后,ASP.NET Core Identity 为用户建立经过身份验证的会话。 该方法 PasskeySignInAsync 会自动处理此问题,创建必要的身份验证 Cookie 和声明。 然后,应用会将用户重定向到受保护的资源或显示个性化内容。

缓解 PublicKeyCredential.toJSON 错误 (TypeError: Illegal invocation

方法返回的 JSON 表示形式。 当应用程序试图通过调用 JSON.stringify 来序列化 PublicKeyCredential 时,密码管理器会调用该方法以注册或验证用户。

某些密码管理器未能正确实现 PublicKeyCredential.toJSON 方法,而正确的实现对于在序列化密钥凭据时使 JSON.stringify 正常工作是必需的。 使用基于 Blazor Web App 项目模板的应用注册或验证用户时,尝试添加通行密钥时,某些密码管理器会引发以下错误:

Error: Could not add a passkey: Illegal invocation

在更新所选密码管理器以正确实现 PublicKeyCredential.toJSON 方法之前,请对应用进行以下更改。 以下代码手动序列化 PublicKeyCredential 的 JSON。

Components/Account/Shared/PasskeySubmit.razor.js 文件中,找到 passkey-submit 自定义元素定义代码块:

customElements.define('passkey-submit', class extends HTMLElement {
  ...
});

将以下 convertToBase64 函数添加到代码块:

convertToBase64(o) {
  if (!o) {
    return undefined;
  }

  // Normalize Array to Uint8Array
  if (Array.isArray(o)) {
    o = Uint8Array.from(o);
  }

  // Normalize ArrayBuffer to Uint8Array
  if (o instanceof ArrayBuffer) {
    o = new Uint8Array(o);
  }

  // Convert Uint8Array to base64
  if (o instanceof Uint8Array) {
    let str = '';
    for (let i = 0; i < o.byteLength; i++) {
      str += String.fromCharCode(o[i]);
    }
    o = window.btoa(str);
  }

  if (typeof o !== 'string') {
    throw new Error("Could not convert to base64 string");
  }

  // Convert base64 to base64url
  o = o.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");

  return o;
}

obtainAndSubmitCredential 代码块的函数中,找到使用用户的凭据调用 JSON.stringify 的行并删除该行:

- const credentialJson = JSON.stringify(credential);

将前面的行替换为以下代码:

const credentialJson = JSON.stringify({
  authenticatorAttachment: credential.authenticatorAttachment,
  clientExtensionResults: credential.getClientExtensionResults(),
  id: credential.id,
  rawId: this.convertToBase64(credential.rawId),
  response: {
    attestationObject: this.convertToBase64(credential.response.attestationObject),
    authenticatorData: this.convertToBase64(credential.response.authenticatorData ?? 
      credential.response.getAuthenticatorData?.() ?? undefined),
    clientDataJSON: this.convertToBase64(credential.response.clientDataJSON),
    publicKey: this.convertToBase64(credential.response.getPublicKey?.() ?? undefined),
    publicKeyAlgorithm: credential.response.getPublicKeyAlgorithm?.() ?? undefined,
    transports: credential.response.getTransports?.() ?? undefined,
    signature: this.convertToBase64(credential.response.signature),
    userHandle: this.convertToBase64(credential.response.userHandle),
  },
  type: credential.type,
});

只有在更新密码管理器以正确实现 PublicKeyCredential.toJSON 方法之前,才需要上述解决方法。 建议跟踪密码管理器的发行说明,并在更新密码管理器后还原上述更改。

其他资源