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

Visão geral

Confira uma visão geral de alto nível das principais etapas envolvidas na autenticação de chaves 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-as ao cliente para que você possa transmiti-las para a chamada de autenticação da chave de acesso (navigator.credentials.get na Web). Depois que o usuário confirmar a autenticação da chave de acesso, a chamada de autenticação será resolvida e retornará 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 as especificidades de 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 seu propósito, você deve:

  1. Garanta que o mesmo desafio nunca seja usado mais de uma vez. Gerar um novo desafio a cada tentativa de login. Descarte o desafio após cada tentativa de login, seja ela concluída ou reprovada. Descarte o desafio após uma determinada duração também. Nunca aceite o mesmo desafio em uma resposta mais de uma vez.
  2. Verifique se o desafio tem segurança criptográfica. Deve ser praticamente impossível adivinhar um desafio. Para criar um desafio do lado do servidor com segurança criptográfica, é melhor usar uma biblioteca FIDO de confiança. Se você criar seus próprios desafios, use a funcionalidade criptográfica integrada disponível no seu conjunto de tecnologias ou procure bibliotecas projetadas para casos de uso criptográficos. Os exemplos incluem iso-crypto em Node.js ou secrets em Python. De acordo com a especificação, o desafio precisa ter pelo menos 16 bytes de comprimento para ser considerado seguro.

Depois de criar um desafio, salve-o na sessão do usuário para verificá-lo mais tarde.

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

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

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 generateAuthenticationOptions.

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

Parte do tempo de publicKeyCredentialRequestOptions campos podem ser constantes. Outros precisam ser definidos dinamicamente no servidor:

  • rpId: a qual ID da RP você espera que a credencial seja associada, por exemplo, example.com. A autenticação só será bem-sucedida se o ID da RP fornecido aqui corresponder ao ID da RP associado à credencial. Para preencher o ID da RP, use o mesmo valor do ID da RP definido no publicKeyCredentialCreationOptions durante o registro da credencial.
  • challenge: dados que o provedor da chave de acesso assinar para provar que o usuário detém a chave no momento da solicitação de autenticação. Revise 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 da RP e Análise detalhada sobre credenciais detectáveis para mais detalhes.
  • userVerification: indica se a verificação do usuário usando o bloqueio de tela do dispositivo é "obrigatória", "preferencial" ou "não recomendado". Consulte Buscar um desafio do servidor da RP.
  • timeout: quanto tempo (em milissegundos) o usuário pode levar para concluir a autenticação. Ele precisa ser razoavelmente generoso e mais curto do que o ciclo de vida do challenge. O valor padrão recomendado é 5 minutos, mas você pode aumentar para até 10 minutos, o que ainda está dentro do intervalo recomendado. Tempos limite longos fazem sentido se você espera que os usuários utilizem o fluxo de trabalho híbrido, que normalmente demora um pouco mais. Se a operação expirar, uma NotAllowedError será gerada.

Depois de criar publicKeyCredentialRequestOptions, envie-o ao cliente.

publicKeyCredentialCreationOptions enviada pelo servidor
Opções enviadas pelo servidor. A decodificação de challenge acontece no lado do cliente.

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

Estamos usando a biblioteca SimpleWebAuthn em nossos exemplos. Aqui, passamos a criação de opções de solicitação de credenciais para a função generateAuthenticationOptions.

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} 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 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 chave 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 de chave de acesso.
  • response.signature, que contém uma assinatura sobre esses 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.
  • Colete as informações necessárias para verificar a declaração e autenticar o usuário:
    • Receba o desafio esperado que você armazenou na sessão quando gerou as opções de autenticação.
    • Consiga a origin e o ID da RP esperados.
    • Encontre no banco de dados quem é o usuário. No caso de credenciais detectáveis, não é possível saber 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 Users, procure a 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 na tabela Users.
    • Encontre no seu banco de dados as informações da credencial de chave pública que correspondem à declaração de autenticação que você recebeu. 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 geralmente oferece uma função utilitária para essa finalidade. O SimpleWebAuthn oferece, por exemplo, o verifyAuthenticationResponse. Saiba o que acontece nos bastidores no Apêndice: verificação da resposta de autenticação.

  • Excluir o desafio mesmo com a verificação concluída para evitar ataques repetidos.

  • Faça o login do usuário. Se a verificação tiver sido 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 que fez login recentemente.

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

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

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  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.');
    }
    // Base64URL decode some values
    const authenticator = {
      credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
      credentialID: isoBase64URL.toBuffer(cred.id),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      authenticator,
      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 RP corresponde ao seu site.
  • Verifique se a origem da solicitação corresponde à origem de login do seu site. Para apps Android, consulte Verificar a origem.
  • Verifique se o dispositivo forneceu o desafio que você criou.
  • Verifique se, durante a autenticação, o usuário seguiu os requisitos que você determina como parte restrita. Se você exigir a verificação do usuário, confira se a flag uv (verificado pelo usuário) 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.
  • Verificar a assinatura. Para verificar a assinatura, você precisa do seguinte:
    • A assinatura, que é o desafio assinado: response.signature
    • A chave pública, com que verificar a assinatura.
    • Os dados originais assinados. Esses são os dados cuja assinatura será verificada.
    • O algoritmo criptográfico usado para criar a assinatura.
.

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