Visão geral
Confira uma visão geral de alto nível das principais etapas envolvidas no registro de chaves de acesso:
- Defina as opções para criar uma chave de acesso. Envie-as ao cliente para que você possa transmiti-las para a chamada de criação da chave de acesso: a chamada da API WebAuthn
navigator.credentials.create
na Web ecredentialManager.createCredential
no Android. Depois que o usuário confirmar a criação da chave de acesso, a chamada de criação será resolvida e retornará uma credencialPublicKeyCredential
. - Verifique e armazene a credencial no servidor.
As seções a seguir detalham as especificidades de cada etapa.
Criar opções de criação de credenciais
A primeira etapa no servidor é criar um objeto PublicKeyCredentialCreationOptions
.
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 generateRegistrationOptions
.
O PublicKeyCredentialCreationOptions
precisa incluir tudo o que é necessário para a criação da chave de acesso: informações sobre o usuário, sobre a RP e uma configuração das propriedades da credencial que você está criando. Depois de definir todos eles, transmita-os conforme necessário para a função na biblioteca do lado do servidor FIDO responsável por criar o objeto PublicKeyCredentialCreationOptions
.
Parte do tempo de PublicKeyCredentialCreationOptions
campos podem ser constantes. Outros precisam ser definidos dinamicamente no servidor:
rpId
: para preencher o ID da RP no servidor, use funções ou variáveis do lado do servidor que forneçam o nome do host do seu aplicativo da Web, comoexample.com
.user.name
euser.displayName
:para preencher esses campos, use as informações da sessão do usuário que fez login ou as informações da nova conta, se o usuário estiver criando uma chave de acesso na inscrição.user.name
normalmente é um endereço de e-mail exclusivo para a parte restrita.user.displayName
é um nome fácil de usar. Nem todas as plataformas usamdisplayName
.user.id
: uma string aleatória e exclusiva gerada na criação da conta. Ele deve ser permanente, ao contrário de um nome de usuário que pode ser editável. O ID do usuário identifica uma conta, mas não pode conter informações de identificação pessoal (PII). É provável que você já tenha um ID de usuário no seu sistema, mas, se necessário, crie um ID específico para chaves de acesso e evite PIIs.excludeCredentials
: uma lista das credenciais atuais. IDs para evitar a duplicação de uma chave de acesso do provedor. Para preencher esse campo, procure as credenciais deste usuário no banco de dados. Analise os detalhes em Impedir a criação de uma nova chave de acesso, se já houver uma.challenge
: para o registro de credenciais, o desafio não é relevante, a menos que você use atestados, uma técnica mais avançada para verificar a identidade de um provedor de chaves de acesso e os dados que ele emite. No entanto, mesmo que você não use atestado, o desafio ainda é um campo obrigatório. Nesse caso, para simplificar, você pode definir o desafio como um único0
. Veja as instruções para criar um desafio de autenticação seguro em Autenticação de chaves de acesso do lado do servidor.
Codificação e decodificação
PublicKeyCredentialCreationOptions
incluem campos que são ArrayBuffer
s, por isso não têm suporte de JSON.stringify()
. Isso significa que, no momento, para entregar PublicKeyCredentialCreationOptions
por HTTPS, alguns campos precisam ser codificados manualmente no servidor usando base64URL
e decodificados no cliente.
- No servidor, a codificação e a decodificação geralmente são realizadas pela biblioteca do lado do servidor FIDO.
- No cliente, a codificação e a decodificação precisam ser feitas manualmente no momento. Isso será mais fácil no futuro: um método para converter opções como JSON em
PublicKeyCredentialCreationOptions
estará disponível. Confira o status da implementação no Chrome.
Exemplo de código: criar opções de criação de credenciais
Estamos usando a biblioteca SimpleWebAuthn em nossos exemplos. Aqui, transferimos a criação de opções de credenciais de chave pública para a função generateRegistrationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
const { user } = res.locals;
// 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 {
// `excludeCredentials` prevents users from re-registering existing
// credentials for a given passkey provider
const excludeCredentials = [];
const credentials = Credentials.findByUserId(user.id);
if (credentials.length > 0) {
for (const cred of credentials) {
excludeCredentials.push({
id: isoBase64URL.toBuffer(cred.id),
type: 'public-key',
transports: cred.transports,
});
}
}
// Generate registration options for WebAuthn create
const options = generateRegistrationOptions({
rpName: process.env.RP_NAME,
rpID: process.env.HOSTNAME,
userID: user.id,
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
requireResidentKey: true
},
});
// Keep the challenge in the session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Armazenar a chave pública
Quando navigator.credentials.create
é resolvido no cliente, isso significa que uma chave de acesso foi criada. Um objeto PublicKeyCredential
é retornado.
O objeto PublicKeyCredential
contém um objeto AuthenticatorAttestationResponse
, que representa a resposta do provedor de chave de acesso à instrução do cliente para criar uma chave de acesso. Ele contém informações sobre a nova credencial de que você precisa como RP para autenticar o usuário mais tarde. Saiba mais sobre AuthenticatorAttestationResponse
no Apêndice: AuthenticatorAttestationResponse
.
Envie o objeto PublicKeyCredential
para o servidor. Verifique-o após o recebimento.
Entregue essa etapa de verificação à biblioteca do lado do servidor FIDO. Normalmente, ele oferece uma função utilitária para essa finalidade. O SimpleWebAuthn oferece, por exemplo, o verifyRegistrationResponse
. Saiba o que acontece nos bastidores no Apêndice: verificação da resposta do registro.
Depois que a verificação for bem-sucedida, armazene as informações das credenciais no seu banco de dados para que o usuário possa fazer a autenticação mais tarde com a chave de acesso associada a essa credencial.
Use uma tabela dedicada para credenciais de chave pública associadas a chaves de acesso. Um usuário só pode ter uma senha, mas várias chaves de acesso. Por exemplo, uma chave de acesso sincronizada pelas Chaves do iCloud da Apple e uma pelo Gerenciador de senhas do Google.
Confira um exemplo de esquema que pode ser usado para armazenar informações de credenciais:
- Tabela Users:
user_id
: o ID do usuário principal. Um ID aleatório, exclusivo e permanente para o usuário. Use-o como uma chave primária para sua tabela Users.username
Um nome de usuário definido pelo usuário, potencialmente editável.passkey_user_id
: o ID do usuário sem PII específico da chave de acesso, representado poruser.id
nas opções de registro. Quando o usuário tentar fazer a autenticação mais tarde, o autenticador vai disponibilizar essapasskey_user_id
na resposta de autenticação nouserHandle
. Recomendamos que você não definapasskey_user_id
como chave primária. As chaves primárias tendem a se tornar PII de fato nos sistemas, porque são amplamente usadas.
- Tabela de credenciais de chave pública:
id
: ID da credencial. Use como chave primária para sua tabela de Credenciais de chave pública.public_key
: chave pública da credencial.passkey_user_id
: use como uma chave externa para estabelecer um link com a tabela Usuários.backed_up
: uma chave de acesso será armazenada em backup se for sincronizada pelo provedor. Armazenar o estado do backup é útil se você quer descartar senhas no futuro para usuários com chaves de acessobacked_up
. Verifique se a chave de acesso foi armazenada em backup examinando as flags noauthenticatorData
ou usando um recurso de biblioteca do lado do servidor FIDO, normalmente disponível para oferecer acesso fácil a essas informações. Armazenar a qualificação para backup pode ser útil para atender a possíveis dúvidas de usuários.name
: opcionalmente, um nome de exibição para a credencial para permitir que os usuários atribuam nomes personalizados às credenciais.transports
: uma matriz de transportes. Armazenar transportes é útil para a experiência do usuário de autenticação. Quando os transportes estão disponíveis, o navegador pode se comportar corretamente e mostrar uma interface que corresponde ao transporte que o provedor da chave de acesso usa para se comunicar com os clientes, principalmente para casos de uso de reautenticação em que oallowCredentials
não está vazio.
Outras informações podem ser úteis para armazenar a experiência do usuário, incluindo itens como o provedor da chave de acesso, a hora de criação da credencial e a hora do último uso. Leia mais em Design da interface do usuário de chaves de acesso.
Exemplo de código: armazenar a credencial
Estamos usando a biblioteca SimpleWebAuthn em nossos exemplos.
Aqui, transferimos a verificação de resposta de registro para a função verifyRegistrationResponse
.
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
const response = req.body;
// This sample code is for registering a passkey for an existing,
// signed-in user
// 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 {
// Verify the credential
const { verified, registrationInfo } = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
requireUserVerification: false,
});
if (!verified) {
throw new Error('Verification failed.');
}
const { credentialPublicKey, credentialID } = registrationInfo;
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
publicKey: base64PublicKey,
// Optional: set the platform as a default name for the credential
// (example: "Pixel 7")
name: req.useragent.platform,
transports: response.response.transports,
passkey_user_id: user.passkey_user_id,
backed_up: registrationInfo.credentialBackedUp
});
// Kill the challenge for this session
delete req.session.challenge;
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Apêndice: AuthenticatorAttestationResponse
AuthenticatorAttestationResponse
contém dois objetos importantes:
response.clientDataJSON
é uma versão JSON dos dados do cliente, que na Web são dados vistos pelo navegador. Ele contém a origem da parte restrita, o desafio eandroidPackageName
, se o cliente for um app Android. Como parte restrita, a leitura declientDataJSON
concede acesso às informações que o navegador viu no momento da solicitaçãocreate
.response.attestationObject
contém duas informações:attestationStatement
, que não é relevante, a menos que você use atestados.authenticatorData
são os dados conforme vistos pelo provedor da chave de acesso. Como RP, a leitura deauthenticatorData
oferece acesso aos dados vistos pelo provedor da chave de acesso e retornados no momento da solicitaçãocreate
.
authenticatorData
contém informações essenciais sobre a credencial de chave pública associada à chave de acesso recém-criada:
- A própria credencial de chave pública e um ID de credencial exclusivo para ela.
- O ID da RP associado à credencial.
- Flags que descrevem o status do usuário quando a chave de acesso foi criada: se um usuário estava realmente presente e se ele foi verificado (consulte
userVerification
). - AAGUID, que identifica o provedor da chave de acesso. Mostrar o provedor de chaves de acesso pode ser útil para seus usuários, especialmente se eles tiverem uma chave de acesso registrada para seu serviço em vários provedores de chaves de acesso.
Mesmo que o authenticatorData
esteja aninhado em attestationObject
, as informações que ele contém são necessárias para a implementação da chave de acesso, independentemente de você usar atestados ou não. authenticatorData
é codificado e contém campos que são codificados em um formato binário. A biblioteca do lado do servidor normalmente lida com análise e decodificação. Se você não estiver usando uma biblioteca do lado do servidor, use a getAuthenticatorData()
no lado do cliente para economizar um pouco de análise e decodificação do trabalho do lado do servidor.
Apêndice: verificação da resposta de registro
Nos bastidores, a verificação da resposta do registro consiste nas seguintes verificações:
- Verifique se o ID da RP corresponde ao seu site.
- Verifique se a origem da solicitação é uma origem esperada para seu site (URL do site principal, app Android).
- Se você precisar da verificação do usuário, confira se a sinalização de verificação do usuário
authenticatorData.uv
étrue
. Verifique se a flag de presença do usuárioauthenticatorData.up
étrue
, já que a presença do usuário sempre é necessária para chaves de acesso. - Verifique se o cliente conseguiu usar o desafio proposto. Se você não usar atestado, essa verificação não será importante. No entanto, implementar essa verificação é uma prática recomendada: ela garante que seu código esteja pronto se você decidir usar atestados no futuro.
- Verifique se o ID da credencial ainda não está registrado para nenhum usuário.
- Verifique se o algoritmo usado pelo provedor da chave de acesso para criar a credencial é um algoritmo que você listou em cada campo
alg
depublicKeyCredentialCreationOptions.pubKeyCredParams
, que normalmente é definido na biblioteca do lado do servidor e não pode ser visto por você. Isso garante que os usuários só possam se registrar com algoritmos que você permitir.
Para saber mais, consulte o código-fonte de verifyRegistrationResponse
do SimpleWebAuthn ou veja a lista completa de verificações na especificação (em inglês).