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:
- Listar todas as credenciais do usuário: busque todos os registros de
StoredPasskeyCredentialque correspondem ao ID do usuário conectado. - Atualizar nomes de credenciais: aceite um novo apelido personalizado para um ID de credencial específico e mantenha a atualização no servidor.
- Excluir credenciais: remova um ID de credencial específico do banco de dados.
- Atualizar os detalhes da conta de usuário: aceite os valores atualizados de
name(nome de usuário) edisplayNamepara 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:
- 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").
- Mapear metadados do AAGUID: para cada chave de acesso, procure a propriedade
aaguidno registro local para renderizar os detalhes do provedor. Consulte a seção Determinar o provedor de chave de acesso pelo AAGUID para mais detalhes. - 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
registeredAtpersistido 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 invocasignalCurrentUserDetails(). - Botão "Excluir": aciona a exclusão da credencial (
DELETE /api/credential/:id) e sincroniza os IDs de credencial restantes usandosignalAllAcceptedCredentials().
- 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 odisplayName(PUT /api/user) e sincronize imediatamente os detalhes atualizados do usuário com o gerenciador de senhas usandoPublicKeyCredential.signalCurrentUserDetails().
- Ofereça um controle que permita ao usuário atualizar o nome de usuário da conta (
- 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.authenticatorAttachmentna chamadanavigator.credentials.create().
- 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
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)eif (PublicKeyCredential.signalCurrentUserDetails)). - Todos os parâmetros
userIde de ID de credencial transmitidos para os métodos da API Signal (signalAllAcceptedCredentials,signalCurrentUserDetails) PRECISAM ser strings codificadas em Base64URL.
- Sempre faça a detecção de recursos dos métodos da API Signal antes de invocá-los (
- 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 eventosDOMContentLoaded, inicialização de módulo ou hook de montagem de componente de framework, comouseEffect).
- O aplicativo PRECISA invocar
- 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 (nameoudisplayName) após a busca.
- O aplicativo PRECISA invocar
// 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.