Registro de chave de acesso

Este guia detalha como permitir que os usuários registrem uma chave de acesso para a conta, oferecendo uma alternativa de login sem senha altamente segura e resistente a phishing.

Requisitos do banco de dados

Para oferecer suporte a registros de chave de acesso, a tabela de credenciais do banco de dados precisa armazenar os seguintes campos:

export interface StoredPasskeyCredential {
  id: string; // Base64URL-encoded credential ID (unique lookup key)
  passkeyUserId: string; // Associated application user ID
  credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
  credentialType: "public-key";
  credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
  credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
  aaguid: string; // Authenticator Attestation GUID
  providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
  name: string; // Provider name derived from AAGUID registry
  transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
  lastUsedAt?: number; // Optional epoch timestamp of last sign-in
  registeredAt: number; // Registration epoch timestamp
  counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}

Lado do servidor

Geração de opções

Crie um endpoint que gere parâmetros de criação do WebAuthn. Use uma biblioteca aprovada de acordo com os padrões da categoria em vez de criar uma criptografia personalizada.

  1. Usar o ID de RP predefinido: use o ID de RP adequado predefinido como uma string constante.
  2. Crie um desafio seguro: gere um buffer aleatório de alta entropia e criptograficamente seguro no servidor, armazene-o com segurança na sessão do usuário e codifique-o como Base64URL para entrega de opções.
  3. Evite chaves de acesso duplicadas: mapeie os IDs de credenciais pré-registrados do usuário para a matriz de opções excludeCredentials. Isso impede que o autenticador registre credenciais duplicadas na mesma conta do provedor de chaves de acesso.
  4. Aplicar credenciais detectáveis: defina requireResidentKey: true e residentKey: "required" nas opções de authenticatorSelection para solicitar uma credencial detectável, que é necessária para logins detectáveis.
  5. Configurar a verificação do usuário: especifique userVerification: "preferred" ou userVerification: "required". Muitos casos de uso de compliance (por exemplo, finanças, saúde) exigem que o 'required' aplique a verificação do usuário na criação.
  6. Determinar o escopo do anexo:
    • Fluxo de promoção: ao propor a criação de chaves de acesso logo após o login com senha padrão ou promoções pós-inscrição, defina authenticatorAttachment: "platform" para aplicar o autenticador de plataforma e ignorar as solicitações de chave de segurança externa.
    • Fluxo de gerenciamento: quando chamado de um painel dedicado de configurações ou segurança em que chaves de segurança externas são aceitas além do autenticador de plataforma, omita totalmente a propriedade authenticatorAttachment.
    • Dica: aceite uma flag de solicitação promotion: boolean para processar os dois fluxos de forma condicional com um único endpoint.
// Options generation example
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
  rp: { id: "example.com", name: "Secure Application" },
  user: {
    id: userBase64UrlId, // Unique base64url string identifying the account
    name: "user@example.com",
    displayName: "Jane Doe",
  },
  pubKeyCredParams: [
    {
      type: "public-key",
      alg: -7,
    },
    {
      type: "public-key",
      alg: -257,
    },
  ],
  excludeCredentials: userExistingCredentials.map((cred) => ({
    type: "public-key",
    id: cred.id,
    transports: cred.transports,
  })),
  authenticatorSelection: {
    residentKey: "required",
    requireResidentKey: true,
    userVerification: "preferred",
    ...(isPromotionFlow && { authenticatorAttachment: "platform" }),
  },
};

Verificação

  1. Verificação do desafio: verifique com segurança o desafio em relação ao desafio vinculado à sessão esperado.
  2. Verificar presença do usuário:
    • Verifique se a flag de presença do usuário (UP, na sigla em inglês) retornada nos dados analisados do autenticador é true para confirmar a presença física do usuário no momento da criação.
  3. Relaxamento da verificação para "preferred":
    • Quando as opções de criação especificarem userVerification: "preferred", a chamada de verificação do lado do servidor PRECISA ser configurada com requireUserVerification: false. Caso contrário, os autenticadores que se registrarem sem verificação do usuário (por exemplo, bloqueios de tela desativados) vão acionar falhas de verificação do servidor.

Lógica do lado do cliente

  1. Bloquear a interface no carregamento da página:
    • No carregamento da página, chame PublicKeyCredential.getClientCapabilities() e desative o botão "Criar chave de acesso" se conditionalGet ou passkeyPlatformAuthenticator não estiverem disponíveis.
  2. Invoque a criação e serialize: decodifique as opções do servidor com PublicKeyCredential.parseCreationOptionsFromJSON() e transmita a configuração resultante para navigator.credentials.create().
    • Chame credential.toJSON() para codificar o AuthenticatorAttestationResponse em um objeto válido e serializável em JSON antes de buscar o endpoint de verificação.
  3. Tratar exceções do WebAuthn:
    • InvalidStateError: já existe uma chave de acesso correspondente (correspondida por excludeCredentials).
    • NotAllowedError: o usuário cancelou ou o tempo limite da caixa de diálogo da chave de acesso de autenticação expirou.
    • AbortError: a operação foi cancelada.
    • SecurityError: origens seguras (HTTPS) ou erros de incompatibilidade de ID de RP (problemas de configuração).
  4. Segregação de Try/Catch para a API Signal:
    • Envolva a chamada de verificação do servidor fetch() em um bloco try/catch. Chame signalUnknownCredential() quando a busca de verificação do servidor falhar (qualquer status response.ok === false ou lançamento de rede).
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";

async function registerPasskey(isPromotion = false) {
  // Verify passkey capability and conditional UI are available
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (
    !capabilities.passkeyPlatformAuthenticator ||
    !capabilities.conditionalGet
  ) {
    // Hide "Create passkey" buttons and fall back to password flows instead
    showStandardPasswordFallbackUI();
    return;
  }

  const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
  const publicKey =
    PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // passkey prompt execution
    credential = await navigator.credentials.create({ publicKey });
  } catch (err) {
    if (err.name === "InvalidStateError") {
      console.log("A passkey already exists for this account.");
      alert("A passkey already exists for this account.");
    } else if (err.name === "SecurityError") {
      console.error("Configuration RP ID or Secure Context error.");
      alert("Configuration RP ID or Secure Context error.");
    } else if (err.name === "NotAllowedError") {
      console.log("User cancelled the passkey dialog.");
    } else if (err.name === "AbortError") {
      console.log("The creation operation has been aborted.");
    }
    return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
  }

  // Server Verification phase (Segregated Try/Catch)
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // Server verification failed to verify/authenticate the credential (orphaned)
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Server verification network failure:", serverErr);
    await publickeycredential.signalunknowncredential({
      rpId, // RP ID must match the one defined on the server
      credentialid: encodedresponse.id, // base64url-encoded credential id
    });
  }
}

Estratégias de fallback

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: se o navegador não oferecer suporte a PublicKeyCredential.signalUnknownCredential, a chamada DEVERÁ ser ignorada com segurança usando a detecção de recursos (if (PublicKeyCredential.signalUnknownCredential)), e a verificação do lado do servidor simplesmente registrará a falha sem acionar atualizações do gerenciador.

Easy JSON Serialization Fallback

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

import 'webauthn-polyfills';