En esta guía, se detalla cómo implementar la autenticación de usuarios recurrentes con credenciales detectables, tanto a través de activadores de botones explícitos como de sugerencias de autocompletado del navegador sin interrupciones (IU condicional).
Del servidor
Generación de opciones
Crea un extremo que genere parámetros de solicitud de WebAuthn con una biblioteca verificada según los estándares.
- Usa el ID de RP predefinido: Usa el ID de RP adecuado predefinido como una cadena constante.
- Genera un desafío: Genera un búfer aleatorio criptográficamente seguro y de alta entropía, almacénalo de forma segura en la sesión del usuario y codifícalo como Base64URL.
- Asignación de credenciales detectables: Especifica un array vacío
[]paraallowCredentials. Esto solicita credenciales detectables, lo que significa que el usuario no necesita ingresar su nombre de usuario primero; el proveedor de llaves de acceso presentará las cuentas disponibles. - Nivel de verificación del usuario: Establece
userVerification: "preferred"(o"required"si lo exigen explícitamente las políticas de cumplimiento corporativas).- El nivel de restricción
userVerificationsolicitado DEBE conservarse dentro del registro de la sesión del servidor en el extremo de opciones, en lugar de que el cliente lo devuelva a través de cadenas de consulta. Esto permite que el extremo de verificación aplique restricciones de coincidencia estrictas de forma segura sin riesgo de manipulación por parte del cliente.
- El nivel de restricción
// 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";
Extremo de verificación
Verifica de forma segura la aserción que devuelve el cliente para autenticar al usuario:
- Validar el desafío de la sesión: Aplica una coincidencia estricta del desafío entre la respuesta del cliente y el desafío esperado almacenado en la sesión.
- Aplicar las preferencias de UV:
- Permite autenticadores sin UV (p.ej., bloqueos de pantalla del autenticador inhabilitados) si la
expectedUserVerificationde la sesión solicitó"preferred", pasandorequireUserVerification: falsea tu biblioteca de verificación del servidor. Si se solicita"required", aplica estrictamente la entrada de datos biométricos o PIN.
- Permite autenticadores sin UV (p.ej., bloqueos de pantalla del autenticador inhabilitados) si la
- Error de servidor limpio 404: Si el ID de credencial que devuelve el cliente no se encuentra en la base de datos, devuelve un error HTTP
404explícito para que el cliente pueda activar la API de Signal.
Lógica del cliente
Anotación de formulario HTML
Anota tus entradas de nombre de usuario y contraseña para aprovechar de forma nativa la IU condicional. Los tokens de autocompletado combinan los parámetros de la especificación de webauthn, y el enfoque automático activa la ventana emergente de autocompletado del navegador de inmediato cuando se enfoca la entrada.
<!-- 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>
Flujo de botones explícito
Activa la autenticación con llave de acceso cuando un usuario haga clic en el botón "Acceder con llave de acceso". Anula cualquier llamada de autocompletado de formularios (Conditional Get) en curso antes de invocar el mensaje de la llave de acceso.
Flujo de mediación condicional (autocompletado de formularios)
Activa las sugerencias de autocompletado de formularios cuando se cargue la página para ofrecer la autenticación con llave de acceso de forma nativa cuando los usuarios se enfoquen en los campos de acceso:
- Detección de funciones: Llama a
PublicKeyCredential.getClientCapabilities()cuando se cargue la página y omite el acceso con llave de acceso siconditionalGetno está disponible. - Opciones de decodificación: Decodifica el objeto JSON de credenciales recuperado con
PublicKeyCredential.parseRequestOptionsFromJSON(). - Invoca la obtención condicional: Llama a
navigator.credentials.get()conmediation: "conditional"y pasa un indicadorAbortController. Esto registra la función de autocompletar de forma silenciosa sin renderizar un diálogo de llave de acceso. - Segregación de excepciones try/catch: Une la llamada
navigator.credentials.geten el bloque try/catch:NotAllowedError: El usuario canceló el mensaje de acceso con llave de acceso o se agotó el tiempo de espera.AbortError: La solicitud de autenticación se canceló de manera programática.
- API de Call Signal: Une la llamada de verificación del servidor
fetch()en un bloque try/catch:- Mostrar un mensaje de error para que el usuario comprenda qué salió mal
- Llama a
signalUnknownCredential()SOLO cuando el servidor responda de forma explícita con el estado HTTP404(No se encontró la credencial) y el usuario no esté autenticado. - El parámetro
credentialIdque se pasa asignalUnknownCredential()DEBE ser estrictamente la cadena de ID de credencial codificada en Base64URL (p.ej.,encoded.id), NO el objeto ArrayBuffer sin procesarcredential.rawId.
- Codifica la respuesta: Codifica la credencial
AuthenticatorAssertionResponsecon.toJSON()antes de enviarla al servidor para su verificación.
// 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);
Estrategias de resguardo
Resguardo de la detección de funciones de llaves de acceso
Instala siempre "webauthn-polyfills" y, luego, impórtalo en el contexto.
Ten en cuenta que, siempre que se admita PublicKeyCredential, también se admitirá PublicKeyCredential.getClientCapabilities.
import 'webauthn-polyfills';
Resguardo de sincronización de la API de Signal
La API de Signal de WebAuthn (webauthn-signals) es una optimización progresiva que se usa para mantener los administradores de contraseñas sincronizados con el estado de las credenciales del servidor.
- Experiencia de resguardo: Se restringe a través de
if (PublicKeyCredential.signalUnknownCredential). Si no se admite, la sincronización de la verificación en segundo plano se omite sin problemas y sin arrojar excepciones del navegador.
Resguardo de serialización de JSON sencillo
Instala siempre "webauthn-polyfills" y, luego, impórtalo en el contexto.
Ten en cuenta que, siempre que se admita PublicKeyCredential, también se admitirán PublicKeyCredential.parseRequestOptionsFromJSON y PublicKeyCredential.prototype.toJSON.
import 'webauthn-polyfills';