Registro da chave de acesso do lado do servidor

Visão geral

Confira uma visão geral das principais etapas envolvidas no registro de chaves de acesso:

Fluxo de registro da chave de acesso

  • Defina opções para criar uma chave de acesso. Envie-os ao cliente para que possam ser transmitidos à chamada de criação de chaves de acesso: a chamada da API WebAuthn navigator.credentials.create na Web e credentialManager.createCredential no Android. Depois que o usuário confirma a criação da chave de acesso, a chamada de criação é resolvida e retorna uma credencial PublicKeyCredential.
  • Verifique a credencial e armazene-a no servidor.

As seções a seguir detalham cada etapa.

Criar opções de criação de credenciais

A primeira etapa no servidor é criar um objeto PublicKeyCredentialCreationOptions.

Para fazer isso, use a biblioteca do lado do servidor FIDO. 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 de chaves de acesso: informações sobre o usuário, sobre o RP e uma configuração para as propriedades da credencial que você está criando. Depois de definir tudo isso, transmita conforme necessário para a função na biblioteca do lado do servidor FIDO responsável por criar o objeto PublicKeyCredentialCreationOptions.

Alguns campos de PublicKeyCredentialCreationOptions podem ser constantes. Outros precisam ser definidos dinamicamente no servidor:

  • rpId: para preencher o ID da parte confiável no servidor, use funções ou variáveis do lado do servidor que forneçam o nome do host do seu aplicativo da Web, como example.com.
  • user.name e user.displayName:para preencher esses campos, use as informações da sessão do usuário conectado ou as informações da nova conta de usuário, se ele estiver criando uma chave de acesso ao se inscrever. user.name normalmente é um endereço de e-mail e é exclusivo para o RP. user.displayName é um nome fácil de usar. Nem todas as plataformas usam displayName.
  • user.id: uma string aleatória e exclusiva gerada na criação da conta. Ele precisa ser permanente, ao contrário de um nome de usuário que pode ser editado. O ID de 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 especificamente para chaves de acesso e evite informações de identificação pessoal.
  • excludeCredentials: uma lista de IDs de credenciais atuais para evitar a duplicação de uma chave de acesso do provedor. Para preencher esse campo, procure no seu banco de dados as credenciais atuais do usuário. Confira os detalhes em Impedir a criação de uma nova chave de acesso se já existir uma.
  • challenge: para o registro de credenciais, o desafio não é relevante, a menos que você use o atestado, uma técnica mais avançada para verificar a identidade de um provedor de chaves de acesso e os dados emitidos por ele. No entanto, mesmo que você não use o atestado, o desafio ainda é um campo obrigatório. As instruções para criar um desafio seguro para autenticação estão disponíveis em Autenticação de chaves de acesso do lado do servidor.

Codificação e decodificação

PublicKeyCredentialCreationOptions enviado pelo servidor
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 inclui campos que são ArrayBuffers, portanto, não são compatíveis com JSON.stringify(). Isso significa que, no momento, para entregar PublicKeyCredentialCreationOptions por HTTPS, alguns campos precisam ser codificados manualmente no servidor usando base64URL e depois decodificados no cliente.

  • No servidor, a codificação e a decodificação geralmente são feitas pela biblioteca do lado do servidor do FIDO.
  • No cliente, a codificação e a decodificação precisam ser feitas manualmente no momento. No futuro, isso vai ficar mais fácil: um método para converter opções como JSON em PublicKeyCredentialCreationOptions vai 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 nos nossos exemplos. Aqui, entregamos a criação de opções de credenciais de chave pública à 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 = await 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 enviado pelo servidor
navigator.credentials.create retorna um objeto PublicKeyCredential.

Quando navigator.credentials.create é resolvido com sucesso no cliente, 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 chave de acesso. 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 para o servidor. Depois de receber, faça a verificação.

Transfira essa etapa de verificação para sua biblioteca do lado do servidor FIDO. Normalmente, ele 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.

Depois que a verificação for concluída, armazene as informações de credenciais no seu banco de dados para que o usuário possa se autenticar depois 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 pode ter várias chaves de acesso, por exemplo, uma sincronizada pelo Keychain do iCloud da Apple e outra pelo Gerenciador de senhas do Google.

Confira um exemplo de esquema que você pode usar para armazenar informações de credenciais:

Esquema de banco de dados para chaves de acesso

  • Tabela Users:
    • user_id: o ID do usuário principal. Um ID aleatório, exclusivo e permanente do usuário. Use isso como uma chave primária para sua tabela Users.
    • username. Um nome de usuário definido pelo usuário, que pode ser editado.
    • passkey_user_id: o ID do usuário sem PII específico da chave de acesso, representado por user.id nas suas opções de registro. Quando o usuário tentar se autenticar mais tarde, o autenticador vai disponibilizar esse passkey_user_id na resposta de autenticação em userHandle. Recomendamos que você não defina passkey_user_id como uma chave primária. As chaves primárias tendem a se tornar PII de fato nos sistemas porque são usadas extensivamente.
  • Tabela Credenciais de chave pública:
    • id: ID da credencial. Use isso como uma chave primária para sua tabela Credenciais de 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 Users.
    • backed_up: uma chave de acesso é armazenada em backup se for sincronizada pelo provedor dela. Armazenar o estado de backup é útil se você quiser considerar a remoção de senhas no futuro para usuários que têm chaves de acesso backed_up. Para verificar se a chave de acesso está armazenada em backup, examine a flag BE em authenticatorData ou use um recurso de biblioteca do lado do servidor FIDO, que geralmente está disponível para facilitar o acesso a essas informações. Armazenar a qualificação para backup pode ser útil para responder a possíveis dúvidas dos usuários.
    • name: opcionalmente, um nome de exibição para a credencial que permite aos usuários dar nomes personalizados a elas.
    • 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 de acordo e mostrar uma interface que corresponda ao transporte usado pelo provedor de chaves de acesso para se comunicar com os clientes, principalmente para casos de uso de reautenticação em que allowCredentials não está vazio.

Outras informações podem ser úteis para armazenar com o objetivo de melhorar a experiência do usuário, incluindo itens como o provedor de chave de acesso, o horário de criação da credencial e o último horário de 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 nos nossos exemplos. Aqui, transferimos a verificação da 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 {       aaguid,       credentialPublicKey,       credentialID,       credentialBackedUp     } = registrationInfo;      // Name the credential based on AAGUID     const name =       aaguid === undefined ||       aaguid === '000000-0000-0000-0000-00000000' ?         req.useragent?.platform : aaguids[aaguid].name;      const base64CredentialID = isoBase64URL.fromBuffer(credentialID);     const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);      // Existing, signed-in user     const { user } = res.locals;      // Save the credential     await Credentials.update({       id: base64CredentialID,       passkey_user_id: user.passkey_user_id,       publicKey: base64PublicKey,       name,       aaguid,       transports: response.response.transports,       backed_up: credentialBackedUp,       registered_at: new Date().getTime()     });      // 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 os dados vistos pelo navegador. Ele contém a origem da RP, o desafio e androidPackageName se o cliente for um app Android. Como uma RP, a leitura de clientDataJSON dá acesso a informações que o navegador viu no momento da solicitação create.
  • response.attestationObjectcontém duas informações:
    • attestationStatement, que não é relevante, a menos que você use o atestado.
    • authenticatorData são dados conforme vistos pelo provedor de chaves de acesso. Como um RP, a leitura de authenticatorData dá acesso aos dados vistos pelo provedor de chaves de acesso e retornados no momento da solicitação de create.

authenticatorDatacontém informações essenciais sobre a credencial de chave pública associada à chave de acesso recém-criada:

Embora authenticatorData esteja aninhado em attestationObject, as informações que ele contém são necessárias para a implementação da chave de acesso, com ou sem atestado. authenticatorData é codificado e contém campos codificados em formato binário. Normalmente, sua biblioteca do lado do servidor processa a análise e a decodificação. Se você não estiver usando uma biblioteca do lado do servidor, considere usar o getAuthenticatorData() do lado do cliente para economizar trabalho de análise e decodificação do lado do servidor.

Apêndice: verificação da resposta de registro

Por baixo dos panos, a verificação da resposta de 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 principal do site, app Android).
  • Se você exigir a verificação do usuário, confira se a flag authenticatorData.uv está true.
  • Normalmente, a flag de presença do usuário authenticatorData.up é true, mas se a credencial for criada condicionalmente, ela será false.
  • Verifique se o cliente conseguiu fornecer o desafio que você deu. Se você não usa 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 a comprovação no futuro.
  • Verifique se o ID da credencial ainda não está registrado para nenhum usuário.
  • Verifique se o algoritmo usado pelo provedor de chaves de acesso para criar a credencial é um dos que você listou (em cada campo alg de publicKeyCredentialCreationOptions.pubKeyCredParams, que geralmente é definido na biblioteca do lado do servidor e não é visível para você). Isso garante que os usuários só possam se registrar com algoritmos que você permitiu.

Para saber mais, confira o código-fonte do SimpleWebAuthn para verifyRegistrationResponse ou confira a lista completa de verificações na especificação.

A seguir

Autenticação de chaves de acesso do lado do servidor