Panduan ini menjelaskan cara mengizinkan pengguna mendaftarkan kunci sandi untuk akun mereka, sehingga memberikan alternatif login tanpa sandi yang sangat aman dan tahan terhadap phishing.
Persyaratan Database
Untuk mendukung pendaftaran kunci sandi, tabel kredensial database Anda harus menyimpan kolom berikut:
export interface StoredPasskeyCredential {
id: string; // Base64URL-encoded credential ID (unique lookup key)
passkeyUserId: string; // Associated application user ID
credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
credentialType: "public-key";
credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
aaguid: string; // Authenticator Attestation GUID
providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
name: string; // Provider name derived from AAGUID registry
transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
lastUsedAt?: number; // Optional epoch timestamp of last sign-in
registeredAt: number; // Registration epoch timestamp
counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}
Sisi Server
Pembuatan Opsi
Buat endpoint yang menghasilkan parameter pembuatan WebAuthn. Mengandalkan library yang telah diperiksa sesuai standar kategori, bukan kriptografi yang dibuat sendiri.
- Gunakan RP ID yang telah ditetapkan: Gunakan RP ID yang tepat dan telah ditetapkan sebagai string konstan.
- Buat Tantangan yang aman: Buat buffer acak yang aman secara kriptografis dan memiliki entropi tinggi di server, simpan dengan aman di sesi pengguna, dan lakukan enkode sebagai Base64URL untuk pengiriman opsi.
- Hindari Kunci Sandi Duplikat: Petakan ID kredensial pengguna yang sudah terdaftar sebelumnya ke array opsi
excludeCredentials. Tindakan ini mencegah pengautentikasi mendaftarkan kredensial duplikat di akun penyedia kunci sandi yang sama. - Terapkan Kredensial yang Dapat Ditemukan: Tetapkan
requireResidentKey: truedanresidentKey: "required"di opsiauthenticatorSelectionuntuk meminta kredensial yang dapat ditemukan, yang diperlukan untuk login yang dapat ditemukan. - Konfigurasi Verifikasi Pengguna: Tentukan
userVerification: "preferred"atauuserVerification: "required". Banyak kasus penggunaan kepatuhan (misalnya, keuangan, layanan kesehatan) memerlukan'required'untuk menerapkan verifikasi pengguna saat pembuatan. - Menentukan Cakupan Lampiran:
- Alur Promosi: Saat mengusulkan pembuatan kunci sandi tepat setelah login dengan sandi standar atau promosi setelah pendaftaran, tetapkan
authenticatorAttachment: "platform"untuk menerapkan pengautentikasi platform dan mengabaikan perintah kunci keamanan eksternal. - Alur Pengelolaan: Saat dipanggil dari panel setelan atau keamanan khusus yang mendukung kunci keamanan eksternal selain pengautentikasi platform, hapus properti
authenticatorAttachmentsepenuhnya. - Tips: Terima flag permintaan
promotion: booleanuntuk menangani kedua alur secara bersyarat dengan satu endpoint.
- Alur Promosi: Saat mengusulkan pembuatan kunci sandi tepat setelah login dengan sandi standar atau promosi setelah pendaftaran, tetapkan
// Options generation example
const options = {
challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
rp: { id: "example.com", name: "Secure Application" },
user: {
id: userBase64UrlId, // Unique base64url string identifying the account
name: "user@example.com",
displayName: "Jane Doe",
},
pubKeyCredParams: [
{
type: "public-key",
alg: -7,
},
{
type: "public-key",
alg: -257,
},
],
excludeCredentials: userExistingCredentials.map((cred) => ({
type: "public-key",
id: cred.id,
transports: cred.transports,
})),
authenticatorSelection: {
residentKey: "required",
requireResidentKey: true,
userVerification: "preferred",
...(isPromotionFlow && { authenticatorAttachment: "platform" }),
},
};
Verifikasi
- Verifikasi Tantangan: Verifikasi tantangan dengan aman terhadap tantangan terikat sesi yang diharapkan.
- Memverifikasi Kehadiran Pengguna:
- Pastikan flag User Present (UP) yang ditampilkan dalam data pengautentikasi yang diurai adalah
trueuntuk mengonfirmasi kehadiran fisik pengguna pada saat pembuatan.
- Pastikan flag User Present (UP) yang ditampilkan dalam data pengautentikasi yang diurai adalah
- Melonggarkan Verifikasi untuk 'preferred':
- Jika opsi pembuatan menentukan
userVerification: "preferred", panggilan verifikasi sisi server HARUS dikonfigurasi denganrequireUserVerification: false. Jika tidak, pengautentikasi yang mendaftar tanpa verifikasi pengguna (misalnya, kunci layar dinonaktifkan) akan memicu kegagalan verifikasi server palsu.
- Jika opsi pembuatan menentukan
Logika Sisi Klien
- Membatasi UI saat pemuatan halaman:
- Saat pemuatan halaman, panggil
PublicKeyCredential.getClientCapabilities()dan nonaktifkan tombol "Buat kunci sandi" jikaconditionalGetataupasskeyPlatformAuthenticatortidak tersedia.
- Saat pemuatan halaman, panggil
- Panggil pembuatan & Serialisasi: Dekode opsi server dengan
PublicKeyCredential.parseCreationOptionsFromJSON()dan teruskan konfigurasi yang dihasilkan kenavigator.credentials.create().- Panggil
credential.toJSON()untuk mengenkodeAuthenticatorAttestationResponsemenjadi objek yang dapat diserialisasi JSON dan valid sebelum mengambil endpoint verifikasi.
- Panggil
- Menangani Pengecualian WebAuthn:
InvalidStateError: Kunci sandi yang cocok sudah ada (dicocokkan olehexcludeCredentials).NotAllowedError: Pengguna membatalkan atau kehabisan waktu dialog kunci sandi autentikasi.AbortError: Operasi telah dibatalkan.SecurityError: Error asal aman (HTTPS) atau ketidakcocokan RP ID (masalah konfigurasi).
- Pemisahan Try/Catch untuk Signal API:
- Gabungkan panggilan
fetch()verifikasi server dalam blok try/catch. PanggilsignalUnknownCredential()saat pengambilan verifikasi server gagal (statusresponse.ok === falseatau jaringan menampilkan error).
- Gabungkan panggilan
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";
async function registerPasskey(isPromotion = false) {
// Verify passkey capability and conditional UI are available
const capabilities = await PublicKeyCredential.getClientCapabilities();
if (
!capabilities.passkeyPlatformAuthenticator ||
!capabilities.conditionalGet
) {
// Hide "Create passkey" buttons and fall back to password flows instead
showStandardPasswordFallbackUI();
return;
}
const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
const publicKey =
PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);
let credential;
try {
// passkey prompt execution
credential = await navigator.credentials.create({ publicKey });
} catch (err) {
if (err.name === "InvalidStateError") {
console.log("A passkey already exists for this account.");
alert("A passkey already exists for this account.");
} else if (err.name === "SecurityError") {
console.error("Configuration RP ID or Secure Context error.");
alert("Configuration RP ID or Secure Context error.");
} else if (err.name === "NotAllowedError") {
console.log("User cancelled the passkey dialog.");
} else if (err.name === "AbortError") {
console.log("The creation operation has been aborted.");
}
return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
}
// Server Verification phase (Segregated Try/Catch)
let encodedResponse = credential.toJSON();
try {
const response = await registerVerifyFetch(encodedResponse);
if (!response.ok) {
// Server verification failed to verify/authenticate the credential (orphaned)
await PublicKeyCredential.signalUnknownCredential({
rpId, // RP ID must match the one defined on the server
credentialId: encodedResponse.id, // Base64URL-encoded credential ID
});
}
} catch (serverErr) {
console.error("Server verification network failure:", serverErr);
await publickeycredential.signalunknowncredential({
rpId, // RP ID must match the one defined on the server
credentialid: encodedresponse.id, // base64url-encoded credential id
});
}
}
Strategi Penggantian
Penggantian Sinkronisasi Signal API
WebAuthn Signal API (webauthn-signals) adalah pengoptimalan progresif yang digunakan untuk menjaga pengelola sandi tetap sinkron dengan status kredensial server.
- Pengalaman Penggantian: Jika
PublicKeyCredential.signalUnknownCredentialtidak didukung oleh browser, panggilan HARUS dilewati dengan aman melalui gerbang deteksi fitur (if (PublicKeyCredential.signalUnknownCredential)), dan verifikasi sisi server hanya mencatat kegagalan tanpa memicu pembaruan pengelola.
Penggantian Serialisasi JSON yang Mudah
Selalu instal 'webauthn-polyfills' dan impor dalam konteks.
Pertimbangkan bahwa selama PublicKeyCredential didukung, PublicKeyCredential.parseCreationOptionsFromJSON dan PublicKeyCredential.prototype.toJSON juga didukung.
import 'webauthn-polyfills';