Autenticação da chave de acesso do lado do servidor

Visão geral

Confira uma visão geral das principais etapas envolvidas na autenticação com chave de acesso:

Fluxo de autenticação da chave de acesso

  • Defina o desafio e outras opções necessárias para autenticar com uma chave de acesso. Envie-os ao cliente para que você possa transmiti-los à chamada de autenticação da chave de acesso (navigator.credentials.get na Web). Depois que o usuário confirma a autenticação da chave de acesso, a chamada de autenticação é resolvida e retorna 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 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 o propósito, você precisa:

  1. Garanta que o mesmo desafio nunca seja usado mais de uma vez. Gere um novo desafio a cada tentativa de login. Descarte o desafio após cada tentativa de login, seja ela bem-sucedida ou não. Descarte o desafio após um determinado período. Nunca aceite o mesmo desafio em uma resposta mais de uma vez.
  2. Verifique se o desafio é criptograficamente seguro. Um desafio deve ser praticamente impossível de adivinhar. Para criar um desafio criptograficamente seguro no lado do servidor, é melhor usar uma biblioteca FIDO confiável do lado do servidor. Se você criar seus próprios desafios, use a funcionalidade criptográfica integrada disponível na sua pilha de tecnologia ou procure bibliotecas projetadas para casos de uso criptográficos. Por exemplo, iso-crypto em Node.js ou secrets em Python. De acordo com a especificação, o desafio precisa ter pelo menos 16 bytes para ser considerado seguro.

Depois de criar um desafio, salve-o na sessão do usuário para verificação posterior.

Criar opções de solicitação de credencial

Crie opções de solicitação de credencial como um objeto publicKeyCredentialRequestOptions.

Para isso, use a biblioteca do lado do servidor FIDO. Normalmente, ela oferece uma função utilitária que pode criar essas opções para você. O SimpleWebAuthn oferece, por exemplo, generateAuthenticationOptions.

publicKeyCredentialRequestOptions precisa conter todas as informações necessárias para a autenticação com 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.

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

  • rpId: o ID da parte confiável que você espera que seja associado à credencial, por exemplo, example.com. A autenticação só vai funcionar se o ID da parte confiável fornecido aqui corresponder ao ID da parte confiável associado à credencial. Para preencher o ID da RP, use o mesmo valor que você definiu em publicKeyCredentialCreationOptions durante o registro da credencial.
  • challenge: um dado que o provedor de chaves de acesso vai assinar para provar que o usuário tem a chave de acesso no momento da solicitação de autenticação. Confira 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 RP e Investigação detalhada das credenciais detectáveis para mais detalhes.
  • userVerification: indica se a verificação do usuário usando o bloqueio de tela do dispositivo é "required", "preferred" ou "discouraged". Consulte Buscar um desafio no servidor da parte confiável.
  • timeout: quanto tempo (em milissegundos) o usuário tem para concluir a autenticação. Ele precisa ser razoavelmente generoso e menor que o ciclo de vida do challenge. O valor padrão recomendado é de 5 minutos, mas é possível aumentá-lo para até 10 minutos, o que ainda está dentro do intervalo recomendado. Os tempos limite longos fazem sentido se você espera que os usuários usem o fluxo de trabalho híbrido, que normalmente leva um pouco mais de tempo. Se a operação atingir o tempo limite, uma NotAllowedError será gerada.

Depois de criar o publicKeyCredentialRequestOptions, envie-o ao cliente.

publicKeyCredentialCreationOptions enviadas pelo servidor
Opções enviadas pelo servidor. challenge ocorre no lado do cliente.

Exemplo de código: criar opções de solicitação de credencial

Estamos usando a biblioteca SimpleWebAuthn nos nossos exemplos. Aqui, entregamos a criação de opções de solicitação de credencial para a função generateAuthenticationOptions.

import {
  generateAuthenticationOptions,
} 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 com sucesso no cliente, ele retorna um objeto PublicKeyCredential.

Objeto PublicKeyCredential enviado pelo servidor
navigator.credentials.get retorna um PublicKeyCredential.

O response é um AuthenticatorAssertionResponse. Ela representa a resposta do provedor de chaves 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.authenticatorDataeresponse.clientDataJSON, como na etapa de registro da chave de acesso.
  • response.signature, que contém uma assinatura desses valores.

Envie o objeto PublicKeyCredential para o servidor.

No servidor, faça o seguinte:

Database schema
Esquema de banco de dados sugerido. Saiba mais sobre esse design em Registro de chave de acesso do lado do servidor.
  • Reúna as informações necessárias para verificar a declaração e autenticar o usuário:
    • Recupere o desafio esperado que você armazenou na sessão ao gerar as opções de autenticação.
    • Consiga a origem e o ID da RP esperados.
    • Encontre no banco de dados quem é o usuário. No caso de credenciais detectáveis, você não sabe 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 objeto PublicKeyCredential. Na tabela Usuários, procure o passkey_user_id que corresponde a userHandle.
      • Opção 2: use a credencial id presente no objeto PublicKeyCredential. Na tabela Credenciais de chave pública, procure a credencial id que corresponde à credencial id presente no objeto PublicKeyCredential. Em seguida, procure o usuário correspondente usando a chave externa passkey_user_id para a tabela Users.
    • Encontre no banco de dados as informações de credencial de chave pública que correspondem à declaração de autenticação recebida. Para fazer isso, na tabela Credenciais de chave pública, procure a credencial id que corresponde à credencial id presente no objeto PublicKeyCredential.
  • Verifique a declaração de autenticação. Entregue essa etapa de verificação à biblioteca do lado do servidor FIDO, que normalmente oferece uma função utilitária para essa finalidade. O SimpleWebAuthn oferece, por exemplo, verifyAuthenticationResponse. Saiba o que acontece nos bastidores no Apêndice: verificação da resposta de autenticação.

  • Exclua o desafio, independentemente de a verificação ser bem-sucedida ou não, para evitar ataques de repetição.

  • Fazer o login do usuário. Se a verificação for 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 recém-conectado.

Exemplo de código: verificar e fazer login do usuário

Estamos usando a biblioteca SimpleWebAuthn nos nossos exemplos. Aqui, entregamos a verificação da resposta de autenticação à função verifyAuthenticationResponse.

import {
  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.');
    }
    // Prepare credential information for verification
    const credential = {
      id: cred.id,
      publicKey: isoBase64URL.toBuffer(cred.publicKey),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      credential,
      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 parte confiável corresponde ao seu site.
  • Verifique se a origem da solicitação corresponde à origem de login do seu site. Para apps Android, consulte Verificar origem.
  • Verifique se o dispositivo conseguiu fornecer o desafio que você deu a ele.
  • Verifique se, durante a autenticação, o usuário seguiu os requisitos que você exige como parte confiável. Se você exigir a verificação do usuário, confira se a flag uv (usuário verificado) em authenticatorData é true. Verifique se a flag up (usuário presente) em authenticatorData é true, já que a presença do usuário é sempre necessária para chaves de acesso.
  • Verifique a assinatura. Para verificar a assinatura, você precisa do seguinte:
    • A assinatura, que é o desafio assinado: response.signature
    • A chave pública, para verificar a assinatura.
    • Os dados originais assinados. São os dados cuja assinatura será verificada.
    • O algoritmo criptográfico usado para criar a assinatura.

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