Zarządzanie kluczami dostępu

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ą:

  1. Wyświetlanie listy wszystkich danych logowania użytkownika: pobierz wszystkie rekordy StoredPasskeyCredential pasujące do identyfikatora zalogowanego użytkownika.
  2. Aktualizowanie nazw danych logowania: akceptowanie nowej niestandardowej nazwy dla konkretnego identyfikatora danych logowania i zapisywanie aktualizacji na serwerze.
  3. Usuwanie danych logowania: usuń z bazy danych określony identyfikator danych logowania.
  4. Aktualizowanie danych konta użytkownika: akceptuj zaktualizowane wartości name (nazwa użytkownika) i displayName dla 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:

  1. 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”).
  2. Mapowanie metadanych AAGUID: w przypadku każdego klucza dostępu wyszukaj jego właściwość aaguid w rejestrze lokalnym, aby wyświetlić szczegóły dostawcy. Więcej informacji znajdziesz w sekcji Określanie dostawcy klucza dostępu na podstawie identyfikatora AAGUID.
  3. 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 registeredAt zapisana 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 lastUsedAt sformatowana 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łuje signalCurrentUserDetails().
    • Przycisk usuwania: powoduje usunięcie danych logowania (DELETE /api/credential/:id) i synchronizuje pozostałe identyfikatory danych logowania za pomocą signalAllAcceptedCredentials().
  4. Interfejs zmiany nazwy konta użytkownika:
    • Udostępnij opcję umożliwiającą użytkownikowi zaktualizowanie nazwy użytkownika konta (name) lub displayName (PUT /api/user) i natychmiast zsynchronizuj zaktualizowane dane użytkownika z menedżerem haseł za pomocą PublicKeyCredential.signalCurrentUserDetails().
  5. 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.authenticatorAttachment w wywołaniu navigator.credentials.create().

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) i if (PublicKeyCredential.signalCurrentUserDetails)).
    • Wszystkie parametry userId i identyfikatora danych logowania przekazywane do metod interfejsu Signal API (signalAllAcceptedCredentials, signalCurrentUserDetails) MUSZĄ być ciągami znaków zakodowanymi w standardzie Base64URL.
  • 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 jak useEffect).
  • 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 (name lub displayName).
// 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.