Criar uma chave de acesso para logins sem senha

As chaves de acesso tornam as contas dos usuários mais seguras, simples e fáceis de usar.

O uso de chaves de acesso aumenta a segurança, simplifica os logins e substitui as senhas. Ao contrário das senhas normais, que os usuários precisam lembrar e inserir manualmente, as chaves de acesso usam mecanismos de bloqueio de tela do dispositivo, como biometria ou PIN, e reduzem os riscos de phishing e roubo de credenciais.

As chaves de acesso são sincronizadas entre dispositivos que usam provedores de chaves de acesso, como o Gerenciador de senhas do Google e o iCloud Keychain.

Uma chave de acesso precisa ser criada, armazenando a chave privada com segurança no provedor de chave de acesso, junto com os metadados necessários e a chave pública armazenada no servidor para autenticação. A chave privada emite uma assinatura após a verificação do usuário no domínio válido, tornando as chaves de acesso resistentes a phishing. A chave pública verifica a assinatura sem armazenar credenciais sensíveis, tornando as chaves de acesso resistentes a roubo de credenciais.

Como criar uma chave de acesso

Antes que um usuário possa fazer login com uma chave de acesso, você precisa criar a chave de acesso, associá-la a uma conta de usuário e armazenar a chave pública no servidor.

Você pode pedir que os usuários criem uma chave de acesso em uma das seguintes situações:

  • Durante ou depois da inscrição.
  • Depois de fazer login.
  • Depois de fazer login usando uma chave de acesso de outro dispositivo (ou seja, [authenticatorAttachment](https://web.dev/articles/passkey-form-autofill#authenticator-attachment) é cross-platform).
  • Em uma página dedicada em que os usuários podem gerenciar as chaves de acesso.

Para criar uma chave de acesso, use a API WebAuthn.

Os quatro componentes do fluxo de registro da chave de acesso são:

  • Back-end: armazena os detalhes da conta do usuário, incluindo a chave pública.
  • Front-end: se comunica com o navegador e busca os dados necessários do back-end.
  • Navegador: executa seu JavaScript e interage com a API WebAuthn.
  • Provedor de chaves de acesso: cria e armazena a chave de acesso. Normalmente, é um gerenciador de senhas, como o Gerenciador de senhas do Google, ou uma chave de segurança.
O processo de criação e registro de uma chave de acesso
O processo de criação e registro de uma chave de acesso.

Antes de criar uma chave de acesso, verifique se o sistema atende aos seguintes pré-requisitos:

  • A conta de usuário é verificada por um método seguro (por exemplo, e-mail, verificação por telefone ou federação de identidade) em um período curto.

  • O front-end e o back-end podem se comunicar com segurança para trocar dados de credenciais.

  • O navegador oferece suporte para WebAuthn e criação de chaves de acesso.

Vamos mostrar como verificar a maioria delas nas próximas seções.

Quando o sistema atende a essas condições, o processo a seguir é executado para criar uma chave de acesso:

  1. O sistema aciona o processo de criação de chaves de acesso quando o usuário iniciar a ação (por exemplo, clicando no botão "Criar uma chave de acesso" na página de gerenciamento de chaves de acesso ou depois de concluir o registro).
  2. O front-end solicita os dados de credencial necessários do back-end, incluindo informações do usuário, um desafio e IDs de credencial para evitar duplicatas.
  3. O front-end chama navigator.credentials.create() para solicitar que o provedor de chave de acesso do dispositivo gere uma chave de acesso usando as informações do back-end. Essa chamada retorna uma promessa.
  4. O dispositivo do usuário autentica o usuário usando um método biométrico, PIN ou padrão para criar a chave de acesso.
  5. O provedor de chave de acesso cria uma chave de acesso e retorna uma credencial de chave pública para o front-end, resolvendo a promessa.
  6. O front-end envia a credencial de chave pública gerada para o back-end.
  7. O back-end armazena a chave pública e outros dados importantes para autenticação futura.
  8. O back-end notifica o usuário (por exemplo, usando e-mail) para confirmar a criação da chave de acesso e detectar possíveis acessos não autorizados.

Esse processo garante um processo de registro de chave de acesso seguro e simples para os usuários.

Compatibilidades

A maioria dos navegadores oferece suporte ao WebAuthn, com algumas pequenas lacunas. Consulte passkeys.dev para detalhes sobre a compatibilidade com navegadores e SOs.

Criar uma chave de acesso

Para criar uma nova chave de acesso, o front-end precisa seguir este processo:

  1. Verifique a compatibilidade.
  2. Buscar informações do back-end.
  3. Chamar a API WebAuth para criar uma chave de acesso.
  4. Enviar a chave pública retornada para o back-end.
  5. Salve a credencial.

As seções a seguir mostram como fazer isso.

Verificar a compatibilidade

Antes de mostrar um botão "Criar uma chave de acesso", o front-end precisa verificar se:

  • O navegador oferece suporte para WebAuthn com PublicKeyCredential.

Browser Support

  • Chrome: 67.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

  • O dispositivo oferece suporte a um autenticador de plataforma (pode criar uma chave de acesso e autenticar com ela) com PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().

Browser Support

  • Chrome: 67.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

Browser Support

  • Chrome: 108.
  • Edge: 108.
  • Firefox: 119.
  • Safari: 16.

Source

O snippet de código abaixo mostra como verificar a compatibilidade antes de mostrar as opções relacionadas à chave de acesso.

// Availability of `window.PublicKeyCredential` means WebAuthn is usable.  
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.  
// `isConditionalMediationAvailable` means the feature detection is usable.  
if (window.PublicKeyCredential &&  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&  
    PublicKeyCredential.isConditionalMediationAvailable) {  
  // Check if user verifying platform authenticator is available.  
  Promise.all([  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),  
    PublicKeyCredential.isConditionalMediationAvailable(),  
  ]).then(results => {  
    if (results.every(r => r === true)) {  
      // Display "Create a new passkey" button  
    }  
  });  
}  

Neste exemplo, o botão Criar uma chave de acesso só vai aparecer se todas as condições forem atendidas.

Buscar informações do back-end

Quando o usuário clicar no botão, extraia as informações necessárias do back-end para chamar navigator.credentials.create().

O snippet de código abaixo mostra um objeto JSON com as informações necessárias para chamar navigator.credentials.create():

// Example `PublicKeyCredentialCreationOptions` contents
{
  challenge: *****,
  rp: {
    name: "Example",
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },
  pubKeyCredParams: [{
    alg: -7, type: "public-key"
  },{
    alg: -257, type: "public-key"
  }],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

Os pares de chave-valor no objeto contêm as seguintes informações:

  • challenge: um desafio gerado pelo servidor em ArrayBuffer para este registro.
  • rp.id: um ID de parte confiável (RP, na sigla em inglês), um domínio e um site podem especificar o próprio domínio ou um sufixo registrável. Por exemplo, se a origem de uma RP for https://login.example.com:1337, o ID da RP poderá ser login.example.com ou example.com. Se o ID da RP for especificado como example.com, o usuário poderá fazer a autenticação em login.example.com ou em qualquer subdomínio em example.com. Consulte Permitir a reutilização de chaves de acesso em todos os sites com solicitações de origem relacionadas para mais informações.
  • rp.name: o nome da parte confiável (RP). Essa propriedade foi descontinuada no WebAuthn L3, mas foi incluída por motivos de compatibilidade.
  • user.id: um ID de usuário exclusivo em ArrayBuffer, gerado na criação da conta. Ele precisa ser permanente, ao contrário de um nome de usuário que pode ser editado. O ID do usuário identifica uma conta, mas não pode conter informações de identificação pessoal (PII). Você provavelmente já tem um ID de usuário no seu sistema, mas, se necessário, crie um específico para chaves de acesso para que ele não tenha PII.
  • user.name: um identificador exclusivo da conta que o usuário vai reconhecer, como o endereço de e-mail ou o nome de usuário. e vai ser exibido no seletor de contas.
  • user.displayName: um nome obrigatório e mais fácil de usar para a conta. Ele não precisa ser exclusivo e pode ser o nome escolhido pelo usuário. Se o site não tiver um valor adequado para incluir aqui, transmita uma string vazia. Essa informação pode ser exibida no seletor de contas dependendo do navegador.
  • pubKeyCredParams: especifica os algoritmos de chave pública compatíveis com a RP (parte confiável). Recomendamos definir como [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. Isso especifica a compatibilidade de ECDSA com P-256 e RSA PKCS#1, além de oferecer cobertura completa.
  • excludeCredentials: uma lista de IDs de credencial já registrados. Evita o registro do mesmo dispositivo duas vezes fornecendo uma lista de IDs de credencial já registrados. O membro transports, se fornecido, precisa conter o resultado da chamada de getTransports() durante o registro de cada credencial.
  • authenticatorSelection.authenticatorAttachment: defina como "platform" com hint: ['client-device'] se a criação da chave de acesso for um upgrade de uma senha, por exemplo, em uma promoção após o login. "platform" indica que o RP quer um autenticador de plataforma (um autenticador incorporado ao dispositivo da plataforma) que não exija, por exemplo, a inserção de uma chave de segurança USB. O usuário tem uma opção mais simples para criar uma chave de acesso.
  • authenticatorSelection.requireResidentKey: define como um true booleano. Uma credencial detectável (chave residente) armazena informações do usuário na chave de acesso e permite que os usuários selecionem a conta após a autenticação.
  • authenticatorSelection.userVerification: indica se a verificação de um usuário usando o bloqueio de tela do dispositivo é "required", "preferred" ou "discouraged". O padrão é "preferred", o que significa que o autenticador pode pular a verificação do usuário. Defina como "preferred" ou omita a propriedade.

Recomendamos construir o objeto no servidor, codificando o ArrayBuffer com Base64URL e extraindo-o do front-end. Dessa forma, é possível decodificar o payload usando PublicKeyCredential.parseCreationOptionsFromJSON() e transmiti-lo diretamente para navigator.credentials.create().

O snippet de código a seguir mostra como buscar e decodificar as informações necessárias para criar a chave de acesso.

// Fetch an encoded `PubicKeyCredentialCreationOptions` from the server.
const _options = await fetch('/webauthn/registerRequest');

// Deserialize and decode the `PublicKeyCredentialCreationOptions`.
const decoded_options = JSON.parse(_options);
const options = PublicKeyCredential.parseCreationOptionsFromJSON(decoded_options);
...

Chamar a API WebAuthn para criar uma chave de acesso

Chame navigator.credentials.create() para criar uma nova chave de acesso. A API retorna uma promessa, aguardando a interação do usuário exibindo uma caixa de diálogo modal.

Browser Support

  • Chrome: 60.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

Source

// Invoke WebAuthn to create a passkey.
const credential = await navigator.credentials.create({
  publicKey: options
});

Enviar a credencial de chave pública retornada para o back-end

Depois que o usuário é verificado usando a tela de bloqueio do dispositivo, uma chave de acesso é criada e a promessa é resolvida, retornando um objeto PublicKeyCredential para o front-end.

A promessa pode ser rejeitada por diferentes motivos. É possível processar esses erros verificando a propriedade name do objeto Error:

  • InvalidStateError: uma chave de acesso já existe no dispositivo. Nenhuma caixa de diálogo de erro será mostrada ao usuário. O site não deve tratar isso como um erro. O usuário queria que o dispositivo local fosse registrado, e ele foi.
  • NotAllowedError: o usuário cancelou a operação.
  • AbortError: a operação foi interrompida.
  • Outras exceções: algo inesperado aconteceu. O navegador mostra uma caixa de diálogo de erro para o usuário.

O objeto de credencial de chave pública contém as seguintes propriedades:

  • id: um ID codificado em Base64URL da chave de acesso criada. Esse ID ajuda o navegador a determinar se uma chave de acesso correspondente está no dispositivo após a autenticação. Esse valor precisa ser armazenado no banco de dados no back-end.
  • rawId: uma versão ArrayBuffer do ID da credencial.
  • response.clientDataJSON: dados do cliente codificados em ArrayBuffer.
  • response.attestationObject: um objeto de atestado codificado por ArrayBuffer. Ele contém informações importantes, como um ID da RP, sinalizações e uma chave pública.
  • authenticatorAttachment: retorna "platform" quando a credencial é criada em um dispositivo com suporte à chave de acesso.
  • type: esse campo é sempre definido como "public-key".

Codifique o objeto com o método .toJSON(), serialize-o com JSON.stringify() e envie-o para o servidor.

...

// Encode and serialize the `PublicKeyCredential`.
const _result = credential.toJSON();
const result = JSON.stringify(_result);

// Encode and send the credential to the server for verification.  
const response = await fetch('/webauthn/registerResponse', {
  method: 'post',
  credentials: 'same-origin',
  body: result
});
...

Salvar a credencial

Depois de receber a credencial de chave pública no back-end, recomendamos usar uma biblioteca ou solução do lado do servidor em vez de escrever seu próprio código para processar uma credencial de chave pública.

Em seguida, é possível armazenar as informações extraídas da credencial no banco de dados para uso futuro.

A lista a seguir inclui as propriedades recomendadas para salvar:

  • ID da credencial: o ID da credencial retornado com a credencial de chave pública.
  • Nome da credencial: o nome da credencial. Dê um nome ao provedor de chave de acesso que a criou, que pode ser identificado com base no AAGUID.
  • ID do usuário: o ID do usuário usado para criar a chave de acesso.
  • Chave pública: a chave pública retornada com a credencial de chave pública. Isso é necessário para verificar uma declaração de chave de acesso.
  • Data e hora de criação: registre a data e a hora de criação da chave de acesso. Isso é útil para identificar a chave de acesso.
  • Data e hora de uso pela última vez: registra a data e a hora em que o usuário usou a chave de acesso para fazer login. Isso é útil para determinar qual chave de acesso o usuário usou (ou não).
  • AAGUID: um identificador exclusivo do provedor de chave de acesso.
  • Backup Eligibility flag: verdadeiro se o dispositivo estiver qualificado para a sincronização de chave de acesso. Essas informações ajudam os usuários a identificar chaves de acesso sincronizáveis e chaves de acesso vinculadas ao dispositivo (não sincronizáveis) na página de gerenciamento de chaves de acesso.

Siga as instruções mais detalhadas em Registro de chave de acesso no servidor.

Sinalizar se o registro falhar

Se o registro de uma chave de acesso falhar, isso pode confundir o usuário. Se houver uma chave de acesso no provedor de chave de acesso e ela estiver disponível para o usuário, mas a chave pública associada não estiver armazenada no lado do servidor, as tentativas de login usando a chave de acesso nunca serão bem-sucedidas, e será difícil resolver o problema. Informe o usuário se esse for o caso.

Para evitar essa condição, você pode sinalizar uma chave de acesso desconhecida para o provedor usando a API Signal. Ao chamar PublicKeyCredential.signalUnknownCredential() com um ID de RP e um ID de credencial, o RP pode informar ao provedor de chave de acesso que a credencial especificada foi removida ou não existe. Cabe ao provedor de chave de acesso como lidar com esse indicador, mas, se houver suporte, a chave de acesso associada será removida.

// Detect authentication failure due to lack of the credential
if (response.status === 404) {
  // Feature detection
  if (PublicKeyCredential.signalUnknownCredential) {
    await PublicKeyCredential.signalUnknownCredential({
      rpId: "example.com",
      credentialId: "vI0qOggiE3OT01ZRWBYz5l4MEgU0c7PmAA" // base64url encoded credential ID
    });
  } else {
    // Encourage the user to delete the passkey from the password manager nevertheless.
    ...
  }
}

Para saber mais sobre a API Signal, leia Manter as chaves de acesso consistentes com as credenciais no seu servidor com a API Signal.

Enviar uma notificação para o usuário

Enviar uma notificação (como um e-mail) quando uma chave de acesso for registrada ajuda os usuários a detectar acesso não autorizado à conta. Se um invasor criar uma chave de acesso sem o conhecimento do usuário, ela vai continuar disponível para uso indevido no futuro, mesmo depois que a senha for alterada. A notificação alerta o usuário e ajuda a evitar isso.

Lista de verificação

  • Verifique o usuário (de preferência usando e-mail ou um método seguro) antes de permitir que ele crie uma chave de acesso.
  • Evite criar chaves de acesso duplicadas para o mesmo provedor usando excludeCredentials.
  • Salve o AAGUID para identificar o provedor de chave de acesso e nomear a credencial do usuário.
  • Indica se uma tentativa de registrar uma chave de acesso falha com PublicKeyCredential.signalUnknownCredential().
  • Enviar uma notificação ao usuário depois de criar e registrar uma chave de acesso para a conta dele.

Recursos

Próxima etapa: Fazer login com uma chave de acesso usando o preenchimento automático de formulários.