PASSKEYS WEBAUTHN AUTHENTICATION PASSWORDLESS USER-EXPERIENCE SECURITY FRONTEND BACKEND MODERN-WEB CROSS-DEVICE

Mengoptimalkan Autentikasi Tanpa Kata Sandi: Jurus Rahasia Passkeys dengan Sinkronisasi Lintas Perangkat dan Conditional UI

⏱️ 25 menit baca
👨‍💻

Mengoptimalkan Autentikasi Tanpa Kata Sandi: Jurus Rahasia Passkeys dengan Sinkronisasi Lintas Perangkat dan Conditional UI

1. Pendahuluan

Di era digital yang serba cepat ini, kata sandi telah menjadi beban. Sulit diingat, rentan terhadap serangan phishing, dan seringkali menjadi titik lemah dalam keamanan aplikasi web. Passkeys hadir sebagai revolusi, menawarkan metode autentikasi yang lebih aman, lebih mudah, dan tanpa kata sandi. Namun, Passkeys bukan hanya tentang menghilangkan kata sandi; mereka juga membawa fitur-fitur canggih yang secara fundamental mengubah cara pengguna berinteraksi dengan proses login.

Dua fitur kunci yang membuat Passkeys begitu kuat dan ramah pengguna adalah Sinkronisasi Lintas Perangkat (Cross-Device Sync) dan Conditional UI. Sinkronisasi lintas perangkat memastikan kredensial Anda selalu tersedia di semua perangkat yang terhubung, sementara Conditional UI memungkinkan browser untuk secara cerdas menyarankan Passkeys yang relevan, membuat proses login terasa lebih mulus dan instan.

Artikel ini akan membawa Anda menyelami lebih dalam kedua konsep ini. Kita akan membahas bagaimana Passkeys memanfaatkan kemampuan platform untuk sinkronisasi, dan bagaimana developer dapat mengimplementasikan Conditional UI untuk menciptakan pengalaman autentikasi yang benar-benar modern, aman, dan tanpa gesekan bagi pengguna di aplikasi web Anda.

2. Mengingat Kembali Passkeys dan WebAuthn

Sebelum kita melangkah lebih jauh, mari kita ulas singkat apa itu Passkeys dan hubungannya dengan WebAuthn.

Passkeys adalah kredensial digital yang memungkinkan Anda login ke aplikasi dan situs web tanpa kata sandi. Alih-alih mengingat kombinasi huruf, angka, dan simbol, Anda menggunakan metode autentikasi biometrik (sidik jari, pemindaian wajah) atau PIN perangkat Anda. Passkeys didesain untuk menjadi tahan phishing, kuat terhadap serangan rekayasa sosial, dan mudah digunakan.

Di balik Passkeys, ada standar web terbuka yang disebut Web Authentication (WebAuthn). WebAuthn adalah API JavaScript yang memungkinkan aplikasi web berinteraksi dengan authenticator (perangkat atau software yang menyimpan Passkeys) untuk melakukan pendaftaran (registrasi) dan verifikasi (autentikasi). Setiap kali Anda membuat Passkey, Anda sebenarnya sedang membuat kredensial WebAuthn.

📌 Poin Penting: Passkeys adalah implementasi WebAuthn yang memanfaatkan kemampuan platform (seperti iCloud Keychain di Apple, Google Password Manager di Android/Chrome, atau Microsoft Authenticator di Windows) untuk menyimpan dan menyinkronkan kredensial secara aman.

3. Sinkronisasi Lintas Perangkat: Passkeys Selalu Bersama Anda

Salah satu keunggulan terbesar Passkeys dibandingkan metode autentikasi lain (termasuk kunci keamanan fisik) adalah kemampuannya untuk disinkronkan secara mulus di seluruh perangkat pengguna.

Bagaimana Cara Kerjanya?

Ketika Anda membuat Passkey untuk sebuah situs web atau aplikasi, Passkey tersebut tidak hanya disimpan di perangkat tempat Anda membuatnya. Sebaliknya, Passkey akan disimpan di penyedia Passkey (Passkey provider) yang terkait dengan akun Anda di sistem operasi (misalnya, akun Google Anda, Apple ID Anda). Penyedia Passkey ini kemudian akan secara otomatis menyinkronkan Passkey tersebut ke semua perangkat lain yang terhubung dengan akun yang sama dan mendukung Passkeys.

💡 Contoh Konkret: Jika Anda membuat Passkey untuk aplikasiku.com di iPhone Anda, Passkey itu akan disimpan di iCloud Keychain. Secara otomatis, Passkey yang sama akan tersedia di iPad, MacBook, atau bahkan Apple Watch Anda, asalkan semua perangkat tersebut login dengan Apple ID yang sama. Hal serupa berlaku untuk ekosistem Google dan Microsoft.

Implikasi untuk Developer: Ini adalah berita bagus! Sebagai developer, Anda tidak perlu lagi khawatir tentang pengguna yang kehilangan akses karena perangkatnya hilang atau lupa membawa kunci keamanan fisiknya. Selama pengguna memiliki akses ke salah satu perangkat yang disinkronkan, mereka dapat login. Ini juga mengurangi beban dukungan pelanggan terkait masalah login.

Manfaat Sinkronisasi Lintas Perangkat:

4. Conditional UI: Autentikasi Lebih Mulus dan Instan

Conditional UI adalah fitur WebAuthn yang memungkinkan browser menampilkan antarmuka pengguna (UI) untuk memilih Passkey secara otomatis, tanpa perlu interaksi eksplisit dari pengguna untuk memicu proses autentikasi. Ini membuat proses login terasa lebih cepat dan intuitif.

Apa Itu mediation: 'conditional'?

Biasanya, saat login dengan WebAuthn, Anda perlu mengklik tombol “Login dengan Passkey” atau semacamnya untuk memicu permintaan kredensial. Dengan Conditional UI, Anda dapat menggunakan opsi mediation: 'conditional' dalam permintaan navigator.credentials.get(). Ini memberi tahu browser untuk secara pasif menunggu input dari pengguna (seperti menyentuh sensor sidik jari atau memasukkan PIN) atau untuk secara proaktif menyarankan Passkeys yang tersedia.

🎯 Tujuan Conditional UI: Mengintegrasikan autentikasi Passkey secara lebih erat dengan alur login berbasis nama pengguna/kata sandi tradisional, memberikan pilihan Passkey yang mulus dan cepat sebagai alternatif.

Implementasi di Frontend (HTML dan JavaScript)

Untuk mengaktifkan Conditional UI, ada dua bagian utama yang perlu Anda perhatikan:

  1. Atribut HTML autocomplete="webauthn": Tambahkan atribut ini ke elemen <input type="text"> untuk bidang nama pengguna atau email Anda. Ini adalah sinyal bagi browser bahwa bidang input ini dapat digunakan untuk mengidentifikasi akun pengguna yang berpotensi memiliki Passkey.

    <form id="login-form">
        <label for="username">Nama Pengguna:</label>
        <input type="text" id="username" name="username" autocomplete="username webauthn">
        <button type="submit">Login</button>
    </form>

    Ketika pengguna fokus pada bidang input ini, browser dapat menampilkan UI yang menyarankan Passkey yang tersimpan untuk situs tersebut.

  2. JavaScript WebAuthn API dengan mediation: 'conditional': Di sisi JavaScript, Anda perlu membuat permintaan navigator.credentials.get() dengan opsi mediation: 'conditional'. Permintaan ini akan membuat kredensial yang dapat ditemukan (discoverable credential) tersedia untuk browser.

    async function initConditionalUi() {
        if (!window.PublicKeyCredential) {
            console.warn('WebAuthn tidak didukung di browser ini.');
            return;
        }
    
        try {
            const publicKeyCredentialRequestOptions = {
                challenge: Uint8Array.from('random_string_dari_backend', c => c.charCodeAt(0)), // Tantangan dari backend
                rpId: window.location.hostname, // Domain Anda
                userVerification: 'preferred', // Bisa 'required', 'preferred', atau 'discouraged'
                mediation: 'conditional', // Ini kuncinya!
                // Pastikan allowCredentials kosong atau berisi array kosong
                // jika Anda ingin browser menyarankan semua passkey yang cocok
                allowCredentials: []
            };
    
            // Membuat sebuah AbortController untuk mengelola permintaan secara pasif
            const abortController = new AbortController();
            const signal = abortController.signal;
    
            // Memulai permintaan secara pasif
            const credentialPromise = navigator.credentials.get({
                publicKey: publicKeyCredentialRequestOptions,
                signal: signal
            });
    
            // Jika pengguna memilih Passkey melalui UI browser, promise akan ter-resolve
            // Jika tidak, kita bisa membatalkan permintaan setelah beberapa waktu
            // atau menunggu hingga pengguna berinteraksi dengan form.
    
            // Contoh: Jika ada interaksi form, kita batalkan permintaan pasif ini
            document.getElementById('login-form').addEventListener('submit', async (e) => {
                e.preventDefault();
                abortController.abort(); // Batalkan permintaan conditional UI
    
                // Lanjutkan dengan alur login biasa atau permintaan Passkey eksplisit
                // ...
            });
    
            // Tunggu hingga Passkey dipilih atau permintaan dibatalkan
            const credential = await credentialPromise;
            if (credential) {
                // Passkey dipilih, kirim ke backend untuk verifikasi
                console.log('Passkey dipilih:', credential);
                await sendCredentialToBackend(credential);
            }
        } catch (error) {
            console.error('Error saat inisialisasi Conditional UI:', error);
        }
    }
    
    // Panggil saat halaman dimuat
    initConditionalUi();

    ⚠️ Perhatian:

    • challenge harus dihasilkan secara unik oleh backend untuk setiap permintaan.
    • rpId adalah ID pihak yang mengandalkan (domain Anda).
    • allowCredentials: [] sangat penting untuk mediation: 'conditional' agar browser dapat menyarankan Passkeys yang dapat ditemukan. Jika Anda mengisi allowCredentials dengan ID kredensial spesifik, browser hanya akan mencari kredensial tersebut, membatasi fungsi Conditional UI.

Alur Kerja Conditional UI

  1. Pengguna membuka halaman login.
  2. JavaScript menginisialisasi navigator.credentials.get() dengan mediation: 'conditional'.
  3. Browser secara pasif mendengarkan input pengguna.
  4. Ketika pengguna fokus pada input username dengan autocomplete="webauthn", browser menampilkan saran Passkey (misalnya, pop-up atau daftar di bawah input).
  5. Pengguna memilih Passkey yang disarankan.
  6. Browser meminta autentikasi (sidik jari/wajah/PIN) dari pengguna.
  7. Setelah berhasil, objek PublicKeyCredential dikembalikan ke JavaScript.
  8. Objek ini dikirim ke backend untuk verifikasi.
  9. Backend memverifikasi kredensial dan melakukan login pengguna.

5. Studi Kasus: Membangun Alur Login dengan Conditional UI

Mari kita lihat contoh yang lebih terstruktur untuk alur login menggunakan Conditional UI.

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login dengan Passkey Conditional UI</title>
    <style>
        body { font-family: sans-serif; margin: 2em; }
        form { display: flex; flex-direction: column; gap: 1em; max-width: 300px; }
        input, button { padding: 0.8em; border-radius: 4px; border: 1px solid #ccc; }
        button { background-color: #007bff; color: white; border: none; cursor: pointer; }
        button:hover { background-color: #0056b3; }
        #message { margin-top: 1em; padding: 0.8em; border-radius: 4px; }
        .success { background-color: #d4edda; color: #155724; border-color: #c3e6cb; }
        .error { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
    </style>
</head>
<body>
    <h1>Login ke Aplikasi Saya</h1>

    <form id="login-form">
        <label for="username">Nama Pengguna:</label>
        <input type="text" id="username" name="username" autocomplete="username webauthn">

        <label for="password">Kata Sandi (opsional):</label>
        <input type="password" id="password" name="password" autocomplete="current-password">
        
        <button type="submit">Login</button>
        <p>Atau gunakan Passkey dengan fokus pada input nama pengguna!</p>
    </form>

    <div id="message"></div>

    <script>
        const usernameInput = document.getElementById('username');
        const passwordInput = document.getElementById('password');
        const loginForm = document.getElementById('login-form');
        const messageDiv = document.getElementById('message');

        let abortController = null; // Untuk mengelola conditional UI request

        function showMessage(msg, type) {
            messageDiv.textContent = msg;
            messageDiv.className = `message ${type}`;
        }

        async function registerPasskey() {
            // Ini adalah alur untuk mendaftar Passkey baru
            // Dalam contoh ini, kita fokus pada login, jadi kita akan membuat ini sederhana.
            // Anda akan memanggil API backend untuk mendapatkan options pendaftaran.
            console.log("Mendaftar Passkey...");
            // ... implementasi pendaftaran passkey ...
        }

        async function loginWithPasskey(credential) {
            try {
                // 1. Mengirim credential ke backend untuk verifikasi
                const response = await fetch('/api/login-passkey', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        id: credential.id,
                        rawId: Array.from(new Uint8Array(credential.rawId)),
                        response: {
                            clientDataJSON: Array.from(new Uint8Array(credential.response.clientDataJSON)),
                            authenticatorData: Array.from(new Uint8Array(credential.response.authenticatorData)),
                            signature: Array.from(new Uint8Array(credential.response.signature)),
                            userHandle: credential.response.userHandle ? Array.from(new Uint8Array(credential.response.userHandle)) : null
                        },
                        type: credential.type,
                        clientExtensionResults: credential.clientExtensionResults
                    })
                });

                const data = await response.json();

                if (response.ok) {
                    showMessage(`Login berhasil! Selamat datang, ${data.username}!`, 'success');
                    // Redirect atau update UI setelah login
                } else {
                    showMessage(`Login gagal: ${data.message || 'Verifikasi Passkey gagal.'}`, 'error');
                }
            } catch (error) {
                console.error('Error saat login dengan Passkey:', error);
                showMessage('Terjadi kesalahan saat mencoba login dengan Passkey.', 'error');
            }
        }

        async function initConditionalUi() {
            if (!window.PublicKeyCredential) {
                showMessage('WebAuthn (Passkeys) tidak didukung di browser ini.', 'error');
                return;
            }

            // Batalkan permintaan sebelumnya jika ada
            if (abortController) {
                abortController.abort();
            }
            abortController = new AbortController();
            const signal = abortController.signal;

            try {
                // Dapatkan tantangan (challenge) dari backend
                // Untuk contoh ini, kita mock saja. Di produksi, ini harus dari server.
                const challengeResponse = await fetch('/api/get-passkey-challenge');
                const { challenge } = await challengeResponse.json();

                const publicKeyCredentialRequestOptions = {
                    challenge: Uint8Array.from(atob(challenge), c => c.charCodeAt(0)), // Decode Base64 challenge
                    rpId: window.location.hostname,
                    userVerification: 'preferred',
                    mediation: 'conditional',
                    allowCredentials: [] // Penting agar browser bisa menyarankan
                };

                // Memulai permintaan secara pasif
                const credential = await navigator.credentials.get({
                    publicKey: publicKeyCredentialRequestOptions,
                    signal: signal
                });

                if (credential) {
                    console.log('Passkey dipilih melalui Conditional UI:', credential);
                    await loginWithPasskey(credential);
                }
            } catch (error) {
                // Cek jika error adalah karena pembatalan (abort)
                if (error.name === 'AbortError') {
                    console.log('Conditional UI request dibatalkan.');
                } else {
                    console.warn('Gagal menginisialisasi Conditional UI (mungkin tidak ada Passkey atau user membatalkan):', error);
                }
            }
        }

        // Panggil initConditionalUi setiap kali pengguna fokus ke input username
        usernameInput.addEventListener('focus', initConditionalUi);

        loginForm.addEventListener('submit', async (e) => {
            e.preventDefault();
            // Batalkan Conditional UI jika pengguna memilih submit manual
            if (abortController) {
                abortController.abort();
            }

            // Implementasi login username/password tradisional
            const username = usernameInput.value;
            const password = passwordInput.value;

            if (username && password) {
                try {
                    const response = await fetch('/api/login-traditional', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ username, password })
                    });
                    const data = await response.json();
                    if (response.ok) {
                        showMessage(`Login berhasil dengan kata sandi! Selamat datang, ${data.username}!`, 'success');
                    } else {
                        showMessage(`Login gagal: ${data.message}`, 'error');
                    }
                } catch (error) {
                    showMessage('Terjadi kesalahan saat login tradisional.', 'error');
                }
            } else {
                showMessage('Nama pengguna dan kata sandi tidak boleh kosong (jika tidak pakai Passkey).', 'error');
            }
        });

        // Contoh endpoint backend (mockup)
        // Di aplikasi nyata, Anda akan memiliki server-side code untuk ini.
        // /api/get-passkey-challenge akan mengembalikan { challenge: "BASE64_ENCODED_CHALLENGE" }
        // /api/login-passkey akan memverifikasi credential dan mengembalikan { username: "..." }
        // /api/login-traditional akan memverifikasi username/password dan mengembalikan { username: "..." }
    </script>
</body>
</html>

Penjelasan Backend (Konseptual)

Di sisi backend, Anda perlu memiliki dua endpoint utama:

  1. /api/get-passkey-challenge:

    • Endpoint ini bertanggung jawab untuk menghasilkan challenge unik yang aman.
    • challenge ini harus berupa byte array acak yang kuat secara kriptografis dan dienkode (misalnya, Base64 URL-safe) sebelum dikirim ke frontend.
    • Backend harus menyimpan challenge ini di sesi pengguna untuk verifikasi nanti.
  2. /api/login-passkey:

    • Endpoint ini menerima objek PublicKeyCredential yang dikirim dari frontend.
    • Backend akan menggunakan library WebAuthn (seperti @simplewebauthn/server di Node.js, web-authn/webauthn di PHP Laravel, atau serupa di bahasa lain) untuk memverifikasi credential terhadap challenge yang tersimpan.
    • Verifikasi mencakup memeriksa tanda tangan kriptografis, rpId, origin, dan parameter lainnya.
    • Setelah verifikasi berhasil, backend mengidentifikasi pengguna berdasarkan credential.id dan membuat sesi login.

6. Best Practices dan Pertimbangan

Kesimpulan

Passkeys, dengan dukungan sinkronisasi lintas perangkat dan Conditional UI, adalah lompatan besar menuju masa depan autentikasi yang tanpa kata sandi, lebih aman, dan lebih mudah. Sebagai developer, memahami dan mengimplementasikan fitur-fitur ini adalah kunci untuk membangun aplikasi