WebAuthn ve Passkey Nedir? Neden Önemli?
Parolalar güvenliğin en zayıf halkası olmaya devam ediyor: sızıntılar, kimlik avı, parola tekrar kullanımı ve sosyal mühendislik riski her zamankinden yüksek. WebAuthn (W3C) ve FIDO2 standardı, kullanıcıların cihazlarını (telefon, güvenlik anahtarı, bilgisayar) bir Passkey olarak kullanmasını sağlayarak parolasız, kimlik avına dayanıklı ve kullanıcı dostu bir kimlik doğrulama sunar. Bu yazıda, modern bir web uygulamasında WebAuthn ve Passkey ile parolasız kimlik doğrulamayı adım adım nasıl kuracağınızı anlatıyorum.
Mimari ve Akışın Özeti
WebAuthn iki temel akışa dayanır: kayıt (registration) ve giriş (authentication). Kayıt sırasında sunucu bir “challenge” üretir, tarayıcı ve cihaz bu challenge’ı cihaza özgü özel anahtarla imzalar ve sunucu bu kaydı güvenli şekilde saklar. Girişte yine challenge üretilir ve kullanıcı cihazıyla imza atar; sunucu saklanan genel anahtar ile imzayı doğrular. Bu süreçte kullanıcı deneyimi Touch ID/Face ID, Windows Hello veya bir güvenlik anahtarı dokunuşu kadar basittir.
Önkoşullar ve Dikkat Edilecekler
- HTTPS zorunludur (localhost hariç).
- RP ID (Relying Party ID) genellikle alan adınızdır (ör. example.com).
- Passkey senkronizasyonu için kullanıcılar Apple/Google/Microsoft hesabına giriş yapmış olabilir; güvenlik anahtarı (roaming authenticator) ile de çalışır.
- Tarayıcı desteği modern tarayıcılarda çok iyi; mobilde de yaygın.
Sunucu Tarafı: Challenge Üretimi ve Doğrulama
Sunucuda iki uç nokta gerekir: kayıt için “options” ve “verify”, giriş için “options” ve “verify”. Node.js dünyasında @simplewebauthn/server popüler ve güvendiğim bir çözümdür. Aşağıda yalın bir örnek görebilirsiniz.
// npm i @simplewebauthn/server express cors
import express from 'express';
import cors from 'cors';
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from '@simplewebauthn/server';
const app = express();
app.use(cors());
app.use(express.json());
const rpID = 'example.com';
const rpName = 'Örnek Uygulama';
const db = { users: new Map() }; // { userId: { id, username, credentials: [] } }
app.post('/webauthn/register/options', (req, res) => {
const { userId, username } = req.body;
const user = db.users.get(userId) || { id: userId, username, credentials: [] };
const opts = generateRegistrationOptions({
rpID,
rpName,
userID: user.id,
userName: user.username,
attestationType: 'none',
excludeCredentials: user.credentials.map(c => ({ id: c.credentialID, type: 'public-key' })),
authenticatorSelection: { residentKey: 'required', userVerification: 'required' },
});
user.currentChallenge = opts.challenge;
db.users.set(userId, user);
res.json(opts);
});
app.post('/webauthn/register/verify', async (req, res) => {
const { userId, attResp } = req.body;
const user = db.users.get(userId);
const verification = await verifyRegistrationResponse({
response: attResp,
expectedChallenge: user.currentChallenge,
expectedRPID: rpID,
expectedOrigin: 'https://example.com',
});
if (verification.verified) {
const { credentialPublicKey, credentialID, counter } = verification.registrationInfo;
user.credentials.push({ credentialID, credentialPublicKey, counter });
db.users.set(userId, user);
}
res.json({ ok: verification.verified });
});
app.post('/webauthn/auth/options', (req, res) => {
const { userId } = req.body;
const user = db.users.get(userId);
const opts = generateAuthenticationOptions({
rpID,
userVerification: 'required',
allowCredentials: user.credentials.map(c => ({ id: c.credentialID, type: 'public-key' })),
});
user.currentChallenge = opts.challenge;
db.users.set(userId, user);
res.json(opts);
});
app.post('/webauthn/auth/verify', async (req, res) => {
const { userId, authResp } = req.body;
const user = db.users.get(userId);
const verification = await verifyAuthenticationResponse({
response: authResp,
expectedChallenge: user.currentChallenge,
expectedRPID: rpID,
expectedOrigin: 'https://example.com',
authenticator: user.credentials.find(c => c.credentialID.equals(Buffer.from(authResp.rawId, 'base64url'))),
});
res.json({ ok: verification.verified });
});
app.listen(3000);
Ön Uç: Tarayıcıda Kayıt ve Giriş
Tarayıcı API’si navigator.credentials üzerinden çalışır. Dönüş tipleri ArrayBuffer içerdiğinden Base64URL dönüşümleri gerekir. Basitlik için @github/webauthn-json veya @simplewebauthn/browser kullanabilirsiniz.
// npm i @simplewebauthn/browser
import {
create as webauthnCreate,
get as webauthnGet,
} from '@simplewebauthn/browser';
async function registerPasskey(userId, username) {
const opts = await fetch('/webauthn/register/options', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ userId, username }),
}).then(r => r.json());
const attResp = await webauthnCreate({ publicKey: opts });
const verify = await fetch('/webauthn/register/verify', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ userId, attResp }),
}).then(r => r.json());
return verify.ok;
}
async function loginWithPasskey(userId) {
const opts = await fetch('/webauthn/auth/options', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ userId }),
}).then(r => r.json());
const authResp = await webauthnGet({ publicKey: opts });
const verify = await fetch('/webauthn/auth/verify', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ userId, authResp }),
}).then(r => r.json());
return verify.ok;
}
Güvenlik ve UX İpuçları
- Kullanıcı doğrulamasını “required” tutun; cihaz biyometri veya PIN ister.
- Passkey’i birincil giriş yöntemi yapın, parolayı opsiyonel geri dönüş kanalı olarak değerlendirin.
- Aynı kullanıcı için birden fazla autentikatör kaydına izin verin (telefon, laptop, güvenlik anahtarı).
- Cihaz değiştirmede QR ile “cihazlar arası” passkey oturumu deneyimini etkinleştirin (Chrome/Safari destekler).
- Kaydı tamamlamadan önce challenge ve RP ID eşleşmelerini mutlaka doğrulayın.
Hata Ayıklama: Sık Karşılaşılan Sorunlar
- origin ve rpID eşleşmiyor: HTTPS alan adınız ile RP ID aynı olmalı (subdomain farklarına dikkat).
- Base64URL dönüşümleri eksik: webauthn-json veya simplewebauthn/browser kullanın.
- localhost testi: http://localhost için istisna var, ancak 127.0.0.1 farklı kabul edilebilir; tutarlı kullanın.
- allowCredentials boş: Kullanıcının kayıtlı credential listesini girişte doğru döndürdüğünüzden emin olun.
Sonuç
WebAuthn ve Passkey ile parolasız kimlik doğrulama artık niş bir teknoloji değil; büyük tarayıcılar ve platformlarca olgun şekilde destekleniyor. Doğru kütüphanelerle kurulum süreci şaşırtıcı derecede kısa, güvenlik kazanımı ise çok büyük. Ürününüzde parolaları azaltmak ve kimlik avına dayanıklı, modern bir deneyim sunmak istiyorsanız, bugün küçük bir POC ile başlayın ve kullanıcı tabanınıza kademeli olarak yayınlayın.