Passkey nedir ve neden şimdi?
Son yıllarda şifre sızıntıları, kimlik avı saldırıları ve kullanıcı deneyimi sorunları; geliştiricileri “şifresiz giriş” modellerine yöneltti. Passkey, FIDO2 ve WebAuthn standartlarını temel alarak kullanıcıların cihazlarında biyometrik (Face ID, Touch ID, Windows Hello) veya donanımsal güvenlik anahtarıyla güvenli, hızlı ve kimlik avına dayanıklı oturum açmalarını sağlar. Chrome, Safari, Edge ve Firefox’un güncel sürümleri ile iOS, Android ve Windows ekosistemleri passkey’i yerel olarak destekliyor.
Mimariye hızlı bakış
Passkey, sunucu (Relying Party) ve tarayıcı/cihaz (Authenticator) arasında challenge temelli bir akış kullanır. Sunucu, kayıt veya giriş için benzersiz bir challenge üretir; istemci tarafında navigator.credentials.create() (kayıt) veya navigator.credentials.get() (giriş) çağrısı yapılır ve oluşan kriptografik yanıt sunucuya geri gönderilir. Sunucu, bu yanıtı doğrular ve oturum açılır.
Önkoşullar ve en iyi uygulamalar
- Üretimde HTTPS zorunludur (localhost geliştirmede istisna).
- RP ID alan adınız olmalı (örn. example.com). Alt alan adılarında doğrulama için RP ID’yi üst alan adına ayarlayın.
- Kullanıcı kimlikleri ve cihazdan dönen credential ID’leri veritabanında güvenle saklanmalı.
- Kimlik avına dayanıklılık için cross-origin isteklerden kaçının; Origin ve Relying Party ID tutarlı olmalı.
Adım adım: Basit bir Node.js/Express ve WebAuthn entegrasyonu
Örnek, sunucuda @simplewebauthn/server kütüphanesini kullanır. İstemci tarafında ise tarayıcı API’lerini çağırıyoruz. Bu iskelet, tek başına üretime hazır değildir; amaç kavramı göstermektir.
1) Sunucuyu hazırlayın
// server.js
import express from 'express';
import cors from 'cors';
import session from 'express-session';
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from '@simplewebauthn/server';
const app = express();
app.use(cors({ origin: 'https://app.example.com', credentials: true }));
app.use(express.json());
app.use(session({ secret: 'change-me', resave: false, saveUninitialized: true }));
// Demo bellek içi depolar
const users = new Map(); // userId -> { id, username, credentials: [] }
const rpID = 'example.com';
const rpName = 'Örnek Uygulama';
// Kayıt: seçenek üret
app.post('/webauthn/register/options', (req, res) => {
const { username, userId } = req.body;
let user = users.get(userId);
if (!user) {
user = { id: userId, username, credentials: [] };
users.set(userId, user);
}
const options = generateRegistrationOptions({
rpName,
rpID,
userID: user.id,
userName: user.username,
attestationType: 'none',
excludeCredentials: user.credentials.map(c => ({ id: c.id, type: 'public-key' })),
authenticatorSelection: { residentKey: 'preferred', userVerification: 'preferred' },
});
req.session.currentChallenge = options.challenge;
res.json(options);
});
// Kayıt: doğrula
app.post('/webauthn/register/verify', async (req, res) => {
const { userId, attResp } = req.body;
const user = users.get(userId);
const verification = await verifyRegistrationResponse({
response: attResp,
expectedChallenge: req.session.currentChallenge,
expectedOrigin: 'https://app.example.com',
expectedRPID: rpID,
});
if (verification.verified) {
const { credentialPublicKey, credentialID, counter } = verification.registrationInfo;
user.credentials.push({ id: credentialID, publicKey: credentialPublicKey, counter });
}
res.json({ ok: verification.verified });
});
// Giriş: seçenek üret
app.post('/webauthn/login/options', (req, res) => {
const { userId } = req.body;
const user = users.get(userId);
const options = generateAuthenticationOptions({
rpID,
userVerification: 'preferred',
allowCredentials: user ? user.credentials.map(c => ({ id: c.id, type: 'public-key' })) : [],
});
req.session.currentChallenge = options.challenge;
res.json(options);
});
// Giriş: doğrula
app.post('/webauthn/login/verify', async (req, res) => {
const { userId, authResp } = req.body;
const user = users.get(userId);
const dbCred = user.credentials.find(c => Buffer.compare(c.id, Buffer.from(authResp.rawId, 'base64url')) === 0);
const verification = await verifyAuthenticationResponse({
response: authResp,
expectedChallenge: req.session.currentChallenge,
expectedOrigin: 'https://app.example.com',
expectedRPID: rpID,
authenticator: dbCred,
});
if (verification.verified) {
dbCred.counter = verification.authenticationInfo.newCounter;
// Burada oturumu işaretleyin (req.session.userId = userId gibi)
}
res.json({ ok: verification.verified });
});
app.listen(3000, () => console.log('Server listening on http://localhost:3000'));
2) İstemci tarafı: Kayıt ve giriş akışı
// register.js (tarayıcı)
async function startRegistration(username, userId) {
const opts = await fetch('/webauthn/register/options', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ username, userId })
}).then(r => r.json());
// ArrayBuffer alanlarını dönüştürün
opts.challenge = base64urlToBuffer(opts.challenge);
opts.user.id = new TextEncoder().encode(opts.user.id);
const cred = await navigator.credentials.create({ publicKey: opts });
const attResp = credentialToJSON(cred);
const res = await fetch('/webauthn/register/verify', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ userId, attResp })
}).then(r => r.json());
return res.ok;
}
async function startLogin(userId) {
const opts = await fetch('/webauthn/login/options', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ userId })
}).then(r => r.json());
opts.challenge = base64urlToBuffer(opts.challenge);
opts.allowCredentials = (opts.allowCredentials || []).map(a => ({ ...a, id: base64urlToBuffer(a.id) }));
const assertion = await navigator.credentials.get({ publicKey: opts });
const authResp = credentialToJSON(assertion);
const res = await fetch('/webauthn/login/verify', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ userId, authResp })
}).then(r => r.json());
return res.ok;
}
// Yardımcılar
function base64urlToBuffer(b64url) {
const pad = '='.repeat((4 - (b64url.length % 4)) % 4);
const b64 = (b64url + pad).replace(/-/g, '+').replace(/_/g, '/');
const raw = atob(b64);
const buf = new ArrayBuffer(raw.length);
const view = new Uint8Array(buf);
for (let i = 0; i < raw.length; ++i) view[i] = raw.charCodeAt(i);
return buf;
}
function credentialToJSON(cred) {
return JSON.parse(JSON.stringify(cred, (k, v) => v instanceof ArrayBuffer
? bufferToBase64url(v)
: v));
}
function bufferToBase64url(buf) {
const bytes = new Uint8Array(buf);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/,'');
}
Güvenlik, UX ve üretim tavsiyeleri
- Kullanıcı adısız (usernameless) giriş için “resident key” tercih edin ve uygun akışı tasarlayın.
- Birden fazla passkey kaydına izin vererek cihaz kaybına karşı dayanıklılık sağlayın.
- Platform passkey (cihazda) ile çapraz platform passkey (iCloud Keychain, Google Password Manager) farklarını kullanıcılara açıklayın.
- Hata yönetimini özenle yapın: tarayıcıda NotAllowedError vb. hataları kullanıcı dostu mesajlara çevirin.
- Oturum yönetiminde kısa ömürlü JWT veya güvenli, HttpOnly, SameSite=strict cookie tercih edin.
Sorun giderme
- Origin ve RP ID uyuşmazlığı en sık hatadır: “https://app.example.com” ile rpID “example.com” olmalı.
- Sunucunuz ve istemci cihazınızın saat farkları challenge doğrulamasını bozabilir; NTP senkronizasyonunu sağlayın.
- Geliştirme aşamasında sadece localhost özel izni vardır; kendi alan adınızda test ederken mutlaka HTTPS kullanın.
- Kurumsal ortamlarda WebAuthn politikaları kısıtlanmış olabilir; tarayıcı ve OS politikalarını kontrol edin.
Sonuç
Passkey/WebAuthn, hem güvenlik hem de kullanıcı deneyimi tarafında anlamlı bir sıçrama sunuyor. Doğru RP ID, sağlam challenge yönetimi ve tutarlı origin politikasıyla kurulum basit hale geliyor. Üretime geçişte çoklu cihaz desteği, kullanıcı eğitimleri ve geri dönüş (fallback) stratejileri planlandığında, şifresiz geleceğe hazır bir kimlik doğrulama katmanı elde edebilirsiniz.