Pengelolaan Kunci Sandi

Panduan ini menjelaskan cara mengizinkan pengguna melihat, mengganti nama, dan menghapus kunci sandi terdaftar mereka, sekaligus menjaga kredensial yang disimpan dan detail profil pengguna tetap disinkronkan antara server dan pengelola sandi pengguna menggunakan Signal API.

Operasi Sisi Server

Lapisan database backend dan endpoint Anda HARUS mendukung tindakan CRUD umum untuk kredensial terdaftar serta memperbarui detail profil pengguna. Terpisah dari library khusus framework, server mengekspos endpoint untuk:

  1. Mencantumkan semua kredensial pengguna: Ambil semua data StoredPasskeyCredential yang cocok dengan ID pengguna yang login.
  2. Memperbarui nama kredensial: Terima nama panggilan kustom baru untuk ID kredensial tertentu dan pertahankan update di server.
  3. Menghapus kredensial: Menghapus ID kredensial tertentu dari database.
  4. Memperbarui detail akun pengguna: Terima nilai name (nama pengguna) dan displayName yang diperbarui untuk profil pengguna yang login sehingga klien dapat menyinkronkannya dengan penyedia kunci sandi.
// 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 Pengelolaan Sisi Klien

Render panel setelan khusus yang memungkinkan pengguna mengaudit dan mengelola opsi autentikasi terdaftar dan detail profil akun mereka dengan mudah:

  1. Menampilkan daftar tersimpan: Ambil daftar dari endpoint Anda dan render baris kredensial satu per satu. Jika respons kosong, render pesan status kosong yang bermanfaat (misalnya, "Tidak ada kunci sandi yang ditemukan").
  2. Memetakan Metadata AAGUID: Untuk setiap kunci sandi, cari properti aaguid-nya di registry lokal Anda untuk merender detail penyedianya. Lihat bagian Menentukan penyedia kunci sandi dari AAGUID untuk mengetahui detail selengkapnya.
  3. Persyaratan UI Per Item: Setiap baris di dalam penampung daftar HARUS merender:
    • Ikon Penyedia: URI data atau gambar yang berasal dari AAGUID.
    • Nama Penyedia/Kustom: Nama yang berasal dari AAGUID atau string yang diganti namanya oleh pengguna.
    • Registration Date: Stempel waktu epoch mentah yang dipertahankan database registeredAt diformat menjadi tanggal yang dapat dibaca manusia untuk ditampilkan kepada klien.
    • Tanggal Terakhir Digunakan: Stempel waktu epoch mentah yang dipertahankan database lastUsedAt yang diformat menjadi tanggal yang dapat dibaca manusia (jika ada) untuk tampilan klien.
    • Tombol Ganti Nama: Memicu tindakan ganti nama untuk memperbarui nama panggilan kustom kredensial di server (PUT /api/credential/:id). Catatan: Mengganti nama panggilan kunci sandi individual adalah perubahan label sisi server dan TIDAK memanggil signalCurrentUserDetails().
    • Tombol Delete: Memicu penghapusan kredensial (DELETE /api/credential/:id) dan menyinkronkan ID kredensial yang tersisa melalui signalAllAcceptedCredentials().
  4. UI Penggantian Nama Akun Pengguna:
    • Sediakan kontrol yang memungkinkan pengguna memperbarui nama pengguna akun (name) atau displayName (PUT /api/user) mereka, dan segera menyinkronkan detail pengguna yang diperbarui dengan pengelola sandi melalui PublicKeyCredential.signalCurrentUserDetails().
  5. Tombol "Buat Kunci Sandi" Bersyarat:
    • Tawarkan tombol pemicu pendaftaran "Buat kunci sandi" yang terlihat jelas di halaman pengelolaan. Sebelum merender elemen UI ini, halaman HARUS mendeteksi fitur kemampuan menggunakan PublicKeyCredential.getClientCapabilities() untuk memverifikasi bahwa pengautentikasi platform didukung. Jika kunci sandi tidak didukung, sembunyikan tombol ini dan dorong pendaftaran MFA standar dengan baik.
    • Mengizinkan pendaftaran kunci keamanan dengan menghilangkan authenticatorSelection.authenticatorAttachment pada panggilan navigator.credentials.create().

Sinkronisasi Signal API

Signal API memungkinkan aplikasi mengomunikasikan status kredensial ke pengelola sandi, sehingga vault yang disinkronkan pengguna dan database backend Anda tetap sinkron.

  • Aturan Deteksi Fitur & Encoding Parameter:
    • Selalu deteksi fitur metode Signal API sebelum memanggilnya (if (PublicKeyCredential.signalAllAcceptedCredentials) dan if (PublicKeyCredential.signalCurrentUserDetails)).
    • Semua parameter userId dan ID kredensial yang diteruskan ke metode Signal API (signalAllAcceptedCredentials, signalCurrentUserDetails) HARUS berupa string berenkode Base64URL.
  • Memulai Sinkronisasi Pemuatan Halaman:
    • Aplikasi HARUS memanggil signalAllAcceptedCredentials() secara otomatis saat pemuatan halaman pengelolaan (misalnya, di dalam pemroses peristiwa DOMContentLoaded, inisialisasi modul, atau hook pemasangan komponen framework seperti useEffect).
  • Sinkronisasi Update Pengelolaan:
    • Aplikasi HARUS segera memanggil signalAllAcceptedCredentials() dalam pengendali klik hapus kredensial setelah pengambilan.
    • Aplikasi HARUS segera memanggil signalCurrentUserDetails() dalam pengendali klik ganti nama akun pengguna (name atau displayName) setelah pengambilan.
// 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);

Menentukan penyedia kunci sandi dari AAGUID

AAGUID (Authenticator Attestation Globally Unique Identifier) adalah ID unik 128-bit yang mewakili model pengautentikasi, bukan instance tertentu. AAGUID disertakan dalam data pengautentikasi selama pendaftaran kunci sandi dan dapat digunakan untuk menentukan penyedia kunci sandi (misalnya, Pengelola Sandi Google, iCloud Keychain, 1Password) yang membuat kredensial.

AAGUID hanya boleh digunakan untuk membantu pengguna mengelola kunci sandi. Kunci sandi ini dapat diubah kecuali jika dibuktikan secara kriptografis, yang saat ini tidak didukung oleh kunci sandi platform.

1. Registry AAGUID

Pemetaan JSON yang dikelola komunitas dari AAGUID ke nama dan ikon penyedia tersedia di:

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

Setiap entri memiliki skema berikut:

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

2. Menggunakan AAGUID Setelah Pendaftaran

Setelah memverifikasi respons pendaftaran, baca aaguid dari hasil pendaftaran dan cari di registry untuk mengisi name dan providerIcon kredensial:

Sebelum mencari AAGUID di registry, periksa apakah nilainya sama dengan '00000000-0000-0000-0000-000000000000'. Jika demikian, lewati pencarian registry dan tetapkan name ke penggantian (misalnya, nama perangkat dari agen pengguna, atau "Penyedia kunci sandi tidak dikenal") dan providerIcon ke undefined. Hanya cari registry untuk AAGUID yang bukan nol.

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,
  };
}

Strategi Penggantian

Penggantian deteksi fitur kunci sandi

Selalu instal 'webauthn-polyfills' dan impor dalam konteks. Pertimbangkan bahwa selama PublicKeyCredential didukung, PublicKeyCredential.getClientCapabilities juga didukung.

import 'webauthn-polyfills';

Penggantian Sinkronisasi Signal API

Jika browser tidak mendukung PublicKeyCredential.parseRequestOptionsFromJSON, gunakan 'webauthn-polyfills':

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

Tindakan ini juga akan menambahkan dukungan untuk PublicKeyCredential.prototype.toJSON.