Parolasız kimlik doğrulama, kullanıcı deneyimini iyileştirirken hesap güvenliğini artırmanın en pratik yolu haline geldi. WebAuthn ve Passkey teknolojileri sayesinde kullanıcılar artık SMS kodları veya karmaşık parolalar yerine cihazlarındaki biyometrik sensörlerle giriş yapabiliyor. Bu yazıda, Next.js üzerinde WebAuthn tabanlı Passkey entegrasyonunu adım adım anlatarak, hem teknik detayları hem de üretim ortamı için ipuçlarını paylaşacağım.
Kısaca özetlemek gerekirse, WebAuthn FIDO2 standardının bir parçasıdır ve tarayıcılar aracılığıyla donanımsal (Güvenli Anahtarlar, Touch ID, Windows Hello vb.) veya platform tabanlı kimlik doğrulayıcılarla anahtar çifti üretip doğrulamayı mümkün kılar. RP (Relying Party) ID, origin ve challenge mekanizmaları doğru kurgulandığında, hem phishing dirençli hem de kullanıcı dostu bir giriş deneyimi elde edersiniz.
Önkoşullar ve Mimari
Gerekenler: Node.js 18+, Next.js 13+ (App Router tercihen), HTTPS (yerelde self-signed veya localhost), ve bir sunucu tarafı doğrulama paketi. Toplulukta yaygın kullanılan kütüphanelerden biri @simplewebauthn. Akış şu şekilde işler: Sunucu, kayıt veya giriş için tek kullanımlık bir challenge üretir ve istemciye döner. İstemci, tarayıcıdaki navigator.credentials API’sini (veya sarmalayıcı kütüphane) çağırarak kimlik doğrulayıcıyla işlem yapar ve imzalı yanıtı sunucuya gönderir. Sunucu, public key ve imza doğrulamasıyla işlemi sonuçlandırır.
Kurulum
Yeni bir Next.js projesi oluşturarak başlayalım:
npx create-next-app@latest passkey-demo --typescript
cd passkey-demo
npm i @simplewebauthn/server @simplewebauthn/browser cookie uuid
Basit bir yapı için .env dosyanıza aşağıdaki değerleri ekleyin (yerel geliştirme için):
RP_ID=localhost
RP_NAME=Passkey Demo
ORIGIN=http://localhost:3000
Kayıt (Registration) Akışı
Kullanıcı bir Passkey oluşturmak istediğinde önce sunucudan registration options alır. Bu seçenekler, kimlik doğrulayıcıya hangi parametrelerle anahtar üretmesi gerektiğini söyler. Next.js App Router kullandığınızı varsayarak endpoint’leri şu şekilde düşünebilirsiniz:
// app/api/webauthn/registration/options/route.ts
import { generateRegistrationOptions } from '@simplewebauthn/server';
import { cookies } from 'next/headers';
import { v4 as uuid } from 'uuid';
export async function GET() {
const userId = uuid(); // Demo için rastgele. Gerçekte gerçek kullanıcı ID'si olmalı.
const opts = await generateRegistrationOptions({
rpName: process.env.RP_NAME!,
rpID: process.env.RP_ID!,
userID: userId,
userName: '[email protected]',
attestationType: 'none',
authenticatorSelection: {
residentKey: 'preferred',
userVerification: 'preferred',
},
});
// Challenge'ı oturumda saklayın
cookies().set('regChallenge', opts.challenge, { httpOnly: true });
return new Response(JSON.stringify({ userId, options: opts }), { status: 200 });
}
İstemci tarafında, tarayıcı API’si ile uğraşmayı kolaylaştırmak için @simplewebauthn/browser paketini kullanabiliriz:
// app/register/page.tsx
'use client';
import { startRegistration } from '@simplewebauthn/browser';
export default function Register() {
const onRegister = async () => {
const res = await fetch('/api/webauthn/registration/options');
const { userId, options } = await res.json();
const attRes = await startRegistration(options);
await fetch('/api/webauthn/registration/verify', {
method: 'POST',
body: JSON.stringify({ userId, attRes }),
});
alert('Passkey oluşturuldu!');
};
return <button onClick={onRegister}>Passkey Oluştur</button>;
}
Sunucu, istemciden gelen yanıtı doğrulamalıdır. Doğrulama sonunda public key’i ve ilgili metadata’yı kullanıcı profiline kalıcı olarak kaydedersiniz.
// app/api/webauthn/registration/verify/route.ts
import { verifyRegistrationResponse } from '@simplewebauthn/server';
import { cookies } from 'next/headers';
export async function POST(req: Request) {
const { userId, attRes } = await req.json();
const expectedChallenge = cookies().get('regChallenge')?.value;
const verification = await verifyRegistrationResponse({
response: attRes,
expectedChallenge,
expectedOrigin: process.env.ORIGIN!,
expectedRPID: process.env.RP_ID!,
});
if (!verification.verified) return new Response('Doğrulanamadı', { status: 400 });
// verification.registrationInfo'dan publicKey ve credentialID'yi alıp DB'ye kaydedin
return new Response('OK', { status: 200 });
}
Giriş (Authentication) Akışı
Girişte benzer şekilde bir challenge üretip istemciye gönderirsiniz. Kullanıcı cihazındaki Passkey ile imzalanan yanıtı sunucu doğrular. Burada kullanıcıya ait kayıtlı credentialID’lerinizi ve public key’lerinizi DB’den çekmeniz gerekir.
// app/api/webauthn/authentication/options/route.ts
import { generateAuthenticationOptions } from '@simplewebauthn/server';
import { cookies } from 'next/headers';
export async function GET() {
const opts = await generateAuthenticationOptions({
rpID: process.env.RP_ID!,
userVerification: 'preferred',
});
cookies().set('authChallenge', opts.challenge, { httpOnly: true });
return new Response(JSON.stringify({ options: opts }), { status: 200 });
}
// app/login/page.tsx
'use client';
import { startAuthentication } from '@simplewebauthn/browser';
export default function Login() {
const onLogin = async () => {
const res = await fetch('/api/webauthn/authentication/options');
const { options } = await res.json();
const assertion = await startAuthentication(options);
await fetch('/api/webauthn/authentication/verify', {
method: 'POST',
body: JSON.stringify({ assertion }),
});
alert('Giriş başarılı!');
};
return <button onClick={onLogin}>Passkey ile Giriş</button>;
}
// app/api/webauthn/authentication/verify/route.ts
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
import { cookies } from 'next/headers';
// Not: Burada kullanıcıya ait credential'ları DB'den çekmelisiniz.
export async function POST(req: Request) {
const { assertion } = await req.json();
const expectedChallenge = cookies().get('authChallenge')?.value;
const verification = await verifyAuthenticationResponse({
response: assertion,
expectedChallenge,
expectedOrigin: process.env.ORIGIN!,
expectedRPID: process.env.RP_ID!,
authenticator: {
credentialID: Buffer.from('...'), // DB'den
credentialPublicKey: Buffer.from('...'), // DB'den
counter: 0,
},
});
if (!verification.verified) return new Response('Hatalı kimlik doğrulama', { status: 401 });
return new Response('OK', { status: 200 });
}
Üretime Hazırlık: İpuçları ve En İyi Uygulamalar
- HTTPS zorunlu: WebAuthn, localhost harici kökenlerde güvenli bağlam ister. Prod ortamda gerçek sertifika kullanın.
- RP ID ve Origin uyumu: RP_ID alan adınızla, ORIGIN ise protokol ve host’la birebir eşleşmeli. Alt alan adları ve çoklu çevrelerde yanlış yapılandırma en yaygın hatadır.
- Challenge ve oturum: Challenge değerini kısa ömürlü ve kullanıcıya bağlı saklayın. CSRF ve yeniden oynatma saldırılarına karşı dikkatli olun.
- Kullanıcı deneyimi: Platform authenticator (cihaz içi) ve cross-platform (USB/NFC) anahtarları desteklediğinizden emin olun. Cihaz değişimlerinde çoklu Passkey veya hesap kurtarma akışları tasarlayın.
- Günlükleme ve ölçüm: Başarısız doğrulama nedenlerini (origin uyuşmuyor, RP ID yanlış, kullanıcı doğrulaması başarısız vb.) log’layın; destek taleplerini azaltır.
Sonuç
WebAuthn ve Passkey ile parolasız kimlik doğrulama, hem saldırı yüzeyini küçültür hem de kullanıcı memnuniyetini artırır. Next.js üzerinde @simplewebauthn ile kurulum oldukça hızlıdır: kayıt ve giriş için challenge kurgusu, doğru RP/Origin ayarı ve güvenli oturum yönetimi yapı taşlarını oluşturur. Bu mimariyi uygulayarak uygulamanızda modern, phishing dirençli ve yüksek dönüşüm oranına sahip bir giriş deneyimine geçiş yapabilirsiniz.