Giriş
Parolasız giriş, kullanıcı deneyimini iyileştirirken güvenlik risklerini ciddi şekilde azaltan modern bir yaklaşım. Passkey teknolojisi, FIDO2/WebAuthn standartları üzerine kurulu olup, kimlik doğrulamayı biyometri (Face ID, Touch ID, Windows Hello) veya cihaz PIN’i gibi yerel yöntemlere devrediyor. Bu rehberde, Next.js tabanlı bir projeye Passkey (WebAuthn) eklemenin pratik bir yolunu adım adım anlatıyorum. Amacımız, hızlıca çalışan bir kayıt (registration) ve giriş (authentication) akışı kurmak.
Neden Passkey?
Parola sızıntıları, kimlik avı (phishing) ve zayıf şifreler artık klasik güvenlik açıkları. Passkey, özel anahtarın cihazda güvenli biçimde saklanması ve sitenizin alan adına (RP ID) bağlanması nedeniyle phishing’e karşı dayanıklıdır. Kullanıcılar şifre hatırlamak zorunda kalmaz; cihazlarının biyometrik sensörleri ile tek dokunuşta oturum açabilirler.
Ön Koşullar
- Next.js 13+ (App Router önerilir), Node.js 18+
- HTTPS ortamı (yerelde localhost istisnası)
- Modern bir tarayıcı (Chrome, Edge, Safari, Firefox; mobil platformlarda da destek artıyor)
- Temel bir veritabanı (PostgreSQL, MySQL veya Prisma ile soyutlama)
Gerekli Paketler
WebAuthn işlemlerini kolaylaştırmak için yaygın olarak kullanılan @simplewebauthn paketlerini kullanacağız. Terminalde aşağıdaki komutu çalıştırın:
npm i @simplewebauthn/server @simplewebauthn/browser zod
Mimariyi Anlamak
Kayıt akışında sunucu, kullanıcı için bir “challenge” üretir ve istemci bu challenge’ı cihazdaki güvenlik anahtarında imzalayıp geri gönderir. Sunucu, gelen yanıtı doğrular ve kimlik bilgilerini (credential) veritabanına yazar. Giriş akışı benzer şekilde çalışır ancak var olan credential ile imzalama yapılır. Tüm süreçte origin (https://alanadiniz.com) ve RP ID (alanadiniz.com) uyumu kritik önemdedir.
Adım 1: Kayıt (Registration) API’si
App Router kullandığınızı varsayalım. Kayıt başlatma için “/api/webauthn/register/options” ve doğrulama için “/api/webauthn/register/verify” uç noktaları oluşturalım.
// app/api/webauthn/register/options/route.ts
import { NextResponse } from 'next/server';
import { generateRegistrationOptions } from '@simplewebauthn/server';
export async function POST() {
const rpName = 'Uygulama Adı';
const rpID = process.env.RP_ID || 'localhost'; // üretimde alanadiniz.com
const user = { id: 'user-123', name: '[email protected]', displayName: 'Ali' };
const options = await generateRegistrationOptions({
rpName,
rpID,
userID: user.id,
userName: user.name,
timeout: 60000,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'preferred',
userVerification: 'preferred',
},
});
// challenge'ı oturum/cookie/cache'te saklayın
// ör: await saveChallenge(user.id, options.challenge)
return NextResponse.json(options);
}
Kullanıcı, tarayıcıda bu seçeneklerle passkey kaydı başlatır. Ardından istemciden dönen yanıtı doğrulamak için verify uç noktası:
// app/api/webauthn/register/verify/route.ts
import { NextResponse } from 'next/server';
import {
verifyRegistrationResponse,
} from '@simplewebauthn/server';
export async function POST(req: Request) {
const body = await req.json();
const rpID = process.env.RP_ID || 'localhost';
const expectedOrigin = process.env.EXPECTED_ORIGIN || 'http://localhost:3000';
// const expectedChallenge = await getChallenge(userId)
const verification = await verifyRegistrationResponse({
response: body,
expectedChallenge: '...challenge...',
expectedOrigin,
expectedRPID: rpID,
});
const { verified, registrationInfo } = verification;
if (verified && registrationInfo) {
const {
credentialPublicKey,
credentialID,
counter,
credentialBackedUp,
credentialDeviceType,
} = registrationInfo;
// Veritabanına kaydedin:
// credentialID (base64url), publicKey (Buffer), counter, userId vb.
return NextResponse.json({ ok: true });
}
return NextResponse.json({ ok: false }, { status: 400 });
}
Adım 2: İstemci (Kayıt)
İstemci tarafında @simplewebauthn/browser paketini kullanın. Aşağıda basit bir örnek var:
import {
startRegistration,
} from '@simplewebauthn/browser';
async function registerPasskey() {
const optionsRes = await fetch('/api/webauthn/register/options', { method: 'POST' });
const options = await optionsRes.json();
const attResp = await startRegistration(options);
const verifyRes = await fetch('/api/webauthn/register/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(attResp),
});
if (verifyRes.ok) {
alert('Passkey kaydedildi!');
}
}
Adım 3: Giriş (Authentication) API’si
Girişte de benzer iki uç nokta gerekir: “/api/webauthn/auth/options” ve “/api/webauthn/auth/verify”.
// app/api/webauthn/auth/options/route.ts
import { NextResponse } from 'next/server';
import { generateAuthenticationOptions } from '@simplewebauthn/server';
export async function POST() {
const rpID = process.env.RP_ID || 'localhost';
// kullanıcıyı e-posta ile tespit ettiğinizi varsayın ve onun credentialID'lerini çekin
// const allowCredentials = [...] // veritabanından
const options = await generateAuthenticationOptions({
rpID,
timeout: 60000,
userVerification: 'preferred',
// allowCredentials,
});
// challenge saklanır: await saveAuthChallenge(userId, options.challenge)
return NextResponse.json(options);
}
// app/api/webauthn/auth/verify/route.ts
import { NextResponse } from 'next/server';
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
export async function POST(req: Request) {
const body = await req.json();
const rpID = process.env.RP_ID || 'localhost';
const expectedOrigin = process.env.EXPECTED_ORIGIN || 'http://localhost:3000';
// const expectedChallenge = await getAuthChallenge(userId)
// const authenticator = await getAuthenticator(credentialID)
const verification = await verifyAuthenticationResponse({
response: body,
expectedChallenge: '...challenge...',
expectedOrigin,
expectedRPID: rpID,
authenticator: {
credentialPublicKey: Buffer.from('...'),
credentialID: Buffer.from('...'),
counter: 0,
transports: ['internal', 'hybrid'],
},
});
const { verified, authenticationInfo } = verification;
if (verified && authenticationInfo) {
const { newCounter } = authenticationInfo;
// counter'ı güncelleyin ve oturum açın (JWT/Session)
return NextResponse.json({ ok: true });
}
return NextResponse.json({ ok: false }, { status: 401 });
}
Adım 4: İstemci (Giriş)
İstemci tarafında startAuthentication ile challenge’ı imzalatıp doğrulamaya gönderin.
import {
startAuthentication,
} from '@simplewebauthn/browser';
async function loginWithPasskey() {
const optionsRes = await fetch('/api/webauthn/auth/options', { method: 'POST' });
const options = await optionsRes.json();
const asseResp = await startAuthentication(options);
const verifyRes = await fetch('/api/webauthn/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(asseResp),
});
if (verifyRes.ok) {
// yönlendirme veya token işlemleri
}
}
Veritabanı ve Güvenlik Notları
- RP ID üretimde çıplak alan adınız olmalı (ör. alanadiniz.com). Subdomain kullanıyorsanız buna dikkat edin.
- expectedOrigin tam şema ile eşleşmeli (https://alanadiniz.com). HTTP yerine HTTPS zorunludur (localhost hariç).
- Veritabanında şu alanlar saklanır: userId, credentialID (base64url), credentialPublicKey (Buffer), counter, transports, deviceType/backup bilgisi.
- Aynı kullanıcı için birden fazla credential destekleyin; kullanıcı yeni cihaz ekleyebilir.
- 0-RTT veya platform senkronizasyonu (iCloud Anahtar Zinciri, Google Password Manager) sayesinde cihazlar arası passkey geçişi mümkün hale gelir.
Sorun Giderme
- NotAllowedError: Genellikle origin veya user gesture eksikliği. Butona tıklama gibi bir kullanıcı etkileşimiyle çağırın.
- SecurityError: RP ID ile origin uyuşmuyor. Ortam değişkenlerini (RP_ID, EXPECTED_ORIGIN) kontrol edin.
- Unknown or unsupported transport: Esnek olun; transports alanını istemci döndürdüğü şekilde saklayın.
- Gömülü tarayıcılar: Bazı uygulama içi web görünümleri WebAuthn’ı kısıtlayabilir; harici tarayıcı önerin.
Sonuç
Passkey ile parolasız giriş, hem kullanıcılar hem de geliştiriciler için büyük bir kazanım. Next.js ve @simplewebauthn ile birkaç uç nokta ve doğru yapılandırma sayesinde modern, phishing’e dayanıklı ve hızlı bir kimlik doğrulama deneyimi sunabilirsiniz. Üretime geçmeden önce HTTPS, alan adı uyumu ve veritabanı bütünlüğü konularını titizlikle test etmeyi unutmayın.