Z tego przewodnika dowiesz się, jak umożliwić użytkownikom wyświetlanie, zmienianie nazw i usuwanie zarejestrowanych kluczy dostępu przy jednoczesnym zachowaniu synchronizacji zapisanych danych logowania i szczegółów profilu użytkownika między serwerem a menedżerami haseł użytkownika za pomocą interfejsu Signal API.
Operacje po stronie serwera
Warstwa bazy danych backendu i punkty końcowe MUSZĄ obsługiwać typowe działania CRUD w przypadku zarejestrowanych danych logowania, a także aktualizowanie szczegółów profilu użytkownika. Serwer jest odłączony od bibliotek powiązanych z platformą i udostępnia punkty końcowe, które umożliwiają:
- Wyświetlanie listy wszystkich danych logowania użytkownika: pobierz wszystkie rekordy
StoredPasskeyCredentialpasujące do identyfikatora zalogowanego użytkownika. - Aktualizowanie nazw danych logowania: akceptowanie nowej niestandardowej nazwy dla konkretnego identyfikatora danych logowania i zapisywanie aktualizacji na serwerze.
- Usuwanie danych logowania: usuń z bazy danych określony identyfikator danych logowania.
- Aktualizowanie danych konta użytkownika: akceptuj zaktualizowane wartości
name(nazwa użytkownika) idisplayNamedla profilu zalogowanego użytkownika, aby klient mógł je zsynchronizować z dostawcą kluczy dostępu.
// 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);
});
Interfejs zarządzania po stronie klienta
Wyświetlaj specjalny panel ustawień, który umożliwia użytkownikom łatwe sprawdzanie zarejestrowanych opcji uwierzytelniania i szczegółów profilu konta oraz zarządzanie nimi:
- Wyświetlanie zapisanej listy: pobierz listę z punktu końcowego i wyrenderuj poszczególne wiersze danych logowania. Jeśli odpowiedź jest pusta, wyświetl przydatny komunikat o pustym stanie (np. „Nie znaleziono kluczy dostępu”).
- Mapowanie metadanych AAGUID: w przypadku każdego klucza dostępu wyszukaj jego właściwość
aaguidw rejestrze lokalnym, aby wyświetlić szczegóły dostawcy. Więcej informacji znajdziesz w sekcji Określanie dostawcy klucza dostępu na podstawie identyfikatora AAGUID. - Wymagania dotyczące interfejsu poszczególnych elementów: każdy wiersz w kontenerze listy MUSI zawierać:
- Ikona dostawcy: obraz pochodzący z identyfikatora AAGUID lub identyfikator URI danych.
- Dostawca/nazwa niestandardowa: nazwa pochodząca z identyfikatora AAGUID lub ciąg znaków zmieniony przez użytkownika.
- Data rejestracji: sygnatura czasowa epoki
registeredAtzapisana w bazie danych, sformatowana w sposób zrozumiały dla człowieka i wyświetlana klientowi. - Data ostatniego użycia: zapisana w bazie danych nieprzetworzona sygnatura czasowa epoki
lastUsedAtsformatowana jako zrozumiała dla człowieka data (jeśli jest dostępna) do wyświetlenia klientowi. - Przycisk zmiany nazwy: powoduje zmianę nazwy, aby zaktualizować niestandardową nazwę danych logowania na serwerze (
PUT /api/credential/:id). Uwaga: zmiana nazwy poszczególnych kluczy dostępu jest zmianą etykiety po stronie serwera i NIE wywołujesignalCurrentUserDetails(). - Przycisk usuwania: powoduje usunięcie danych logowania (
DELETE /api/credential/:id) i synchronizuje pozostałe identyfikatory danych logowania za pomocąsignalAllAcceptedCredentials().
- Interfejs zmiany nazwy konta użytkownika:
- Udostępnij opcję umożliwiającą użytkownikowi zaktualizowanie nazwy użytkownika konta (
name) lubdisplayName(PUT /api/user) i natychmiast zsynchronizuj zaktualizowane dane użytkownika z menedżerem haseł za pomocąPublicKeyCredential.signalCurrentUserDetails().
- Udostępnij opcję umożliwiającą użytkownikowi zaktualizowanie nazwy użytkownika konta (
- Warunkowy przycisk „Utwórz klucz dostępu”:
- Na stronie zarządzania umieść dobrze widoczny przycisk „Utwórz klucz dostępu” do rejestracji. Przed wyrenderowaniem tego elementu interfejsu strona MUSI wykryć funkcje za pomocą
PublicKeyCredential.getClientCapabilities(), aby sprawdzić, czy uwierzytelnianie platformy jest obsługiwane. Jeśli klucze dostępu nie są obsługiwane, ukryj ten przycisk i zachęć do standardowej rejestracji MFA. - Zezwalaj na rejestrowanie klucza bezpieczeństwa przez pominięcie
authenticatorSelection.authenticatorAttachmentw wywołaniunavigator.credentials.create().
- Na stronie zarządzania umieść dobrze widoczny przycisk „Utwórz klucz dostępu” do rejestracji. Przed wyrenderowaniem tego elementu interfejsu strona MUSI wykryć funkcje za pomocą
Synchronizacja interfejsu Signal API
Interfejs Signal API umożliwia aplikacji przekazywanie stanów danych logowania do menedżerów haseł, dzięki czemu zsynchronizowane skarbce użytkownika i baza danych backendu są zawsze aktualne.
- Reguła wykrywania cech i kodowania parametrów:
- Zawsze wykrywaj funkcje metod interfejsu Signal API przed ich wywołaniem (
if (PublicKeyCredential.signalAllAcceptedCredentials)iif (PublicKeyCredential.signalCurrentUserDetails)). - Wszystkie parametry
userIdi identyfikatora danych logowania przekazywane do metod interfejsu Signal API (signalAllAcceptedCredentials,signalCurrentUserDetails) MUSZĄ być ciągami znaków zakodowanymi w standardzie Base64URL.
- Zawsze wykrywaj funkcje metod interfejsu Signal API przed ich wywołaniem (
- Inicjowanie synchronizacji wczytywania strony:
- Aplikacja MUSI automatycznie wywoływać funkcję
signalAllAcceptedCredentials()po wczytaniu strony zarządzania (np. w detektorze zdarzeńDOMContentLoaded, podczas inicjowania modułu lub w ramach hooka montowania komponentu platformy, takiego jakuseEffect).
- Aplikacja MUSI automatycznie wywoływać funkcję
- Synchronizacja aktualizacji zarządzania:
- Aplikacja MUSI wywołać metodę
signalAllAcceptedCredentials()natychmiast w ramach modułu obsługi kliknięć usuwania danych logowania po pobraniu. - Aplikacja MUSI wywołać funkcję
signalCurrentUserDetails()natychmiast po pobraniu w ramach modułu obsługi kliknięć zmiany nazwy konta użytkownika (namelubdisplayName).
- Aplikacja MUSI wywołać metodę
// 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);
Określanie dostawcy klucza dostępu na podstawie identyfikatora AAGUID
AAGUID (Authenticator Attestation Globally Unique Identifier) to 128-bitowy unikalny identyfikator, który reprezentuje model uwierzytelniacza, a nie konkretną instancję. Jest on uwzględniany w danych uwierzytelniających podczas rejestracji klucza dostępu i może służyć do określenia, który dostawca klucza dostępu (np. Menedżer haseł Google, pęk kluczy iCloud, 1Password) utworzył dane logowania.
Identyfikator AAGUID powinien być używany tylko do pomagania użytkownikom w zarządzaniu kluczami dostępu. Można go zmodyfikować, chyba że jest poświadczony kryptograficznie, czego klucze dostępu platformy obecnie nie obsługują.
1. Rejestr identyfikatorów AAGUID
Mapowanie JSON identyfikatorów AAGUID na nazwy i ikony dostawców, które jest utrzymywane przez społeczność, jest dostępne na stronie:
https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/refs/heads/main/combined_aaguid.json
Każdy wpis ma ten schemat:
{
"<aaguid-uuid>": {
"name": "Provider Name",
"icon_light": "data:image/png;base64,...",
"icon_dark": "data:image/png;base64,..."
}
}
2. Używanie identyfikatora AAGUID po rejestracji
Po zweryfikowaniu odpowiedzi rejestracyjnej odczytaj aaguid z wyniku rejestracji i wyszukaj go w rejestrze, aby wypełnić pola name i providerIcon danych logowania:
Zanim wyszukasz identyfikator AAGUID w rejestrze, sprawdź, czy jest on równy '00000000-0000-0000-0000-000000000000'. Jeśli tak, pomiń wyszukiwanie w rejestrze i ustaw name na wartość zastępczą (np. nazwę urządzenia z klienta użytkownika lub „Nieznany dostawca klucza dostępu”), a providerIcon na undefined. Sprawdzaj rejestr tylko w przypadku niezerowych identyfikatorów AAGUID.
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 zastępcze
Zastępcze wykrywanie funkcji klucza dostępu
Zawsze instaluj „webauthn-polyfills” i importuj je w kontekście.
Pamiętaj, że jeśli obsługiwany jest format PublicKeyCredential, obsługiwany jest też format PublicKeyCredential.getClientCapabilities.
import 'webauthn-polyfills';
Zastępcza synchronizacja interfejsu Signal API
Jeśli przeglądarka nie obsługuje PublicKeyCredential.parseRequestOptionsFromJSON, użyj „webauthn-polyfills”:
<script type="module">
if (!PublicKeyCredential.parseRequestOptionsFromJSON) {
await import('https://unpkg.com/webauthn-polyfills');
}
</script>
Spowoduje to też dodanie obsługi PublicKeyCredential.prototype.toJSON.