Gerenciamento de chaves de acesso

Este guia detalha como permitir que os usuários visualizem, renomeiem e excluam as chaves de acesso registradas, mantendo as credenciais salvas e os detalhes do perfil de usuário sincronizados entre o servidor e os gerenciadores de senhas do usuário usando a API Signal.

Operações do lado do servidor

A camada de banco de dados de back-end e os endpoints PRECISAM oferecer suporte a ações comuns de CRUD para credenciais registradas, bem como atualizar os detalhes do perfil de usuário. Desvinculado de bibliotecas específicas do framework, o servidor expõe endpoints para:

  1. Listar todas as credenciais do usuário: busque todos os registros de StoredPasskeyCredential que correspondem ao ID do usuário conectado.
  2. Atualizar nomes de credenciais: aceite um novo apelido personalizado para um ID de credencial específico e mantenha a atualização no servidor.
  3. Excluir credenciais: remova um ID de credencial específico do banco de dados.
  4. Atualizar os detalhes da conta de usuário: aceite os valores atualizados de name (nome de usuário) e displayName para o perfil do usuário conectado, de modo que o cliente possa sincronizá-los com o provedor de chaves de acesso.
// Node.js routing example for credential CRUD and user profile rename
router.get('/api/credentials', checkUserAuthenticated, async (req, res) => {
  const list = await db.findCredentialsByUserId(req.user.id);
  return res.json(list);
});

router.put('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
  const { id } = req.params;
  const { name } = req.body;
  const cred = await db.findCredentialById(id);
  if (!cred || cred.passkeyUserId !== req.user.id) {
    return res.status(404).json({ error: 'Credential not found.' });
  }
  cred.name = name;
  await db.saveCredential(cred);
  return res.json(cred);
});

router.delete('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
  const { id } = req.params;
  const cred = await db.findCredentialById(id);
  if (!cred || cred.passkeyUserId !== req.user.id) {
    return res.status(404).json({ error: 'Credential not found.' });
  }
  await db.deleteCredential(id);
  return res.json({ success: true });
});

router.put('/api/user', checkUserAuthenticated, async (req, res) => {
  const { name, displayName } = req.body;
  const updatedUser = await db.updateUser(req.user.id, { name, displayName });
  return res.json(updatedUser);
});

Interface de gerenciamento do lado do cliente

Renderize um painel de configurações dedicado que permite aos usuários auditar e gerenciar facilmente as opções de autenticação registradas e os detalhes do perfil da conta:

  1. Mostrar lista salva: busque a lista no endpoint e renderize as linhas de credenciais individuais. Se a resposta estiver vazia, renderize uma mensagem de estado vazio útil (por exemplo, "Nenhuma chave de acesso encontrada").
  2. Mapear metadados do AAGUID: para cada chave de acesso, procure a propriedade aaguid no registro local para renderizar os detalhes do provedor. Consulte a seção Determinar o provedor de chave de acesso pelo AAGUID para mais detalhes.
  3. Requisitos de interface por item: cada linha dentro do contêiner de lista PRECISA renderizar:
    • Ícone do provedor: imagem derivada do AAGUID ou URI de dados.
    • Nome do provedor/personalizado: nome derivado do AAGUID ou string renomeada pelo usuário.
    • Data de registro: o carimbo de data/hora bruto registeredAt persistido no banco de dados formatado para uma data legível por humanos para exibição do cliente.
    • Data do último uso: o carimbo de data/hora bruto da época lastUsedAt, mantido no banco de dados, formatado para uma data legível por humanos (se houver) para exibição ao cliente.
    • Botão "Renomear": aciona uma ação de renomeação para atualizar o apelido personalizado da credencial no servidor (PUT /api/credential/:id). Observação: renomear o apelido de uma chave de acesso individual é uma mudança de rótulo do lado do servidor e NÃO invoca signalCurrentUserDetails().
    • Botão "Excluir": aciona a exclusão da credencial (DELETE /api/credential/:id) e sincroniza os IDs de credencial restantes usando signalAllAcceptedCredentials().
  4. Interface de renomeação da conta de usuário:
    • Ofereça um controle que permita ao usuário atualizar o nome de usuário da conta (name) ou o displayName (PUT /api/user) e sincronize imediatamente os detalhes atualizados do usuário com o gerenciador de senhas usando PublicKeyCredential.signalCurrentUserDetails().
  5. Botão condicional "Criar chave de acesso":
    • Ofereça um botão de acionamento de registro "Criar chave de acesso" em destaque na página de gerenciamento. Antes de renderizar esse elemento da interface, a página PRECISA detectar os recursos usando PublicKeyCredential.getClientCapabilities() para verificar se o autenticador da plataforma é compatível. Se as chaves de acesso não forem compatíveis, oculte esse botão e incentive as inscrições padrão de MFA.
    • Permitir o registro de uma chave de segurança omitindo authenticatorSelection.authenticatorAttachment na chamada navigator.credentials.create().

Sincronização da API Signal

A API Signal permite que o aplicativo comunique estados de credenciais aos gerenciadores de senhas, mantendo os cofres sincronizados do usuário e o banco de dados de back-end em sincronia.

  • Regra de detecção de recursos e codificação de parâmetros:
    • Sempre faça a detecção de recursos dos métodos da API Signal antes de invocá-los (if (PublicKeyCredential.signalAllAcceptedCredentials) e if (PublicKeyCredential.signalCurrentUserDetails)).
    • Todos os parâmetros userId e de ID de credencial transmitidos para os métodos da API Signal (signalAllAcceptedCredentials, signalCurrentUserDetails) PRECISAM ser strings codificadas em Base64URL.
  • Sincronização de carregamento de página iniciadora:
    • O aplicativo PRECISA invocar signalAllAcceptedCredentials() automaticamente quando a página de gerenciamento é carregada (por exemplo, dentro de um listener de eventos DOMContentLoaded, inicialização de módulo ou hook de montagem de componente de framework, como useEffect).
  • Sincronização de atualizações de gerenciamento:
    • O aplicativo PRECISA invocar signalAllAcceptedCredentials() imediatamente no gerenciador de cliques de exclusão de credenciais após a busca.
    • O aplicativo PRECISA invocar signalCurrentUserDetails() imediatamente no manipulador de cliques de renomeação da conta de usuário (name ou displayName) após a busca.
// Client-side management synchronization ES module
import { listFetch, renameCredentialFetch, updateUserFetch, deleteFetch } from './api.js';

// Base64URL-encoded User ID string (illustration only)
const base64UrlUserId = "M2YPl-KGnA8";

async function syncAcceptedCredentials(currentCredentialsList) {
  if (!window.PublicKeyCredential || !PublicKeyCredential.signalAllAcceptedCredentials) return;
  try {
    const credentialIds = currentCredentialsList.map(c => c.id); // Array of Base64URL credential ID strings
    
    await PublicKeyCredential.signalAllAcceptedCredentials({
      rpId, // RP ID must match the one defined on the server
      userId: base64UrlUserId, // User ID Base64URL-encoded string
      allAcceptedCredentialIds: credentialIds
    });
  } catch (e) {
    console.error('SignalAllAcceptedCredentials sync failure:', e);
  }
}

async function loadManagementPanel() {
  const response = await listFetch();
  const list = await response.json();
  
  renderUI(list);
  // Sync accepted credentials on initial load
  await syncAcceptedCredentials(list);
}

async function performDelete(credentialId) {
  const response = await deleteFetch(credentialId);
  if (response.ok) {
    const updatedResponse = await listFetch();
    const updatedList = await updatedResponse.json();
    
    renderUI(updatedList);
    // Sync remaining accepted credentials after deletion
    await syncAcceptedCredentials(updatedList);
  }
}

// Renaming a passkey's nickname updates the server record only
async function performCredentialRename(credentialId, newCredentialName) {
  const response = await renameCredentialFetch(credentialId, { name: newCredentialName });
  if (response.ok) {
    const updatedResponse = await listFetch();
    renderUI(await updatedResponse.json());
  }
}

// Renaming the user's account username or displayName triggers signalCurrentUserDetails
async function performUserRename(rpId, userId, updatedName, updatedDisplayName) {
  const response = await updateUserFetch({ name: updatedName, displayName: updatedDisplayName });
  if (response.ok && window.PublicKeyCredential && PublicKeyCredential.signalCurrentUserDetails) {
    try {
      await PublicKeyCredential.signalCurrentUserDetails({
        rpId, // RP ID must match the one defined on the server
        userId, // Base64URL-encoded user ID
        name: updatedName, // Updated username
        displayName: updatedDisplayName // Updated display name
      });
    } catch (e) {
      console.error('SignalCurrentUserDetails sync failure:', e);
    }
  }
}

window.addEventListener('DOMContentLoaded', loadManagementPanel);

Determinar o provedor de chaves de acesso pelo AAGUID

Um AAGUID (identificador global exclusivo de atestado do autenticador) é um identificador de 128 bits que representa o modelo do autenticador, não uma instância específica. Ele é incluído nos dados do autenticador durante o registro da chave de acesso e pode ser usado para determinar qual provedor de chaves de acesso (por exemplo, Gerenciador de Senhas do Google, Chaves do iCloud, 1Password) criou uma credencial.

O AAGUID só deve ser usado para ajudar os usuários no gerenciamento de chaves de acesso. Ele pode ser modificado, a menos que seja atestado criptograficamente, o que as chaves de acesso da plataforma não aceitam no momento.

1. Registro de AAGUID

Um mapeamento JSON mantido pela comunidade de AAGUIDs para nomes e ícones de provedores está disponível em:

https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/refs/heads/main/combined_aaguid.json

Cada entrada tem o seguinte esquema:

{
  "<aaguid-uuid>": {
    "name": "Provider Name",
    "icon_light": "data:image/png;base64,...",
    "icon_dark": "data:image/png;base64,..."
  }
}

2. Como usar o AAGUID após o registro

Depois de verificar uma resposta de registro, leia o aaguid do resultado do registro e procure-o no registro para preencher o name e o providerIcon da credencial:

Antes de procurar o AAGUID no registro, verifique se ele é igual a '00000000-0000-0000-0000-000000000000'. Se sim, pule a pesquisa de registro e defina name como um substituto (por exemplo, o nome do dispositivo do user agent ou "Provedor de chave de acesso desconhecido") e providerIcon como undefined. Procure no registro apenas AAGUIDs que não sejam zero.

import aaguids from './aaguids.json' with { type: 'json' };

const { aaguid } = registrationInfo;
if (aaguid === '00000000-0000-0000-0000-000000000000') {
  // use the device name as the passkey provider based on
  // the information derived from the user agent string,
  // or just say "Unknown passkey provider"
} else {
  const provider = aaguids[aaguid];
  const credential = {
    // ...other fields
    aaguid,
    name: provider?.name || 'Unknown passkey provider',
    providerIcon: provider?.icon_light,
  };
}

Estratégias de fallback

Substituto para detecção de recursos de chave de acesso

Sempre instale "webauthn-polyfills" e importe-o no contexto. Se PublicKeyCredential for compatível, PublicKeyCredential.getClientCapabilities também será.

import 'webauthn-polyfills';

Substituição de sincronização da API Signal

Se o navegador não for compatível com PublicKeyCredential.parseRequestOptionsFromJSON, use "webauthn-polyfills":

<script type="module"> 
  if (!PublicKeyCredential.parseRequestOptionsFromJSON) { 
     await import('https://unpkg.com/webauthn-polyfills'); 
   } 
 </script> 

Isso também vai adicionar suporte para PublicKeyCredential.prototype.toJSON.