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 da 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-as ao cliente para que você possa transmiti-las à 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), que 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 mergulham nos detalhes 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 sua finalidade, 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 depois de cada tentativa de login, seja ela bem-sucedida ou não. Descartar o desafio após uma certa duração também. Nunca aceite o mesmo desafio em uma resposta mais de uma vez.
  2. Garanta que o desafio seja criptograficamente seguro. Um desafio é praticamente impossível de adivinhar. Para criar um desafio no lado do servidor com criptografia segura, recomendamos que você use uma biblioteca FIDO confiável no lado do servidor. Se você criar seus próprios desafios, use a funcionalidade criptográfica integrada do seu conjunto de tecnologias ou procure bibliotecas projetadas para casos de uso de criptografia. Por exemplo, iso-crypto no Node.js ou secrets em Python. De acordo com a especificação, o desafio precisa ter pelo menos 16 bytes para ser considerado seguro.

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

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

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

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, 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 FIDO do lado do servidor responsável por criar o objeto publicKeyCredentialRequestOptions.

Alguns dos campos da publicKeyCredentialRequestOptions podem ser constantes. Outras precisam ser definidas 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 que o ID da RP definido em publicKeyCredentialCreationOptions durante o registro da credencial.
  • challenge: um dado que o provedor da chave de acesso vai assinar para provar que o usuário tem a chave de acesso no momento da solicitação de autenticação. Analise os detalhes em Criar o desafio.
  • allowCredentials: uma matriz de credenciais aceitáveis para esta 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. Para saber mais, consulte Buscar um desafio do servidor da parte restrita e Análise detalhada de credenciais detectáveis.
  • userVerification: indica se a verificação do usuário com o bloqueio de tela do dispositivo é "obrigatória", "preferida" ou "não recomendada". Consulte Buscar um desafio no servidor da RP.
  • timeout: quanto tempo (em milissegundos) o usuário pode levar para concluir a autenticação. Ele precisa ser razoavelmente generoso e menor que o ciclo de vida do challenge. O valor padrão recomendado é 5 minutos, mas é possível aumentá-lo até 10 minutos, que ainda está dentro do intervalo recomendado. Tempos limite longos são úteis 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 para o 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 credencial

Vamos usar a biblioteca SimpleWebAuthn (link em inglês) nos exemplos. Aqui, passamos a criação de opções de solicitação de credencial 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 com sucesso no cliente, ele retorna um objeto PublicKeyCredential.

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

O response é um AuthenticatorAssertionResponse. Ela representa a resposta do provedor da 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 da chave de acesso.
  • response.signature, que contém uma assinatura sobre esses valores.

Envie o objeto PublicKeyCredential ao servidor.

No servidor, faça o seguinte:

Esquema do banco de dados
Esquema de banco de dados sugerido. Saiba mais sobre esse design em Registro da chave de acesso do lado do servidor.
  • Colete as informações necessárias para verificar a declaração e autenticar o usuário:
    • Encontre 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 seu 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. Você tem duas opções para descobrir:
      • Opção 1: use o response.userHandle no objeto PublicKeyCredential. Na tabela Usuários, 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 estrangeira passkey_user_id na sua tabela Usuários.
    • Encontre no seu banco de dados as informações das credenciais de chave pública que correspondem à declaração de autenticação que você recebeu. Para fazer isso, na tabela Public key credentials, 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 à sua biblioteca FIDO do lado do servidor, que normalmente oferecerá uma função utilitária para essa finalidade. O SimpleWebAuthn oferece, por exemplo, verifyAuthenticationResponse. Saiba o que acontece nos bastidores em Apêndice: verificação da resposta de autenticação.

  • Exclua o desafio independentemente de a verificação ter sido bem-sucedida ou não para evitar ataques repetidos.

  • Faça o login do usuário. Se a verificação foi concluída, 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

Vamos usar a biblioteca SimpleWebAuthn (link em inglês) nos exemplos. Aqui, entregamos 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 parte restrita corresponde ao seu site.
  • Verifique se a origem da solicitação corresponde à origem de login do seu site. No caso de apps Android, consulte Verificar origem.
  • Verifique se o dispositivo foi capaz de apresentar o desafio que você apresentou.
  • Verifique se, durante a autenticação, o usuário seguiu os requisitos que você determina como RP. Se você exigir a verificação do usuário, confira se a sinalização uv (verificado pelo usuário) em authenticatorData é true. Verifique se a flag up (presente do usuário) 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 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 de verifyAuthenticationResponse do SimpleWebAuthn ou veja a lista completa de verificações na especificação.