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.
- Usar o ID de RP predefinido: use o ID de RP adequado predefinido como uma string constante.
- 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.
- 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. - Aplicar credenciais detectáveis: defina
requireResidentKey: trueeresidentKey: "required"nas opções deauthenticatorSelectionpara solicitar uma credencial detectável, que é necessária para logins detectáveis. - Configurar a verificação do usuário: especifique
userVerification: "preferred"ouuserVerification: "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. - 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: booleanpara processar os dois fluxos de forma condicional com um único endpoint.
- 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
// 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
- Verificação do desafio: verifique com segurança o desafio em relação ao desafio vinculado à sessão esperado.
- 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 é
truepara confirmar a presença física do usuário no momento da criação.
- Verifique se a flag de presença do usuário (UP, na sigla em inglês) retornada nos dados analisados do autenticador é
- 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 comrequireUserVerification: 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.
- Quando as opções de criação especificarem
Lógica do lado do cliente
- 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" seconditionalGetoupasskeyPlatformAuthenticatornão estiverem disponíveis.
- No carregamento da página, chame
- Invoque a criação e serialize: decodifique as opções do servidor com
PublicKeyCredential.parseCreationOptionsFromJSON()e transmita a configuração resultante paranavigator.credentials.create().- Chame
credential.toJSON()para codificar oAuthenticatorAttestationResponseem um objeto válido e serializável em JSON antes de buscar o endpoint de verificação.
- Chame
- Tratar exceções do WebAuthn:
InvalidStateError: já existe uma chave de acesso correspondente (correspondida porexcludeCredentials).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).
- Segregação de Try/Catch para a API Signal:
- Envolva a chamada de verificação do servidor
fetch()em um bloco try/catch. ChamesignalUnknownCredential()quando a busca de verificação do servidor falhar (qualquer statusresponse.ok === falseou lançamento de rede).
- Envolva a chamada de verificação do servidor
// 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';