Visão geral
Confira uma visão geral de alto nível das principais etapas envolvidas na autenticação de chaves de acesso:
- Defina o desafio e outras opções necessárias para autenticar com uma chave de acesso. Envie-as ao cliente para que você possa transmiti-las para a chamada de autenticação da chave de acesso (
navigator.credentials.get
na Web). Depois que o usuário confirmar a autenticação da chave de acesso, a chamada de autenticação será resolvida e retornará uma credencial (PublicKeyCredential
). A credencial contém uma declaração de autenticação.
- Verifique a declaração de autenticação.
- Se a declaração de autenticação for válida, autentique o usuário.
As seções a seguir detalham as especificidades de cada etapa.
Criar o desafio
Na prática, um desafio é uma matriz de bytes aleatórios, representada como um objeto ArrayBuffer
.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
Para garantir que o desafio cumpra seu propósito, você deve:
- Garanta que o mesmo desafio nunca seja usado mais de uma vez. Gerar um novo desafio a cada tentativa de login. Descarte o desafio após cada tentativa de login, seja ela concluída ou reprovada. Descarte o desafio após uma determinada duração também. Nunca aceite o mesmo desafio em uma resposta mais de uma vez.
- Verifique se o desafio tem segurança criptográfica. Deve ser praticamente impossível adivinhar um desafio. Para criar um desafio do lado do servidor com segurança criptográfica, é melhor usar uma biblioteca FIDO de confiança. Se você criar seus próprios desafios, use a funcionalidade criptográfica integrada disponível no seu conjunto de tecnologias ou procure bibliotecas projetadas para casos de uso criptográficos. Os exemplos incluem iso-crypto em Node.js ou secrets em Python. De acordo com a especificação, o desafio precisa ter pelo menos 16 bytes de comprimento para ser considerado seguro.
Depois de criar um desafio, salve-o na sessão do usuário para verificá-lo mais tarde.
Criar opções de solicitação de credenciais
Crie opções de solicitação de credenciais como um objeto publicKeyCredentialRequestOptions
.
Para isso, use a biblioteca do lado do servidor FIDO. Ele normalmente oferece uma função utilitária que pode criar essas opções para você. O SimpleWebAuthn oferece, por exemplo, o generateAuthenticationOptions
.
publicKeyCredentialRequestOptions
precisa conter todas as informações necessárias para a autenticação da chave de acesso. Transmita essas informações para a função na biblioteca do lado do servidor FIDO responsável por criar o objeto publicKeyCredentialRequestOptions
.
Parte do tempo de publicKeyCredentialRequestOptions
campos podem ser constantes. Outros precisam ser definidos dinamicamente no servidor:
rpId
: a qual ID da RP você espera que a credencial seja associada, por exemplo,example.com
. A autenticação só será bem-sucedida se o ID da RP fornecido aqui corresponder ao ID da RP associado à credencial. Para preencher o ID da RP, use o mesmo valor do ID da RP definido nopublicKeyCredentialCreationOptions
durante o registro da credencial.challenge
: dados que o provedor da chave de acesso assinar para provar que o usuário detém a chave no momento da solicitação de autenticação. Revise os detalhes em Criar o desafio.allowCredentials
: uma matriz de credenciais aceitáveis para essa autenticação. Transmita uma matriz vazia para permitir que o usuário selecione uma chave de acesso disponível em uma lista mostrada pelo navegador. Consulte Buscar um desafio do servidor da RP e Análise detalhada sobre credenciais detectáveis para mais detalhes.userVerification
: indica se a verificação do usuário usando o bloqueio de tela do dispositivo é "obrigatória", "preferencial" ou "não recomendado". Consulte Buscar um desafio do servidor da RP.timeout
: quanto tempo (em milissegundos) o usuário pode levar para concluir a autenticação. Ele precisa ser razoavelmente generoso e mais curto do que o ciclo de vida dochallenge
. O valor padrão recomendado é 5 minutos, mas você pode aumentar para até 10 minutos, o que ainda está dentro do intervalo recomendado. Tempos limite longos fazem sentido se você espera que os usuários utilizem o fluxo de trabalho híbrido, que normalmente demora um pouco mais. Se a operação expirar, umaNotAllowedError
será gerada.
Depois de criar publicKeyCredentialRequestOptions
, envie-o ao cliente.
Exemplo de código: criar opções de solicitação de credenciais
Estamos usando a biblioteca SimpleWebAuthn em nossos exemplos. Aqui, passamos a criação de opções de solicitação de credenciais para a função generateAuthenticationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
router.post('/signinRequest', csrfCheck, async (req, res) => {
// Ensure you nest calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Use the generateAuthenticationOptions function from SimpleWebAuthn
const options = await generateAuthenticationOptions({
rpID: process.env.HOSTNAME,
allowCredentials: [],
});
// Save the challenge in the user session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).json({ error: e.message });
}
});
Verificar e fazer login do usuário
Quando navigator.credentials.get
é resolvido no cliente, ele retorna um objeto PublicKeyCredential
.
O response
é um AuthenticatorAssertionResponse
. Ela representa a resposta do provedor de chave de acesso à instrução do cliente para criar o que é necessário para tentar autenticar com uma chave de acesso na RP. Ele contém:
response.authenticatorData
eresponse.clientDataJSON
, como na etapa de registro de chave de acesso.response.signature
, que contém uma assinatura sobre esses valores.
Envie o objeto PublicKeyCredential
para o servidor.
No servidor, faça o seguinte:
- Colete as informações necessárias para verificar a declaração e autenticar o usuário:
- Receba o desafio esperado que você armazenou na sessão quando gerou as opções de autenticação.
- Consiga a origin e o ID da RP esperados.
- Encontre no banco de dados quem é o usuário. No caso de credenciais detectáveis, não é possível saber quem é o usuário que está fazendo uma solicitação de autenticação. Para descobrir, você tem duas opções:
- Opção 1: use o
response.userHandle
no objetoPublicKeyCredential
. Na tabela Users, procure apasskey_user_id
que corresponde auserHandle
. - Opção 2: use a credencial
id
presente no objetoPublicKeyCredential
. Na tabela Credenciais de chave pública, procure a credencialid
que corresponde à credencialid
presente no objetoPublicKeyCredential
. Em seguida, procure o usuário correspondente usando a chave externapasskey_user_id
na tabela Users.
- Opção 1: use o
- Encontre no seu banco de dados as informações da credencial de chave pública que correspondem à declaração de autenticação que você recebeu. Para fazer isso, na tabela Credenciais de chave pública, procure a credencial
id
que corresponde à credencialid
presente no objetoPublicKeyCredential
.
Verifique a declaração de autenticação. Entregue essa etapa de verificação à biblioteca do lado do servidor FIDO, que geralmente oferece uma função utilitária para essa finalidade. O SimpleWebAuthn oferece, por exemplo, o
verifyAuthenticationResponse
. Saiba o que acontece nos bastidores no Apêndice: verificação da resposta de autenticação.Excluir o desafio mesmo com a verificação concluída para evitar ataques repetidos.
Faça o login do usuário. Se a verificação tiver sido bem-sucedida, atualize as informações da sessão para marcar o usuário como conectado. Também é possível retornar um objeto
user
ao cliente para que o front-end possa usar as informações associadas ao usuário que fez login recentemente.
Exemplo de código: verificar e fazer login do usuário
Estamos usando a biblioteca SimpleWebAuthn em nossos exemplos. Aqui, passamos a verificação da resposta de autenticação à função verifyAuthenticationResponse
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/signinResponse', csrfCheck, async (req, res) => {
const response = req.body;
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Find the credential stored to the database by the credential ID
const cred = Credentials.findById(response.id);
if (!cred) {
throw new Error('Credential not found.');
}
// Find the user - Here alternatively we could look up the user directly
// in the Users table via userHandle
const user = Users.findByPasskeyUserId(cred.passkey_user_id);
if (!user) {
throw new Error('User not found.');
}
// Base64URL decode some values
const authenticator = {
credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
credentialID: isoBase64URL.toBuffer(cred.id),
transports: cred.transports,
};
// Verify the credential
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
authenticator,
requireUserVerification: false,
});
if (!verified) {
throw new Error('User verification failed.');
}
// Kill the challenge for this session.
delete req.session.challenge;
req.session.username = user.username;
req.session['signed-in'] = 'yes';
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).json({ error: e.message });
}
});
Apêndice: verificação da resposta de autenticação
A verificação da resposta de autenticação consiste nas seguintes verificações:
- Verifique se o ID da RP corresponde ao seu site.
- Verifique se a origem da solicitação corresponde à origem de login do seu site. Para apps Android, consulte Verificar a origem.
- Verifique se o dispositivo forneceu o desafio que você criou.
- Verifique se, durante a autenticação, o usuário seguiu os requisitos que você determina como parte restrita. Se você exigir a verificação do usuário, confira se a flag
uv
(verificado pelo usuário) emauthenticatorData
étrue
. Verifique se a flagup
(usuário presente) emauthenticatorData
étrue
, já que a presença do usuário é sempre necessária para chaves de acesso. - Verificar a assinatura. Para verificar a assinatura, você precisa do seguinte:
- A assinatura, que é o desafio assinado:
response.signature
- A chave pública, com que verificar a assinatura.
- Os dados originais assinados. Esses são os dados cuja assinatura será verificada.
- O algoritmo criptográfico usado para criar a assinatura.
- A assinatura, que é o desafio assinado:
Para saber mais sobre essas etapas, consulte o código-fonte para verifyAuthenticationResponse
do SimpleWebAuthn ou confira a lista completa de verificações na especificação (links em inglês).