Visão geral
Confira uma visão geral de alto nível das principais etapas envolvidas no registro da chave de acesso:
- Defina as opções para criar uma chave de acesso. Envie-os ao cliente para que você possa transmiti-los à chamada de criação da chave de acesso: a chamada de API WebAuthn
navigator.credentials.create
na Web ecredentialManager.createCredential
no Android. Depois que o usuário confirma a criação da chave de acesso, a chamada é resolvida e retorna uma credencialPublicKeyCredential
. - Verifique a credencial e armazene-a no servidor.
As seções a seguir mergulham nos detalhes de cada etapa.
Criar opções de criação de credenciais
A primeira etapa no servidor é criar um objeto PublicKeyCredentialCreationOptions
.
Para isso, use sua biblioteca FIDO do lado do servidor. Normalmente, ele oferece uma função utilitária que pode criar essas opções para você. O SimpleWebAuthn oferece, por exemplo, 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 para as propriedades da credencial que você está criando. Depois de definir tudo isso, transmita-os conforme necessário para a função na biblioteca FIDO do lado do servidor responsável por criar o objeto PublicKeyCredentialCreationOptions
.
Alguns dos campos da PublicKeyCredentialCreationOptions
podem ser constantes. Outras precisam ser definidas dinamicamente no servidor:
rpId
: para preencher o ID da RP no servidor, use funções ou variáveis do lado do servidor ou variáveis que forneçam o nome do host do 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 de usuário, se ele estiver criando uma chave de acesso na inscrição.user.name
costuma ser um endereço de e-mail exclusivo da RP.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. Deve ser permanente, ao contrário de um nome de usuário editável. O ID do usuário identifica uma conta, mas não pode conter informações de identificação pessoal (PII). Você provavelmente já tem um ID do usuário no seu sistema, mas, se necessário, crie um especificamente para as chaves de acesso e evite PIIs nele.excludeCredentials
: uma lista dos IDs das credenciais para evitar a duplicação de uma chave de acesso do provedor da chave de acesso. Para preencher este campo, procure no seu banco de dados as credenciais existentes para esse usuário. Confira 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 atestado, 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 atestados, o desafio ainda é um campo obrigatório. Nesse caso, você pode definir esse desafio como um único0
para simplificar. As instruções para criar um desafio seguro para a autenticação estão disponíveis em Autenticação da chave de acesso do lado do servidor.
Codificação e decodificação
![PublicKeyCredentialCreationOptions enviadas pelo servidor](https://developers.google.cn/identity/passkeys/developer-guides/server-registration/images/server-publicKeyCredentialCreationOptions.png?authuser=5&hl=pt-br)
PublicKeyCredentialCreationOptions
enviado pelo servidor. challenge
, user.id
e excludeCredentials.credentials
precisam ser codificados no lado do servidor em base64URL
para que PublicKeyCredentialCreationOptions
possa ser entregue por HTTPS.PublicKeyCredentialCreationOptions
incluem campos que são ArrayBuffer
s, por isso não são aceitos por 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 são responsabilidade da biblioteca FIDO do lado do servidor.
- No cliente, a codificação e a decodificação precisam ser feitas manualmente no momento. Vai ser mais fácil no futuro: um método para converter opções como JSON em
PublicKeyCredentialCreationOptions
será disponibilizado. Confira o status da implementação no Chrome.
Exemplo de código: criar opções de criação de credenciais
Vamos usar a biblioteca SimpleWebAuthn (link em inglês) nos exemplos. Aqui, passamos 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
![PublicKeyCredentialCreationOptions enviadas pelo servidor](https://developers.google.cn/identity/passkeys/developer-guides/server-registration/images/server-publicKeyCredentialCreationOptions.png?authuser=5&hl=pt-br)
navigator.credentials.create
retorna um objeto PublicKeyCredential
.Quando navigator.credentials.create
é resolvido com sucesso 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 chaves de acesso à instrução do cliente para criar uma. Ele contém informações sobre a nova credencial que você precisa como RP para autenticar o usuário mais tarde. Saiba mais sobre AuthenticatorAttestationResponse
no Apêndice: AuthenticatorAttestationResponse
.
Envie o objeto PublicKeyCredential
ao servidor. Faça a verificação assim que você receber o e-mail.
Entregue essa etapa de verificação à biblioteca do lado do servidor da FIDO. Ele geralmente oferece uma função utilitária para essa finalidade. O SimpleWebAuthn oferece, por exemplo, verifyRegistrationResponse
. Saiba o que acontece nos bastidores em Apêndice: verificação da resposta de registro.
Quando a verificação for concluída, armazene as informações da credencial 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 única senha, mas pode ter várias chaves de acesso. Por exemplo, uma chave de acesso sincronizada pelas Chaves do iCloud da Apple e outra 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-a como uma chave primária para sua tabela Usuários.username
. Um nome de usuário definido pelo usuário, possivelmente 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 se autenticar, o autenticador vai disponibilizar essepasskey_user_id
na resposta de autenticação emuserHandle
. Recomendamos não definirpasskey_user_id
como a chave primária. As chaves primárias tendem a se tornar PII de fato nos sistemas, porque são amplamente usadas.
- Tabela de credenciais da chave pública:
id
: ID da credencial. Use-a como uma chave primária para sua tabela de credenciais da chave pública.public_key
: chave pública da credencial.passkey_user_id
: use como uma chave estrangeira para estabelecer um link com a tabela Usuários.backed_up
: o backup de uma chave de acesso será feito se ela for sincronizada pelo provedor da chave de acesso. Armazenar o estado de backup é útil se você quiser descartar senhas de usuários com chaves de acessobacked_up
no futuro. É possível verificar se a chave de acesso está armazenada em backup examinando as flags emauthenticatorData
ou usando um recurso de biblioteca FIDO do lado do servidor, que normalmente está disponível para oferecer acesso fácil a essas informações. Armazenar a qualificação para o backup pode ser útil para resolver possíveis dúvidas dos usuários.name
: opcionalmente, um nome de exibição para a credencial, permitindo que os usuários deem nomes personalizados às credenciais.transports
: uma matriz de transportes. O armazenamento de 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 de chaves de acesso usa para se comunicar com os clientes, principalmente nos casos de uso de reautenticação em queallowCredentials
não está vazio.
Outras informações podem ser úteis para armazenar para fins de experiência do usuário, incluindo itens como o provedor da chave de acesso, o horário de criação da credencial e o horário do último uso. Leia mais em Design da interface do usuário de chaves de acesso.
Exemplo de código: armazenar a credencial
Vamos usar a biblioteca SimpleWebAuthn (link em inglês) nos exemplos.
Aqui, entregamos a verificação de resposta de registro à 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 como são vistos pelo navegador. Ele contém a origem da RP, o desafio eandroidPackageName
, se o cliente for um app Android. Como RP, a leitura declientDataJSON
dá acesso às informações que o navegador encontrou no momento da solicitaçãocreate
.response.attestationObject
contém duas informações:attestationStatement
, que não é relevante, a menos que você use atestado.authenticatorData
são dados conforme vistos pelo provedor da chave de acesso. Como RP, a leitura deauthenticatorData
dá 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 da 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 da chave de acesso pode ser útil para seus usuários, especialmente se eles tiverem uma chave registrada para seu serviço em vários provedores.
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, mesmo que você não use o atestado. O authenticatorData
é codificado e contém campos codificados em formato binário. Sua biblioteca do lado do servidor geralmente lida com a análise e a decodificação. Se você não estiver usando uma biblioteca do lado do servidor, aproveite o getAuthenticatorData()
do lado do cliente para economizar tempo na análise e decodificação do lado do servidor.
Apêndice: verificação da resposta de registro
Internamente, a verificação da resposta de registro consiste nas seguintes verificações:
- Verifique se o ID da parte restrita corresponde ao seu site.
- Verifique se a origem da solicitação é esperada para seu site (URL do site principal, app Android).
- Se você exigir a verificação do usuário, verifique se o sinalizador de verificação do usuário
authenticatorData.uv
étrue
. Verifique se a flag de presença do usuárioauthenticatorData.up
étrue
, já que ela é sempre obrigatória para chaves de acesso. - Verifique se o cliente foi capaz de apresentar o desafio que você apresentou. Se você não usa um atestado, essa verificação não é 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 listado em cada campo
alg
depublicKeyCredentialCreationOptions.pubKeyCredParams
, que normalmente é definido na biblioteca do lado do servidor e não aparece para você. Isso garante que os usuários só possam se registrar com algoritmos que você optou por permitir.
Para saber mais, consulte o código-fonte de verifyRegistrationResponse
do SimpleWebAuthn ou veja a lista completa de verificações na especificação (link em inglês).