hayrettin@dal:~$giriş →tr|en
blog/dotnet

JWT'yi localStorage'a Koymayın: Kendi Siteme Auth Kurarken Öğrendiklerim

Bu sitenin giriş sistemini kurarken verdiğim kararlar: token neden tarayıcıya hiç inmiyor, BFF deseni ne çözüyor ve middleware neden sadece kapıdaki görevli.

kapak görseli · 16:6

Bu sitenin giriş sistemini kurarken ilk refleksim herkesinkiyle aynıydı: login isteği at, dönen JWT'yi localStorage'a koy, her istekte header'a ekle. Üç satır kod, çalışıyor da. Sonra kendime şu soruyu sordum: bu token'ı benden başka kim okuyabilir?

# tehdit_modeli

Cevap rahatsız edici: sayfada çalışan her JavaScript. localStorage'ın XSS'e karşı hiçbir savunması yok — enjekte edilen tek satırlık bir script, token'ı alıp istediği yere gönderebilir. "Benim sitemde XSS mi olacak?" diyorsanız, saldırı yüzeyi sadece sizin yazdığınız kod değil: npm'den gelen her bağımlılık, onların bağımlılıkları, bir gün eklediğiniz üçüncü parti analytics script'i... Supply-chain saldırılarında ele geçirilen popüler paketlerin ilk yaptığı şeylerden biri localStorage'ı süpürmek.

httpOnly cookie ise adı üstünde: JavaScript'e görünmez. document.cookie yazdırın, orada yok. XSS hâlâ kötü bir gündür ama saldırgan token'ı çalamaz.

# desen_bff

Benim kurduğum yapı BFF (Backend-for-Frontend) deseninin sadeleştirilmiş hali. Kural tek cümle: token tarayıcıya hiç inmez.

BFF auth akışı: tarayıcı Next.js route handler üzerinden .NET API ile konuşur, JWT httpOnly cookie olarak saklanır

Giriş formu doğrudan .NET API'ye değil, Next.js'in kendi route handler'ına POST eder. Handler, API'den JWT'yi alır ve httpOnly cookie olarak yazar:

// app/api/auth/login/route.ts
export async function POST(req: Request) {
  const res = await fetch(`${API_BASE}/api/auth/login`, {
    method: "POST",
    body: JSON.stringify(await req.json()),
    headers: { "Content-Type": "application/json" },
  });
  if (!res.ok) return NextResponse.json({ message: "giriş başarısız" }, { status: 400 });

  const { token, role } = await res.json();
  const out = NextResponse.json({ role });
  out.cookies.set("hd-token", token, {
    httpOnly: true, secure: true, sameSite: "lax", maxAge: 60 * 60 * 12,
  });
  return out;
}

# sunucuda_bearer

Peki korumalı veriyi kim çekiyor? Server component'ler ve server action'lar. İkisi de sunucuda çalıştığı için cookie'yi okuyup API'ye Bearer olarak iletebiliyorlar — token yine tarayıcı JavaScript'ine değmeden:

// lib/server-api.ts (sunucuda çalışır)
import { cookies } from "next/headers";

export async function adminGet(path: string) {
  const token = cookies().get("hd-token")?.value;
  return fetch(`${API_BASE}${path}`, {
    headers: { Authorization: `Bearer ${token}` },
    cache: "no-store",
  });
}

# middleware_sadece_kapidaki_gorevli

Next.js middleware'i /panel gibi sayfaları koruyor ama bunu bir güvenlik sınırı sanmamak lazım. Middleware UX içindir: giriş yapmamış kullanıcıyı kibarca login'e yönlendirir. Asıl koruma .NET tarafında, her admin endpoint'indeki [Authorize(Roles = "Admin")]'de. Rol bilgisini tutan okunabilir bir cookie'yi kurcalasanız bile API 403 döner — kapıdaki görevliyi kandırabilirsiniz, kasadaki kilidi değil.

# kucuk_ama_onemli

Tek başına desen yetmiyor; şunlar da pakete dahil:

SameSite=Lax — cookie tabanlı auth'ta CSRF'i ciddiye alın; mutasyonlarım server action olduğu ve cross-site POST'larda cookie gitmediği için buradaki risk daralıyor. Secure — üretimde cookie sadece HTTPS ile taşınır. Rate limit — login endpoint'i dakikada birkaç denemeyle sınırlı; şifre deneme botlarının işini zorlaştırır. JWT anahtarı — koddaki varsayılan anahtarla üretime çıkmayı uygulama kendisi reddediyor: Jwt__Key env'den gelmezse Production'da hiç açılmıyor. En sevdiğim güvenlik önlemi, unutulması imkânsız olanı.

# ozet

JWT'nin yeri tarayıcı depolaması değil. Next.js kullanıyorsanız BFF kurmak yarım günlük iş: token'ı route handler'da httpOnly cookie'ye yazın, korumalı çağrıları sunucuda yapın, middleware'i UX için kullanın, asıl yetkiyi API'de doğrulayın. Bu sitede şu an okuduğunuz sayfa dahil her şey bu düzenle çalışıyor.

// diğer_yazılarls -la blog/ →