WEBAUTHN PASSKEYS AUTHENTICATION SECURITY PASSWORDLESS FIDO2 WEB-SECURITY FRONTEND BACKEND IMPLEMENTATION BEST-PRACTICES DEVELOPER-SECURITY

Membangun Autentikasi Tanpa Kata Sandi dengan WebAuthn: Panduan Praktis Implementasi dari Frontend hingga Backend

⏱️ 16 menit baca
👨‍💻

Membangun Autentikasi Tanpa Kata Sandi dengan WebAuthn: Panduan Praktis Implementasi dari Frontend hingga Backend

1. Pendahuluan

Kata sandi. Siapa di antara kita yang tidak pernah merasa frustrasi dengannya? Lupa kata sandi, harus membuat kombinasi yang rumit, atau khawatir kata sandi kita dicuri karena menggunakan yang sama di banyak situs. Masalah ini bukan hanya merepotkan pengguna, tapi juga menjadi salah satu celah keamanan terbesar bagi aplikasi web. Serangan phishing, credential stuffing, dan man-in-the-middle seringkali berawal dari kelemahan kata sandi.

Untungnya, ada solusi yang datang sebagai “penyelamat”: WebAuthn (Web Authentication API). WebAuthn adalah standar autentikasi web modern yang memungkinkan pengguna untuk masuk ke aplikasi tanpa perlu mengetik kata sandi. Sebagai gantinya, WebAuthn memanfaatkan kriptografi kunci publik dan perangkat autentikasi (seperti sensor sidik jari, pengenalan wajah, atau kunci keamanan fisik) untuk memverifikasi identitas pengguna. Ini adalah fondasi di balik konsep “Passkeys” yang sedang populer.

Artikel ini akan membawa Anda menyelami dunia WebAuthn, mulai dari konsep dasar hingga panduan praktis implementasinya dari sisi frontend (browser) hingga backend (server). Bersiaplah untuk membangun sistem autentikasi yang lebih aman, praktis, dan modern!

2. Memahami Fondasi WebAuthn: Kunci Publik dan Authenticator

Sebelum melangkah ke implementasi, mari kita pahami beberapa konsep kunci di balik WebAuthn.

FIDO2 dan WebAuthn API

WebAuthn adalah bagian inti dari standar FIDO2, sebuah inisiatif dari FIDO Alliance untuk menciptakan autentikasi yang lebih kuat dan tanpa kata sandi di web. WebAuthn adalah API JavaScript yang memungkinkan browser untuk berkomunikasi dengan perangkat autentikasi (disebut Authenticator).

Komponen Utama WebAuthn:

  1. Authenticator: Ini adalah perangkat yang menyimpan kunci kriptografi dan dapat memverifikasi identitas pengguna (misalnya, melalui biometrik atau PIN). Contohnya:
    • Sensor sidik jari di laptop atau ponsel Anda.
    • Kamera untuk pengenalan wajah.
    • Kunci keamanan fisik seperti YubiKey.
    • Modul TPM (Trusted Platform Module) di komputer Anda.
    • Bahkan, di era Passkeys, Authenticator bisa disinkronkan antar perangkat melalui penyedia cloud (Apple iCloud Keychain, Google Password Manager).
  2. Relying Party (RP): Ini adalah server atau aplikasi web Anda yang ingin mengautentikasi pengguna. RP akan berkomunikasi dengan browser pengguna untuk meminta operasi WebAuthn.
  3. Credential: Ini adalah pasangan kunci kriptografi (kunci publik dan kunci privat) yang dibuat oleh Authenticator untuk sebuah akun pengguna di Relying Party tertentu.
    • Kunci Privat (Private Key): Disimpan secara aman di dalam Authenticator dan tidak pernah meninggalkan perangkat tersebut.
    • Kunci Publik (Public Key): Dikirim ke Relying Party (server) dan disimpan bersama informasi akun pengguna. Kunci publik ini akan digunakan oleh server untuk memverifikasi tanda tangan kriptografi yang dibuat oleh kunci privat saat autentikasi.

Analogi: Kunci Fisik dan Gembok Digital 🔑🔒

Bayangkan WebAuthn seperti sistem kunci dan gembok super canggih.

📌 Poin Penting: Kunci privat tidak pernah meninggalkan Authenticator, dan kata sandi tidak pernah dikirim ke server. Ini adalah fondasi keamanannya!

3. Alur Registrasi WebAuthn (Pendaftaran Kunci)

Alur registrasi adalah proses di mana pengguna membuat Credential baru (pasangan kunci publik/privat) dan mendaftarkannya ke server aplikasi Anda.

Alur Kerja Umum:

  1. Pengguna meminta registrasi di aplikasi web Anda (misalnya, klik tombol “Daftar dengan sidik jari”).
  2. Server (Relying Party) menghasilkan “challenge” kriptografi yang unik dan acak. Ini adalah nilai yang harus ditandatangani oleh Authenticator untuk membuktikan kepemilikannya. Server juga menyiapkan opsi lain (misalnya, nama pengguna, ID pengguna, Relying Party ID).
  3. Server mengirimkan challenge dan opsi ini ke frontend (browser).
  4. Frontend memanggil WebAuthn API (navigator.credentials.create()) dengan challenge dan opsi tersebut.
  5. Browser berkomunikasi dengan Authenticator pengguna. Authenticator meminta pengguna untuk memverifikasi identitasnya (misalnya, sentuh sensor sidik jari).
  6. Authenticator membuat pasangan kunci baru (publik/privat) dan menandatangani challenge dengan kunci privatnya.
  7. Authenticator mengirimkan Credential baru (berisi kunci publik, ID Credential, dan tanda tangan challenge) kembali ke browser.
  8. Browser mengirimkan Credential ini ke server.
  9. Server memverifikasi Credential (memastikan challenge ditandatangani dengan benar, berasal dari Relying Party yang benar, dll.) dan menyimpan kunci publik serta ID Credential yang baru dibuat ke database pengguna.

Contoh Kode:

💡 Frontend (JavaScript):

// Asumsi ada library WebAuthn helper, misal @simplewebauthn/browser
import { startRegistration } from '@simplewebauthn/browser';

async function registerWebAuthn(username) {
  try {
    // 1. Dapatkan opsi registrasi dari backend
    const resp = await fetch('/api/register/options', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username }),
    });
    const attestationOptions = await resp.json();

    // 2. Minta browser membuat credential baru
    const attestationResponse = await startRegistration(attestationOptions);

    // 3. Kirim hasil attestation kembali ke backend untuk verifikasi dan penyimpanan
    const verificationResp = await fetch('/api/register/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        username,
        attestationResponse,
      }),
    });
    const verificationResult = await verificationResp.json();

    if (verificationResult.verified) {
      alert('Registrasi WebAuthn berhasil!');
      // Arahkan ke halaman login atau dashboard
    } else {
      alert('Registrasi WebAuthn gagal.');
    }
  } catch (error) {
    console.error('Error saat registrasi WebAuthn:', error);
    alert('Registrasi WebAuthn dibatalkan atau gagal.');
  }
}

// Panggil fungsi saat pengguna ingin mendaftar
// registerWebAuthn('john.doe');

Backend (Node.js - pseudo-code dengan library helper):

// Asumsi ada library WebAuthn helper, misal @simplewebauthn/server
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoUint8Array } from '@simplewebauthn/server/helpers';

// Database dummy untuk menyimpan user dan credentials
const users = {}; // { username: { id: '...', credentials: [] } }

// Endpoint untuk mendapatkan opsi registrasi
app.post('/api/register/options', (req, res) => {
  const { username } = req.body;
  const user = users[username] || { id: isoUint8Array.fromUTF8String(username), username, credentials: [] };
  users[username] = user; // Simpan user baru jika belum ada

  const options = generateRegistrationOptions({
    rpName: 'My Awesome App', // Nama aplikasi Anda
    rpID: 'localhost', // Domain aplikasi Anda (misal: myapp.com)
    userID: user.id,
    userName: user.username,
    // Batasi Authenticator yang diizinkan (opsional)
    attestationType: 'none',
    excludeCredentials: user.credentials.map(cred => ({
      id: cred.id,
      type: 'public-key',
    })),
    authenticatorSelection: {
      authenticatorAttachment: 'platform', // 'platform' (biometrik), 'cross-platform' (kunci fisik)
      userVerification: 'preferred', // 'required', 'preferred', 'discouraged'
    },
    // challenge akan di-generate otomatis
  });

  // Simpan challenge di sesi pengguna atau database sementara untuk verifikasi nanti
  req.session.currentChallenge = options.challenge;

  res.json(options);
});

// Endpoint untuk memverifikasi hasil registrasi
app.post('/api/register/verify', async (req, res) => {
  const { username, attestationResponse } = req.body;
  const user = users[username];

  if (!user || !req.session.currentChallenge) {
    return res.status(400).json({ error: 'Sesi atau user tidak valid' });
  }

  let verification;
  try {
    verification = await verifyRegistrationResponse({
      response: attestationResponse,
      expectedChallenge: req.session.currentChallenge,
      expectedOrigin: 'http://localhost:3000', // Origin aplikasi Anda
      expectedRPID: 'localhost', // RP ID yang sama dengan saat generate options
      requireUserVerification: true, // Pastikan user diverifikasi oleh Authenticator
    });
  } catch (error) {
    console.error('Verifikasi registrasi gagal:', error);
    return res.status(400).json({ error: error.message });
  }

  const { verified, registrationInfo } = verification;

  if (verified && registrationInfo) {
    const { credentialPublicKey, credentialID, counter } = registrationInfo;

    // Simpan credential baru ke database user
    user.credentials.push({
      id: credentialID,
      publicKey: credentialPublicKey,
      counter: counter,
      // Tambahkan info lain seperti device name, dll.
    });
    // Hapus challenge dari sesi
    req.session.currentChallenge = undefined;

    return res.json({ verified: true, message: 'Registrasi berhasil!' });
  }

  res.json({ verified: false, message: 'Registrasi gagal.' });
});

⚠️ Penting: Challenge harus selalu unik dan dihasilkan oleh server untuk setiap permintaan registrasi atau autentikasi. Ini mencegah serangan replay attack.

4. Alur Autentikasi WebAuthn (Verifikasi Kunci)

Setelah pengguna mendaftarkan Credential-nya, mereka bisa menggunakannya untuk masuk.

Alur Kerja Umum:

  1. Pengguna meminta autentikasi (misalnya, klik tombol “Masuk dengan Passkey”).
  2. Server (Relying Party) menghasilkan “challenge” yang unik dan acak, mirip dengan registrasi. Server juga menyiapkan opsi lain, termasuk daftar ID Credential yang terdaftar untuk pengguna tersebut (jika ada).
  3. Server mengirimkan challenge dan opsi ini ke frontend.
  4. Frontend memanggil WebAuthn API (navigator.credentials.get()) dengan challenge dan opsi.
  5. Browser berkomunikasi dengan Authenticator pengguna. Jika ada beberapa Credential yang cocok, browser mungkin meminta pengguna memilih satu. Authenticator meminta pengguna untuk memverifikasi identitasnya.
  6. Authenticator menggunakan kunci privatnya yang sesuai dengan ID Credential yang dipilih untuk menandatangani challenge.
  7. Authenticator mengirimkan “Assertion” (berisi ID Credential, tanda tangan challenge, dan data lain seperti counter) kembali ke browser.
  8. Browser mengirimkan Assertion ini ke server.
  9. Server memverifikasi Assertion:
    • Memastikan tanda tangan valid menggunakan kunci publik yang disimpan di database untuk Credential ID tersebut.
    • Memastikan challenge yang ditandatangani cocok dengan challenge yang dikirim.
    • Memeriksa signature counter untuk mencegah replay attack.
  10. Jika verifikasi berhasil, pengguna dianggap terautentikasi dan server dapat membuat sesi login.

Contoh Kode:

🎯 Frontend (JavaScript):

// Asumsi ada library WebAuthn helper, misal @simplewebauthn/browser
import { startAuthentication } from '@simplewebauthn/browser';

async function loginWebAuthn(username) {
  try {
    // 1. Dapatkan opsi autentikasi dari backend
    const resp = await fetch('/api/login/options', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username }),
    });
    const assertionOptions = await resp.json();

    // 2. Minta browser mendapatkan assertion dari Authenticator
    const assertionResponse = await startAuthentication(assertionOptions);

    // 3. Kirim hasil assertion kembali ke backend untuk verifikasi
    const verificationResp = await fetch('/api/login/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        username,
        assertionResponse,
      }),
    });
    const verificationResult = await verificationResp.json();

    if (verificationResult.verified) {
      alert('Login WebAuthn berhasil!');
      // Arahkan ke dashboard
    } else {
      alert('Login WebAuthn gagal.');
    }
  } catch (error) {
    console.error('Error saat login WebAuthn:', error);
    alert('Login WebAuthn dibatalkan atau gagal.');
  }
}

// Panggil fungsi saat pengguna ingin login
// loginWebAuthn('john.doe');

Backend (Node.js - pseudo-code dengan library helper):

// Asumsi ada library WebAuthn helper, misal @simplewebauthn/server
import {
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from '@simplewebauthn/server';

// Endpoint untuk mendapatkan opsi autentikasi
app.post('/api/login/options', (req, res) => {
  const { username } = req.body;
  const user = users[username]; // Ambil user dari database

  if (!user || user.credentials.length === 0) {
    return res.status(400).json({ error: 'User tidak ditemukan atau belum mendaftar WebAuthn' });
  }

  const options = generateAuthenticationOptions({
    rpID: 'localhost',
    allowCredentials: user.credentials.map(cred => ({
      id: cred.id,
      type: 'public-key',
      transports: ['usb', 'nfc', 'ble', 'internal'], // Opsi transport Authenticator
    })),
    userVerification: 'preferred',
    // challenge akan di-generate otomatis
  });

  // Simpan challenge di sesi pengguna
  req.session.currentChallenge = options.challenge;

  res.json(options);
});

// Endpoint untuk memverifikasi hasil autentikasi
app.post('/api/login/verify', async (req, res) => {
  const { username, assertionResponse } = req.body;
  const user = users[username];

  if (!user || !req.session.currentChallenge) {
    return res.status(400).json({ error: 'Sesi atau user tidak valid' });
  }

  // Cari credential yang digunakan untuk login berdasarkan ID
  const credential = user.credentials.find(cred => cred.id === assertionResponse.id);

  if (!credential) {
    return res.status(400).json({ error: 'Credential tidak ditemukan' });
  }

  let verification;
  try {
    verification = await verifyAuthenticationResponse({
      response: assertionResponse,
      expectedChallenge: req.session.currentChallenge,
      expectedOrigin: 'http://localhost:3000',
      expectedRPID: 'localhost',
      authenticator: credential, // Informasi credential yang disimpan di database
      requireUserVerification: true,
    });
  } catch (error) {
    console.error('Verifikasi autentikasi gagal:', error);
    return res.status(400).json({ error: error.message });
  }

  const { verified, authenticationInfo } = verification;

  if (verified && authenticationInfo) {
    const { newCounter } = authenticationInfo;

    // PENTING: Update counter di database untuk credential ini
    credential.counter = newCounter;
    // Hapus challenge dari sesi
    req.session.currentChallenge = undefined;

    // Buat sesi login untuk pengguna
    req.session.userId = user.id;

    return res.json({ verified: true, message: 'Login berhasil!' });
  }

  res.json({ verified: false, message: 'Login gagal.' });
});

⚠️ Penting: Selalu perbarui signature counter yang disimpan di database Anda setelah verifikasi autentikasi. Ini adalah mekanisme kunci untuk mencegah replay attack (serangan di mana penyerang mencoba menggunakan ulang respons autentikasi yang sudah lewat).

5. Tantangan dan Best Practices Implementasi

Meskipun WebAuthn menawarkan banyak keuntungan, ada beberapa hal yang perlu dipertimbangkan saat implementasi:

Kesalahan Umum:

Praktik Terbaik:

6. Keamanan WebAuthn: Mengapa Lebih Unggul?

WebAuthn secara signifikan meningkatkan keamanan dibandingkan autentikasi berbasis kata sandi tradisional:

Kesimpulan

Autentikasi tanpa kata sandi dengan WebAuthn adalah masa depan keamanan web. Ini bukan hanya tentang kenyamanan pengguna, tetapi juga tentang membangun pertahanan yang jauh lebih kuat terhadap ancaman siber yang terus berkembang. Dengan memahami fondasi kriptografi kunci publik dan mengikuti praktik terbaik implementasi, Anda dapat menyediakan pengalaman autentikasi yang mulus dan aman bagi pengguna aplikasi Anda.

Meskipun implementasinya membutuhkan sedikit penyesuaian dari pola autentikasi tradisional, investasi ini sangat selayak. Mulailah bereksperimen dengan WebAuthn di proyek Anda dan dorong adopsi autentikasi yang lebih aman di ekosistem web!

🔗 Baca Juga