Dirigir a un usuario no autenticado a una página de acceso dedicada cuando hace clic en un vínculo "Acceder", inicia el proceso de confirmación de la compra, guarda un artículo en favoritos o llega a un muro de pago de un artículo provoca la pérdida de contexto y aumenta el abandono de los usuarios. El modo de IU inmediato (uiMode: 'immediate' en navigator.credentials.get()) permite accesos rápidos contextuales, ya que presenta un selector de cuentas de navegador unificado de inmediato cuando existen llaves de acceso locales (publicKey) o contraseñas guardadas (password: true) en el dispositivo.
A diferencia de los mensajes modales estándar de WebAuthn, que siempre muestran la IU del navegador y recurren a códigos QR multidispositivo o llaves de seguridad externas, el modo de IU inmediata falla rápidamente con un NotAllowedError si no hay credenciales aptas disponibles de forma local en el dispositivo. Esto permite que tu aplicación autentique a los usuarios recurrentes en el momento con un solo toque, mientras que los usuarios sin credenciales locales se dirigen sin problemas a tu página de acceso estándar, a la opción de comprar como invitado o al formulario de acceso intercalado.
Patrones y antipatrones de UX recomendados
Activa el modo de IU inmediata dentro de un controlador de eventos iniciado por el usuario (que requiere la activación transitoria del usuario) en momentos de intención clara del usuario justo antes de salir de la página actual. Si la autenticación inmediata se realiza correctamente, completa la intención del usuario de inmediato (ya sea actualizando la página actual o dirigiéndolo directamente a su destino autenticado). Si falla con NotAllowedError, muestra un flujo de acceso o registro con una ruta para completar el intent del usuario cuando finalice.
Posibles casos de uso:
- Intercepción del vínculo o botón de acceso del encabezado: Intercepta un clic en un vínculo global de "Acceder" (
event.preventDefault()) para autenticar a los usuarios recurrentes de inmediato o seguir navegando a la página de acceso dedicada enNotAllowedError. - Acceso dinámico antes de la confirmación de la compra: Autentica a los compradores recurrentes con un solo toque cuando hagan clic en "Proceder al pago" para que lleguen directamente al paso de envío y pago, o bien pasen a la pantalla de acceso estándar o de confirmación de la compra como invitado en caso de resguardo.
- Acceso intercalado para microacciones (Favorito, Me gusta, Marcar, Guardar): Autentica en el lugar cuando un usuario que no accedió a su cuenta hace clic en un botón de microacción y completa la acción de inmediato (por ejemplo, activar o desactivar
aria-pressed="true") o abre un modal de acceso de resguardo o una página de acceso con un parámetro de URL de retorno. - Muros de pago flexibles en el artículo: Autentica a los lectores en el momento en que hacen clic en "Seguir leyendo con tu cuenta" en un banner de muro de pago o expande un formulario de acceso o registro intercalado como resguardo.
Antipatrones que se deben evitar:
- NO uses el modo de IU inmediata en una página de acceso dedicada: Nunca adjuntes
uiMode: 'immediate'al botón principal "Acceder" o "Acceder con una llave de acceso" en tu página de acceso de destino final. Los usuarios llegan a la página de acceso dedicada específicamente cuando no tienen credenciales locales en el dispositivo actual o cuando quieren usar una llave de acceso multidispositivo (escaneando un código QR con un teléfono) o una llave de seguridad física externa. Debido a queuiMode: 'immediate'suprime las opciones de código QR y llave de seguridad multidispositivo, y arrojaNotAllowedErrorde inmediato cuando no existen credenciales locales, usarlo en una página de acceso dedicada atrapa al usuario en un callejón sin salida en el que hacer clic en el botón no hace nada. En una página de acceso dedicada, usa la IU condicional (mediation: 'conditional'conautocomplete="username webauthn") en las entradas de formulario vinculadas con un botón modal estándar denavigator.credentials.get({ publicKey })(consulta Autenticación con llaves de acceso). - NO uses el modo de IU inmediato para la autenticación de aumento de nivel: Nunca uses
uiMode: 'immediate'para volver a autenticar a un usuario que ya accedió antes de realizar operaciones sensibles (como cambiar una contraseña, actualizar una dirección de envío, editar formas de pago guardadas o agregar una llave de acceso nueva). La reautenticación debe restringir la solicitud a las credenciales del usuario que accedió a través de una listaallowCredentialsno vacía, que el modo de IU inmediato rechaza conNotAllowedError, y omitirallowCredentialspermitiría que el usuario seleccione de forma involuntaria una cuenta diferente de la que accedió actualmente. Para la reautenticación de aumento de nivel, sigue las instrucciones de Reautenticación con llaves de acceso.
Restricciones de privacidad y seguridad
Los navegadores aplican reglas estrictas de privacidad y seguridad cuando se solicita uiMode: 'immediate':
- Se requiere la activación transitoria del usuario: DEBES llamar a
navigator.credentials.get({ uiMode: 'immediate' })dentro de un controlador de eventos iniciado por el usuario (como un objeto de escuchaclicken un elemento<button>o<a>) para evitar el sondeo silencioso de credenciales. Nunca la invoques automáticamente en la carga de página (DOMContentLoaded). Llamar a la API no consume la activación transitoria. - No pases una lista
allowCredentialsque no esté vacía: NO completespublicKey.allowCredentialscuando se especifiqueuiMode: 'immediate'. Los navegadores rechazan de inmediato cualquier solicitud de modo de IU inmediato con una listaallowCredentialsno vacía arrojandoNotAllowedErrorpara que las partes de confianza no puedan buscar IDs de credenciales específicos para inferir visitas anteriores o hacer un seguimiento de los usuarios en las sesiones. - No pases un
AbortSignal(signal): NO configures la propiedadsignalennavigator.credentials.get()cuando se especifiqueuiMode: 'immediate', ya que los navegadores no permiten el cierre programático del diálogo de acceso inmediato. Si tu página también ejecuta una solicitud de IU condicional (mediation: 'conditional') en segundo plano, llama aabort()en esa IU condicionalAbortControllerantes de invocar el modo de IU inmediata y omitesignalde la solicitud inmediata. - Sesiones de navegación privada y de incógnito: Las solicitudes que se realizan en ventanas de navegación privada o de incógnito siempre se rechazan con
NotAllowedErrorpara que las sesiones privadas no se puedan distinguir de las sesiones sin credenciales locales.
Configuración del servidor
Extremo de generación de opciones
Expón un extremo que genere opciones de solicitud de WebAuthn detectables para los visitantes no autenticados:
- Genera un desafío vinculado a la sesión: Crea un búfer de desafío criptográficamente aleatorio, almacénalo en la sesión del servidor y codifícalo como Base64URL.
- Conservar la preferencia de verificación del usuario: Establece
userVerification: 'preferred'y almacenaexpectedUserVerification: 'preferred'en la sesión. - Omite
allowCredentials: No incluyas una listaallowCredentialsno vacía en las opciones del modo de IU inmediato, ya que los navegadores rechazan las listas de entidades permitidas no vacías conNotAllowedError.
// Server-side options generation example (/api/login/options)
router.post('/api/login/options', (req, res) => {
const options = {
challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
rpId: 'example.com',
userVerification: 'preferred',
};
req.session.challenge = options.challenge;
req.session.expectedUserVerification = 'preferred';
return res.json(options);
});
Extremos de verificación de credenciales dobles
Como navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) se puede resolver con una llave de acceso (PublicKeyCredential) o una contraseña guardada (PasswordCredential), proporciona controladores de verificación para ambos tipos de credenciales:
- Verificación de aserción de WebAuthn (
/api/login/verify): Verifica la aserción con el desafío de la sesión y la clave pública almacenada. Si no se encuentra el ID de credencial en la base de datos, devuelve el estado HTTP404para que el cliente pueda notificar al administrador de contraseñas a través dePublicKeyCredential.signalUnknownCredential(). - Verificación de contraseña (
/api/login/password): Verifica elusername(cred.id) y lapassword(cred.password) con la base de datos de usuarios y establece una sesión autenticada.
Implementación del cliente
Solicitud de credenciales unificada
- Solicita llaves de acceso y contraseñas: Decodifica las opciones del servidor con
PublicKeyCredential.parseRequestOptionsFromJSON()y llama anavigator.credentials.get()dentro de un controlador de clics iniciado por el usuario conpassword: true,publicKeyyuiMode: 'immediate'(sin pasarsignal). - Verificación de la sucursal por parte de
cred.type:- Si es
cred.type === 'public-key', serializa la aserción concred.toJSON()y envíala con POST a tu extremo de verificación de WebAuthn. Cuando el servidor responda con el código HTTP404, llama aPublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }). - Si es
cred.type === 'password', envía una solicitud POST{ username: cred.id, password: cred.password }a tu extremo de verificación de contraseñas.
- Si es
- Controla
NotAllowedErrorcuando no existan credenciales locales: CapturaNotAllowedError(se arroja de inmediato cuando no existen llaves de acceso locales ni contraseñas guardadas en el dispositivo, o cuando el usuario descarta el selector) y realiza la transición a tu flujo de acceso estándar (como abrir un diálogo de acceso intercalado o navegar a/signincon una URL de retorno).
let isAuthenticating = false;
async function authenticateWithImmediateUi() {
const optionsResponse = await fetch('/api/login/options', { method: 'POST' });
if (!optionsResponse.ok) {
throw new Error('Failed to fetch authentication options');
}
const optionsJSON = await optionsResponse.json();
const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON);
// MANDATORY: Request both password and publicKey with uiMode: 'immediate' and NO signal parameter
const cred = await navigator.credentials.get({
password: true,
publicKey,
uiMode: 'immediate',
});
if (!cred) {
throw new DOMException('No credential returned', 'NotAllowedError');
}
if (cred.type === 'public-key') {
const encoded = cred.toJSON();
const verifyResponse = await fetch('/api/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(encoded),
});
if (verifyResponse.status === 404) {
await PublicKeyCredential.signalUnknownCredential({
rpId: publicKey.rpId || window.location.hostname,
credentialId: encoded.id, // Base64URL-encoded credential ID string
});
throw new Error('Passkey credential is no longer recognized by the server');
}
if (!verifyResponse.ok) {
throw new Error('Passkey verification failed');
}
return await verifyResponse.json();
}
if (cred.type === 'password') {
const verifyResponse = await fetch('/api/login/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: cred.id,
password: cred.password,
}),
});
if (!verifyResponse.ok) {
throw new Error('Password verification failed');
}
return await verifyResponse.json();
}
throw new Error(`Unsupported credential type: ${cred.type}`);
}
// Example: Intercepting an inline micro-action (Favorite button) or Header Sign-in click
const favoriteButton = document.getElementById('favorite-btn');
favoriteButton.addEventListener('click', async (event) => {
event.preventDefault();
if (isAuthenticating) return;
// If already signed in, toggle the micro-action state immediately
if (document.body.dataset.signedIn === 'true') {
const nextPressed = favoriteButton.getAttribute('aria-pressed') !== 'true';
favoriteButton.setAttribute('aria-pressed', String(nextPressed));
return;
}
isAuthenticating = true;
try {
const user = await authenticateWithImmediateUi();
// Complete the user's intended action immediately
document.body.dataset.signedIn = 'true';
favoriteButton.setAttribute('aria-pressed', 'true');
updateHeaderUserState(user);
} catch (err) {
if (err.name === 'NotAllowedError') {
// No local credentials available or user dismissed dialog:
// transition smoothly to fallback sign-in UI or redirect with return URL
showFallbackSignInUi({ returnAction: 'favorite' });
return;
}
console.error('Authentication error:', err);
} finally {
isAuthenticating = false;
}
});
Estrategias de resguardo
Resguardo inmediato del modo de IU
- Experiencia de mejora progresiva: Trata
uiMode: 'immediate'como una mejora progresiva. No infieras la compatibilidad a partir del nombre o la versión de un navegador. El único indicador de compatibilidad confiable es la verificación del entorno de ejecución decapabilities.immediateGeta través dePublicKeyCredential.getClientCapabilities(). OBLIGATORIO: Siempre verificacapabilities.immediateGetantes de llamar anavigator.credentials.get({ uiMode: 'immediate' }). Los navegadores que no reconocenuiMode: 'immediate'ignoran la clave de diccionario desconocida y abren un diálogo modal de llave de acceso y código QR que bloquea la pantalla. Cuandocapabilities.immediateGetes falso o cuandonavigator.credentials.get()rechaza conNotAllowedError, recurre a la experiencia de acceso estándar de tu aplicación (como abrir un modal de acceso intercalado o navegar a la página de acceso dedicada con un parámetro de URL de retorno), de modo que los usuarios de navegadores no compatibles vean tu flujo de acceso existente sin cambios:
async function isImmediateUiSupported() {
if (!window.PublicKeyCredential || !PublicKeyCredential.getClientCapabilities) {
return false;
}
try {
const capabilities = await PublicKeyCredential.getClientCapabilities();
return Boolean(capabilities?.immediateGet);
} catch {
return false;
}
}
async function authenticateWithImmediateUi() {
// MANDATORY: Check immediateGet support so unsupported browsers do not open a modal QR dialog
const supported = await isImmediateUiSupported();
if (!supported) {
// Throw NotAllowedError so the caller's single catch path treats "unsupported" like "no local credentials"
throw new DOMException('Immediate UI mode is not supported', 'NotAllowedError');
}
// Proceed with fetching options and calling navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' })
}
Resguardo de detección de capacidades del cliente
Instala siempre 'webauthn-polyfills' y, luego, impórtalo en tu módulo de cliente para que PublicKeyCredential.getClientCapabilities() esté disponible siempre que se admita PublicKeyCredential:
import 'webauthn-polyfills';
Resguardo de credenciales de contraseña
Pasar password: true junto con publicKey en navigator.credentials.get() permite que los navegadores compatibles incluyan contraseñas guardadas del administrador de contraseñas del navegador en el selector de cuentas inmediato unificado.
- Experiencia de resguardo: Los navegadores que no admiten
PasswordCredentialignoran la opciónpassword: trueo recurren a la verificación de funciones decapabilities.immediateGetpara volver al formulario de acceso estándar, en el que<input autocomplete="username webauthn">y<input type="password" autocomplete="current-password">proporcionan autocompletado de contraseñas nativo.
Resguardo sencillo de serialización de JSON
Instala siempre 'webauthn-polyfills' y, luego, impórtalo en el paquete del cliente para que PublicKeyCredential.parseRequestOptionsFromJSON() y PublicKeyCredential.prototype.toJSON() funcionen de manera coherente en los navegadores que admiten PublicKeyCredential:
import 'webauthn-polyfills';
Resguardo de sincronización de la API de Signal
La API de Signal de WebAuthn (webauthn-signals) es una mejora progresiva que quita las llaves de acceso inactivas del administrador de contraseñas del usuario cuando el servidor responde con HTTP 404.
- Experiencia de resguardo: Controla las llamadas con
if (PublicKeyCredential.signalUnknownCredential). Cuando no sea compatible, omite la llamada de señalización de forma correcta y muestra un mensaje claro que guíe al usuario para que acceda con un método alternativo.