条件式创建通行密钥(登录后推广)

本指南详细介绍了如何在用户成功使用密码登录后,立即自动且静默地为其注册通行密钥,从而最大限度地减少阻碍并提高通行密钥采用率。

合适的触发时机

自动创建通行密钥(也称为“条件式创建”或“登录后静默提示”)只能在涉及密码的完整登录成功后立即触发。

  • 请勿尝试为无密码流程(例如魔法链接、短信动态密码或身份联合)进行条件式创建。
  • 如果需要多重身份验证,您必须等到所有验证因素都成功后,才能启动条件创建。
  • 在向创建端点发出请求之前,请确保存在有效的、经过身份验证的用户会话。

实施步骤

1. 中止之前的自动填充操作

如果登录页面使用表单自动填充(条件式界面/获取),则必须中止活跃的凭证获取调用,以防止浏览器冲突。

  • 在调用 navigator.credentials.create() 之前,先对附加到待处理 navigator.credentials.get() 自动填充请求的 AbortController 调用 abortController.abort()。

2. 功能检测

通过使用 PublicKeyCredential.getClientCapabilities() 检查 conditionalCreate,确定条件式创建是否可用。

const capabilities = await PublicKeyCredential.getClientCapabilities();
if (capabilities.conditionalCreate) {
  // Conditional create is available
}

3. 使用 Conditional Create 创建通行密钥

  • 在 navigator.credentials.create() 选项中传递 mediation: 'conditional'。这会向浏览器发出信号,让其在后台或根据上下文静默处理通行密钥创建流程,而不会弹出突兀的模态对话框。
  • 使用用户现有的通行密钥凭证 ID 填充 excludeCredentials,以避免注册重复的密钥。

4. 静默错误处理

  • 将通行密钥创建提示 (navigator.credentials.create) 封装在 try/catch 块中。您必须捕获并静默忽略典型的面向用户的异常(InvalidStateError、NotAllowedError、AbortError),而不向用户呈现任何错误界面。

5. 服务器端用户在线状态验证

  • 仅在验证由条件创建触发器生成的凭证时,服务器端验证端点才必须放宽用户在线状态 (UP) 要求 (requireUserPresence: false)。对于标准显式创建,必须始终启用严格存在性验证。

6. 妥善处理服务器验证失败的情况

  • 如果 navigator.credentials.create() 成功,但服务器验证提取返回了错误响应(例如,签名验证失败),请调用 PublicKeyCredential.signalUnknownCredential(),以防止孤立凭证在通行密钥提供程序中逗留。

代码示例

// optionsFetch and registerVerifyFetch are app-defined server endpoint requests
import { optionsFetch, registerVerifyFetch } from './api.js';

async function triggerConditionalCreate(loginAbortController) {
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalCreate !== true) {
    return; // Platform does not support conditional creation
  }

  // 1. Abort any active autofill conditional-get controllers to clear the WebAuthn pipeline
  loginAbortController.abort();

  // 2. Fetch creation options signaling the backend that this is a conditional request
  const creationOptionsJSON = await optionsFetch({ conditional: true });
  const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // 3. Invoke silent credentials creation prompt
    credential = await navigator.credentials.create({ 
      publicKey,
      mediation: 'conditional' // Silent background creation mediation
    });
  } catch (e) {
    // 4. Silently swallow common WebAuthn browser exceptions
    if (['InvalidStateError', 'NotAllowedError', 'AbortError'].includes(e.name)) {
      return; 
    }
    console.error('Unexpected conditional create error:', e);
    return;
  }

  // 5. Server verification step using dedicated Try/Catch block
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // If the server verification fails, clean up using Signal API
      if (PublicKeyCredential.signalUnknownCredential) {
        await PublicKeyCredential.signalUnknownCredential({
          rpId, // RP ID must match the one defined on the server
          credentialId: encodedResponse.id
        });
      }
    }
  } catch (serverErr) {
    console.error('Verification network failure:', serverErr);
    if (PublicKeyCredential.signalUnknownCredential) {
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id
      });
    }
  }
}

后备策略

通行密钥功能检测回退

始终安装“webauthn-polyfills”并将其导入到上下文中。 请注意,只要支持 PublicKeyCredential,就支持 PublicKeyCredential.getClientCapabilities。

import 'webauthn-polyfills';

Signal API 同步回退

WebAuthn Signal API (webauthn-signals) 是一种渐进式优化,用于使密码管理工具与服务器凭证状态保持同步。

  • 备用体验:通过 if (PublicKeyCredential.signalUnknownCredential) 限制访问。如果不支持,系统会顺利绕过后台验证同步,而不会抛出浏览器异常。

Easy JSON Serialization Fallback

始终安装“webauthn-polyfills”并将其导入到上下文中。 请注意,只要支持 PublicKeyCredential,就同样支持 PublicKeyCredential.parseCreationOptionsFromJSON 和 PublicKeyCredential.prototype.toJSON。

import 'webauthn-polyfills';