front-end geliştirme etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
front-end geliştirme etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

19 Kasım 2025 Çarşamba

Next.js ile Passkey (WebAuthn/FIDO2) Entegrasyonu: Şifresiz Giriş Nasıl Eklenir?

Özet: Passkey (WebAuthn/FIDO2) ile şifresiz kimlik doğrulama, kimlik avına dayanıklı ve kullanıcı deneyimi yüksek modern bir giriş yöntemidir. Bu rehberde, Next.js üzerinde passkey kaydı ve giriş akışını adım adım nasıl kuracağınızı anlatıyorum. Amaç, pratik bir örnekle üretime yakın bir mimari oluşturmak ve SEO dostu bir içerikle temel kavramları sade şekilde aktarmaktır.

Passkey Nedir ve Neden Önemli?

Passkey, kullanıcıların parolasız şekilde oturum açmasını sağlayan, cihazın güvenli donanım modülü veya güvenli alanında saklanan FIDO2/WebAuthn kimlik bilgilerine dayanan bir yöntemdir. Phishing’e karşı dirençlidir, ikinci faktör ihtiyacını ortadan kaldırabilir ve kullanıcı tarafında dokunma/yüz tanıma gibi biyometrik doğrulamalarla çalışır. iOS, Android, Windows ve macOS’ta platform desteği geniştir.

Gereksinimler

- Next.js 13/14 (App Router veya Pages Router)
- Node.js 18+
- HTTPS (geliştirmede localhost istisnası vardır)
- Sunucu tarafında oturum (session) veya kısa süreli challenge saklama mekanizması
- Bir veritabanı (ör. PostgreSQL, SQLite veya MongoDB)

Mimari ve Terimler

RP ID (Relying Party ID): Genellikle alan adınızın eTLD+1 formatı (ör. example.com). Localhost için rpID: localhost kullanılabilir.
Challenge: Sunucuda üretilen tek seferlik rastgele dize; hem kayıt hem girişte imzalanır ve doğrulanır.
Credential: Kullanıcıya ait public key, credentialId, counter ve transports gibi bilgilerden oluşur.

Kurulum

Next.js projesi oluşturun ve gerekli paketleri ekleyin:

npx create-next-app passkey-app
cd passkey-app
npm i @simplewebauthn/server @simplewebauthn/browser base64url

Veritabanında şu alanlara ihtiyacınız olacak: userId, credentialId (Buffer/Base64URL), publicKey (PEM/COSE), counter (number), transports (string[]). Örnek bir tablo/collection şeması oluşturun.

Kayıt (Registration) Akışı

1) Sunucu tarafında kayıt seçeneklerini üretin ve challenge’ı oturumda saklayın:

// app/api/webauthn/register-options/route.ts
import { generateRegistrationOptions } from '@simplewebauthn/server';

export async function GET() {
  const rpID = process.env.RP_ID || 'localhost';
  const user = { id: 'u_123', name: '[email protected]', displayName: 'Ali' };

  const options = await generateRegistrationOptions({
    rpName: 'Passkey Demo',
    rpID,
    userID: user.id,
    userName: user.name,
    userDisplayName: user.displayName,
    attestationType: 'none',
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'required',
      authenticatorAttachment: 'platform',
    },
  });

  // TODO: session.set('currentChallenge', options.challenge)
  return Response.json(options);
}

2) İstemci tarafında kullanıcının cihazında passkey oluşturun:

// app/register/page.tsx
'use client';
import { startRegistration } from '@simplewebauthn/browser';

export default function Register() {
  const onRegister = async () => {
    const opts = await fetch('/api/webauthn/register-options').then(r => r.json());
    const attRes = await startRegistration(opts);
    await fetch('/api/webauthn/register-verify', {
      method: 'POST',
      body: JSON.stringify(attRes),
    });
  };
  return <button onClick={onRegister}>Passkey Oluştur</button>;
}

3) Sunucu tarafında yanıtı doğrulayın ve kimlik bilgilerini veritabanına kaydedin:

// app/api/webauthn/register-verify/route.ts
import { verifyRegistrationResponse } from '@simplewebauthn/server';

export async function POST(req: Request) {
  const body = await req.json();
  const expectedChallenge = /* session.get('currentChallenge') */ '';
  const rpID = process.env.RP_ID || 'localhost';
  const origin = process.env.ORIGIN || 'http://localhost:3000';

  const { verified, registrationInfo } = await verifyRegistrationResponse({
    expectedRPID: rpID,
    expectedOrigin: origin,
    expectedChallenge,
    response: body,
  });

  if (verified && registrationInfo) {
    const { credentialPublicKey, credentialID, counter, credentialDeviceType, credentialBackedUp } = registrationInfo;
    // TODO: DB'ye userId ile birlikte credentialID, credentialPublicKey, counter, transports kaydedin
  }
  return Response.json({ verified });
}

Giriş (Authentication) Akışı

1) Sunucuda kullanıcıya ait credentialId listesini kullanarak seçenek üretin:

// app/api/webauthn/auth-options/route.ts
import { generateAuthenticationOptions } from '@simplewebauthn/server';

export async function GET() {
  const rpID = process.env.RP_ID || 'localhost';
  const userCreds = [ /* DB'den credentialId'ler */ ];

  const options = await generateAuthenticationOptions({
    rpID,
    userVerification: 'required',
    allowCredentials: userCreds.map(c => ({
      id: c.credentialId,
      type: 'public-key',
      transports: c.transports || ['internal'],
    })),
  });

  // TODO: session.set('currentChallenge', options.challenge)
  return Response.json(options);
}

2) İstemci tarafında kimlik doğrulaması başlatın ve yanıtı sunucuya gönderin:

// app/login/page.tsx
'use client';
import { startAuthentication } from '@simplewebauthn/browser';

export default function Login() {
  const onLogin = async () => {
    const opts = await fetch('/api/webauthn/auth-options').then(r => r.json());
    const assertion = await startAuthentication(opts);
    const res = await fetch('/api/webauthn/auth-verify', {
      method: 'POST',
      body: JSON.stringify(assertion),
    });
    const { verified } = await res.json();
    if (verified) alert('Giriş başarılı');
  };
  return <button onClick={onLogin}>Passkey ile Giriş</button>;
}

3) Sunucuda doğrulama yapın ve counter değerini güncelleyin:

// app/api/webauthn/auth-verify/route.ts
import { verifyAuthenticationResponse } from '@simplewebauthn/server';

export async function POST(req: Request) {
  const body = await req.json();
  const expectedChallenge = /* session.get('currentChallenge') */ '';
  const rpID = process.env.RP_ID || 'localhost';
  const origin = process.env.ORIGIN || 'http://localhost:3000';

  // TODO: DB'den credentialId'ye göre publicKey ve counter'ı çekin
  const dbCred = {/* ... */};

  const { verified, authenticationInfo } = await verifyAuthenticationResponse({
    expectedRPID: rpID,
    expectedOrigin: origin,
    expectedChallenge,
    response: body,
    authenticator: {
      credentialID: dbCred.credentialId,
      credentialPublicKey: dbCred.publicKey,
      counter: dbCred.counter,
      transports: dbCred.transports,
    },
  });

  if (verified) {
    // TODO: DB'de counter'ı authenticationInfo.newCounter ile güncelleyin
    // TODO: uygulama oturumunu başlatın
  }
  return Response.json({ verified });
}

Koşullu UI ve Kullanılabilirlik İpuçları

- Chrome’da “conditional UI” ile giriş formu göstermeden tarayıcı otomatik passkey önerisi sunabilir. startAuthentication çağrısını buna göre optimize edin.
- Kullanıcıya “Aygıtınızda passkey saklanacak” gibi kısa, anlaşılır mesajlarla rehberlik edin.
- Eşitleme: iCloud Keychain veya Google Password Manager ile passkey’ler cihazlar arasında senkronize olabilir.

Güvenlik ve Üretim Notları

- Her zaman HTTPS kullanın; origin ve rpID tutarlılığı hayati önem taşır.
- Challenge değerlerini kısa ömürlü ve tek kullanımlık tutun.
- Çoklu cihaz senaryolarında bir kullanıcıya birden fazla credential kaydına izin verin.
- Hata Yönetimi: NotAllowedError genelde kullanıcı iptali veya zaman aşımından kaynaklanır; kullanıcı dostu hata mesajları gösterin.

Sonuç olarak, Next.js üzerinde passkey entegrasyonu birkaç API uç noktası ve doğru challenge yönetimiyle oldukça akıcı bir hale geliyor. Parolaları terk etmek hem güvenliği artırır hem de dönüşüm oranlarını yükseltir. Üretime alırken oturum süreleri, cihaz yönetimi ve kurtarma seçeneklerini (ör. e-posta magic link) birlikte kurgulamanızı öneririm.