Passkey (WebAuthn) ile Parolasız Giriş: Neden ve Nasıl?
Parolasız giriş dünyasında Passkey teknolojisi (WebAuthn/FIDO2) hızla standart hâline geliyor. Kimlik avı direnci, cihazlar arası senkronizasyon ve hızlı kullanıcı deneyimi sayesinde klasik parolaların yerini alıyor. Bu yazıda, Node.js ve @simplewebauthn kütüphanesini kullanarak bir web uygulamasına Passkey tabanlı kimlik doğrulamayı nasıl entegre edeceğinizi adım adım anlatıyorum. Rehber, modern tarayıcılar (Chrome, Safari, Edge) ve platformlar (iOS, Android, macOS, Windows) ile uyumludur.
Ön Koşullar ve Genel Mimari
Başlamadan önce Node.js 18+ sürümü, HTTPS ile çalışan bir alan adı (localhost için self-signed sertifika), temel Express.js bilgisi ve veri saklamak için bir veritabanı (ör. PostgreSQL, MongoDB veya basit bir bellek deposu) gerekli. Mimari olarak iki akış vardır: Kayıt (Registration) ve Giriş (Authentication). Kayıtta sunucu, kullanıcıya bir challenge üretir ve tarayıcı navigator.credentials.create() ile güvenli bir anahtar çifti oluşturur. Girişte ise sunucu yeni bir challenge üretir ve tarayıcı navigator.credentials.get() ile imza üretip doğrulatır.
Proje Kurulumu
mkdir passkey-demo && cd passkey-demo komutlarıyla klasörü oluşturun. Ardından npm init -y ve npm i express @simplewebauthn/server cors cookie-session komutlarını çalıştırın. Geliştirme için npm i -D typescript ts-node @types/express @types/cookie-session ekleyebilirsiniz. HTTPS için bir ters proxy ya da self-signed sertifika kullanın; WebAuthn çoğu senaryoda güvenli köken (https) ister.
Sunucu Tarafı: Temel Ayarlar
Express uygulamasında kök alan adınızı rpID olarak tanımlayın (ör. example.com). origin değeri tam protokol ve alan adını içermeli (ör. https://example.com). Kullanıcı oturumunda veya Redis gibi bir depoda challenge saklayın.
import express from 'express';
import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server';
import session from 'cookie-session';
const app = express();
app.use(express.json());
app.use(session({ name: 'sess', keys: ['secret'], maxAge: 600000 }));
const rpID = 'example.com';
const origin = 'https://example.com';
Kayıt Akışı (Registration)
1) Options uç noktası: Kullanıcı kayıt olurken sunucu challenge üretir ve istemciye gönderir. Kullanıcı tanımlayıcısını (user.id) kalıcı bir değerden oluşturun.
app.post('/register/options', async (req, res) => {
const { username, displayName } = req.body;
const userId = 'user-' + username; // Örnek amaçlı
const options = await generateRegistrationOptions({
rpName: 'Passkey Demo',
rpID,
userID: userId,
userName: username,
userDisplayName: displayName || username,
attestationType: 'none',
});
req.session.challenge = options.challenge;
res.json(options);
});
2) Doğrulama uç noktası: İstemci, navigator.credentials.create() sonucunu bu uç noktaya gönderir. Sunucu doğrular, credential’ı veritabanına kaydeder.
app.post('/register/verify', async (req, res) => {
const body = req.body; // client response
const expectedChallenge = req.session.challenge;
const verification = await verifyRegistrationResponse({
response: body,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
});
if (!verification.verified) return res.status(400).json({ ok: false });
// credential kaydet: id, publicKey, counter, transports
res.json({ ok: true });
});
Giriş Akışı (Authentication)
1) Options uç noktası: Sunucu, kullanıcıya bağlı mevcut credential’lara göre allowCredentials ile bir challenge üretir.
app.post('/login/options', async (req, res) => {
const { username } = req.body;
const userCreds = await loadUserCredentials(username); // DB'den çekin
const options = await generateAuthenticationOptions({
rpID,
allowCredentials: userCreds.map(c => ({ id: c.id, type: 'public-key' })),
});
req.session.challenge = options.challenge;
res.json(options);
});
2) Doğrulama uç noktası: İstemciden gelen imzayı doğrulayın, sayaç değerini güncelleyin ve oturumu başlatın.
app.post('/login/verify', async (req, res) => {
const body = req.body;
const expectedChallenge = req.session.challenge;
const user = await findUserByCredentialId(body.rawId);
const verification = await verifyAuthenticationResponse({
response: body,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
authenticator: user.authenticator, // publicKey & counter
});
if (!verification.verified) return res.status(401).json({ ok: false });
// counter güncelle, session başlat
res.json({ ok: true });
});
İstemci Tarafı: WebAuthn API Kullanımı
Kayıt sırasında sunucudan aldığınız PublicKeyCredentialCreationOptions nesnesini navigator.credentials.create() içine verin. Tarayıcı, platform anahtarı (ör. iCloud Anahtar Zinciri, Google Password Manager) veya güvenlik anahtarı (YubiKey) ile cihaz üzerinde anahtar üretir.
const opts = await fetch('/register/options', { method: 'POST', body: JSON.stringify({ username }) }).then(r => r.json());
const cred = await navigator.credentials.create({ publicKey: opts });
await fetch('/register/verify', { method: 'POST', body: toJSON(cred) });
Girişte navigator.credentials.get() çağrısı yapılır. Sunucunun sağladığı PublicKeyCredentialRequestOptions ile imza üretilir ve doğrulama uç noktasına gönderilir.
const opts = await fetch('/login/options', { method: 'POST', body: JSON.stringify({ username }) }).then(r => r.json());
const assertion = await navigator.credentials.get({ publicKey: opts });
await fetch('/login/verify', { method: 'POST', body: toJSON(assertion) });
Test, Hata Ayıklama ve Uyumluluk
Geliştirmede https zorunludur; aksi takdirde tarayıcı çağrıları reddedebilir. Mobil cihazlarda test için aynı ağda çalışan https bir endpoint kullanın veya tünelleme (ngrok, Cloudflare Tunnel) tercih edin. Safari’de rpID uyumsuzluğu sık görülür; alan adınız ile origin’iniz birebir eşleşsin. Hata mesajlarını ayrıntılarıyla log’layın; özellikle challenge uyuşmazlığı, origin hatası ve RP ID hataları en yaygın sorunlardır.
Güvenlik ve UX İpuçları
- Challenge değerlerini tek kullanımlık ve kısa ömürlü saklayın; oturum veya Redis idealdir.
- Kullanıcı başına birden fazla credential kaydına izin verin; cihaz değişimlerinde deneyimi iyileştirir.
- resident key ve user verification politikalarını ihtiyaca göre ayarlayın; güçlü güvenlik için required tercih edin.
- Geriye dönük uyumluluk için geçici olarak sihirli bağlantı (magic link) veya tek kullanımlık kodları sunabilirsiniz.
- Üretimde anahtar materyalini ve sayaç değerlerini güvenilir bir veritabanında şifreli saklayın; yedeklemeleri planlayın.
Sonuç
Passkey (WebAuthn/FIDO2) ile parolasız giriş, hem güvenliği hem de kullanıcı deneyimini ileriye taşır. Node.js ve @simplewebauthn ile kurulum birkaç uç nokta ve doğru yapılandırma ile tamamlanabilir. Doğru rpID/origin eşleşmesi, güvenli challenge yönetimi ve çoklu credential desteği ile modern, kimlik avına dayanıklı ve hızlı bir giriş akışı elde edersiniz. Ürününüz büyüdükçe; cihaz senkronizasyonu, kurtarma stratejileri ve kurumsal güvenlik anahtarları ile çözümü olgunlaştırabilirsiniz.