Tam kimlik doğrulama manzarası: session vs JWT ne zaman kullanılır, OAuth 2.0/OIDC akışları, refresh token rotasyonu, passkey'ler (WebAuthn) ve Next.js'te gerçekten kullandığım auth kalıpları.
Kimlik doğrulama, web geliştirmenin "çalışıyor"un asla yeterli olmadığı tek alanıdır. Tarih seçicindeki bir hata sinir bozucudur. Auth sistemindeki bir hata ise veri ihlalidir.
Sıfırdan kimlik doğrulama implemente ettim, sağlayıcılar arası geçiş yaptım, token hırsızlığı olaylarını debug ettim ve "güvenliği sonra düzeltiriz" kararlarının sonuçlarıyla uğraştım. Bu yazı, başlarken sahip olmak istediğim kapsamlı rehberdir. Sadece teori değil — gerçek ödünleşimler, gerçek güvenlik açıkları ve production baskısı altında ayakta kalan kalıplar.
Session'lar, JWT'ler, OAuth 2.0, passkey'ler, MFA ve yetkilendirme konularının tamamını ele alacağız. Sonunda yalnızca her mekanizmanın nasıl çalıştığını değil, ne zaman kullanılacağını ve alternatiflerin neden var olduğunu da anlayacaksınız.
Bu, karşılaşacağınız ilk karardır ve internet bu konuda kötü tavsiyelerle doludur. Gerçekten neyin önemli olduğunu ortaya koyayım.
Session'lar orijinal yaklaşımdır. Sunucu bir session kaydı oluşturur, bunu bir yerde saklar (veritabanı, Redis, bellek) ve istemciye bir çerez içinde opak bir session ID verir.
// Basitleştirilmiş session oluşturma
import { randomBytes } from "crypto";
import { cookies } from "next/headers";
interface Session {
userId: string;
createdAt: Date;
expiresAt: Date;
ipAddress: string;
userAgent: string;
}
async function createSession(userId: string, request: Request): Promise<string> {
const sessionId = randomBytes(32).toString("hex");
const session: Session = {
userId,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 saat
ipAddress: request.headers.get("x-forwarded-for") ?? "unknown",
userAgent: request.headers.get("user-agent") ?? "unknown",
};
// Veritabanınızda veya Redis'te saklayın
await redis.set(`session:${sessionId}`, JSON.stringify(session), "EX", 86400);
const cookieStore = await cookies();
cookieStore.set("session_id", sessionId, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 86400,
path: "/",
});
return sessionId;
}Avantajları gerçektir:
Dezavantajları da gerçektir:
JWT'ler modeli tersine çevirir. Session durumunu sunucuda saklamak yerine, bunu istemcinin tuttuğu imzalı bir token'a kodlarsınız.
import { SignJWT, jwtVerify } from "jose";
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
async function createAccessToken(userId: string, role: string): Promise<string> {
return new SignJWT({ sub: userId, role })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("15m")
.setIssuer("https://example.com")
.setAudience("https://example.com")
.sign(secret);
}
async function verifyAccessToken(token: string) {
try {
const { payload } = await jwtVerify(token, secret, {
issuer: "https://example.com",
audience: "https://example.com",
});
return payload;
} catch {
return null;
}
}Avantajları:
Dezavantajları — ve bunlar insanların üzerinden geçtiği şeylerdir:
Temel kuralım:
Şu durumlarda session kullanın: Monolitik bir uygulamanız var, anında iptal ihtiyacınız var, hesap güvenliğinin kritik olduğu tüketici odaklı bir ürün yapıyorsunuz veya auth gereksinimleriniz sık değişebilir.
Şu durumlarda JWT kullanın: Servislerin kimliği bağımsız olarak doğrulaması gereken bir mikroservis mimariniz var, API'den API'ye iletişim yapıyorsunuz veya üçüncü taraf bir kimlik doğrulama sistemi implemente ediyorsunuz.
Pratikte: Çoğu uygulama session kullanmalıdır. "JWT'ler daha ölçeklenebilir" argümanı yalnızca session depolamanın çözemediği gerçek bir ölçekleme sorununuz varsa geçerlidir — ve Redis saniyede milyonlarca session araması yapar. Daha modern göründükleri için JWT seçen, sonra session'lardan daha karmaşık bir engelleme listesi ve yenileme token sistemi inşa eden çok fazla proje gördüm.
Session tabanlı auth seçseniz bile, OAuth, OIDC ve üçüncü taraf entegrasyonları aracılığıyla JWT'lerle karşılaşacaksınız. İç yapıyı anlamak tartışmasızdır.
Bir JWT, noktalarla ayrılmış üç bölümden oluşur: header.payload.signature
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcwOTMxMjAwMCwiZXhwIjoxNzA5MzEyOTAwfQ.
kQ8s7nR2xC...
Header — algoritmayı ve token tipini bildirir:
{
"alg": "RS256",
"typ": "JWT"
}Payload — claim'leri içerir. Standart claim'lerin kısa isimleri vardır:
{
"sub": "user_123", // Subject (bu kimin hakkında)
"iss": "https://auth.example.com", // Issuer (bunu kim oluşturdu)
"aud": "https://api.example.com", // Audience (bunu kim kabul etmeli)
"iat": 1709312000, // Issued At (Unix zaman damgası)
"exp": 1709312900, // Expiration (Unix zaman damgası)
"role": "admin" // Özel claim
}Signature — token'ın kurcalanmadığını kanıtlar. Kodlanmış header ve payload'ın gizli bir anahtarla imzalanmasıyla oluşturulur.
HS256 (HMAC-SHA256) — simetrik. Aynı gizli anahtar imzalar ve doğrular. Basit, ama token'ları doğrulaması gereken her servisin gizli anahtara sahip olması gerekir. Herhangi biri ele geçirilirse, saldırgan token'lar sahteleyebilir.
RS256 (RSA-SHA256) — asimetrik. Özel anahtar imzalar, genel anahtar doğrular. Yalnızca auth sunucusu özel anahtara ihtiyaç duyar. Herhangi bir servis genel anahtarla doğrulayabilir. Bir doğrulama servisi ele geçirilirse, saldırgan token'ları okuyabilir ama sahteleyemez.
import { SignJWT, jwtVerify, importPKCS8, importSPKI } from "jose";
// RS256 — birden fazla servis token doğruladığında bunu kullanın
const privateKeyPem = process.env.JWT_PRIVATE_KEY!;
const publicKeyPem = process.env.JWT_PUBLIC_KEY!;
async function signWithRS256(payload: Record<string, unknown>) {
const privateKey = await importPKCS8(privateKeyPem, "RS256");
return new SignJWT(payload)
.setProtectedHeader({ alg: "RS256", typ: "JWT" })
.setIssuedAt()
.setExpirationTime("15m")
.sign(privateKey);
}
async function verifyWithRS256(token: string) {
const publicKey = await importSPKI(publicKeyPem, "RS256");
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ["RS256"], // KRİTİK: algoritmaları her zaman kısıtlayın
});
return payload;
}Kural: Token'lar servis sınırlarını aştığında RS256 kullanın. HS256'yı yalnızca aynı servis hem imzalayıp hem doğruladığında kullanın.
alg: none SaldırısıBu, en ünlü JWT güvenlik açığıdır ve utanç verici derecede basittir. Bazı JWT kütüphaneleri eskiden şunları yapıyordu:
alg alanını okualg: "none" ise, imza doğrulamayı tamamen atlaBir saldırgan geçerli bir JWT alabilir, payload'ı değiştirebilir (örneğin, "role": "admin" ayarlayabilir), alg'ı "none" olarak ayarlayabilir, imzayı kaldırabilir ve gönderebilir. Sunucu bunu kabul ederdi.
// GÜVENLİK AÇIĞI — bunu asla yapmayın
function verifyJwt(token: string) {
const [headerB64, payloadB64, signature] = token.split(".");
const header = JSON.parse(atob(headerB64));
if (header.alg === "none") {
// "İmza gerekmiyor" — FELAKET
return JSON.parse(atob(payloadB64));
}
// ... imzayı doğrula
}Çözüm basittir: beklenen algoritmayı her zaman açıkça belirtin. Token'ın size nasıl doğrulayacağınızı söylemesine asla izin vermeyin.
// GÜVENLİ — algoritma sabit kodlanmış, token'dan okunmuyor
const { payload } = await jwtVerify(token, key, {
algorithms: ["RS256"], // Yalnızca RS256 kabul et — header'ı yok say
});jose gibi modern kütüphaneler bunu varsayılan olarak doğru bir şekilde ele alır, ama yine de derinlemesine savunma olarak algorithms seçeneğini açıkça geçirmelisiniz.
Yukarıdakiyle ilişkili: Bir sunucu RS256 kabul edecek şekilde yapılandırılmışsa, bir saldırgan:
alg: "HS256" ile bir token oluşturabilirSunucu alg header'ını okuyup HS256 doğrulamaya geçerse, genel anahtar (herkesin bildiği) paylaşılan gizli anahtar haline gelir. İmza geçerlidir. Saldırgan bir token sahtelemiştir.
Yine, çözüm aynıdır: token header'ından algoritmaya asla güvenmeyin. Her zaman sabit kodlayın.
JWT kullanıyorsanız, bir refresh token stratejisine ihtiyacınız var. Uzun ömürlü bir access token göndermek sorun istemektir — çalınırsa, saldırgan tüm ömür boyunca erişime sahip olur.
Kalıp şudur:
import { randomBytes } from "crypto";
interface RefreshTokenRecord {
tokenHash: string;
userId: string;
familyId: string; // İlişkili token'ları gruplar
used: boolean;
expiresAt: Date;
createdAt: Date;
}
async function issueTokenPair(userId: string) {
const familyId = randomBytes(16).toString("hex");
const accessToken = await createAccessToken(userId);
const refreshToken = randomBytes(64).toString("hex");
const refreshTokenHash = await hashToken(refreshToken);
// Refresh token kaydını sakla
await db.refreshToken.create({
data: {
tokenHash: refreshTokenHash,
userId,
familyId,
used: false,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
createdAt: new Date(),
},
});
return { accessToken, refreshToken };
}İstemci yeni bir access token almak için her refresh token kullandığında, yeni bir refresh token verirsiniz ve eskisini geçersiz kılarsınız:
async function rotateTokens(incomingRefreshToken: string) {
const tokenHash = await hashToken(incomingRefreshToken);
const record = await db.refreshToken.findUnique({
where: { tokenHash },
});
if (!record) {
// Token mevcut değil — olası hırsızlık
return null;
}
if (record.expiresAt < new Date()) {
// Token'ın süresi dolmuş
await db.refreshToken.delete({ where: { tokenHash } });
return null;
}
if (record.used) {
// BU TOKEN ZATEN KULLANILDI.
// Birisi tekrar oynatıyor — ya meşru kullanıcı
// ya da saldırgan. Her iki durumda da tüm aileyi öldür.
await db.refreshToken.deleteMany({
where: { familyId: record.familyId },
});
console.error(
`Kullanıcı ${record.userId}, aile ${record.familyId} için refresh token yeniden kullanımı tespit edildi. Ailedeki tüm token'lar geçersiz kılındı.`,
);
return null;
}
// Mevcut token'ı kullanılmış olarak işaretle (silme — yeniden kullanım tespiti için gerekli)
await db.refreshToken.update({
where: { tokenHash },
data: { used: true },
});
// Aynı aile ID'si ile yeni çift ver
const newRefreshToken = randomBytes(64).toString("hex");
const newRefreshTokenHash = await hashToken(newRefreshToken);
await db.refreshToken.create({
data: {
tokenHash: newRefreshTokenHash,
userId: record.userId,
familyId: record.familyId, // Aynı aile
used: false,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
createdAt: new Date(),
},
});
const newAccessToken = await createAccessToken(record.userId);
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}Şu senaryoyu düşünün:
Yeniden kullanım tespiti olmadan, kullanıcı sadece bir hata alır. Saldırgan B token'ı ile devam eder. Kullanıcı tekrar giriş yapar, hesabının ele geçirildiğini asla bilmez.
Yeniden kullanım tespiti ve aile geçersiz kılma ile: kullanıcı zaten kullanılmış A token'ını kullanmaya çalıştığında, sistem yeniden kullanımı tespit eder, ailedeki her token'ı (B dahil) geçersiz kılar ve hem kullanıcıyı hem de saldırganı yeniden kimlik doğrulamaya zorlar. Kullanıcı bir "lütfen tekrar giriş yapın" istemi alır ve bir şeylerin yanlış olduğunu fark edebilir.
Bu, Auth0, Okta ve Auth.js tarafından kullanılan yaklaşımdır. Mükemmel değildir — saldırgan token'ı meşru kullanıcıdan önce kullanırsa, yeniden kullanım uyarısını tetikleyen meşru kullanıcı olur. Ama taşıyıcı token'larla yapabileceğimizin en iyisi budur.
OAuth 2.0 ve OpenID Connect, "Google/GitHub/Apple ile Giriş Yap"ın arkasındaki protokollerdir. Bir kütüphane kullansanız bile bunları anlamak zorunludur, çünkü işler bozulduğunda — ve bozulacaktır — protokol seviyesinde neler olduğunu bilmeniz gerekir.
OAuth 2.0 bir yetkilendirme protokolüdür. Şu soruyu yanıtlar: "Bu uygulama bu kullanıcının verilerine erişebilir mi?" Sonuç, belirli izinler (scope'lar) veren bir access token'dır.
OpenID Connect (OIDC), OAuth 2.0'ın üzerine inşa edilmiş bir kimlik doğrulama katmanıdır. Şu soruyu yanıtlar: "Bu kullanıcı kim?" Sonuç, kullanıcı kimlik bilgilerini içeren bir ID token'dır (bir JWT).
"Google ile Giriş Yap" dediğinizde, OIDC kullanıyorsunuz. Google uygulamanıza kullanıcının kim olduğunu söyler (kimlik doğrulama). Ayrıca takvimlerine veya drive'larına erişmek için OAuth scope'ları da isteyebilirsiniz (yetkilendirme).
Bu, web uygulamaları için kullanmanız gereken akıştır. PKCE (Proof Key for Code Exchange) başlangıçta mobil uygulamalar için tasarlandı, ancak artık sunucu taraflı uygulamalar dahil tüm istemciler için önerilmektedir.
import { randomBytes, createHash } from "crypto";
// Adım 1: PKCE değerlerini oluşturun ve kullanıcıyı yönlendirin
function initiateOAuthFlow() {
// Code verifier: rastgele 43-128 karakter dizesi
const codeVerifier = randomBytes(32).toString("base64url").slice(0, 43);
// Code challenge: verifier'ın SHA256 hash'i, base64url-kodlanmış
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
// State: CSRF koruması için rastgele değer
const state = randomBytes(16).toString("hex");
// Yönlendirmeden önce her ikisini de session'da saklayın (sunucu taraflı!)
// code_verifier'ı ASLA bir çereze veya URL parametresine koymayın
session.codeVerifier = codeVerifier;
session.oauthState = state;
const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
authUrl.searchParams.set("client_id", process.env.GOOGLE_CLIENT_ID!);
authUrl.searchParams.set("redirect_uri", "https://example.com/api/auth/callback/google");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("scope", "openid email profile");
authUrl.searchParams.set("state", state);
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");
return authUrl.toString();
}// Adım 2: Callback'i işleyin
async function handleOAuthCallback(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const error = url.searchParams.get("error");
// Sağlayıcıdan hata kontrolü
if (error) {
throw new Error(`OAuth hatası: ${error}`);
}
// State'in eşleştiğini doğrulayın (CSRF koruması)
if (state !== session.oauthState) {
throw new Error("State uyuşmazlığı — olası CSRF saldırısı");
}
// Authorization code'u token'larla değiştirin
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: code!,
redirect_uri: "https://example.com/api/auth/callback/google",
client_id: process.env.GOOGLE_CLIENT_ID!,
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
code_verifier: session.codeVerifier, // PKCE: bu akışı bizim başlattığımızı kanıtlar
}),
});
const tokens = await tokenResponse.json();
// tokens.access_token — Google'a API çağrıları için
// tokens.id_token — kullanıcı kimliği içeren JWT (OIDC)
// tokens.refresh_token — yeni access token'lar almak için
// Adım 3: ID token'ı doğrulayın ve kullanıcı bilgilerini çıkarın
const idTokenPayload = await verifyGoogleIdToken(tokens.id_token);
return {
googleId: idTokenPayload.sub,
email: idTokenPayload.email,
name: idTokenPayload.name,
picture: idTokenPayload.picture,
};
}Her OAuth/OIDC sağlayıcısı bunları sunar:
state parametresi, OAuth callback'ine yönelik CSRF saldırılarını önler. Onsuz:
https://sizinapp.com/callback?code=SALDIRGAN_KODUstate ile: uygulamanız rastgele bir değer üretir, session'da saklar ve authorization URL'ye dahil eder. Callback geldiğinde state'in eşleştiğini doğrularsınız. Saldırgan bunu sahteleyemez çünkü kurbanın session'ına erişimi yoktur.
Auth.js, çoğu Next.js projesinde ilk başvurduğum şeydir. OAuth dansını, session yönetimini, veritabanı kalıcılığını ve CSRF korumasını ele alır. İşte production'a hazır bir kurulum.
// src/lib/auth.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import { verifyPassword } from "@/lib/password";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
// Daha iyi güvenlik için veritabanı session'ları kullanın (JWT değil)
session: {
strategy: "database",
maxAge: 30 * 24 * 60 * 60, // 30 gün
updateAge: 24 * 60 * 60, // Her 24 saatte session'ı uzat
},
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
// Belirli scope'lar isteyin
authorization: {
params: {
scope: "openid email profile",
prompt: "consent",
access_type: "offline", // Refresh token al
},
},
}),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
// E-posta/şifre girişi (dikkatli kullanın)
Credentials({
credentials: {
email: { label: "E-posta", type: "email" },
password: { label: "Şifre", type: "password" },
},
authorize: async (credentials) => {
if (!credentials?.email || !credentials?.password) {
return null;
}
const user = await prisma.user.findUnique({
where: { email: credentials.email as string },
});
if (!user || !user.passwordHash) {
return null;
}
const isValid = await verifyPassword(credentials.password as string, user.passwordHash);
if (!isValid) {
return null;
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
};
},
}),
],
callbacks: {
// Kimin giriş yapabileceğini kontrol edin
async signIn({ user, account }) {
// Yasaklı kullanıcıların girişini engelleyin
if (user.id) {
const dbUser = await prisma.user.findUnique({
where: { id: user.id },
select: { banned: true },
});
if (dbUser?.banned) return false;
}
return true;
},
// Session'a özel alanlar ekleyin
async session({ session, user }) {
if (session.user) {
session.user.id = user.id;
// Veritabanından rol getir
const dbUser = await prisma.user.findUnique({
where: { id: user.id },
select: { role: true },
});
session.user.role = dbUser?.role ?? "user";
}
return session;
},
},
pages: {
signIn: "/login",
error: "/auth/error",
verifyRequest: "/auth/verify",
},
});// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;// src/middleware.ts
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isAuthPage = req.nextUrl.pathname.startsWith("/login") || req.nextUrl.pathname.startsWith("/register");
const isProtectedRoute =
req.nextUrl.pathname.startsWith("/dashboard") ||
req.nextUrl.pathname.startsWith("/settings") ||
req.nextUrl.pathname.startsWith("/admin");
const isAdminRoute = req.nextUrl.pathname.startsWith("/admin");
// Giriş yapmış kullanıcıları auth sayfalarından yönlendir
if (isLoggedIn && isAuthPage) {
return NextResponse.redirect(new URL("/dashboard", req.nextUrl));
}
// Kimliği doğrulanmamış kullanıcıları giriş sayfasına yönlendir
if (!isLoggedIn && isProtectedRoute) {
const callbackUrl = encodeURIComponent(req.nextUrl.pathname);
return NextResponse.redirect(new URL(`/login?callbackUrl=${callbackUrl}`, req.nextUrl));
}
// Admin rolünü kontrol et
if (isAdminRoute && req.auth?.user?.role !== "admin") {
return NextResponse.redirect(new URL("/dashboard", req.nextUrl));
}
return NextResponse.next();
});
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*", "/admin/:path*", "/login", "/register"],
};// src/app/dashboard/page.tsx
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return (
<div>
<h1>Hoş geldin, {session.user.name}</h1>
<p>Rol: {session.user.role}</p>
</div>
);
}"use client";
import { useSession } from "next-auth/react";
export function UserMenu() {
const { data: session, status } = useSession();
if (status === "loading") {
return <div>Yükleniyor...</div>;
}
if (status === "unauthenticated") {
return <a href="/login">Giriş Yap</a>;
}
return (
<div>
<img
src={session?.user?.image ?? "/default-avatar.png"}
alt={session?.user?.name ?? "Kullanıcı"}
/>
<span>{session?.user?.name}</span>
</div>
);
}Passkey'ler yıllardır kimlik doğrulama alanında en önemli gelişmedir. Oltalamaya dayanıklı, tekrar oynatmaya dayanıklı ve şifre ile ilgili güvenlik açıklarının tüm kategorisini ortadan kaldırır. 2026'da yeni bir proje başlıyorsanız, passkey'leri desteklemelisiniz.
Passkey'ler, biyometri veya cihaz PIN'leri tarafından desteklenen açık anahtar kriptografisi kullanır:
Paylaşılan hiçbir sır ağ üzerinden geçmez. Oltalanacak, sızacak veya doldurulacak hiçbir şey yoktur.
Bir passkey oluşturulduğunda, origin'e (örneğin, https://example.com) bağlanır. Tarayıcı, passkey'i yalnızca oluşturulduğu tam origin'de kullanacaktır. Bir saldırgan https://exarnple.com adresinde benzer bir site oluşturursa, passkey basitçe sunulmayacaktır. Bu, kullanıcı ihtiyatlılığı tarafından değil, tarayıcı tarafından uygulanır.
Bu, kullanıcıların sayfa doğru göründüğü için rutin olarak kimlik bilgilerini oltalama sitelerine girdiği şifrelerden temelden farklıdır.
SimpleWebAuthn, önerdiğim kütüphanedir. WebAuthn protokolünü doğru bir şekilde ele alır ve iyi TypeScript tipleri vardır.
// Sunucu taraflı: Kayıt
import { generateRegistrationOptions, verifyRegistrationResponse } from "@simplewebauthn/server";
import type { GenerateRegistrationOptionsOpts, VerifiedRegistrationResponse } from "@simplewebauthn/server";
const rpName = "example.com";
const rpID = "example.com";
const origin = "https://example.com";
async function startRegistration(userId: string, userEmail: string) {
// Hariç tutmak için kullanıcının mevcut passkey'lerini al
const existingCredentials = await db.credential.findMany({
where: { userId },
select: { credentialId: true, transports: true },
});
const options: GenerateRegistrationOptionsOpts = {
rpName,
rpID,
userID: new TextEncoder().encode(userId),
userName: userEmail,
attestationType: "none", // Çoğu uygulama için attestation'a ihtiyacımız yok
excludeCredentials: existingCredentials.map((cred) => ({
id: cred.credentialId,
transports: cred.transports,
})),
authenticatorSelection: {
residentKey: "preferred",
userVerification: "preferred",
},
};
const registrationOptions = await generateRegistrationOptions(options);
// Meydan okumayı geçici olarak saklayın — doğrulama için ihtiyacımız var
await redis.set(
`webauthn:challenge:${userId}`,
registrationOptions.challenge,
"EX",
300, // 5 dakika süre sonu
);
return registrationOptions;
}
async function finishRegistration(userId: string, response: unknown) {
const expectedChallenge = await redis.get(`webauthn:challenge:${userId}`);
if (!expectedChallenge) {
throw new Error("Meydan okuma süresi doldu veya bulunamadı");
}
let verification: VerifiedRegistrationResponse;
try {
verification = await verifyRegistrationResponse({
response: response as any,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
});
} catch (error) {
throw new Error(`Kayıt doğrulaması başarısız oldu: ${error}`);
}
if (!verification.verified || !verification.registrationInfo) {
throw new Error("Kayıt doğrulaması başarısız oldu");
}
const { credential } = verification.registrationInfo;
// Kimlik bilgisini veritabanında saklayın
await db.credential.create({
data: {
userId,
credentialId: credential.id,
publicKey: Buffer.from(credential.publicKey),
counter: credential.counter,
transports: credential.transports ?? [],
},
});
// Temizlik
await redis.del(`webauthn:challenge:${userId}`);
return { verified: true };
}// Sunucu taraflı: Kimlik Doğrulama
import { generateAuthenticationOptions, verifyAuthenticationResponse } from "@simplewebauthn/server";
async function startAuthentication(userId?: string) {
let allowCredentials;
// Kullanıcıyı biliyorsak (örneğin, e-postasını girdi), passkey'lerini sınırla
if (userId) {
const credentials = await db.credential.findMany({
where: { userId },
select: { credentialId: true, transports: true },
});
allowCredentials = credentials.map((cred) => ({
id: cred.credentialId,
transports: cred.transports,
}));
}
const options = await generateAuthenticationOptions({
rpID,
allowCredentials,
userVerification: "preferred",
});
// Doğrulama için meydan okumayı saklayın
const challengeKey = userId ? `webauthn:auth:${userId}` : `webauthn:auth:${options.challenge}`;
await redis.set(challengeKey, options.challenge, "EX", 300);
return options;
}
async function finishAuthentication(response: any, expectedChallenge: string, userId: string) {
const credential = await db.credential.findUnique({
where: { credentialId: response.id },
});
if (!credential) {
throw new Error("Kimlik bilgisi bulunamadı");
}
const verification = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
credential: {
id: credential.credentialId,
publicKey: credential.publicKey,
counter: credential.counter,
transports: credential.transports,
},
});
if (!verification.verified) {
throw new Error("Kimlik doğrulama doğrulaması başarısız oldu");
}
// ÖNEMLİ: Tekrar oynatma saldırılarını önlemek için sayacı güncelleyin
await db.credential.update({
where: { credentialId: response.id },
data: {
counter: verification.authenticationInfo.newCounter,
},
});
return { verified: true, userId: credential.userId };
}// İstemci taraflı: Kayıt
import { startRegistration as webAuthnRegister } from "@simplewebauthn/browser";
async function registerPasskey() {
// Sunucunuzdan seçenekleri alın
const optionsResponse = await fetch("/api/auth/webauthn/register", {
method: "POST",
});
const options = await optionsResponse.json();
try {
// Bu, tarayıcının passkey arayüzünü tetikler (biyometrik istem)
const credential = await webAuthnRegister(options);
// Kimlik bilgisini doğrulama için sunucunuza gönderin
const verifyResponse = await fetch("/api/auth/webauthn/register/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(credential),
});
const result = await verifyResponse.json();
if (result.verified) {
console.log("Passkey başarıyla kaydedildi!");
}
} catch (error) {
if ((error as Error).name === "NotAllowedError") {
console.log("Kullanıcı passkey kaydını iptal etti");
}
}
}Karşılaşacağınız iki terim:
attestationType: "none" ayarlayın.Passkey'lere rağmen, TOTP aracılığıyla MFA'nın gerekli olduğu senaryolarla karşılaşacaksınız — şifrelerin yanında ikinci faktör olarak passkey'ler veya cihazları passkey'leri desteklemeyen kullanıcıları desteklemek.
TOTP, Google Authenticator, Authy ve 1Password'ün arkasındaki protokoldür. Şu şekilde çalışır:
import { createHmac, randomBytes } from "crypto";
// Bir kullanıcı için TOTP gizli anahtarı oluşturun
function generateTOTPSecret(): string {
const buffer = randomBytes(20);
return base32Encode(buffer);
}
function base32Encode(buffer: Buffer): string {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let result = "";
let bits = 0;
let value = 0;
for (const byte of buffer) {
value = (value << 8) | byte;
bits += 8;
while (bits >= 5) {
result += alphabet[(value >>> (bits - 5)) & 0x1f];
bits -= 5;
}
}
if (bits > 0) {
result += alphabet[(value << (5 - bits)) & 0x1f];
}
return result;
}
// QR kodu için TOTP URI'si oluşturun
function generateTOTPUri(secret: string, userEmail: string, issuer: string = "example.com"): string {
const encodedIssuer = encodeURIComponent(issuer);
const encodedEmail = encodeURIComponent(userEmail);
return `otpauth://totp/${encodedIssuer}:${encodedEmail}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=30`;
}// Bir TOTP kodunu doğrulayın
function verifyTOTP(secret: string, code: string, window: number = 1): boolean {
const secretBuffer = base32Decode(secret);
const now = Math.floor(Date.now() / 1000);
// Geçerli zaman adımını ve komşularını kontrol edin (saat kayması toleransı)
for (let i = -window; i <= window; i++) {
const timeStep = Math.floor(now / 30) + i;
const expectedCode = generateTOTPCode(secretBuffer, timeStep);
// Zamanlama saldırılarını önlemek için sabit zamanlı karşılaştırma
if (timingSafeEqual(code, expectedCode)) {
return true;
}
}
return false;
}
function generateTOTPCode(secret: Buffer, timeStep: number): string {
// Zaman adımını 8 baytlık big-endian buffer'a dönüştür
const timeBuffer = Buffer.alloc(8);
timeBuffer.writeBigInt64BE(BigInt(timeStep));
// HMAC-SHA1
const hmac = createHmac("sha1", secret).update(timeBuffer).digest();
// Dinamik kesme
const offset = hmac[hmac.length - 1] & 0x0f;
const code =
((hmac[offset] & 0x7f) << 24) |
((hmac[offset + 1] & 0xff) << 16) |
((hmac[offset + 2] & 0xff) << 8) |
(hmac[offset + 3] & 0xff);
return (code % 1_000_000).toString().padStart(6, "0");
}
function timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
return createHmac("sha256", "key").update(bufA).digest().equals(createHmac("sha256", "key").update(bufB).digest());
}Kullanıcılar telefonlarını kaybeder. MFA kurulumu sırasında her zaman yedek kodlar oluşturun:
import { randomBytes, createHash } from "crypto";
function generateBackupCodes(count: number = 10): string[] {
return Array.from(
{ length: count },
() => randomBytes(4).toString("hex").toUpperCase(), // 8 karakterlik hex kodları
);
}
async function storeBackupCodes(userId: string, codes: string[]) {
// Saklamadan önce kodları hash'leyin — onlara şifreler gibi davranın
const hashedCodes = codes.map((code) => createHash("sha256").update(code).digest("hex"));
await db.backupCode.createMany({
data: hashedCodes.map((hash) => ({
userId,
codeHash: hash,
used: false,
})),
});
// Düz kodları kullanıcının kaydetmesi için BİR KERE döndürün
// Bundan sonra, yalnızca hash'lere sahibiz
return codes;
}
async function verifyBackupCode(userId: string, code: string): Promise<boolean> {
const codeHash = createHash("sha256").update(code.toUpperCase().replace(/\s/g, "")).digest("hex");
const backupCode = await db.backupCode.findFirst({
where: {
userId,
codeHash,
used: false,
},
});
if (!backupCode) return false;
// Kullanılmış olarak işaretle — her yedek kod tam olarak bir kez çalışır
await db.backupCode.update({
where: { id: backupCode.id },
data: { used: true, usedAt: new Date() },
});
return true;
}MFA kurtarma, çoğu öğreticinin atladığı ve çoğu gerçek uygulamanın berbat ettiği kısımdır. İşte implemente ettiklerim:
Bekleme süresi kritiktir. Bir saldırgan kullanıcının e-postasını ele geçirdiyse, MFA'yı anında devre dışı bırakmasına izin vermek istemezsiniz. 24 saatlik gecikme, meşru kullanıcıya e-postayı fark edip müdahale etme zamanı verir.
async function initiateAccountRecovery(email: string) {
const user = await db.user.findUnique({ where: { email } });
if (!user) {
// Hesabın var olup olmadığını ifşa etmeyin
return { message: "Bu e-posta mevcutsa, kurtarma talimatları gönderdik." };
}
const recoveryToken = randomBytes(32).toString("hex");
const tokenHash = createHash("sha256").update(recoveryToken).digest("hex");
await db.recoveryRequest.create({
data: {
userId: user.id,
tokenHash,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 saat
status: "pending",
},
});
// Kurtarma bağlantısı içeren e-posta gönderin
await sendEmail(email, {
subject: "Hesap Kurtarma Talebi",
body: `
Hesabınızda MFA'yı devre dışı bırakma talebi yapıldı.
Bu sizseniz, 24 saat sonra aşağıdaki bağlantıya tıklayın: ...
Bu siz DEĞİLSENİZ, lütfen şifrenizi hemen değiştirin.
`,
});
return { message: "Bu e-posta mevcutsa, kurtarma talimatları gönderdik." };
}Kimlik doğrulama size birinin kim olduğunu söyler. Yetkilendirme ise ne yapmalarına izin verildiğini söyler. Bunu yanlış yapmak haberlere çıkmanıza yol açar.
RBAC (Rol Tabanlı Erişim Kontrolü): Kullanıcıların rolleri vardır, rollerin izinleri vardır. Basit, akıl yürütmesi kolay, çoğu uygulama için çalışır.
// RBAC — doğrudan rol kontrolleri
type Role = "user" | "editor" | "admin" | "super_admin";
const ROLE_PERMISSIONS: Record<Role, string[]> = {
user: ["read:own_profile", "update:own_profile", "read:posts"],
editor: ["read:own_profile", "update:own_profile", "read:posts", "create:posts", "update:posts"],
admin: [
"read:own_profile",
"update:own_profile",
"read:posts",
"create:posts",
"update:posts",
"delete:posts",
"read:users",
"update:users",
],
super_admin: ["*"], // Joker karakterlerle dikkatli olun
};
function hasPermission(role: Role, permission: string): boolean {
const permissions = ROLE_PERMISSIONS[role];
return permissions.includes("*") || permissions.includes(permission);
}
// Bir API route'unda kullanım
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await auth();
if (!session?.user) {
return Response.json({ error: "Yetkisiz" }, { status: 401 });
}
if (!hasPermission(session.user.role as Role, "delete:posts")) {
return Response.json({ error: "Yasaklı" }, { status: 403 });
}
const { id } = await params;
await db.post.delete({ where: { id } });
return Response.json({ success: true });
}ABAC (Özellik Tabanlı Erişim Kontrolü): İzinler kullanıcının, kaynağın ve bağlamın özelliklerine bağlıdır. Daha esnek ama daha karmaşık.
// ABAC — RBAC yeterli olmadığında
interface PolicyContext {
user: {
id: string;
role: string;
department: string;
clearanceLevel: number;
};
resource: {
type: string;
ownerId: string;
classification: string;
department: string;
};
action: string;
environment: {
ipAddress: string;
time: Date;
mfaVerified: boolean;
};
}
function evaluatePolicy(context: PolicyContext): boolean {
const { user, resource, action, environment } = context;
// Kullanıcılar kendi kaynaklarını her zaman okuyabilir
if (action === "read" && resource.ownerId === user.id) {
return true;
}
// Adminler kendi departmanlarındaki herhangi bir kaynağı okuyabilir
if (action === "read" && user.role === "admin" && user.department === resource.department) {
return true;
}
// Gizli kaynaklar MFA ve minimum yetki seviyesi gerektirir
if (resource.classification === "confidential") {
if (!environment.mfaVerified) return false;
if (user.clearanceLevel < 3) return false;
}
// Yıkıcı eylemler mesai saatleri dışında engellenir
if (action === "delete") {
const hour = environment.time.getHours();
if (hour < 9 || hour > 17) return false;
}
return false; // Varsayılan reddet
}Bu, en önemli tek yetkilendirme ilkesidir: her güven sınırında izinleri kontrol edin, yalnızca arayüz seviyesinde değil.
// KÖTÜ — yalnızca bileşende kontrol
function DeleteButton({ post }: { post: Post }) {
const { data: session } = useSession();
// Bu düğmeyi gizler, ama silmeyi engellemez
if (session?.user?.role !== "admin") return null;
return <button onClick={() => deletePost(post.id)}>Sil</button>;
}
// AYRICA KÖTÜ — bir sunucu eyleminde kontrol ama API route'unda değil
async function deletePostAction(postId: string) {
const session = await auth();
if (session?.user?.role !== "admin") throw new Error("Yasaklı");
await db.post.delete({ where: { id: postId } });
}
// Bir saldırgan yine de POST /api/posts/123'ü doğrudan çağırabilir
// İYİ — her sınırda kontrol et
// 1. Arayüzde düğmeyi gizle (UX, güvenlik değil)
// 2. Sunucu eyleminde kontrol et (derinlemesine savunma)
// 3. API route'unda kontrol et (gerçek güvenlik sınırı)
// 4. İsteğe bağlı olarak, middleware'de kontrol et (route seviyesinde koruma)Arayüz kontrolü kullanıcı deneyimi içindir. Sunucu kontrolü güvenlik içindir. Asla yalnızca birine güvenmeyin.
Middleware, eşleşen her istekten önce çalışır. Kaba taneli erişim kontrolü için iyi bir yerdir:
// "Bu kullanıcının bu bölüme erişmesine izin veriliyor mu?"
// İnce taneli kontroller ("Bu kullanıcı BU gönderiyi düzenleyebilir mi?") route handler'da olmalıdır
// çünkü middleware istek gövdesine veya route parametrelerine kolayca erişemez.
export default auth((req) => {
const path = req.nextUrl.pathname;
const role = req.auth?.user?.role;
// Route seviyesinde erişim kontrolü
const routeAccess: Record<string, Role[]> = {
"/admin": ["admin", "super_admin"],
"/editor": ["editor", "admin", "super_admin"],
"/dashboard": ["user", "editor", "admin", "super_admin"],
};
for (const [route, allowedRoles] of Object.entries(routeAccess)) {
if (path.startsWith(route)) {
if (!role || !allowedRoles.includes(role as Role)) {
return NextResponse.redirect(new URL("/unauthorized", req.nextUrl));
}
}
}
return NextResponse.next();
});Bunlar gerçek kod tabanlarında en sık gördüğüm saldırılardır. Bunları anlamak zorunludur.
Saldırı: Bir saldırgan sitenizde geçerli bir session oluşturur, ardından kurbanı bu session ID'yi kullanmaya kandırır (örneğin, bir URL parametresi aracılığıyla veya bir alt alan adı üzerinden çerez ayarlayarak). Kurban giriş yaptığında, saldırganın session'ı artık kimliği doğrulanmış bir kullanıcıya sahiptir.
Çözüm: Başarılı kimlik doğrulamadan sonra session ID'yi her zaman yeniden oluşturun. Kimlik doğrulama öncesi session ID'nin kimlik doğrulama sonrası session'a taşınmasına asla izin vermeyin.
async function login(credentials: { email: string; password: string }, request: Request) {
const user = await verifyCredentials(credentials);
if (!user) throw new Error("Geçersiz kimlik bilgileri");
// KRİTİK: Eski session'ı sil ve yeni bir tane oluştur
const oldSessionId = getSessionIdFromCookie(request);
if (oldSessionId) {
await redis.del(`session:${oldSessionId}`);
}
// Yeni bir ID ile tamamen yeni bir session oluştur
const newSessionId = await createSession(user.id, request);
return newSessionId;
}Saldırı: Bir kullanıcı sitenize giriş yapmıştır. Sitenize istek yapan kötü amaçlı bir sayfayı ziyaret eder. Çerezler otomatik olarak gönderildiği için, istek kimliği doğrulanmıştır.
Modern çözüm: SameSite çerezler. SameSite: Lax ayarı (artık çoğu tarayıcıda varsayılan), çapraz origin POST isteklerinde çerezlerin gönderilmesini engeller ve bu çoğu CSRF senaryosunu kapsar.
// SameSite=Lax çoğu CSRF senaryosunu kapsar:
// - Çapraz origin POST, PUT, DELETE'de çerezleri engeller
// - Çapraz origin GET'te çerezlere izin verir (üst düzey navigasyon)
// Bu sorun değil çünkü GET isteklerinin yan etkileri olmamalıdır
cookieStore.set("session_id", sessionId, {
httpOnly: true,
secure: true,
sameSite: "lax", // Bu sizin CSRF korumanızdır
maxAge: 86400,
path: "/",
});JSON kabul eden API'ler için ek koruma ücretsiz gelir: Content-Type: application/json başlığı HTML formları tarafından ayarlanamaz ve CORS diğer origin'lerdeki JavaScript'in özel başlıklarla istek yapmasını engeller.
Daha güçlü garantilere ihtiyacınız varsa (örneğin, form gönderilerini kabul ediyorsanız), çift gönderimli çerez kalıbı veya senkronize edici token kullanın. Auth.js bunu sizin için ele alır.
Saldırı: Bir saldırgan, kimlik doğrulama sonrasında kendi sitesine yönlendiren bir OAuth callback URL'si hazırlar: https://sizinapp.com/callback?redirect_to=https://kotu.com/token-cali
Callback handler'ınız redirect_to parametresine körü körüne yönlendirirse, kullanıcı saldırganın sitesine ulaşır ve potansiyel olarak URL'de token'larla birlikte.
// GÜVENLİK AÇIĞI
async function handleCallback(request: Request) {
const url = new URL(request.url);
const redirectTo = url.searchParams.get("redirect_to") ?? "/";
// ... kullanıcının kimliğini doğrula ...
return Response.redirect(redirectTo); // https://kotu.com olabilir!
}
// GÜVENLİ
async function handleCallback(request: Request) {
const url = new URL(request.url);
const redirectTo = url.searchParams.get("redirect_to") ?? "/";
// Yönlendirme URL'sini doğrula
const safeRedirect = sanitizeRedirectUrl(redirectTo, request.url);
// ... kullanıcının kimliğini doğrula ...
return Response.redirect(safeRedirect);
}
function sanitizeRedirectUrl(redirect: string, baseUrl: string): string {
try {
const url = new URL(redirect, baseUrl);
const base = new URL(baseUrl);
// Yalnızca aynı origin'e yönlendirmelere izin ver
if (url.origin !== base.origin) {
return "/";
}
// Yalnızca yol yönlendirmelerine izin ver (javascript: veya data: URI'leri yok)
if (!url.pathname.startsWith("/")) {
return "/";
}
return url.pathname + url.search;
} catch {
return "/";
}
}Token'ları URL'lere koyarsanız (koymayın), kullanıcılar bağlantılara tıkladığında Referer başlığı aracılığıyla sızacaktır. Bu, GitHub dahil gerçek ihlallere neden olmuştur.
Kurallar:
Referrer-Policy: strict-origin-when-cross-origin (veya daha katı) ayarlayın// Next.js middleware veya layout'unuzda
const headers = new Headers();
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");Daha az bilinen bir saldırı: bazı JWT kütüphaneleri, doğrulayıcıya genel anahtarın nerede bulunacağını söyleyen bir jwk veya jku başlığını destekler. Bir saldırgan:
jwk başlığını kendi genel anahtarına işaret edecek şekilde ayarlayabilirKütüphaneniz jwk başlığından anahtarı körü körüne getirip kullanırsa, imza doğrulanır. Çözüm: token'ın kendi doğrulama anahtarını belirlemesine asla izin vermeyin. Her zaman kendi yapılandırmanızdaki anahtarları kullanın.
Yıllarca kimlik doğrulama sistemleri inşa ettikten sonra, bugün gerçekten kullandıklarım bunlar.
Bu, yeni projeler için varsayılan yığınımdır:
Session stratejisi veritabanı desteklidir (JWT değil), bu bana anında iptal ve basit session yönetimi sağlar.
// Yeni bir proje için tipik auth.ts dosyam
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import Passkey from "next-auth/providers/passkey";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: "database" },
providers: [
Google,
GitHub,
Passkey({
// Auth.js v5 yerleşik passkey desteğine sahiptir
// Bu, perde arkasında SimpleWebAuthn kullanır
}),
],
experimental: {
enableWebAuthn: true,
},
});Yönetilen bir auth sağlayıcısına şu durumlarda başvuruyorum:
Yönetilen sağlayıcıların ödünleşimleri:
jose, passkey'ler için @simplewebauthn/server, şifreler için bcrypt veya argon2 kullanıyorum. Asla el yapımı değil.crypto.randomBytes(32) kullanın. UUID v4 kabul edilebilir ama ham rastgele baytlardan daha az entropiye sahiptir.Bahsettiğimize göre — 2026'da şifreleri doğru şekilde nasıl hashlersiniz:
import { hash, verify } from "@node-rs/argon2";
// Argon2id önerilen algoritmadır
// Bunlar bir web uygulaması için makul varsayılanlar
async function hashPassword(password: string): Promise<string> {
return hash(password, {
memoryCost: 65536, // 64 MB
timeCost: 3, // 3 iterasyon
parallelism: 4, // 4 iş parçacığı
});
}
async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
try {
return await verify(hashedPassword, password);
} catch {
return false;
}
}Neden argon2id bcrypt'ten üstün? Argon2id bellek zorlayıcıdır, yani saldırmak yalnızca CPU gücü değil aynı zamanda büyük miktarlarda RAM da gerektirir. Bu, GPU ve ASIC saldırılarını önemli ölçüde daha pahalı hale getirir. bcrypt hala iyidir — kırılmamıştır — ama argon2id yeni projeler için daha iyi bir seçimdir.
Herhangi bir kimlik doğrulama sistemini yayınlamadan önce doğrulayın:
HttpOnly, Secure, SameSite=Lax veya Strictalg başlığına güvenme)Referrer-Policy başlığı ayarlanmışBu liste kapsamlı değildir, ama production sistemlerinde en sık gördüğüm güvenlik açıklarını kapsar.
Kimlik doğrulama, manzaranın sürekli geliştiği ama temellerin aynı kaldığı alanlardan biridir: kimliği doğrula, gereken minimum kimlik bilgilerini ver, her sınırda izinleri kontrol et ve ihlali varsay.
2026'daki en büyük değişim passkey'lerin ana akım olmasıdır. Tarayıcı desteği evrenseldir, platform desteği (iCloud Keychain, Google Şifre Yöneticisi) kullanıcı deneyimini sorunsuz hale getirir ve güvenlik özellikleri daha önce sahip olduğumuz her şeyden gerçekten üstündür. Yeni bir uygulama yapıyorsanız, passkey'leri birincil giriş yönteminiz yapın ve şifreleri yedek olarak ele alın.
İkinci en büyük değişim, kendi auth'unuzu yazmanın gerekçelendirilmesinin zorlaşmasıdır. Auth.js v5, Clerk ve benzeri çözümler zor kısımları doğru bir şekilde ele alır. Özel gidebilmenizin tek nedeni, gereksinimlerinizin gerçekten mevcut hiçbir çözüme uymamasıdır — ve bu, çoğu geliştiricinin düşündüğünden daha nadirdir.
Ne seçerseniz seçin, auth'unuzu bir saldırgan gibi test edin. Token'ları tekrar oynatmayı, imzaları sahtelemeyi, olmamanız gereken route'lara erişmeyi ve yönlendirme URL'lerini manipüle etmeyi deneyin. Lansmandan önce bulduğunuz hatalar, haberlere çıkmayan hatalardır.