Autenticação com chave de acesso

Este guia detalha como implementar a autenticação de usuários recorrentes usando credenciais detectáveis, tanto por acionadores de botão explícitos quanto por sugestões de preenchimento automático do navegador (IU condicional).

Lado do servidor

Geração de opções

Crie um endpoint que gere parâmetros de solicitação da WebAuthn usando uma biblioteca aprovada de acordo com os padrões.

  1. Usar o ID de RP predefinido: use o ID de RP adequado predefinido como uma string constante.
  2. Gerar desafio: gere um buffer aleatório criptograficamente seguro e de alta entropia, armazene-o com segurança na sessão do usuário e codifique-o como Base64URL.
  3. Mapeamento de credenciais detectáveis: especifique uma matriz vazia [] para allowCredentials. Isso solicita credenciais detectáveis, o que significa que o usuário não precisa inserir o nome de usuário primeiro. O provedor de chaves de acesso vai apresentar as contas disponíveis.
  4. Nível de verificação do usuário: defina userVerification: "preferred" (ou "required" se exigido explicitamente pelas políticas de compliance corporativo).
    • O nível de restrição userVerification solicitado PRECISA ser mantido no registro da sessão do servidor no endpoint de opções, em vez de ser transmitido de volta pelo cliente usando strings de consulta. Isso permite que o endpoint de verificação aplique restrições de correspondência estritas com segurança, sem risco de manipulação do cliente.
// Options generation example (discoverable flow)
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
  rpId: "example.com",
  allowCredentials: [], // Request discoverable passkeys
  userVerification: "preferred",
};

// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";

Endpoint de verificação

Verifique com segurança a declaração retornada pelo cliente para autenticar o usuário:

  1. Validar desafio de sessão: aplique uma correspondência estrita de desafio entre a resposta do cliente e o desafio esperado armazenado na sessão.
  2. Aplicar preferências de UV:
    • Permita autenticadores sem UV (por exemplo, bloqueios de tela do autenticador desativados) se o expectedUserVerification da sessão solicitar "preferred", transmitindo requireUserVerification: false para sua biblioteca de verificação do lado do servidor. Se solicitado "required", aplique a biometria/PIN de forma rigorosa.
  3. Erro de servidor limpo 404: se o ID da credencial retornado pelo cliente não for encontrado no banco de dados, retorne um erro HTTP 404 explícito para que o cliente possa acionar a API Signal.

Lógica do lado do cliente

Anotação de formulário HTML

Anote as entradas de nome de usuário e senha para usar a interface condicional de forma nativa. Os tokens de preenchimento automático combinam os parâmetros de especificação do webauthn, e o foco automático aciona o pop-up de preenchimento automático do navegador imediatamente quando a entrada é focada.

<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
  <input
    type="text"
    name="username"
    autocomplete="username webauthn"
    autofocus
    data-testid="username-field"
  />
  <input type="password" name="password" autocomplete="current-password" />
  <button type="submit">Sign in</button>
</form>

Fluxo de botão explícito

Acione a autenticação por chave de acesso quando um usuário clicar no botão "Fazer login com chave de acesso". Interrompa as chamadas de preenchimento automático de formulários (Conditional Get) em andamento antes de invocar o prompt da chave de acesso.

Fluxo de mediação condicional (preenchimento automático de formulários)

Ative as sugestões de preenchimento automático de formulários no carregamento da página para oferecer autenticação de chave de acesso de forma nativa quando os usuários se concentrarem nos campos de login:

  1. Detecção de recursos: chame PublicKeyCredential.getClientCapabilities() no carregamento da página e pule o login com chave de acesso se conditionalGet não estiver disponível.
  2. Opções de decodificação: decodifique o objeto JSON de credencial buscado com PublicKeyCredential.parseRequestOptionsFromJSON().
  3. Invocar o Get condicional: chame navigator.credentials.get() com mediation: "conditional" e transmita um indicador AbortController. Isso registra o preenchimento automático de forma silenciosa, sem renderizar uma caixa de diálogo de chave de acesso.
  4. Segregação de exceções Try/Catch: envolva a chamada navigator.credentials.get em um bloco try/catch:
    • NotAllowedError: o usuário cancelou ou o tempo limite do prompt de login da chave de acesso expirou.
    • AbortError: a solicitação de autenticação foi cancelada de forma programática.
  5. API Call Signal: envolva a chamada de verificação do servidor fetch() em um bloco try/catch:
    • Mostre uma mensagem de erro para que o usuário entenda o que deu errado.
    • Chame signalUnknownCredential() SOMENTE quando o servidor responder explicitamente com o status HTTP 404 (credencial não encontrada) e o usuário não estiver autenticado.
    • O parâmetro credentialId transmitido para signalUnknownCredential() PRECISA ser estritamente a string de ID da credencial codificada em Base64URL (por exemplo, encoded.id), NÃO o objeto ArrayBuffer bruto credential.rawId.
  6. Codificar a resposta: codifique a credencial AuthenticatorAssertionResponse com .toJSON() antes de enviá-la ao servidor para verificação.
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";

let autofillAbortController = new AbortController();

async function initializeConditionalAutofill() {
  // Feature detect Conditional Get autofill support
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true) {
    const loginOptionsJSON = await optionsFetch();
    const publicKey =
      PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

    try {
      // Initiate Conditional UI form autofill suggestions
      const credential = await navigator.credentials.get({
        publicKey,
        signal: autofillAbortController.signal,
        mediation: "conditional",
      });

      // Segregated verification fetch
      const encoded = credential.toJSON();
      const response = await loginVerifyFetch(encoded);
      if (!response.ok && response.status === 404) {
        // Note: this code path runs pre-authentication, satisfying the unauth precondition
        if (PublicKeyCredential.signalUnknownCredential) {
          await PublicKeyCredential.signalUnknownCredential({
            rpId, // RP ID must match the one defined on the server
            credentialId: encoded.id,
          });
        }
      }
    } catch (err) {
      // Silently swallow expected client WebAuthn exceptions
      if (["NotAllowedError", "AbortError"].includes(err.name)) {
        return;
      }
      console.error("Unexpected conditional get error:", err);
    }
  }
}

async function triggerButtonAuthentication() {
  // Abort any pending Conditional Get call to prevent passkey prompt collisions
  autofillAbortController.abort();
  autofillAbortController = new AbortController(); // Reset controller for next triggers

  const loginOptionsJSON = await optionsFetch();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

  let credential;
  try {
    // Passkey explicit prompt trigger
    credential = await navigator.credentials.get({
      publicKey,
      signal: autofillAbortController.signal,
    });
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled passkey login.");
    } else if (err.name === "AbortError") {
      console.log("The authentication operation was aborted.");
    }
    // Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
    initializeConditionalAutofill();
    return; // Safe exit
  }

  // Segregated verification try/catch (HTTP 404 trigger)
  const encoded = credential.toJSON();
  try {
    const response = await loginVerifyFetch(encoded);
    if (!response.ok && response.status === 404) {
      // Note: this code path runs pre-authentication, satisfying the unauth precondition
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encoded.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Verification request error:", serverErr);
  }
}

// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);

Estratégias de fallback

Substituto para detecção de recursos de chave de acesso

Sempre instale "webauthn-polyfills" e importe-o no contexto. Se PublicKeyCredential for compatível, PublicKeyCredential.getClientCapabilities também será.

import 'webauthn-polyfills';

Substituição de sincronização da API Signal

A API WebAuthn Signal (webauthn-signals) é uma otimização progressiva usada para manter os gerenciadores de senhas sincronizados com o estado das credenciais do servidor.

  • Experiência de fallback: restrita por if (PublicKeyCredential.signalUnknownCredential). Se não houver suporte, a sincronização de verificação em segundo plano será ignorada sem gerar exceções do navegador.

Easy JSON Serialization Fallback

Sempre instale "webauthn-polyfills" e importe-o no contexto. Se PublicKeyCredential for compatível, PublicKeyCredential.parseRequestOptionsFromJSON e PublicKeyCredential.prototype.toJSON também serão.

import 'webauthn-polyfills';