Direcionar um usuário não autenticado para uma página de login dedicada quando ele clica em um link "Fazer login", inicia o pagamento, salva um item nos favoritos ou chega a um paywall de artigo causa perda de contexto e aumenta o abandono do usuário. O modo de interface imediata (uiMode: 'immediate' em navigator.credentials.get()) permite logins rápidos contextuais, apresentando um seletor de contas unificado do navegador imediatamente quando há chaves de acesso locais (publicKey) ou senhas salvas (password: true) no dispositivo.
Ao contrário dos prompts modais padrão da WebAuthn, que sempre mostram a interface do navegador e usam QR codes entre dispositivos ou chaves de segurança externas como alternativa, o modo de interface imediata falha rapidamente com um NotAllowedError se não houver credenciais qualificadas disponíveis localmente no dispositivo. Isso permite que o aplicativo autentique usuários recorrentes com um único toque, enquanto faz a transição de usuários sem credenciais locais para a página de login padrão, a compra sem login ou o formulário de login inline.
Padrões e antipadrões de UX recomendados
Acione o modo de IU imediata em um manipulador de eventos iniciado pelo usuário (que exige ativação temporária do usuário) em momentos de clara intenção do usuário logo antes de sair da página atual. Se a autenticação imediata for bem-sucedida, conclua a intenção do usuário imediatamente (atualizando a página atual ou acessando diretamente o destino autenticado). Se falhar com NotAllowedError, mostre um fluxo de login ou registro com um caminho para concluir a intent do usuário após a conclusão.
Possíveis casos de uso:
- Intercepção do link ou botão de login do cabeçalho: intercepte um clique em um link global de login (
event.preventDefault()) para autenticar usuários recorrentes imediatamente ou continue navegando até a página de login dedicada emNotAllowedError. - Login dinâmico antes da finalização da compra: autentique os compradores recorrentes com um único toque quando eles clicarem em "Finalizar compra" para que eles acessem diretamente a etapa de envio e pagamento ou façam a transição para a tela de login padrão ou de compra sem login em caso de fallback.
- Login inline para microações (favoritar, curtir, marcar, salvar): autentique no local quando um usuário desconectado clicar em um botão de microação e concluir a ação imediatamente (por exemplo, alternar
aria-pressed="true") ou abrir um modal de login substituto ou uma página de login com um parâmetro de URL de retorno. - Paywalls flexíveis em artigos: autentique os leitores no local quando eles clicarem em "Continuar lendo com sua conta" em um banner de paywall ou expandirem um formulário de login ou registro inline como alternativa.
Antipadrões a serem evitados:
- NÃO use o modo de IU imediata em uma página de login dedicada: nunca anexe
uiMode: 'immediate'ao botão principal "Fazer login" ou "Fazer login com uma chave de acesso" na página de login de destino final. Os usuários acessam a página de login dedicada especificamente quando não têm credenciais locais no dispositivo atual ou quando pretendem usar uma chave de acesso entre dispositivos (lendo um QR code com um smartphone) ou uma chave de segurança de hardware externa. ComouiMode: 'immediate'suprime as opções de QR code e chave de segurança entre dispositivos e gera imediatamenteNotAllowedErrorquando não há credenciais locais, o uso dele em uma página de login dedicada deixa o usuário em um beco sem saída, em que clicar no botão não faz nada. Em uma página de login dedicada, use a interface condicional (mediation: 'conditional'comautocomplete="username webauthn") em entradas de formulário combinadas com um botão modal padrãonavigator.credentials.get({ publicKey })(consulte Autenticação com chave de acesso). - NÃO use o modo de interface imediata para reautenticação de nível superior: nunca use
uiMode: 'immediate'para reautenticar um usuário que já fez login antes de operações sensíveis, como mudar uma senha, atualizar um endereço de entrega, editar formas de pagamento salvas ou adicionar uma nova chave de acesso. A reautenticação precisa restringir a solicitação às credenciais do usuário conectado usando uma listaallowCredentialsnão vazia, que o modo de IU imediata rejeita comNotAllowedError. OmitirallowCredentialspermitiria que o usuário selecionasse inadvertidamente uma conta diferente da que está conectada no momento. Para a reautenticação de step-up, siga as instruções em Reautenticação com chave de acesso.
Restrições de privacidade e segurança
Os navegadores aplicam regras rígidas de privacidade e segurança quando uiMode: 'immediate' é solicitado:
- A ativação temporária do usuário é obrigatória: você PRECISA chamar
navigator.credentials.get({ uiMode: 'immediate' })dentro de um manipulador de eventos iniciado pelo usuário (como um listenerclickem um elemento<button>ou<a>) para evitar a sondagem silenciosa de credenciais. Nunca a invoque automaticamente no carregamento de página (DOMContentLoaded). Chamar a API não consome a ativação temporária. - Não transmita uma lista
allowCredentialsnão vazia: NÃO preenchapublicKey.allowCredentialsquandouiMode: 'immediate'for especificado. Os navegadores rejeitam imediatamente qualquer solicitação de modo de IU imediato com uma listaallowCredentialsnão vazia, gerandoNotAllowedErrorpara que as partes confiáveis não possam sondar IDs de credenciais específicos para inferir visitas anteriores ou rastrear usuários em várias sessões. - Não transmita um
AbortSignal(signal): NÃO defina a propriedadesignalemnavigator.credentials.get()quandouiMode: 'immediate'for especificado, porque os navegadores não permitem o encerramento programático da caixa de diálogo de login imediato. Se a página também executar uma solicitação de interface condicional (mediation: 'conditional') em segundo plano, chameabort()nessa interface condicionalAbortControllerantes de invocar o modo de interface imediata e omitasignalda própria solicitação imediata. - Sessões de navegação anônima e privada: as solicitações feitas em janelas de navegação anônima ou privada sempre são rejeitadas com
NotAllowedErrorpara que as sessões privadas permaneçam indistinguíveis das sessões sem credenciais locais.
Configuração do lado do servidor
Endpoint de geração de opções
Exponha um endpoint que gere opções de solicitação detectáveis do WebAuthn para visitantes não autenticados:
- Gerar um desafio vinculado à sessão: crie um buffer de desafio criptograficamente aleatório, armazene-o na sessão do servidor e codifique-o como Base64URL.
- Persistir a preferência de verificação do usuário: defina
userVerification: 'preferred'e armazeneexpectedUserVerification: 'preferred'na sessão. - Omitir
allowCredentials: não inclua uma listaallowCredentialsnão vazia nas opções do modo de IU imediata, porque os navegadores rejeitam listas de permissões não vazias comNotAllowedError.
// 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);
});
Endpoints de verificação de credenciais duplas
Como navigator.credentials.get({ password: true, publicKey, uiMode: 'immediate' }) pode ser resolvido com uma chave de acesso (PublicKeyCredential) ou uma senha salva (PasswordCredential), forneça gerenciadores de verificação para os dois tipos de credencial:
- Verificação de declaração do WebAuthn (
/api/login/verify): verifique a declaração em relação ao desafio da sessão e à chave pública armazenada. Se o ID da credencial não for encontrado no banco de dados, retorne o status HTTP404para que o cliente possa notificar o gerenciador de senhas usandoPublicKeyCredential.signalUnknownCredential(). - Verificação de senha (
/api/login/password): verifique ousername(cred.id) e apassword(cred.password) no banco de dados do usuário e estabeleça uma sessão autenticada.
Implementação do lado do cliente
Solicitação de credencial unificada
- Solicitar chaves de acesso e senhas: decodifique as opções do servidor usando
PublicKeyCredential.parseRequestOptionsFromJSON()e chamenavigator.credentials.get()em um gerenciador de cliques iniciado pelo usuário compassword: true,publicKeyeuiMode: 'immediate'(sem transmitirsignal). - Verificação de agência por
cred.type:- Se for
cred.type === 'public-key', serialize a declaração comcred.toJSON()e envie-a por POST para o endpoint de verificação do WebAuthn. Quando o servidor responder com HTTP404, chamePublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rpId, credentialId: encoded.id }). - Se for
cred.type === 'password', envie um POST{ username: cred.id, password: cred.password }para o endpoint de verificação de senha.
- Se for
- Processar
NotAllowedErrorquando não houver credenciais locais: captureNotAllowedError(gerado imediatamente quando não houver chaves de acesso locais ou senhas salvas no dispositivo ou quando o usuário dispensar o seletor) e faça a transição para o fluxo de login padrão (como abrir uma caixa de diálogo de login inline ou navegar até/signincom um 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;
}
});
Estratégias de fallback
Contingência imediata do modo de interface
- Experiência de aprimoramento progressivo: trate
uiMode: 'immediate'como um aprimoramento progressivo. Não deduza a compatibilidade pelo nome ou pela versão de um navegador. O único indicador confiável é a verificação de ambiente de execuçãocapabilities.immediateGetusandoPublicKeyCredential.getClientCapabilities(). OBRIGATÓRIO: sempre verifiquecapabilities.immediateGetantes de chamarnavigator.credentials.get({ uiMode: 'immediate' }). Os navegadores que não reconhecemuiMode: 'immediate'ignoram a chave de dicionário desconhecida e abrem uma caixa de diálogo modal de bloqueio de chave de acesso e QR code. Quandocapabilities.immediateGetfor falso ou quandonavigator.credentials.get()rejeitar comNotAllowedError, use a experiência de login padrão do seu aplicativo (como abrir um modal de login inline ou navegar até a página de login dedicada com um parâmetro de URL de retorno) para que os usuários em navegadores sem suporte vejam seu fluxo de login atual sem mudanças:
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' })
}
Substituto para detecção de recursos do cliente
Sempre instale o 'webauthn-polyfills' e o importe no módulo do cliente para que o PublicKeyCredential.getClientCapabilities() esteja disponível sempre que houver suporte para o PublicKeyCredential:
import 'webauthn-polyfills';
Substituição de credenciais de senha
Transmitir password: true com publicKey em navigator.credentials.get() permite que os navegadores compatíveis incluam senhas salvas do gerenciador de senhas do navegador no seletor de contas imediato unificado.
- Experiência de substituição: os navegadores que não são compatíveis com
PasswordCredentialignoram a opçãopassword: trueou usam a verificação de recursoscapabilities.immediateGetpara voltar ao formulário de login padrão, em que<input autocomplete="username webauthn">e<input type="password" autocomplete="current-password">oferecem preenchimento automático de senha nativo.
Facilidade de fallback de serialização JSON
Sempre instale 'webauthn-polyfills' e importe-o no pacote do cliente para que PublicKeyCredential.parseRequestOptionsFromJSON() e PublicKeyCredential.prototype.toJSON() funcionem de maneira consistente em todos os navegadores compatíveis com PublicKeyCredential:
import 'webauthn-polyfills';
Substituição de sincronização da API Signal
A API WebAuthn Signal (webauthn-signals) é um aprimoramento progressivo que remove chaves de acesso desatualizadas do gerenciador de senhas do usuário quando o servidor responde com HTTP 404.
- Experiência substituta: as chamadas são bloqueadas por
if (PublicKeyCredential.signalUnknownCredential). Quando não houver suporte, pule a chamada de sinalização e mostre uma mensagem clara orientando o usuário a fazer login com um método alternativo.