Autentikasi Kunci Sandi

Panduan ini menjelaskan cara menerapkan autentikasi pengguna yang kembali menggunakan kredensial yang dapat ditemukan, baik melalui pemicu tombol eksplisit maupun saran isi otomatis browser yang lancar (UI Bersyarat).

Sisi Server

Pembuatan Opsi

Buat endpoint yang menghasilkan parameter permintaan WebAuthn menggunakan library yang telah diperiksa sesuai standar.

  1. Gunakan RP ID yang telah ditetapkan: Gunakan RP ID yang tepat dan telah ditetapkan sebagai string konstan.
  2. Buat verifikasi: Buat buffer acak dengan entropi tinggi dan aman secara kriptografis, simpan dengan aman di sesi pengguna, dan lakukan enkode sebagai Base64URL.
  3. Pemetaan Discoverable Credentials: Tentukan array kosong [] untuk allowCredentials. Tindakan ini akan meminta kredensial yang dapat ditemukan, yang berarti pengguna tidak perlu memasukkan nama pengguna terlebih dahulu; penyedia kunci sandi akan menampilkan akun yang tersedia.
  4. User Verification level: Tetapkan userVerification: "preferred" (atau "required" jika diamanatkan secara eksplisit oleh kebijakan kepatuhan perusahaan).
    • Tingkat batasan userVerification yang diminta HARUS dipertahankan di dalam data sesi server di endpoint opsi, bukan diteruskan kembali dari klien melalui string kueri. Hal ini memungkinkan endpoint verifikasi menerapkan batasan pencocokan yang ketat dengan aman tanpa risiko manipulasi klien.
// Options generation example (discoverable flow)
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
  rpId: "example.com",
  allowCredentials: [], // Request discoverable passkeys
  userVerification: "preferred",
};

// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";

Endpoint Verifikasi

Verifikasi pernyataan yang ditampilkan oleh klien dengan aman untuk mengautentikasi pengguna:

  1. Memvalidasi verifikasi sesi: Terapkan pencocokan verifikasi yang ketat antara respons klien dan verifikasi yang diharapkan yang disimpan dalam sesi.
  2. Terapkan Preferensi UV:
    • Izinkan pengautentikasi tanpa UV (misalnya, kunci layar pengautentikasi dinonaktifkan) jika expectedUserVerification sesi meminta "preferred", dengan meneruskan requireUserVerification: false ke library verifikasi sisi server Anda. Jika diminta "required", terapkan entri biometrik/PIN secara ketat.
  3. Error Server Bersih 404: Jika ID kredensial yang ditampilkan oleh klien tidak ditemukan dalam database, tampilkan error HTTP 404 eksplisit sehingga klien dapat memicu Signal API.

Logika Sisi Klien

Anotasi Formulir HTML

Beri anotasi pada input nama pengguna dan sandi Anda untuk memanfaatkan UI Bersyarat secara native. Token pelengkapan otomatis menggabungkan parameter spesifikasi webauthn, dan fokus otomatis langsung memicu pop-up isi otomatis browser saat input difokuskan.

<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
  <input
    type="text"
    name="username"
    autocomplete="username webauthn"
    autofocus
    data-testid="username-field"
  />
  <input type="password" name="password" autocomplete="current-password" />
  <button type="submit">Sign in</button>
</form>

Alur Tombol Eksplisit

Memicu autentikasi kunci sandi saat pengguna mengklik tombol "Login dengan kunci sandi". Batalkan panggilan isi otomatis formulir (Conditional Get) yang sedang berlangsung sebelum memanggil dialog kunci sandi.

Alur Mediasi Bersyarat (Isi Otomatis Formulir)

Aktifkan saran isi otomatis formulir saat pemuatan halaman untuk menawarkan autentikasi kunci sandi secara native saat pengguna berfokus pada kolom login:

  1. Deteksi fitur: Panggil PublicKeyCredential.getClientCapabilities() saat pemuatan halaman dan lewati login dengan kunci sandi jika conditionalGet tidak tersedia.
  2. Opsi dekode: Dekode objek JSON kredensial yang diambil dengan PublicKeyCredential.parseRequestOptionsFromJSON().
  3. Memanggil Conditional Get: Panggil navigator.credentials.get() dengan mediation: "conditional" dan teruskan sinyal AbortController. Tindakan ini akan mendaftarkan isi otomatis secara otomatis tanpa merender dialog kunci sandi.
  4. Pemisahan Pengecualian Try/Catch: Gabungkan panggilan navigator.credentials.get dalam blok try/catch:
    • NotAllowedError: Pengguna membatalkan atau kehabisan waktu saat merespons perintah login kunci sandi.
    • AbortError: Permintaan autentikasi dibatalkan secara terprogram.
  5. Call Signal API: Gabungkan panggilan fetch() verifikasi server dalam blok try/catch:
    • Tampilkan pesan error agar pengguna memahami masalah yang terjadi.
    • Panggil signalUnknownCredential() HANYA saat server secara eksplisit merespons dengan status HTTP 404 (Kredensial tidak ditemukan) dan pengguna tidak diautentikasi.
    • Parameter credentialId yang diteruskan ke signalUnknownCredential() HARUS berupa string ID kredensial yang dienkode Base64URL (misalnya, encoded.id), BUKAN objek ArrayBuffer mentah credential.rawId.
  6. Mengenkode respons: Enkode kredensial AuthenticatorAssertionResponse dengan .toJSON() sebelum mengirimkannya ke server untuk verifikasi.
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";

let autofillAbortController = new AbortController();

async function initializeConditionalAutofill() {
  // Feature detect Conditional Get autofill support
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true) {
    const loginOptionsJSON = await optionsFetch();
    const publicKey =
      PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

    try {
      // Initiate Conditional UI form autofill suggestions
      const credential = await navigator.credentials.get({
        publicKey,
        signal: autofillAbortController.signal,
        mediation: "conditional",
      });

      // Segregated verification fetch
      const encoded = credential.toJSON();
      const response = await loginVerifyFetch(encoded);
      if (!response.ok && response.status === 404) {
        // Note: this code path runs pre-authentication, satisfying the unauth precondition
        if (PublicKeyCredential.signalUnknownCredential) {
          await PublicKeyCredential.signalUnknownCredential({
            rpId, // RP ID must match the one defined on the server
            credentialId: encoded.id,
          });
        }
      }
    } catch (err) {
      // Silently swallow expected client WebAuthn exceptions
      if (["NotAllowedError", "AbortError"].includes(err.name)) {
        return;
      }
      console.error("Unexpected conditional get error:", err);
    }
  }
}

async function triggerButtonAuthentication() {
  // Abort any pending Conditional Get call to prevent passkey prompt collisions
  autofillAbortController.abort();
  autofillAbortController = new AbortController(); // Reset controller for next triggers

  const loginOptionsJSON = await optionsFetch();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

  let credential;
  try {
    // Passkey explicit prompt trigger
    credential = await navigator.credentials.get({
      publicKey,
      signal: autofillAbortController.signal,
    });
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled passkey login.");
    } else if (err.name === "AbortError") {
      console.log("The authentication operation was aborted.");
    }
    // Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
    initializeConditionalAutofill();
    return; // Safe exit
  }

  // Segregated verification try/catch (HTTP 404 trigger)
  const encoded = credential.toJSON();
  try {
    const response = await loginVerifyFetch(encoded);
    if (!response.ok && response.status === 404) {
      // Note: this code path runs pre-authentication, satisfying the unauth precondition
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encoded.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Verification request error:", serverErr);
  }
}

// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);

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

WebAuthn Signal API (webauthn-signals) adalah pengoptimalan progresif yang digunakan untuk menjaga pengelola sandi tetap sinkron dengan status kredensial server.

  • Pengalaman Pengganti: Dibatasi melalui if (PublicKeyCredential.signalUnknownCredential). Jika tidak didukung, sinkronisasi verifikasi latar belakang akan dilewati dengan baik tanpa menampilkan pengecualian browser.

Penggantian Serialisasi JSON yang Mudah

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

import 'webauthn-polyfills';