Giriş
Parolalar bitmek bilmeyen bir güvenlik ve kullanıcı deneyimi sorunu. WebAuthn ve Passkey (FIDO2) standartları, hem güvenliği artıran hem de kullanıcıyı zahmetten kurtaran parolasız kimlik doğrulama sunuyor. Bu yazıda, Next.js ve Node.js kullanarak uçtan uca bir WebAuthn akışını nasıl kuracağınızı adım adım anlatıyorum. Hem modern tarayıcı desteğini, hem de üretim ortamında dikkat edilmesi gerekenleri ele alacağız.
WebAuthn ve Passkey Nedir?
WebAuthn, W3C ve FIDO Alliance tarafından geliştirilen, tarayıcı üzerinden güvenli anahtar tabanlı kimlik doğrulama standardıdır. Passkey ise kullanıcıların cihazlarında saklanan ve bulut üzerinden senkronize olabilen (Apple iCloud, Google Password Manager vb.) WebAuthn kimlik bilgileridir. Kısaca: kullanıcı, parolaya gerek duymadan cihaz biyometrisi (Face ID, Touch ID, Windows Hello) veya güvenlik anahtarı (YubiKey) ile giriş yapar.
Kavramsal olarak iki akış var: Kayıt (Registration) ve Giriş (Authentication). Sunucu, benzersiz bir challenge üretir; tarayıcıda navigator.credentials.create() veya navigator.credentials.get() çağrıları yapılır; elde edilen yanıt sunucu tarafından doğrulanır. Doğrulama sonrası, kullanıcı artık parolasız giriş yapabilir.
Mimari Özeti
- Relying Party (RP): Uygulamanızın alan adıyla tanımlanır (ör. rpId: "ornek.com"). Bu değer kök alan adınız olmalıdır.
- Attestation ve Assertion: Kayıtta attestation, girişte assertion doğrulanır. Üretimde çoğunlukla attestation: "none" kullanılır.
- Depolama: Kamu anahtarı, credential ID ve counter gibi veriler sunucuda saklanır. Challenge için kısa süreli bir önbellek (Redis) önerilir.
Ön Koşullar
- Next.js 14+ (App Router önerilir), Node.js 18+.
- HTTPS zorunlu (geliştirmede localhost hariç). Üretimde gerçek bir alan adıyla test edin.
- Kütüphaneler: @simplewebauthn/server ve @simplewebauthn/browser. Bu ikili, challenge üretimi ve doğrulama işini hataya yer bırakmadan kolaylaştırır.
Adım 1: Projeyi Hazırlayın
Next.js projesini oluşturun ve bağımlılıkları kurun: npm i @simplewebauthn/server @simplewebauthn/browser. App Router ile app/api altında dört endpoint planlayın: /webauthn/register/options, /webauthn/register/verify, /webauthn/login/options, /webauthn/login/verify.
Sunucu tarafında yapılandırma için değişkenleri belirleyin: rpName, rpID (alan adınız), origin (ör. https://app.ornek.com). Bu üçü birbirine tam uyumlu olmalıdır; aksi halde tarayıcı doğrulamayı reddeder.
Adım 2: Kayıt (Registration) Akışı
1) Options üretimi: Sunucu, oturumu açık kullanıcının (ör. user.id, user.name) bilgileriyle generateRegistrationOptions çağırır. attestation: "none", authenticatorSelection için residentKey: "preferred" ve userVerification: "preferred" yeterlidir.
2) Challenge saklama: Dönen options.challenge değerini kullanıcının oturumuna veya Redis’e yazın. Bu değer tek kullanımlık ve kısa ömürlü olmalıdır.
3) İstemci işlem: Tarayıcıda startRegistration(options) veya yerel API ile navigator.credentials.create({ publicKey: options }) çağırın. Kullanıcı, cihaz biyometrisiyle onaylar ve bir attestation yanıtı oluşur.
4) Doğrulama: Yanıt sunucuya gönderilir ve verifyRegistrationResponse ile doğrulanır. Başarılıysa credentialID, publicKey, counter, transports gibi alanları veritabanına kaydedin.
Not: Binary verilerin JSON’a taşınmasında base64url dönüşümleri gerekir. @simplewebauthn bu dönüşümleri kolaylaştırır; farklı bir yaklaşım kullanıyorsanız, ArrayBuffer ↔ base64url dönüşümleri için tutarlı yardımcı fonksiyonlar geliştirin.
Adım 3: Giriş (Authentication) Akışı
1) Options üretimi: Sunucu generateAuthenticationOptions ile yeni bir challenge üretir. Dilerseniz sadece kullanıcıya ait allowCredentials listesi dönebilir veya passkey’lerin “discovered” kullanımına izin vermek için boş da bırakabilirsiniz.
2) İstemci işlem: Tarayıcıda startAuthentication(options) ya da navigator.credentials.get({ publicKey: options }) çağrılır. Kullanıcı biyometrik onayı verir.
3) Doğrulama: Sunucu, verifyAuthenticationResponse ile yanıtı doğrular; counter değerini günceller ve başarılıysa oturum veya token üretir (HTTP only, SameSite=Strict bir çerez önerilir).
Güvenlik ve Üretim İpuçları
- Origin ve rpID eşleşmesi: origin tam URL, rpID kök alan adı olmalı. https://login.ornek.com ile rpID: "ornek.com" uyumludur.
- HTTPS zorunlu: Geliştirme dışında güvenli olmayan kökenlerde WebAuthn çalışmaz.
- Attestation politikası: Kurumsal cihaz kökenini doğrulamak gerekmiyorsa none en pratik seçenek.
- Kullanıcı doğrulaması: userVerification: "required" güvenliği artırır; deneyim açısından “preferred” daha esnek olabilir.
- Passkey senkronizasyonu: Çok cihazlı deneyim için kullanıcıya aynı ekosistemde (iCloud / Google) senkronizasyonu etkinleştirmesini anlatın.
- Conditional UI: Chrome’da parolasız “otomatik” akış için navigator.credentials.get({ mediation: "conditional" }) ve ilgili bayrakları düşünün.
- Geri dönüş: İlk aşamada SMS/e-posta yedek yöntem bulundurun ancak uzun vadede phishing’e dayanıklı seçeneklere yönelin.
Sık Karşılaşılan Hatalar
- NotAllowedError: Kullanıcı akışı iptal etti veya tarayıcı UI zaman aşımına uğradı. Zaman aşımını düşürün ve açık geri bildirim gösterin.
- InvalidStateError: Aynı cihazda aynı kullanıcı için tekrar kayıt yapılmak isteniyor olabilir. Kayıt öncesi kullanıcıya mevcut passkey’leri gösterip yönetin.
- RP ID uyuşmazlığı: Alt alan adı/Origin ve rpID yanlış ayarlandığında doğrulama başarısız olur. Üretim alan adınızla birebir test edin.
Dağıtım ve Sonuç
Uygulamayı dağıtırken, ters proxy (Nginx) üzerinden HTTP → HTTPS zorlaması yapın. Load balancer arkasında gerçek istemci IP’sini günlüğe almak için X-Forwarded-For başlığını doğru iletin. CDN veya WAF kullanıyorsanız WebAuthn isteklerinin gövde boyutu ve header’larını kısıtlamadığından emin olun.
Sonuç olarak, WebAuthn ve Passkey ile Next.js + Node.js üzerinde parolasız kimlik doğrulama kurmak düşündüğünüzden daha erişilebilir. Doğru rpID/origin eşleşmesi, sağlam challenge saklama, güvenli çerez ayarları ve iyi bir kullanıcı akışı ile hem güvenlik seviyenizi yükseltir hem de dönüşüm oranlarını artırırsınız. Bugün basit bir kayıt/giriş prototipi hazırlayıp ekibinizle birlikte deneyin; parolaları geride bırakma vakti geldi.