Registro da chave de acesso do lado do servidor

Visão geral

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

Fluxo de registro da chave 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 e credentialManager.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 credencial PublicKeyCredential.
  • 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, como example.com.
  • user.name e user.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 usam displayName.
  • 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 único 0. 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 enviada 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 enviado por HTTPS.

PublicKeyCredentialCreationOptions incluem campos que são ArrayBuffers, 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

PublicKeyCredentialCreationOptions enviada pelo servidor
navigator.credentials.create retorna um objeto PublicKeyCredential.

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:

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 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 por user.id nas opções de registro. Quando o usuário tentar fazer a autenticação mais tarde, o autenticador vai disponibilizar essa passkey_user_id na resposta de autenticação no userHandle. Recomendamos que você não defina passkey_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 acesso backed_up. Verifique se a chave de acesso foi armazenada em backup examinando as flags no authenticatorData 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 o allowCredentials 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 e androidPackageName, se o cliente for um app Android. Como parte restrita, a leitura de clientDataJSON concede acesso às 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 atestados.
    • authenticatorData são os dados conforme vistos pelo provedor da chave de acesso. Como RP, a leitura de authenticatorData oferece acesso aos dados vistos pelo provedor da chave de acesso e retornados no momento da solicitação create.

authenticatorDataconté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ário authenticatorData.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 de publicKeyCredentialCreationOptions.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).

A seguir

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