Questa guida spiega in dettaglio come consentire agli utenti di visualizzare, rinominare ed eliminare le passkey registrate, mantenendo sincronizzate le credenziali salvate e i dettagli del profilo utente tra il server e i gestori delle password dell'utente utilizzando l'API Signal.
Operazioni lato server
Il livello del database di backend e gli endpoint DEVONO supportare le azioni CRUD comuni per le credenziali registrate, nonché l'aggiornamento dei dettagli del profilo utente. Disaccoppiato dalle librerie specifiche del framework, il server espone gli endpoint per:
- Elenca tutte le credenziali utente: recupera tutti i record
StoredPasskeyCredentialcorrispondenti all'ID dell'utente che ha eseguito l'accesso. - Aggiorna i nomi delle credenziali: accetta un nuovo nickname personalizzato per un ID credenziale specifico e mantieni l'aggiornamento sul server.
- Elimina credenziali: rimuovi un ID credenziale specifico dal database.
- Aggiorna i dettagli dell'account utente: accetta i valori aggiornati di
name(nome utente) edisplayNameper il profilo dell'utente che ha eseguito l'accesso, in modo che il client possa sincronizzarli con il provider di passkey.
// 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);
});
UI di gestione lato client
Esegui il rendering di un riquadro delle impostazioni dedicato che consenta agli utenti di controllare e gestire facilmente le opzioni di autenticazione registrate e i dettagli del profilo dell'account:
- Visualizza l'elenco salvato: recupera l'elenco dal tuo endpoint ed esegui il rendering delle singole righe delle credenziali. Se la risposta è vuota, visualizza un messaggio di stato vuoto utile (ad es. "Nessuna passkey trovata").
- Mappa i metadati AAGUID: per ogni passkey, cerca la proprietà
aaguidnel registro locale per visualizzare i dettagli del provider. Per maggiori dettagli, consulta la sezione Determinare il provider di passkey da AAGUID. - Requisiti dell'interfaccia utente per ogni elemento: ogni riga all'interno del contenitore dell'elenco DEVE mostrare:
- Icona del provider: URI di dati o immagine derivato da AAGUID.
- Provider/Nome personalizzato: nome derivato da AAGUID o stringa rinominata dall'utente.
- Data di registrazione: il timestamp non elaborato dell'epoca
registeredAt, memorizzato nel database, formattato in una data leggibile per la visualizzazione del client. - Data dell'ultimo utilizzo: il timestamp non elaborato dell'epoca di Unix
lastUsedAt, memorizzato nel database, formattato in una data leggibile (se presente) per la visualizzazione del client. - Pulsante Rinomina: attiva un'azione di ridenominazione per aggiornare il nickname personalizzato della credenziale sul server (
PUT /api/credential/:id). Nota: la ridenominazione del nickname di una singola passkey è una modifica dell'etichetta lato server e NON richiamasignalCurrentUserDetails(). - Pulsante Elimina: attiva l'eliminazione della credenziale (
DELETE /api/credential/:id) e sincronizza gli ID credenziale rimanenti tramitesignalAllAcceptedCredentials().
- UI per la ridenominazione degli account utente:
- Fornisci un controllo che consenta all'utente di aggiornare il nome utente dell'account (
name) odisplayName(PUT /api/user) e sincronizza immediatamente i dettagli utente aggiornati con il gestore delle password tramitePublicKeyCredential.signalCurrentUserDetails().
- Fornisci un controllo che consenta all'utente di aggiornare il nome utente dell'account (
- Pulsante "Crea passkey" condizionale:
- Offri un pulsante di attivazione della registrazione "Crea passkey" ben visibile nella pagina di gestione. Prima di eseguire il rendering di questo elemento UI, la pagina DEVE rilevare le funzionalità utilizzando
PublicKeyCredential.getClientCapabilities()per verificare che l'autenticatore della piattaforma sia supportato. Se le passkey non sono supportate, nascondi questo pulsante e incoraggia l'utente a registrarsi all'MFA standard. - Consenti la registrazione di un token di sicurezza omettendo
authenticatorSelection.authenticatorAttachmentnella chiamatanavigator.credentials.create().
- Offri un pulsante di attivazione della registrazione "Crea passkey" ben visibile nella pagina di gestione. Prima di eseguire il rendering di questo elemento UI, la pagina DEVE rilevare le funzionalità utilizzando
Sincronizzazione dell'API Signal
L'API Signal consente all'applicazione di comunicare gli stati delle credenziali ai gestori delle password, mantenendo sincronizzati i vault dell'utente e il database di backend.
- Regola di rilevamento delle funzionalità e codifica dei parametri:
- Esegui sempre il rilevamento delle funzionalità dei metodi dell'API Signal prima di richiamarli (
if (PublicKeyCredential.signalAllAcceptedCredentials)eif (PublicKeyCredential.signalCurrentUserDetails)). - Tutti i parametri
userIde ID credenziale passati ai metodi dell'API Signal (signalAllAcceptedCredentials,signalCurrentUserDetails) DEVONO essere stringhe con codifica Base64URL.
- Esegui sempre il rilevamento delle funzionalità dei metodi dell'API Signal prima di richiamarli (
- Sincronizzazione del caricamento della pagina di origine:
- L'applicazione DEVE richiamare
signalAllAcceptedCredentials()automaticamente quando viene caricata la pagina di gestione (ad esempio, all'interno di un listener di eventiDOMContentLoaded, dell'inizializzazione del modulo o dell'hook di montaggio del componente del framework comeuseEffect).
- L'applicazione DEVE richiamare
- Sincronizzazione degli aggiornamenti di gestione:
- L'applicazione DEVE richiamare
signalAllAcceptedCredentials()immediatamente all'interno del gestore dei clic per l'eliminazione delle credenziali dopo il recupero. - L'applicazione DEVE richiamare
signalCurrentUserDetails()immediatamente all'interno del gestore dei clic per la ridenominazione dell'account utente (nameodisplayName) dopo il recupero.
- L'applicazione DEVE richiamare
// 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);
Determina il provider di passkey dall'AAGUID
Un AAGUID (Authenticator Attestation Globally Unique Identifier) è un identificatore univoco a 128 bit che rappresenta il modello dell'autenticatore, non un'istanza specifica. È incluso nei dati dell'autenticatore durante la registrazione della passkey e può essere utilizzato per determinare quale provider di passkey (ad es. Gestore delle password di Google, Portachiavi iCloud, 1Password) ha creato una credenziale.
L'AAGUID deve essere utilizzato solo per aiutare gli utenti con la gestione delle passkey. Può essere modificato, a meno che non sia attestato in modo crittografico, cosa che al momento le passkey della piattaforma non supportano.
1. Registro AAGUID
Una mappatura JSON gestita dalla community degli AAGUID ai nomi e alle icone dei provider è disponibile all'indirizzo:
https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/refs/heads/main/combined_aaguid.json
Ogni voce ha il seguente schema:
{
"<aaguid-uuid>": {
"name": "Provider Name",
"icon_light": "data:image/png;base64,...",
"icon_dark": "data:image/png;base64,..."
}
}
2. Utilizzo dell'AAGUID dopo la registrazione
Dopo aver verificato una risposta di registrazione, leggi il aaguid dal risultato della registrazione e cercalo nel registro per compilare i campi name e providerIcon della credenziale:
Prima di cercare l'AAGUID nel registro, verifica se è uguale a '00000000-0000-0000-0000-000000000000'. In questo caso, salta la ricerca nel registry e imposta name su un valore di riserva (ad es. il nome del dispositivo dallo user agent o "Provider di passkey sconosciuto") e providerIcon su undefined. Cerca nel registry solo gli AAGUID non azzerati.
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,
};
}
Strategie di fallback
Fallback del rilevamento delle funzionalità delle passkey
Installa sempre "webauthn-polyfills" e importalo nel contesto.
Tieni presente che, se PublicKeyCredential è supportato, lo è anche PublicKeyCredential.getClientCapabilities.
import 'webauthn-polyfills';
Sincronizzazione di riserva dell'API Signal
Se il browser non supporta PublicKeyCredential.parseRequestOptionsFromJSON, utilizza i "webauthn-polyfills":
<script type="module">
if (!PublicKeyCredential.parseRequestOptionsFromJSON) {
await import('https://unpkg.com/webauthn-polyfills');
}
</script>
In questo modo verrà aggiunto anche il supporto per PublicKeyCredential.prototype.toJSON.