Proteger seu site usando a autenticação de dois fatores com uma chave de segurança (WebAuthn)

1. O que você vai criar

Você vai começar com um aplicativo da Web básico, que oferece suporte para login baseado em senha.

Em seguida, você vai adicionar suporte à autenticação de dois fatores com uma chave de segurança baseada no WebAuthn. Para isso, você vai implementar o seguinte:

  • Uma maneira para o usuário registrar uma credencial do WebAuthn.
  • Um fluxo de autenticação de dois fatores em que o segundo fator (uma credencial do WebAuthn) é pedido ao usuário, se ele tiver registrado um.
  • Uma interface de gerenciamento de credenciais, que é uma lista de credenciais que permite aos usuários renomear e excluir credenciais.

16ce77744061c5f7.png

Confira e teste o app da Web concluído.

2. Sobre o WebAuthn

Noções básicas sobre o WebAuthn

Por que usar o WebAuthn?

O phishing é um grande problema de segurança na Web. A maioria das violações de contas usa senhas fracas ou roubadas que são reutilizadas em vários sites. A resposta coletiva do setor a esse problema tem sido a autenticação multifator, mas as implementações são fragmentadas e muitas ainda não combatem o phishing de modo adequado.

A API Web Authentication, ou WebAuthn, é um protocolo padrão resistente a phishing que pode ser usado por qualquer aplicativo da Web.

Como funciona

Fonte: webauthn.guide

O WebAuthn permite que os servidores registrem e autentiquem usuários usando criptografia de chave pública em vez de uma senha. Os sites podem criar uma credencial, que consiste em um par de chaves pública/privada.

  • A chave privada é armazenada com segurança no dispositivo do usuário.
  • A chave pública e o ID da credencial gerado de forma aleatória são enviados ao servidor para armazenamento.

A chave pública é usada pelo servidor para provar a identidade do usuário. Ela não é secreta, porque não funciona sem a chave privada correspondente.

Benefícios

O WebAuthn tem dois benefícios principais:

  • Nenhum segredo compartilhado: o servidor não armazena segredos. Isso torna os bancos de dados menos atraentes para hackers, porque as chaves públicas não são úteis para eles.
  • Credenciais com escopo: uma credencial registrada para site.example não pode ser usada em evil-site.example. Isso torna o WebAuthn resistente a phishing.

Casos de uso

Um caso de uso do WebAuthn é a autenticação de dois fatores com uma chave de segurança. Isso pode ser relevante principalmente para aplicativos da Web corporativos.

Suporte do navegador

Ele foi escrito pelo W3C e pela FIDO, com a participação do Google, Mozilla, Microsoft e Yubico, entre outros.

Glossário

  • Autenticador: uma entidade de software ou hardware que pode registrar um usuário e depois declarar a posse da credencial registrada. Há dois tipos de autenticadores:
  • Autenticador de roaming: um autenticador que pode ser usado com qualquer dispositivo em que o usuário esteja tentando fazer login. Exemplo: uma chave de segurança USB, um smartphone.
  • Autenticador de plataforma: um autenticador integrado ao dispositivo do usuário. Exemplo: Touch ID da Apple.
  • Credencial: o par de chaves pública/privada.
  • Entidade confiável: o servidor do site que está tentando autenticar o usuário.
  • Servidor FIDO: o servidor usado para autenticação. A FIDO é uma família de protocolos desenvolvidos pela Aliança FIDO, e um desses protocolos é o WebAuthn.

Neste workshop, vamos usar um autenticador de roaming.

3. Antes de começar

O que é necessário

Para concluir este codelab, você vai precisar do seguinte:

  • Conhecimentos básicos sobre o WebAuthn.
  • Conhecimentos básicos sobre JavaScript e HTML.
  • Um navegador atualizado com suporte para WebAuthn.
  • Uma chave de segurança que esteja em conformidade com U2F.

É possível usar uma das seguintes opções como chave de segurança:

  • Um smartphone com o Android 7 (Nougat) ou mais recente que executa o Google Chrome. Nesse caso, você também vai precisar de uma máquina com Windows, macOS ou ChromeOS e com Bluetooth em funcionamento.
  • Uma chave USB, como YubiKey.

6539dc7ffec2538c.png

Fonte: https://www.yubico.com/products/security-key/

dd56e2cfe0f7ced2.png

O que você vai aprender

Você vai aprender ✅

  • Como registrar e usar uma chave de segurança como segundo fator para a autenticação do WebAuthn.
  • Como facilitar esse processo para o usuário.

Você não vai aprender ❌

  • Como criar um servidor FIDO, que é usado para autenticação. Isso não é um problema porque, normalmente, como desenvolvedor da Web ou de sites, você pode usar as implementações existentes do servidor FIDO. Sempre verifique a funcionalidade e a qualidade das implementações do servidor usadas. Neste codelab, o servidor FIDO usa o SimpleWebAuthn. Veja outras opções na página oficial da FIDO Alliance (link em inglês). Para bibliotecas de código aberto, consulte webauthn.io ou AwesomeWebAuthn (links em inglês).

Exoneração de responsabilidade

O usuário precisa digitar uma senha para fazer login. No entanto, para simplificar este codelab, a senha não será armazenada nem verificada. Em um aplicativo real, é necessário verificar se ela está correta do lado do servidor.

Verificações básicas de segurança, como verificações de CSRF, validação de sessões e limpeza de entrada são implementadas neste codelab. No entanto, muitas medidas de segurança não são. Por exemplo, não há um limite de entradas em senhas para impedir ataques de força bruta. Isso não é importante aqui, porque as senhas não são armazenadas, mas não use esse código como está na produção.

4. Configurar seu autenticador

Se você estiver usando um smartphone Android como autenticador

  • Verifique se o Chrome está atualizado no computador e no smartphone.
  • Abra o Chrome nesses dois dispositivos e faça login com o mesmo perfil que você quer usar no workshop.
  • Ative a sincronização para esse perfil no computador e no smartphone. Para fazer isso, use chrome://settings/syncSetup.
  • Ative o Bluetooth no computador e no smartphone.
  • No Chrome para computador conectado com o mesmo perfil, abra webauthn.io.
  • Digite um nome de usuário simples. Deixe os valores Tipo de atestado e Tipo de autenticador com os valores (padrão) Nenhum e Não especificado. Clique em Registrar.

6b49ff0298f5a0af.png

  • Uma janela do navegador será aberta, pedindo a verificação da identidade. Selecione seu smartphone na lista.

ffebe58ac826eaf2.png 852de328fcd4eb42.png

  • No smartphone, você vai receber a notificação Verificar sua identidade. Toque nela.
  • No smartphone, será pedido que você insira o código PIN ou toque no sensor de impressão digital. Digite-o.
  • Em webauthn.io, no computador, você verá um indicador escrito "Sucesso".

fc0acf00a4d412fa.png

  • Em webauthn.io, no computador, clique no botão Login.
  • Mais uma janela de navegador será aberta. Selecione seu smartphone na lista.
  • No smartphone, toque na notificação que aparecer e insira seu PIN ou toque no sensor de impressão digital.
  • O webauthn.io vai informar que você fez login. Seu smartphone está funcionando corretamente como uma chave de segurança. Está tudo pronto para o workshop!

Se você estiver usando uma chave de segurança USB como autenticador

  • No Chrome para computador, abra webauthn.io.
  • Digite um nome de usuário simples. Deixe os valores Tipo de atestado e Tipo de autenticador com os valores (padrão) Nenhum e Não especificado. Clique em Registrar.
  • Uma janela do navegador será aberta, pedindo a verificação da identidade. Selecione Chave de segurança USB na lista.

ffebe58ac826eaf2.png 9fe75f04e43da035.png

  • Insira a chave de segurança no computador e toque nela.

923d5adb8aa8286c.png

  • Em webauthn.io, no computador, você verá um indicador escrito "Sucesso".

fc0acf00a4d412fa.png

  • Em webauthn.io, no computador, clique no botão Login.
  • Mais uma janela de navegador será aberta. Selecione Chave de segurança USB na lista.
  • Toque na chave.
  • O Webauthn.io vai informar que você fez login. Sua chave de segurança USB está funcionando corretamente. Está tudo pronto para o workshop!

7e1c0bb19c9f3043.png

5. Começar a configuração

Neste codelab, você vai usar o Glitch, um editor de código on-line que implanta seu código de maneira automática e instantânea.

Bifurcar o código inicial

Abra o projeto inicial.

Clique no botão Remix.

Isso cria uma cópia do código inicial. Agora você tem seu próprio código para editar. Você fará todo o trabalho para este codelab na sua bifurcação, chamada de "remix" no Glitch.

cf2b9f552c9809b6.png

Explorar o código inicial

Explore o código inicial que você acabou de bifurcar.

Observe que, em libs, uma biblioteca chamada auth.js já foi fornecida. Essa é uma biblioteca personalizada que cuida da lógica de autenticação do lado do servidor. Ela usa a biblioteca fido como dependência.

6. Implementar o registro de credenciais

Implementar o registro de credenciais

A primeira coisa que precisamos para configurar a autenticação de dois fatores com uma chave de segurança é permitir que o usuário crie uma credencial.

Primeiro, vamos adicionar uma função que faz isso no código do lado do cliente.

Em public/auth.client.js, há uma função chamada registerCredential() que ainda não realiza nenhuma ação. Adicione o seguinte código a ela:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    "/auth/credential-options",
    "POST"
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
    }
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Send the encoded credential to the backend for storage
  return await _fetch("/auth/credential", "POST", encodedCredential);
}

Essa função já foi exportada para você.

Veja o que registerCredential faz:

  • Ela busca as opções de criação de credenciais do servidor (/auth/credential-options).
  • Como as opções do servidor voltam codificadas, ela usa a função utilitária decodeServerOptions para decodificá-las.
  • Ela cria uma credencial chamando a API Web navigator.credential.create. Quando a navigator.credential.create é chamada, o navegador assume o controle e pede que o usuário escolha uma chave de segurança.
  • Ela decodifica a credencial recém-criada.
  • Ela registra a nova credencial no servidor fazendo uma solicitação que contém a credencial codificada para /auth/credential.

Aparte: veja o código do servidor

A registerCredential() faz duas chamadas ao servidor, então vamos ver o que acontece no back-end.

Opções de criação de credenciais

Quando o cliente faz uma solicitação para (/auth/credential-options), o servidor gera um objeto de opções e o envia de volta ao cliente.

Esse objeto é usado pelo cliente na chamada de criação da credencial:

navigator.credentials.create({
    publicKey: {
    // Options generated server-side
    ...credentialCreationOptions
// ...
}

Então, o que há nessa credentialCreationOptions que é usada na registerCredential do lado do cliente que você implementou na etapa anterior?

Confira o código do servidor em router.post("/credential-options", ….

Não vamos analisar cada propriedade, mas você pode ver algumas interessantes no objeto de opções do código do servidor, que é gerado usando a biblioteca fido2 e retornado ao cliente:

  • rpName e rpId descrevem a organização que registra e autentica o usuário. No WebAuthn, o escopo das credenciais é definido para determinado domínio, o que é um benefício de segurança. As propriedades rpName e rpId são usadas aqui para definir o escopo da credencial. Uma rpId válida é, por exemplo, o nome do host do site. Observe como elas são atualizadas automaticamente ao bifurcar o projeto inicial 🧘🏻‍♀️.
  • excludeCredentials é uma lista de credenciais. A nova credencial não pode ser criada em um autenticador que também contém uma das credenciais listadas na excludeCredentials. Em nosso codelab, excludeCredentials é uma lista de credenciais existentes para esse usuário. Com ela e o user.id, cada credencial criada pelo usuário fica em um autenticador (chave de segurança) diferente. Essa é uma prática recomendada porque, se um usuário registrar várias credenciais, elas ficarão disponíveis em diferentes autenticadores. Assim, se o usuário perde uma chave de segurança, não fica impedido de entrar na própria conta.
  • A authenticatorSelection define o tipo de autenticadores que você quer permitir no seu aplicativo da Web. Vamos dar uma olhada mais detalhada na authenticatorSelection:
    • residentKey: preferred significa que o aplicativo não aplica credenciais detectáveis do lado do cliente. Esse tipo de credencial especial possibilita a autenticação do usuário sem precisar que se ele se identifique primeiro. Aqui, configuramos preferred porque este codelab se concentra na implementação básica. Credenciais detectáveis são destinadas a fluxos mais avançados.
    • A propriedade requireResidentKey está presente apenas para compatibilidade com versões anteriores no WebAuthn v1.
    • userVerification: preferred significa que, se o autenticador oferecer suporte para a verificação do usuário, por exemplo, se for uma chave de segurança biométrica ou uma chave com recurso de PIN integrado, a parte confiável vai precisar solicitar essa verificação ao criar a credencial. Se o autenticador não fizer isso (chave básica de segurança), o servidor não vai solicitar a verificação do usuário.
  • A ​​pubKeyCredParam descreve, em ordem de preferência, as propriedades criptográficas desejadas da credencial.

Todas essas opções são decisões que o aplicativo da Web precisa tomar sobre o modelo de segurança. No servidor, essas opções são definidas em um único objeto authSettings.

Desafio

Outra coisa mais interessante aqui é a req.session.challenge = options.challenge;.

Como o WebAuthn é um protocolo criptográfico, ele depende de desafios aleatórios para evitar ataques de repetição, por exemplo, quando um invasor rouba um payload para repetir a autenticação e ele não é o proprietário da chave privada que pode ativar a autenticação.

Para reduzir esse problema, um desafio é gerado e assinado imediatamente. A assinatura é, então, comparada com a esperada. Isso verifica se o usuário detém a chave privada no momento da geração da credencial.

Código de registro de credenciais

Confira o código do servidor em router.post("/credential", ….

É nesse local que a credencial é registrada no servidor.

O que isso significa?

Uma das partes mais importantes deste código é a chamada de verificação via fido2.verifyAttestationResponse:

  • O desafio assinado é verificado. Isso garante que a credencial foi criada por alguém que de fato detinha a chave privada no momento da criação.
  • O ID da parte confiável, vinculado à origem, também é verificado. Isso garante que a credencial seja vinculada a esse aplicativo da Web e somente a ele.

Adicionar esta funcionalidade à IU

Agora que a função para criar uma credencial ``registerCredential(), está pronta, vamos disponibilizá-la para o usuário.

Você fará isso na página Conta, porque ela é um local comum para o gerenciamento de autenticação.

Na marcação de account.html, abaixo do nome de usuário, há uma div vazia até o momento com uma classe de layout class="flex-h-between". Vamos usar essa div para elementos da IU relacionados à funcionalidade de autenticação de dois fatores.

Adicione nessa div:

  • Um título que diz "Autenticação de dois fatores"
  • Um botão para criar uma credencial
 <div class="flex-h-between">
    <h3>
        Two-factor authentication
    </h3>
    <button class="create" id="registerButton" raised>
        ➕ Add a credential
    </button>
</div>

Abaixo da div, adicione uma div de credencial que será necessária mais tarde:

<div class="flex-h-between">
(HTML you've just added)
</div>
<div id="credentials"></div>

No script inline account.html, importe a função que você acabou de criar e adicione uma função register que a chame, assim como um manipulador de eventos anexado ao botão criado.

// Set up the handler for the button that registers credentials
const registerButton = document.querySelector('#registerButton');
registerButton.addEventListener('click', register);

// Register a credential
async function register() {
  let user = {};
  try {
    const user = await registerCredential();
  } catch (e) {
    // Alert the user that something went wrong
    if (Array.isArray(e)) {
      alert(
        // `msg` not `message`, this is the key's name as per the express validator API
        `Registration failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
      );
    } else {
      alert(`Registration failed. ${e}`);
    }
  }
}

Mostrar as credenciais para o usuário

Agora que você adicionou a funcionalidade de criação de uma credencial, os usuários precisam ver as credenciais que adicionaram.

A página Conta é um bom lugar para fazer isso.

Em account.html, procure a função chamada updateCredentialList().

Adicione a ela o código a seguir, que faz uma chamada de back-end para buscar todas as credenciais registradas do usuário conectado no momento e mostra as credenciais retornadas:

// Update the list that displays credentials
async function updateCredentialList() {
  // Fetch the latest credential list from the backend
  const response = await _fetch('/auth/credentials', 'GET');
  const credentials = response.credentials || [];
  // Generate the credential list as HTML and pass remove/rename functions as args
  const credentialListHtml = getCredentialListHtml(
    credentials,
    removeEl,
    renameEl
  );
  // Display the list of credentials in the DOM
  const list = document.querySelector('#credentials');
  render(credentialListHtml, list);
}

Por enquanto, não se preocupe com removeEl e renameEl. Você vai aprender sobre eles mais à frente neste codelab.

Adicione uma chamada a updateCredentialList no início do script inline, em account.html. Com essa chamada, as credenciais disponíveis são buscadas quando o usuário acessa a página da conta.

<script type="module">
    // ... (imports)
    // Initialize the credential list by updating it once on page load
    updateCredentialList();

Agora, chame o método updateCredentialList depois que a função registerCredential for concluída. As listas vão mostrar a credencial recém-criada:

async function register() {
  let user = {};
  try {
    // ...
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

Faça o teste! 👩🏻‍💻

Você concluiu o registro de credenciais. Agora os usuários podem criar credenciais baseadas em chave de segurança e vê-las na página Conta.

Faça um teste:

  • Saia da conta.
  • Faça login com qualquer usuário e senha. Como mencionamos antes, a senha não é verificada quanto à correção para simplificar este codelab. Digite qualquer senha não vazia.
  • Na página Conta, clique em Adicionar uma credencial.
  • Será pedido para você inserir e tocar em uma chave de segurança. Faça isso.
  • Após a criação, a credencial vai aparecer na página da conta.
  • Atualize a página Conta. As credenciais vão aparecer.
  • Se você tiver duas chaves disponíveis, adicione duas chaves de segurança diferentes como credenciais. Ambas vão aparecer.
  • Tente criar duas credenciais com o mesmo autenticador (chave). Você verá que não haverá suporte para isso. Isso é intencional e ocorre devido ao uso de excludeCredentials no back-end.

7. Ativar a autenticação de dois fatores

Seus usuários podem registrar e cancelar o registro de credenciais, mas elas são apenas mostradas e não usadas ainda.

Agora é a hora de dar um uso para elas e configurar a autenticação de dois fatores.

Nesta seção, você vai mudar o fluxo de autenticação no aplicativo da Web deste fluxo básico:

6ff49a7e520836d0.png

Para este fluxo de dois fatores:

e7409946cd88efc7.png

Implementar a autenticação de dois fatores

Primeiro, vamos adicionar a funcionalidade necessária e implementar a comunicação com o back-end. Vamos adicioná-la no front-end em uma próxima etapa.

Você precisa implementar uma função que autentique o usuário com uma credencial.

Em public/auth.client.js, procure a função vazia authenticateTwoFactor e adicione a ela o seguinte código:

async function authenticateTwoFactor() {
  // Fetch the 2F options from the backend
  const optionsFromServer = await _fetch("/auth/two-factor-options", "POST");
  // Decode them
  const decodedOptions = decodeServerOptions(optionsFromServer);
  // Get a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
  const credential = await navigator.credentials.get({
    publicKey: decodedOptions
  });
  // Encode the credential
  const encodedCredential = encodeCredential(credential);
  // Send it to the backend for verification
  return await _fetch("/auth/authenticate-two-factor", "POST", {
    credential: encodedCredential
  });
}

Essa função já foi exportada para você. Vamos precisar dela na próxima etapa.

Veja o que a authenticateTwoFactor faz:

  • Ela solicita duas opções de autenticação de dois fatores do servidor. Assim como as opções de criação de credenciais que você viu anteriormente, elas são definidas no servidor e dependem do modelo de segurança do aplicativo da Web. Analise o código do servidor em router.post("/two-factors-options", ... para ver mais detalhes.
  • Chamar navigator.credentials.get permite que o navegador assuma e peça para o usuário inserir e tocar em uma chave já registrada. O resultado é uma credencial selecionada para essa operação específica de autenticação de dois fatores.
  • A credencial selecionada é transmitida em uma solicitação de back-end para fetch("/auth/authentication-two-factor"`. Se a credencial for válida para o usuário, ele será autenticado.

Aparte: veja o código do servidor

O server.js já cuida de parte da navegação e do acesso, garantindo que a página Conta só possa ser acessada por usuários autenticados e executando alguns redirecionamentos necessários.

Agora, veja o código do servidor em router.post("/initialize-authentication", ....

Há dois pontos interessantes a observar:

  • A senha e a credencial são verificadas simultaneamente neste estágio. Essa é uma medida de segurança porque, para os usuários que têm a autenticação de dois fatores configurada, não queremos que os fluxos da IU sejam diferentes se a senha estiver correta ou não. Portanto, verificamos a senha e a credencial simultaneamente nessa etapa.
  • Se a senha e a credencial forem válidas, concluímos a autenticação chamando completeAuthentication(req, res);. Na prática, isso significa que mudamos de uma sessão auth temporária, em que o usuário ainda não foi autenticado, para a sessão principal main em que o usuário foi autenticado.

Incluir a página de autenticação de dois fatores no fluxo de usuários

Na pasta views, observe a nova página second-factor.html.

Ela tem um botão que diz Usar chave de segurança, mas por enquanto não realiza nenhuma ação.

Faça esse botão chamar authenticateTwoFactor() após o clique.

  • Se a chamada a authenticateTwoFactor() for bem-sucedida, redirecione o usuário para a página Conta.
  • Se não funcionar, avise o usuário que ocorreu um erro. Em um aplicativo real, você implementaria mensagens de erro mais úteis. Para simplificar nossa demonstração, usaremos apenas um alerta de janela.
    <main>
...
    </main>
    <script type="module">
      import { authenticateTwoFactor, authStatuses } from "/auth.client.js";

      const button = document.querySelector("#authenticateButton");
      button.addEventListener("click", async e => {
        try {
          // Ask the user to authenticate with the second factor; this will trigger a browser prompt
          const response = await authenticateTwoFactor();
          const { authStatus } = response;
          if (authStatus === authStatuses.COMPLETE) {
            // The user is properly authenticated => Navigate to the Account page
            location.href = "/account";
          } else {
            throw new Error("Two-factor authentication failed");
          }
        } catch (e) {
          // Alert the user that something went wrong
          alert(`Two-factor authentication failed. ${e}`);
        }
      });
    </script>
  </body>
</html>

Usar a autenticação de dois fatores

Está tudo pronto para você adicionar uma etapa de autenticação de dois fatores.

Agora você precisa adicionar essa etapa do index.html para os usuários que configuraram a autenticação de dois fatores.

322a5c49d865a0d8.png

Em index.html, abaixo de location.href = "/account";, adicione o código que direciona o usuário condicionalmente à página da autenticação de dois fatores, se ela estiver configurada.

Neste codelab, a criação de uma credencial ativa automaticamente a autenticação de dois fatores para o usuário.

O server.js também implementa a verificação de sessão do lado do servidor. Isso garante que apenas usuários autenticados possam acessar account.html.

const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
  // The user is properly authenticated => navigate to account
  location.href = '/account';
} else if (authStatus === authStatuses.NEED_SECOND_FACTOR) {
  // Navigate to the two-factor-auth page because two-factor-auth is set up for this user
  location.href = '/second-factor';
}

Faça o teste! 👩🏻‍💻

  • Faça login com um novo usuário joaosilva.
  • Saia da conta.
  • Faça login na sua conta como joaosilva. Veja que é preciso inserir apenas uma senha.
  • Crie uma credencial. Isso significa que você ativou a autenticação de dois fatores como joaosilva.
  • Saia da conta.
  • Digite o nome de usuário joaosilva e a senha.
  • Veja como você navega automaticamente para a página de autenticação de dois fatores.
  • Tente acessar a página Conta em /account. É feito um redirecionamento para a página de índice, porque a autenticação não está completa: falta um segundo fator.
  • Volte para a página de autenticação de dois fatores e clique em Usar chave de segurança para fazer esse tipo de autenticação.
  • Você já fez login e verá a página Conta.

8. Facilitar o uso de credenciais

Você concluiu a funcionalidade básica da autenticação de dois fatores com uma chave de segurança 🚀.

Mas… Você notou?

No momento, nossa lista de credenciais não é muito conveniente. O ID da credencial e a chave pública são strings longas, que não são úteis ao gerenciar credenciais. Humanos não são muito bons com strings e números longos 🤖.

Vamos melhorar isso e adicionar funcionalidades para nomear e renomear credenciais com strings legíveis.

Conferir renomeCredential

Para economizar tempo na implementação dessa função que não faz nada inovador, uma função para renomear uma credencial foi adicionada no código inicial, em auth.client.js:

async function renameCredential(credId, newName) {
  const params = new URLSearchParams({
    credId,
    name: newName
  });
  return _fetch(
    `/auth/credential?${params}`,
    "PUT"
  );
}

Essa é uma chamada normal de atualização do banco de dados. O cliente envia uma solicitação PUT ao back-end com um ID e um novo nome para a credencial.

Implementar nomes de credenciais personalizados

Em account.html, observe a função vazia rename.

Adicione o seguinte código a ela:

// Rename a credential
async function rename(credentialId) {
  // Let the user input a new name
  const newName = window.prompt(`Name this credential:`);
  // Rename only if the user didn't cancel AND didn't enter an empty name
  if (newName && newName.trim()) {
    try {
      // Make the backend call to rename the credential (the name is sanitized) server-side
      await renameCredential(credentialId, newName);
    } catch (e) {
      // Alert the user that something went wrong
      if (Array.isArray(e)) {
        alert(
          // `msg` not `message`, this is the key's name as per the express validator API
          `Renaming failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
        );
      } else {
        alert(`Renaming failed. ${e}`);
      }
    }
    // Refresh the credential list to display the new name
    await updateCredentialList();
  }
}

Faz mais sentido nomear uma credencial somente depois que ela é criada. Então, vamos criar uma credencial sem nome e, depois do sucesso na criação, dar um nome a ela. No entanto, isso resulta em duas chamadas de back-end.

Use a função rename em register() para permitir que os usuários nomeiem credenciais após o registro:

async function register() {
  let user = {};
  try {
    const user = await registerCredential();
    // Get the latest credential's ID (newly created credential)
    const allUserCredentials = user.credentials;
    const newCredential = allUserCredentials[allUserCredentials.length - 1];
    // Rename it
    await rename(newCredential.credId);
  } catch (e) {
    // ...
  }
  // Refresh the credential list to display the new credential
  await updateCredentialList();
}

A entrada do usuário é validada e limpa no back-end:

  check("name")
    .trim()
    .escape()

Mostrar nomes de credenciais

Vá para getCredentialHtml em templates.js.

Já há um código para mostrar o nome da credencial na parte de cima do card dela:

// Register credential
const getCredentialHtml = (credential, removeEl, renameEl) => {
 const { name, credId, publicKey } = credential;
 return html`
    <div class="credential-card">
      <div class="credential-name">
        ${name
          ? html`
              ${name}
            `
          : html`
              <span class="unnamed">(Unnamed)</span>
            `}
      </div>
     // ...
    </div>
  `;
};

Faça o teste! 👩🏻‍💻

  • Crie uma credencial.
  • Será pedido que você nomeie o arquivo.
  • Digite um novo nome e clique em OK.
  • A credencial foi renomeada.
  • Repita e confira se tudo funciona corretamente ao deixar o campo de nome vazio.

Ativar a renomeação de credenciais

Os usuários podem precisar renomear credenciais, por exemplo, se adicionarem uma segunda chave e quiserem renomear a primeira para diferenciá-las melhor.

Em account.html, procure a função renameEl, que até agora está vazia, e adicione a ela o seguinte código:

// Rename a credential via HTML element
async function renameEl(el) {
  // Define the ID of the credential to update
  const credentialId = el.srcElement.dataset.credentialId;
  // Rename the credential
  await rename(credentialId);
  // Refresh the credential list to display the new name
  await updateCredentialList();
}

Agora, no getCredentialHtml do templates.js, dentro da div class="flex-end", adicione o código a seguir. Ele adiciona um botão Renomear ao modelo do cartão de credenciais. Quando você clicar no botão, ele chamará a função renameEl que acabamos de criar:

const getCredentialHtml = (credential, removeEl, renameEl) => {
// ...
 <div class="flex-end">
  <button
    data-credential-id="${credId}"
    @click="${renameEl}"
    class="secondary right"
  >
   Rename
  </button>
 </div>
 // ...
  `;
};

Faça o teste! 👩🏻‍💻

  • Clique em Renomear.
  • Insira um novo nome quando pedido.
  • Clique em OK.
  • A credencial será renomeada, e a lista será atualizada automaticamente.
  • A atualização da página ainda vai mostrar o novo nome. Isso indica que o novo nome é mantido no lado do servidor.

Mostrar a data de criação das credenciais

A data de criação não está presente nas credenciais criadas por navigator.credential.create().

No entanto, como essa informação pode ser útil para o usuário distinguir as credenciais, ajustamos a biblioteca do lado do servidor no código inicial e adicionamos um campo creationDate igual a Date.now() para armazenar novas credenciais.

Em templates.js na div class="creation-date", adicione o seguinte para mostrar as informações de data de criação para o usuário:

<div class="creation-date">
  <label>Created:</label>
  <div class="info">
    ${new Date(creationDate).toLocaleDateString()}
    ${new Date(creationDate).toLocaleTimeString()}
  </div>
</div>

9. Otimizar o código para uso futuro

Até o momento, pedimos que o usuário registrasse um autenticador de roaming simples, que é usado como segundo fator durante o login.

Uma abordagem mais avançada seria usar um tipo mais avançado de autenticador: um autenticador de roaming com verificação de usuário (UVRA, na sigla em inglês). Um UVRA pode fornecer dois fatores de autenticação e resistência a phishing em fluxos de login de etapa única.

O ideal é oferecer suporte às duas abordagens. Para isso, você precisa personalizar a experiência do usuário:

  • Se um usuário só tem um autenticador de roaming simples, que não faz a verificação do usuário, permita que ele o use para ter uma inicialização de conta resistente a phishing. Ele também vai precisar digitar um nome de usuário e uma senha. Isso é o que nosso codelab já faz.
  • Se outro usuário tem um autenticador de roaming com uma verificação de usuário mais avançada, ele pode pular a etapa de senha e, possivelmente, a etapa de nome de usuário durante a inicialização da conta.

Saiba mais sobre isso em Inicialização de contas resistente a phishing com login opcional sem senha (link em inglês).

Neste codelab, não vamos realmente personalizar a experiência do usuário, mas vamos configurar sua base de código para que você tenha os dados necessários para fazer essa personalização.

Você precisa fazer duas etapas:

  • Defina residentKey: preferred nas configurações do back-end. Isso já está feito.
  • Configure uma maneira de descobrir se uma credencial detectável, também chamada de chave residente, foi criada.

Para saber se uma credencial detectável foi criada ou não:

  • Consulte o valor de credProps na criação da credencial (credProps: true).
  • Consulte o valor de transports na criação da credencial. Isso vai ajudar você a determinar se a plataforma oferece suporte para a funcionalidade UVRA, ou seja, se é um smartphone, por exemplo.
  • Armazene os valores de credProps e transports no back-end. Isso já foi feito no código inicial. Dê uma olhada no auth.js se tiver curiosidade.

Vamos receber os valores de credProps e transports para enviar ao back-end. Em auth.client.js, mude registerCredential da seguinte maneira:

  • Adicione um campo extensions ao chamar navigator.credentials.create.
  • Defina encodedCredential.transports e encodedCredential.credProps antes de enviar a credencial ao back-end para armazenamento.

A registerCredential vai ficar assim:

async function registerCredential() {
  // Fetch the credential creation options from the backend
  const credentialCreationOptionsFromServer = await _fetch(
    '/auth/credential-options',
    'POST'
  );
  // Decode the credential creation options
  const credentialCreationOptions = decodeServerOptions(
    credentialCreationOptionsFromServer
  );
  // Create a credential via the browser API; this will prompt the user
  const credential = await navigator.credentials.create({
    publicKey: {
      ...credentialCreationOptions,
      extensions: {
        credProps: true,
      },
    },
  });
  // Encode the newly created credential to send it to the backend
  const encodedCredential = encodeCredential(credential);
  // Set transports and credProps for more advanced user flows
  encodedCredential.transports = credential.response.getTransports();
  encodedCredential.credProps =
    credential.getClientExtensionResults().credProps;
  // Send the encoded credential to the backend for storage
  return await _fetch('/auth/credential', 'POST', encodedCredential);
}

10. Garantir suporte para vários navegadores

Suporte para navegadores não Chromium

Na função registerCredential do public/auth.client.js, chamamos credential.response.getTransports() na credencial recém-criada para salvar essas informações no back-end como uma dica para o servidor.

Entretanto, o getTransports() não está implementado em todos os navegadores no momento, ao contrário do getClientExtensionResults, que tem suporte de todos os navegadores. A chamada getTransports() gera um erro no Firefox e no Safari, o que impede a criação de credenciais nesses navegadores.

Para garantir que o código seja executado em todos os principais navegadores, envolva a chamada encodedCredential.transports em uma condição:

if (credential.response.getTransports) {
  encodedCredential.transports = credential.response.getTransports();
}

No servidor, transports está definido como transports || []. No Firefox e no Safari, a lista transports não será undefined, mas uma lista vazia [], o que evita erros.

Alertar os usuários que usam navegadores sem suporte para WebAuthn

1e9c1be837d66ce8.png

Mesmo que o WebAuthn tenha suporte de todos os principais navegadores, é recomendável mostrar um aviso em navegadores que não oferecem suporte para o WebAuthn.

Em index.html, observe a presença desta div:

<div id="warningbanner" class="invisible">
⚠️ Your browser doesn't support WebAuthn. Open this demo in Chrome, Edge, Firefox or Safari.
</div>

No script inline de index.html, adicione o código a seguir para mostrar o banner em navegadores que não oferecem suporte para o WebAuthn:

// Display a banner in browsers that don't support WebAuthn
if (!window.PublicKeyCredential) {
  document.querySelector('#warningbanner').classList.remove('invisible');
}

Em um aplicativo real da Web, você faria algo mais elaborado e teria um mecanismo substituto adequado para esses navegadores, mas esse código mostra como verificar o suporte para WebAuthn.

11. Muito bem!

✨Tudo pronto!

Você implementou a autenticação de dois fatores com uma chave de segurança.

Neste codelab, abordamos os princípios básicos. Se quiser saber mais sobre o WebAuthn para autenticação de dois fatores, veja o que você pode tentar fazer:

  • Adicione informações de "Último uso" ao cartão da credencial. Essas informações são úteis para os usuários determinarem se determinada chave de segurança é usada ou não, principalmente se tiverem registrado várias chaves.
  • Implemente um tratamento de erros mais robusto e mensagens de erros mais precisas.
  • Analise auth.js e veja o que acontece quando você muda algumas das authSettings, principalmente ao usar uma chave que oferece suporte para a verificação do usuário.