Login Biometrik Tanpa Email (WebAuthn Passkey)
Implementasi autentikasi kriptografi kunci publik berbasis standar FIDO2/WebAuthn yang memungkinkan pengguna masuk hanya dengan sidik jari atau wajah tanpa mengetik email maupun password.
Membaca sekitar 2 menit
Implementasi autentikasi kriptografi kunci publik berbasis standar FIDO2/WebAuthn yang memungkinkan pengguna masuk hanya dengan sidik jari atau wajah tanpa mengetik email maupun password.
Menggunakan @simplewebauthn dengan mode Discoverable Credentials (residentKey: required) sehingga browser langsung menampilkan daftar passkey perangkat dan server mengenali pengguna dari ID kredensial.
Setiap kali login biometrik berhasil, nilai counter di database wajib diperbarui dari authenticator untuk menangkal serangan kloning kredensial (replay attack).
Masalahnya dulu
Meski verifikasi OTP via email sangat aman, membuka aplikasi email setiap kali ingin mencatat pengeluaran harian tentu terasa merepotkan. Di versi awal, fitur WebAuthn sempat dibuat dengan mewajibkan pengguna mengetik alamat email terlebih dahulu agar server bisa mencari daftar credential milik email tersebut. Namun cara itu masih menyisakan friksi mengetik di layar ponsel.
Beralih ke Discoverable Credentials (Resident Key)
Agar pengguna cukup menekan satu tombol 'Masuk dengan Passkey' dan langsung memindai sidik jari atau wajah (Windows Hello, Touch ID, Face ID, atau biometrik Android), implementasi ditingkatkan ke mode Discoverable Credentials. Saat registrasi passkey, opsi residentKey diatur menjadi 'required' dan authenticatorAttachment dikunci ke 'platform'.
Keamanan terbaik adalah yang paling tidak terasa merepotkan: satu sentuhan biometrik menggantikan ketikan email, password, sekaligus tunggu OTP.
Implementasi di ngodingan ini
Saat login dimulai tanpa email, allowCredentials dikirim kosong sehingga browser menampilkan seluruh passkey yang tersimpan di perangkat untuk domain aplikasi. Lalu pada tahap verifikasi akhir, server mengenali siapa penggunanya langsung dari ID kredensial (assertion.id) yang dikembalikan oleh perangkat:
export async function POST(req: NextRequest) {
const challenge = req.cookies.get(CHALLENGE_COOKIE)?.value;
if (!challenge) return fail("Challenge tidak ditemukan atau sudah kedaluwarsa.");
const { assertion } = await req.json();
const cred = await findCredentialById(assertion.id);
if (!cred) return fail("Credential tidak ditemukan.", 404);
const user = await findActiveUserByUuid(cred.uuiduser);
if (!user) return fail("User tidak ditemukan.", 404);
const verification = await verifyAuthenticationResponse({
response: assertion,
expectedChallenge: challenge,
expectedOrigin: ORIGIN,
expectedRPID: RP_ID,
requireUserVerification: true,
credential: {
id: cred.id,
publicKey: Buffer.from(cred.public_key, "base64"),
counter: cred.counter,
transports: ["internal"],
},
});
if (!verification.verified) return fail("Verifikasi passkey gagal.");
await updateCredentialCounter(cred.id, verification.authenticationInfo.newCounter);
// ... terbitkan JWT sesi penuh & hapus cookie challenge
}Jebakan dan detail yang harus dijaga
- Menyimpan challenge kriptografi di dalam cookie httpOnly berumur pendek dan langsung menghapusnya begitu proses registrasi atau autentikasi selesai untuk mencegah penggunaan ulang challenge (replay).
- Mengonversi tipe data kunci publik secara konsisten: @simplewebauthn/server menghasilkan Uint8Array untuk publicKey yang kemudian disimpan sebagai string base64 di database, dan harus dikembalikan menjadi Buffer saat verifikasi.