Exploring Infinite Possibilities on FrontEnd 🚀
Burak Sağlık
Search...

Sat Oct 10 2026

API Kimlik Doğrulama Yöntemleri: JWT, OAuth2, API Key, mTLS Karşılaştırma

API Kimlik Doğrulama Yöntemleri: JWT, OAuth2, API Key, mTLS Karşılaştırma

🎯 Giriş: Neden API Kimlik Doğrulama ve Yetkilendirme Bu Kadar Kritiktir?

Selam! 👋 Hazırsan başlayalım.

Günümüzde her şey API üzerinden konuşuyor: Single Page Applications, mobil uygulamalar, mikro servisler, hatta IoT cihazları. Her istek bir "kimsin?" sorusuyla başlıyor (veya başlamalı). İşte tam bu noktada kimlik doğrulama (authentication) ve yetkilendirme (authorization) ikinci düşünülen bir detay değil, mimarinin kemiklerinden biri olmalı.

Neden mi? İşte özetle:

  • 🔓 Tek bir hatalı token kontrolü = tüm kullanıcı verileriniz açıkta kalabilir
  • 💸 Ortalama bir veri ihlali maliyeti 2024 verisiyle $4.88 milyon (IBM Cost of a Data Breach Report)
  • ⚖️ Yasal yükümlülükler (KVKK, GDPR, HIPAA...) "bilmiyordum" demenize izin vermiyor
  • 🏗 Mikro servis dünyasında güvenlik servis servise yayılır — merkezi bir strateji yoksa her servis kendi başına bir güvenlik açığı olur

Bu yazıda ne yapacağız?

✅ Yaygın yöntemleri (JWT, OAuth 2.0, API Keys, Session/Cookie, mTLS) yan yana koyup avantaj/dezavantaj tablosu çıkaracağız
✅ Gerçek hayattan hata senaryoları üzerinden "şu yöntemi şu durumda kullanmayın" diyeceğiz
✅ Seçim rehberi ile projenize hangi kombinasyonun uygun olacağını karar vermenize yardımcı olacağız

Hadi yolculuğa çıkalım — kahvenizi alın, arka arkaya geliyoruz ☕🚀


🔁 Temel Kavramlar: Kimlik Doğrulama (Authentication) vs Yetkilendirme (Authorization) Arasındaki İnce Çizgi

Hazırsan bu iki kavramı, günlük hayattan bir benzetmeyle kafanızdan silinmeyecek hale getirelim 🎯


🏢 Ofis Benzetmesi: Kimlik Kartı mı, Anahtar mı?

Diyelim ki bir ofis binasına giriyorsun:

Adım Ne Oluyor? Teknik Karşılığı
Girişte güvenlik kimlik kartını isteyip yüzüne bakıyor "Sen kimsin?" sorusuna cevap veriyorsun Authentication (Kimlik Doğrulama) 🔐
İçeriye girdin, ama her odanın anahtarı sende yok "Ne yapabilirsin?" sorusuna cevap veriliyor Authorization (Yetkilendirme) 🔑

Özetle:

  • Authentication = Kim olduğununu kanıtlamak (Kimlik kartı 🪪)
  • Authorization = Ne yapmaya yetkili olduğun (Oda anahtarı 🗝️)

⚠️ Sık yapılan hata: "Login oldum, artık her şeye erişirim" demek. Hayır! Giriş yaptın (AuthN), ama hangi verilere erişebileceğin başkası karar veriyor (AuthZ).


🧩 OAuth2 / OIDC / JWT Dünyasındaki Ana Oyuncular

Bu protokoller konuşurken sürekli duyacaksın terimler. Hadi tek tek tanıyalım 👇

1. Identity Provider (IdP / Kimlik Sağlayıcı) 🏛️

  • Görevi: Kullanıcının kimliğini doğrular, "Evet bu kişi kimse" der.
  • Örnekler: Google, Microsoft Entra ID (eski Azure AD), Auth0, Keycloak, GitHub.
  • JWT/OIDC bağlamı: id_token'ı imzalayan taraf budur. Token'ın güvenilirliğini IdP sağlar.

2. Resource Server (Kaynak Sunucusu) 📦

  • Görevi: Korumalı verileri (API'ler, dosyalar, veritabanları) barındırır.
  • Davranış: Gelen access_token'ı doğrular → Scope/claim'lere bakar → İzin ver/reddet.
  • Örnek: GET /api/users/123/profile isteğini alan backend servisin.

3. Client (İstemci / Uygulama) 📱

  • Görevi: Kullanıcı adına kaynak sunucusuna erişmek isteyen uygulama.
  • Türleri:
    • Confidential Client (backend, secret saklayabilir 🤫)
    • Public Client (SPA, mobil app, secret saklayamaz 🔓)
  • OAuth2'de: client_id ve client_secret ile tanınır.

4. Scope (Kapsam / İzin Paketi) 🎫

  • Ne? "Bu token ne iş yapmaya yetkili?" sorusunun cevabı.
  • Format: Genelde read:users, write:posts, openid profile email gibi boşlukla ayrılmış string'ler.
  • Önemli: Scope bağlamdan bağımsız bir izin şablonudur. Runtime'ta claim'e dönüşür.

5. Claim (İddia / Nitellik) 🏷️

  • Ne? Token içinde key-value çifti olarak taşınan gerçek veri.
  • Örnekler:
    {
      "sub": "user-123",
      "email": "ayse@ornek.com",
      "role": "admin",
      "scope": "read:users write:posts"
    }
    
  • Nereden gelir? IdP, kullanıcı hakkında bildiği her şeyi claim olarak koyabilir.
  • Resource Server bu claim'leri okuyup karar verir: "role=admin varsa → silme izni ver".

🔗 Bu Terimler Arası İlişki Nasıl İşler?

Basit bir akış:

sequenceDiagram
    User->>Client: Uygulamayı açar
    Client->>IdP: "Kullanıcıyı yönlendir (login sayfası)"
    IdP->>User: Kimlik doğrular (AuthN) ✅
    IdP->>Client: authorization_code / token döner
    Client->>Resource Server: access_token ile API çağırır
    Resource Server->>Resource Server: Token doğrular, claim/scope kontrol eder (AuthZ) 🔍
    Resource Server-->>Client: Veri / 403 Forbidden

Bu sayede ne oluyor?

  • IdP kimlik işinden sorumlu → Single Sign-On (SSO) olur 🎉
  • Resource Server kullanıcı şifresini asla görmez → Güvenlik artar 🛡️
  • Scope/Claim sayesinde fine-grained (ince taneli) yetki kontrolü yaparsın 🎯

💡 Pratik İpucu: Token'ı Açarak Claim'lere Bak

JWT'yi base64url decode edersen (imzayı doğrulamadan sadece okursan), içindeki claim'leri görebilirsin. Geliştirme ortamında jwt.io gibi araçlarla test et — ama production'da asla client-side decode etme, her zaman server-side imza doğrulaması yap! ⛔


Hadi şimdi bu kavramları somut bir OAuth2 Authorization Code Flow örneğinde canlandıralım... 🚀


🛠 Yöntem 1: Session / Cookie Tabanlı Kimlik Doğrulama (Geleneksel ve Hala Geçerli)

Hazırsan bu yöntemi biraz daha yakından inceleyelim. Session/Cookie mantığı, web'in erken yıllarından beri hayatımızda ve hâlâ çok fazla sistemin omurgası durumunda 🎯

Ne oluyor arka planda?

Kullanıcı giriş yaptığında (login), sunucu bir oturum (session) oluşturur ve bu oturuma ait verileri — kullanıcı ID'si, roller, vb. — sunucu tarafında tutar (genelde memory, Redis veya veritabanında). Ardından bu oturumun kimliğini (session ID) bir cookie olarak tarayıcıya gönderir.

Tarayıcı bu cookie'yi saklar ve sonradaki her istekte Cookie header'ı ile sunucuya geri yollar. Sunucu da gelen session ID'yi kullanarak oturumu bulur: "Ah, bu kullanıcı zaten giriş yapmış, tekrar şifre sormama gerek yok" der.

Kısa özetle:

  1. Kullanıcı login olur → Sunucu session oluşturur
  2. Sunucu Set-Cookie header'ı ile session ID'yi tarayıcıya verir
  3. Tarayıcı cookie'yi saklar
  4. Her istekte cookie otomatik gider → Sunucu session'ı bulur → Kimlik doğrulanmış sayılır

Cookie ayarları — güvenlik için çok önemli ❗️

Cookie'yi yollarken şu bayrakları mutlaka set etmelisin:

Bayrak Ne işe yarar?
HttpOnly JavaScript (document.cookie) ile okunamaz — XSS koruması ✅
Secure Sadece HTTPS üzerinden gönderilir — MITM koruması 🔒
SameSite: 'lax' (veya 'strict') CSRF saldırılarını zorlaştırır — cross-site isteklerde cookie gitmez 🛡

Not: SameSite: 'none' kullanacaksan Secure: true zorunlu — aksi takdirde tarayıcı cookie'yi reddeder.

Avantajları 🎯

  • Basitlik: Mantık çok net, stateful, anlaşıldığında debugging kolay
  • Anında iptal: Sunucuda session'ı silersen (veya Redis'ten atarsan) kullanıcı anında logout olur — token tabanlı sistemlerde bu refresh token rotasyonu vs. ile uğraşırsın
  • Hassas veriler sunucuda kalır: Cookie'de sadece ID var, payload değil

Dezavantajları ⚠️

  • Ölçeklenebilirlik: Sunucu state tutuyor → birden fazla instance'ın varsa paylaşımlı session store (Redis, DB) zorunlu
  • CSRF riski: Cookie otomatik gider → SameSite + CSRF token (double submit cookie pattern) ile korunmalısın
  • Çapraz domain (cross-domain) zorluğu: Farklı domainler/portlar arası cookie paylaşımı SameSite ve CORS politikalarıyla kısıtlı
  • SPA & Mobil için eziyet: Native mobil uygulamalarda cookie jar yönetmek, SPA'larda ise credentials: 'include' + CORS preflight darlıkları çıkarıyor 😅

Kısaca: Monolith veya az sayıda servisiniz varsa, Redis'iniz hazırsa bu yöntem hala mükemmel bir seçim. Ama mikro servis mimarisinde, çoklu domainde veya native mobil-firstseniz JWT tarafına kaymak daha rahat olabilir.


Hadi pratik bir örnek üzerinden anlayalım 🛠

Aşağıda Express.js + express-session ile basit bir session middleware kurulumu, cookie ayarları ve login/logout route'ları var. Kodun üzerindeki yorumları okuyorsan niye ne yaptığını anlarsın.

const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default; // Redis store (prod için)
const { createClient } = require('redis');

const app = express();

// 1. Body parser (JSON ve form verisi için)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 2. Redis client (production'da ayrı config dosyasından alınır)
const redisClient = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' });
redisClient.connect().catch(console.error);

// 3. Session middleware kurulumu
app.use(
  session({
    store: new RedisStore({ client: redisClient }), // Sunucu tarafında state: Redis
    secret: process.env.SESSION_SECRET || 'super-gizli-anahtar-buraya-envden-gelsin', // Cookie'yi imzalamak için
    name: 'sid', // Cookie ismi (default: connect.sid) — fingerprinting'i zorlaştırır
    resave: false, // Değişmemiş session'ı tekrar kaydetme
    saveUninitialized: false, // Boş session oluşturma (GDPR dostu)
    cookie: {
      httpOnly: true,        // JS erişemez — XSS koruması ✅
      secure: process.env.NODE_ENV === 'production', // Sadece HTTPS'te gönder — prod'da true ✅
      sameSite: 'lax',       // CSRF koruması — 'strict' de olabilir 🛡
      maxAge: 1000 * 60 * 60 * 24, // 1 gün (ms cinsinden)
    },
  })
);

// 4. Basit bir "kullanıcı veritabanı" (örnek için memory)
const users = new Map();
users.set('ali@ornek.com', { id: 1, email: 'ali@ornek.com', password: '123456', name: 'Ali' }); // Gerçekte hash'lenmiş!

// 5. Login route
app.post('/login', (req, res) => {
  const { email, password } = req.body;

  const user = users.get(email);
  if (!user || user.password !== password) {
    return res.status(401).json({ error: 'Geçersiz email veya şifre' });
  }

  // Session'a kullanıcı bilgisini yaz
  req.session.userId = user.id;
  req.session.userEmail = user.email;

  // Session kaydedildikten sonra yanıt dön (express-session auto-save eder ama emin olmak için)
  req.session.save((err) => {
    if (err) return res.status(500).json({ error: 'Session kaydedilemedi' });
    return res.json({ message: 'Giriş başarılı', user: { id: user.id, email: user.email, name: user.name } });
  });
});

// 6. Logout route
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: 'Çıkış yapılamadı' });
    // Cookie'yi de temizle (tarayıcıdan sil)
    res.clearCookie('sid', { httpOnly: true, secure: true, sameSite: 'lax' });
    return res.json({ message: 'Başarıyla çıkış yapıldı' });
  });
});

// 7. Korumalı bir route örneği (middleware ile)
function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Giriş yapmanız gerekiyor' });
  }
  next();
}

app.get('/profil', requireAuth, (req, res) => {
  res.json({
    message: 'Hoş geldin!',
    userId: req.session.userId,
    email: req.session.userEmail,
  });
});

// Sunucuyu başlat
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`🚀 Server http://localhost:${PORT} adresinde çalışıyor`);
});

Bu kodda ne oluyor? 🤔

  1. RedisStore ile session'lar sunucuda (Redis'de) tutuluyor — birden fazla instance'a scale edersen session'lar kaybolmuyor
  2. cookie objesinde HttpOnly, Secure, SameSite bayrakları set edilmiş — bu production standartları
  3. req.session.userId yazdığında express-session arka planda session'ı güncelliyor ve cookie'yi tarayıcıya Set-Cookie ile yolluyor
  4. /logout route'unda req.session.destroy() ile sunucu tarafı state siliniyor VE res.clearCookie() ile tarayıcıdaki cookie de temizleniyor — çift güvence
  5. requireAuth middleware'i ile korumalı rotalar kontrol ediliyor — session yoksa 401

Küçük bir hatırlatma 💡

Geliştirme ortamında secure: true yaparsan localhost (HTTP) üzerinde cookie gönderilmez — tarayıcı reddeder. Bu yüzden process.env.NODE_ENV === 'production' kontrolü ile sadece prod'da true yapıyoruz. Local'de false kalsın (veya lhost/localhost için Chrome'da "Insecure origins treated as secure" ayarını açarsın).


Özetle: Session/Cookie yöntemi, doğru yapılandırıldığında (HttpOnly + Secure + SameSite + Redis) güvenli, basit ve kontrol edilebilir bir kimlik doğrulama sağlıyor. SPAs ve mobilde biraz daha dikkat (CORS, credentials, SameSite) istiyor ama yine de birçok projede tercih edilen ilk seçenek olmaya devam ediyor ✅


🛠 Yöntem 2: JWT (JSON Web Token) - Stateless Kimlik Doğrulamanın Kralı

Hazırsan JWT’nin iç yapısını parçalayalım, Access/Refresh Token stratejisini konuşalım ve güvenlik tuzaklarını (algorithm confusion, token sızıntısı) vurgulayalım. Sonunda jsonwebtoken ile RS256 imzalı token üretimi, doğrulama middleware’i ve refresh token rotasyonu mantığını görelim. 🚀

JWT’nin üç parçası

JWT, nokta (.) ile birleştirilmiş Base64URL encoded üç kısımdan oluşur:

  • Header → Algoritma ve token tipi (alg, typ)
  • Payload → Claim’ler (sub, exp, iat, custom)
  • Signature → Header + Payload + secret/private key ile imzalanmış hali
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9   // Header
.                                      // ayırıcı
eyJzdWIiOiIxMjM0IiwibmFtZSI6IkpvaG4iLCJleHAiOjE3MDAwMDAwMDB9   // Payload
.                                      // ayırıcı
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c   // Signature

Ne oluyor burada?

  • Header ve Payload açıkça okunabilir (sadece Base64).
  • Signature, kimlik doğrulama ve bütünlük sağlar.

Access Token vs Refresh Token 🎯

Özellik Access Token Refresh Token
Ömür Kısa (5‑15 dk) Uzun (günler/haftalar)
Kullanım Her API isteğinde Authorization: Bearer <access> Access token dolduğunda /auth/refresh endpoint’ine gönderilir
Depolama Memory (JS değişkeni) → XSS riski az httpOnly + Secure + SameSite=Strict cookie → CSRF koruması gerekir
İptal Kısa ömür sayesinde doğal olarak dolar Sunucuda blocklist veya rotasyon ile iptal edilebilir

Tavsiyem: Access token memory’de tut, Refresh token httpOnly cookie’de sakla. Böylece XSS ile access token çalınırken bile refresh token güvenli kalır. 🔐


Algorithm Confusion & Token Sızıntısı ❗️

  • alg: none → İmza doğrulaması yok! Eski kütüphanelerde verify(token, '', { algorithms: ['none'] }) çalışır. Asla none kabul etme.
  • HS256 vs RS256 karışıklığı → Public key’i secret gibi kullanırsan HS256 imzası doğrulanır (public key bilinen bir string). Her zaman algorithms: ['RS256'] (veya ES256) belirt.
  • Token sızıntısı → Access token loglarda, URL’lerde, front‑end state’inde görünürse kötü niyetli kişi süresi dolana kadar API’yi kullanır. Kısa ömür + HTTPS + log temizliği şart.

jsonwebtoken ile RS256 Imzalama & Doğrulama 🛠

Not: Private/public key çiftini openssl ile üretip PRIVATE_KEY / PUBLIC_KEY env değişkenlerine koy.

// auth/jwt.js
const jwt = require('jsonwebtoken');
const fs = require('fs');

// Private / Public key dosyalarını oku (PEM formatında)
const PRIVATE_KEY = fs.readFileSync(process.env.PRIVATE_KEY_PATH, 'utf8');
const PUBLIC_KEY  = fs.readFileSync(process.env.PUBLIC_KEY_PATH,  'utf8');

/**
 * Access token üretir (RS256, 10 dk ömür)
 */
function generateAccessToken(user) {
  return jwt.sign(
    { sub: user.id, roles: user.roles },
    PRIVATE_KEY,
    { algorithm: 'RS256', expiresIn: '10m' }
  );
}

/**
 * Refresh token üretir (RS256, 7 gün ömür)
 * Rotasyon için `tokenVersion` claim’i eklenir.
 */
function generateRefreshToken(user) {
  return jwt.sign(
    { sub: user.id, tokenVersion: user.tokenVersion },
    PRIVATE_KEY,
    { algorithm: 'RS256', expiresIn: '7d' }
  );
}

/**
 * Access token doğrulama middleware
 */
function verifyAccessToken(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token eksik' });
  }
  const token = authHeader.split(' ')[1];
  try {
    const payload = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });
    req.user = payload;          // { sub, roles, iat, exp }
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Geçersiz veya süresi dolmuş token' });
  }
}

module.exports = { generateAccessToken, generateRefreshToken, verifyAccessToken };

Nasıl çalışıyor?

  1. generateAccessToken → Kısa ömürlü, RS256 imzalı access token döner.
  2. generateRefreshToken → Uzun ömürlü, tokenVersion ile rotasyon sağlar.
  3. verifyAccessToken → Her istekte Authorization header’ından token alır, public key ile doğrular, req.user’a koyar.

Refresh Token Rotasyonu & İptal Stratejileri 🔁

Strateji Nasıl Uygulanır? Avantaj
Blocklist (denylist) Veritabanında revokedTokens tablosu tut, her istekte kontrol et. Anlık iptal mümkün.
Short Expiry Access token 5‑15 dk, refresh token 1‑7 gün. Token çalınsa bile pencere dar.
Refresh Token Rotation Her /auth/refresh çağrısında yeni refresh token ver, eskisini revoke et (tokenVersion artır). Çalınan refresh token tek kullanımlık olur.
Device Fingerprint / IP Binding Refresh token payload’ına deviceId/ip ekle, doğrulamada eşleştir. Farklı cihazda kullanım engellenir.

Basit rotasyon mantığı (pseudo‑kod):

// routes/auth.js
router.post('/refresh', async (req, res) => {
  const refreshToken = req.cookies.refreshToken; // httpOnly cookie
  if (!refreshToken) return res.sendStatus(401);

  try {
    const payload = jwt.verify(refreshToken, PUBLIC_KEY, { algorithms: ['RS256'] });
    const user = await User.findById(payload.sub);
    if (!user || user.tokenVersion !== payload.tokenVersion) {
      // Token rotasyonu bozulmuş → tüm oturumları iptal et
      await user.updateOne({ tokenVersion: user.tokenVersion + 1 });
      return res.sendStatus(401);
    }

    // Yeni token çifti üret
    const newAccess  = generateAccessToken(user);
    const newRefresh = generateRefreshToken(user);

    // Yeni refresh token'ı httpOnly cookie olarak set et
    res.cookie('refreshToken', newRefresh, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000 // 7 gün
    });

    res.json({ accessToken: newAccess });
  } catch (err) {
    res.sendStatus(401);
  }
});

Özet:

  • RS256 + public key doğrulama → algorithm confusion önlenir.
  • Access token memory, refresh token httpOnly cookie → XSS/CSRF dengesi.
  • Kısa ömür + rotasyon + blocklist → token sızıntısı riski minimize edilir.

Bu yapıyı projenize entegre edince stateless kimlik doğrulamanın hem performanslı hem de güvenli olduğunu göreceksiniz. ✅


🔁 Yöntem 3: OAuth 2.0 ve OpenID Connect (OIDC) - Yetkilendirme ve Kimlik Katmanı

OAuth 2.0 yetkilendirme (authorization) framework’üdür; kullanıcının verilerine erişim izni verir ama kimlik doğrulaması (authentication) yapmaz.
OpenID Connect (OIDC), OAuth 2.0’nun üzerine ID Token (JWT) ekleyerek bu eksikliği giderir → artık kim olduğunu da biliyoruz 🎯.

Neden PKCE ile Authorization Code Flow?

  • SPA ve mobil uygulamalar için altın standart ✅
  • client_secret’i tarayıcıya/telefona koymak risklidir → PKCE (Proof Key for Code Exchange) bu sorunu çözer.
  • code_verifier (rastgele) → code_challenge (SHA‑256 + base64url) üretilir.
  • Yetkilendirme sunucusu challenge’ı saklar, token isteğinde verifier’ı doğrular.

Diğer yaygın akışlar (kısaca)

  • Client Credentials Flow 🤖 – machine‑to‑machine (servis‑servis) erişimi, kullanıcı yok.
  • Device Authorization Flow 📺 – TV, CLI, IoT cihazlarında kullanıcı kod girerek yetkilendirme.

Önemli endpoint’ler

Endpoint Ne işe yarar?
Authorization Endpoint Kullanıcıyı yönlendirir, consent alır
Token Endpoint authorization_code → access_token / refresh_token / id_token
Introspection Endpoint Token’ın geçerliliğini, scopes’larını sorgular
Revocation Endpoint Access/refresh token’ı iptal eder

🛠 Pratik: PKCE parametreleriyle authorize URL + token exchange (JavaScript – fetch)

Önce yardımcı fonksiyonlar: rastgele code_verifier ve ondan code_challenge üretimi.

// crypto.subtle tarayıcıda doğrudan çalışır, Node’da `crypto` modülü kullanılabilir
async function generatePKCE() {
  // 43‑128 karakter arası rastgele verifier
  const verifier = Array.from(crypto.getRandomValues(new Uint8Array(32)))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');

  // SHA‑256 hash → base64url (padding yok)
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');

  return { verifier, challenge };
}

Authorize URL oluşturma – kullanıcıyı bu URL’e yönlendirirsin.

async function buildAuthUrl(config) {
  const { verifier, challenge } = await generatePKCE();
  // verifier’ı sessionStorage/localStorage’a sakla (token isteğinde lazım)
  sessionStorage.setItem('pkce_verifier', verifier);

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: config.clientId,
    redirect_uri: config.redirectUri,
    scope: 'openid profile email offline_access', // offline_access → refresh_token
    code_challenge: challenge,
    code_challenge_method: 'S256',
    state: crypto.randomUUID() // CSRF koruması
  });

  return `${config.authEndpoint}?${params.toString()}`;
}

Token endpoint’e POST – authorization_code ile access/refresh/id token al.

async function exchangeCodeForTokens(config, authCode) {
  const verifier = sessionStorage.getItem('pkce_verifier');
  if (!verifier) throw new Error('PKCE verifier bulunamadı!');

  const body = new URLSearchParams({
    grant_type: 'authorization_code',
    code: authCode,
    redirect_uri: config.redirectUri,
    client_id: config.clientId,
    code_verifier: verifier
  });

  const resp = await fetch(config.tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body
  });

  if (!resp.ok) {
    const err = await resp.json();
    throw new Error(`Token hatası: ${err.error_description || err.error}`);
  }

  const tokens = await resp.json(); // { access_token, refresh_token, id_token, expires_in, token_type }
  // id_token JWT → parse edip kullanıcı bilgilerini alabilirsin
  return tokens;
}

Nasıl çalışıyor?

  1. generatePKCE() → verifier (gizli) + challenge (herkese açık).
  2. buildAuthUrl() → kullanıcıyı yetkilendirme sunucusuna gönderir, challenge query’de gider.
  3. Kullanıcı onaylar → sunucu code ile redirect_uri’ne döner.
  4. exchangeCodeForTokens() → code + saklanan verifier gönderir → sunucu challenge‑verifier eşleşirse token seti döner.

Bu akış güvenli, standart ve tarayıcı/mobil uyumludur 🚀.


Şimdi dilersen refresh_token ile token yenileme veya introspection/revocation endpoint’lerini nasıl çağıracağımızı görelim…


🛠 Yöntem 4: API Key - Basitlik ve Makine'den Makineye Haberleşme

API Key, servisler arası iletişimde en basit ve yaygın kullanılan kimlik doğrulama yöntemidir. Kullanıcı bağlamı yok, sadece makine makineye konuşuyor — bu yüzden token değiştirme, refresh flow'u vs. yok. Sadece bir anahtar, bir istek.

Ne oluyor burada? 🎯

Client, her istekte bir API-Key header'ı (veya query param) gönderir. Sunucu bu anahtarı doğrular, geçerliyse isteği işler. Çok basit, çok hızlı — ama güvenlik ağırlığı tamamen anahtarın gizliliğine bağlı.

GET /api/v1/users
API-Key: sk_live_abc123xyz789

Veya query param olarak (daha az tercih edilen, loglarda kalabilir):

GET /api/v1/users?api_key=sk_live_abc123xyz789

Best practice: Header kullanın. Loglama, proxy'ler, tarayıcı geçmişi vs. query param'ları yakalar.


Avantajlar ✅

  • Kurulum sıfır — JWT gibi imzalama, doğrulama, expiry yönetimi yok
  • Düşük overhead — Sadece bir string karşılaştırması (hash'liyse hash karşılaştırması)
  • M2M için ideal — CI/CD pipeline'lar, mikro servisler, scheduled job'lar
  • Stateless — Sunucu tarafında session tutmaya gerek yok

Dezavantajlar ⛔

Sorun Neden ciddi?
Revocation zordur Anahtarı iptal etmek için DB'den silmek/ güncellemek gerekir; anında yayılmaz
Scope kısıtlı Genellikle "tüm yetki" veya "hiç yetki" — granular izinler JWT/claims kadar esnek değil
Sızıntı riski .env dosyası commitlanır, loglarda görünür, Slack'e düşer → herkes erişir
Replay saldırısına açık Anahtar çalınırsa, atacak hiçbir şey yok (nonce, timestamp, signature yok)
Rotation zor Eski anahtarı kill etmeden yenisini dağıtmak = pencere açığı

Best Practice'ler: API Key'i Nasıl Yaşatırız? 🛠

  1. Prefix koyun — sk_live_, sk_test_, pk_ gibi. Hangi ortam, hangi tip anahtar anında belli olur.
  2. Hash'leyerek saklayın — Veritabanında plain text asla. bcrypt / argon2 / scrypt kullanın. Key sızsa bile saldırgan hash'i geri çeviremez.
  3. Rotation stratejisi — 90 günde bir zorunlu yenileme, eski anahtarı 24-48 saat grace period ile kapatın.
  4. Rate limiting — Anahtar başına dakikada X istek. Brute-force ve abuse'i engeller.
  5. IP allowlist (opsiyonel) — Hassas servislerde anahtar + IP kombinasyonu.
  6. Audit log — Her başarısız/başarılı deneme loglansın (PII içermez).

Kod Örneği: Hash'li Doğrulama + Rate Limiting Middleware 🧩

Node.js + Express + bcrypt + express-rate-limit ile pratik bir implementasyon:

// middleware/apiKeyAuth.js
const bcrypt = require('bcrypt');
const rateLimit = require('express-rate-limit');

// 1️⃣ Rate limiter: her API key için dakikada max 100 istek
const apiKeyLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 dakika
  max: 100,
  keyGenerator: (req) => req.headers['api-key'] || req.ip, // key yoksa IP'ye fallback
  message: { error: 'Rate limit aşıldı, biraz bekleyin 😅' },
  standardHeaders: true,
  legacyHeaders: false,
});

// 2️⃣ Anahtar doğrulama middleware
async function apiKeyAuth(req, res, next) {
  const providedKey = req.headers['api-key'];

  if (!providedKey) {
    return res.status(401).json({ error: 'API-Key header eksik ❌' });
  }

  // Prefix kontrolü (opsiyonel ama iyi pratik)
  if (!providedKey.startsWith('sk_live_') && !providedKey.startsWith('sk_test_')) {
    return res.status(401).json({ error: 'Geçersiz API Key formatı' });
  }

  try {
    // 3️⃣ DB'den hash'li key'i çek (örnek: Prisma / Sequelize / raw query)
    // const stored = await db.apiKey.findUnique({ where: { prefix: providedKey.slice(0, 12) } });
    // Burada mock data ile gösteriyoruz:
    const stored = {
      id: 'key_123',
      keyHash: '$2b$10$AbCdEfGhIjKlMnOpQrStUvWxYz...', // bcrypt hash
      scopes: ['read:users', 'write:orders'],
      isActive: true,
      rateLimit: 100,
    };

    if (!stored || !stored.isActive) {
      return res.status(401).json({ error: 'API Key geçersiz veya pasif' });
    }

    // 4️⃣ Hash karşılaştırma — timing attack'e karşı bcrypt güvenlidir
    const isValid = await bcrypt.compare(providedKey, stored.keyHash);

    if (!isValid) {
      // Güvenlik logu: başarısız deneme
      console.warn(`[API Key] Invalid attempt: ${providedKey.slice(0, 12)}...`);
      return res.status(401).json({ error: 'API Key hatalı' });
    }

    // 5️⃣ Başarılı — request'e key meta verilerini ekle
    req.apiKey = {
      id: stored.id,
      scopes: stored.scopes,
    };

    next();
  } catch (err) {
    console.error('[API Key] Doğrulama hatası:', err);
    res.status(500).json({ error: 'Sunucu hatası' });
  }
}

module.exports = { apiKeyAuth, apiKeyLimiter };

Kullanım (route'larda):

// routes/users.js
const express = require('express');
const { apiKeyAuth, apiKeyLimiter } = require('../middleware/apiKeyAuth');

const router = express.Router();

// Tüm user route'larına rate limit + auth uygula
router.use(apiKeyLimiter);
router.use(apiKeyAuth);

router.get('/', (req, res) => {
  // req.apiKey.scopes içinde 'read:users' var mı kontrol et (scope middleware ayrı yazılabilir)
  res.json({ message: 'Kullanıcı listesi', keyId: req.apiKey.id });
});

module.exports = router;

Bu sayede ne oluyor? 🤔

Adım Ne sağlar?
Prefix kontrolü Yanlış tip anahtar (test vs live) erken engellenir
bcrypt compare DB sızsa bile plain keyler güvenli — timing attack'e karşı da dayanıklı
Rate limiter keyGenerator Her anahtar için bağımsız limit — bir client diğerini engelleyemez
req.apiKey Downstream middleware / controller'larda scope kontrolü, audit log için hazır

Not: Production'da express-rate-limit yerine Redis-backed limiter (rate-limiter-flexible veya custom Redis lua script) kullanın. Memory store cluster'da paylaşımaz.


Özetle 📌

API Key basitlik king'i — ama güvenlik yükü sizde. Hash'leyin, rate limit koyun, rotation planlayın, loglayın. M2M için JWT'den daha pratik, ama public client'lara (SPA, mobile) asla vermeyin — orada OAuth/OIDC lazım.

Sonraki bölümde JWT ile stateless auth'a dalıyoruz — claim'ler, refresh token rotation, JWKS endpoints... Hazırsan hadi oraya! 🚀


🛠 Yöntem 5: mTLS (Mutual TLS) - Zero Trust ve Service Mesh'in Kemeri

mTLS, her iki tarafın da sertifika ile kimliğini kanıtladığı TLS varyantıdır.
Zero Trust prensibinde "hiç güvenme, her zaman doğrula" mottosunun teknik uygulamasını sunar 🎯.

Neden mTLS?

  • İki yönlü kimlik doğrulama: Client ve server birbirlerini doğrular.
  • Ağ seviyesinde şifreleme: Veri yolda görünmez.
  • Servis meshes (Istio, Linkerd) otomatik sertifika yaşam döngüsünü yönetir 🔁.

Sertifika Yönetimi Zorlukları ❗️

Sorun Açıklama
CA dağıtımı Her servisin güvenilen kök CA'ya erişimi olmalı.
Rotation Sertifikalar periyodik yenilenmeli (genelde 24‑48 sa).
Revocation CRL/OCSP ile iptal edilen sertifikaları takip etmek ekstra altyapı ister.
Ölçek Binlerce pod için manuel yönetim imkansız.

Pratik ipucu: Service Mesh araçları (Istio Citadel, Linkerd Trust Anchor) bu işleri otomatik yapar. Siz sadece PeerAuthentication: STRICT ayarını açarsınız ✅.


Go ile Basit Mutual TLS Sunucu / İstemci 🛠

Aşağıdaki iki blok, CA, sunucu ve istemci sertifikalarını kullanarak doğrulama yapar.
Kodları çalıştırmadan önce ca.pem, server.pem, server.key, client.pem, client.key dosyalarınızın hazır olduğundan emin olun.

Sunucu (server.go)

package main

import (
	"crypto/tls"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	// CA sertifikasını yükle (istemci doğrulaması için)
	caCert, err := tls.LoadX509KeyPair("ca.pem", "ca.key") // sadece CA cert yeterli, key opsiyonel
	if err != nil {
		log.Fatalf("CA yüklenemedi: %v", err)
	}
	caPool := tls.NewCertPool()
	caPool.AppendCertsFromPEM([]byte(string(caCert.Certificate[0])))

	// Sunucu sertifikası ve anahtarı
	serverCert, err := tls.LoadX509KeyPair("server.pem", "server.key")
	if err != nil {
		log.Fatalf("Sunucu sertifikası yüklenemedi: %v", err)
	}

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{serverCert},
		ClientAuth:   tls.RequireAndVerifyClientCert, // 🔑 mutual auth
		ClientCAs:    caPool,
		MinVersion:   tls.VersionTLS12,
	}

	server := &http.Server{
		Addr:      ":8443",
		TLSConfig: tlsConfig,
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			fmt.Fprintln(w, "Merhaba, güvenli dünyaya hoş geldiniz! 🎉")
		}),
	}

	log.Println("mTLS sunucusu 8443 portunda dinleniyor...")
	log.Fatal(server.ListenAndServeTLS("", "")) // sertifikalar config'te verildi
}

İstemci (client.go)

package main

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
)

func main() {
	// CA cert'i yükle (sunucuyu doğrulamak için)
	caPem, err := os.ReadFile("ca.pem")
	if err != nil {
		log.Fatalf("CA okunamadı: %v", err)
	}
	caPool := x509.NewCertPool()
	caPool.AppendCertsFromPEM(caPem)

	// İstemci sertifikası ve anahtarı (sunucuya kanıtlamak için)
	clientCert, err := tls.LoadX509KeyPair("client.pem", "client.key")
	if err != nil {
		log.Fatalf("İstemci sertifikası yüklenemedi: %v", err)
	}

	tlsConfig := &tls.Config{
		RootCAs:      caPool,
		Certificates: []tls.Certificate{clientCert},
		MinVersion:   tls.VersionTLS12,
	}

	client := &http.Client{
		Transport: &http.Transport{TLSClientConfig: tlsConfig},
	}

	resp, err := client.Get("https://localhost:8443")
	if err != nil {
		log.Fatalf("İstek hatası: %v", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Printf("Sunucu cevabı: %s\n", string(body))
}

Ne oluyor burada?

  • Sunucu ClientAuth: RequireAndVerifyClientCert ile istemci sertifikasını zorunlu kılar.
  • İstemci RootCAs ile sunucu sertifikasını doğrular, Certificates ile kendi kimliğini sunar.
  • Her iki taraf da aynı CA'ya güvenir ➡️ Zero Trust ağınızın temel taşı oluşur.

Kubernetes / Service Mesh'de Otomasyon 🚀

Araç Otomatik Sağladığı
Istio (Citadel / Istiod) CA, sertifika üretimi, rotation (varsayılan 24 h), revocation
Linkerd (Trust Anchor) Benzer yaşam döngüsü, mTLS varsayılan on
Consul Connect CA yönetimi + ACL tabanlı yetkilendirme

Yapmanız gereken tek şey: PeerAuthentication politikasını STRICT yapmak ve mesh'in sidecar'larını enjekte etmek. Gerisi arkadaşlarınızın (control plane) halleder 😉.


Özet: mTLS, Zero Trust'in kemeridir. Sertifika yönetimi zordur ama modern service mesh'ler bu yükü sizden alır. Yukarıdaki Go örneği, temel mantığı anlamanız için minimal bir başlangıç noktasıdır. Prodüksiyona geçerken mesh'in sunduğu otomasyonu kullanın ✅.


🎯 Karşılaştırma Tablosu: Güvenlik, Performans, Karmaşıklık ve Kullanım Senaryoları

Hadi beş yöntemi yan yana koyalım ve gerçek hayatta hangi senaryoda ne işe yarar ona bakalım. Tablo altındaki notlar da "neden böyle" sorusuna kısa cevaplar veriyor 👇

| Yöntem        | Güvenlik Seviyesi | Performans/Overhead | Uygulama Karmaşıklığı | Ölçeklenebilirlik | İptal (Revocation) Kolaylığı | En Uygun Olduğu Mimariler          |
|---------------|-------------------|---------------------|------------------------|-------------------|------------------------------|-------------------------------------|
| **Session**   | 🔒 Yüksek (server-side) | ⚡ Düşük overhead, DB/Redis sorgusu | 🟢 Basit (framework desteği) | 🟡 Orta (sticky session/paid Redis gerektirir) | ✅ Kolay (silmek yeterli) | Monolit, SSR, geleneksel web apps   |
| **JWT**       | 🔐 Orta (imza doğrulaması) | ⚡⚡ Çok hızlı (stateless) | 🟢 Basit (kütüphane ile) | ✅ Mükemmel (stateless) | ❌ Zor (kısa TTL/blacklist gerekir) | SPA, Mikroservis, Mobil, M2M        |
| **OAuth2/OIDC** | 🔒🔒 Çok Yüksek (standart protokol) | 🟡 Orta (token exchange, introspection) | 🔴 Yüksek (provider kurulumu, flow yönetimi) | ✅ Mükemmel (merkezi auth server) | ✅ Kolay (provider tarafında) | Enterprise, SSO, 3rd-party entegrasyon, Mikroservis |
| **API Key**   | 🔐 Düşük-Orta (sadece tanımlama) | ⚡⚡⚡ En hızlı (header check) | 🟢 Çok basit | ✅ Mükemmel (stateless) | ✅ Kolay (key rotate/revoke) | M2M, Public API, Server-to-server, Webhook |
| **mTLS**      | 🔒🔒🔒 En Yüksek (kimlik + şifreleme) | 🔴 Yüksek (handshake, cert yönetimi) | 🔴 Çok Yüksek (CA, cert rotation, distribution) | 🟡 Orta (cert dağıtımı zor) | 🟡 Orta (CRL/OCSP, cert yenileme) | Zero-trust network, Mikroservis arası, High-security M2M |

📝 Satır Satır Kısa Yorumlar

Session (Oturum / Cookie-based)

Tarayıcı tabanlı uygulamalarda hala en pratik yöntem. State server tarafında durduğu için iptal anında olur. Ama mikroservislerde sticky session veya paylaşılan Redis zorunluluğu getirir — yatay ölçekleme biraz ağrıtır.

JWT (JSON Web Token)

Stateless kralı. Performans çok iyi, ölçekleme trivial. Tek sakıncası: access token süresi dolana kadar iptal edemezsiniz. Bu yüzden kısa TTL (5-15 dk) + refresh token kombinasyonu şart.

OAuth2 / OIDC

Enterprise standardı. Keycloak, Auth0, Azure AD gibi provider'lar heavy lifting'i üstlenir. Kurulum karmaşıktır ama bir kere ayarlayınca SSO, social login, MFA hepsi kutudan çıkıyor. Mikroservis mimarilerde authorization server merkezi nokta olur.

API Key

En basit, en hızlı — ama en az güvenli. Sadece "kim bu?" der, yetkilendirme (authorization) yapmaz. M2M, webhook, public API'ler için mükemmel. Secret rotation otomatikleştirin, loglarda maskelensin.

mTLS (Mutual TLS)

Güvenlik maximumu. Hem client hem server kimliğini kanıtlar, trafik şifrelenir. Ama sertifika yönetimi (CA, rotation, distribution) ayrı bir operasyon yüküdür. Service mesh (Istio, Linkerd) bu yükü काफी azaltır. Zero-trust ağların vazgeçilmezi.


❗️ Yaygın Hata Senaryoları ve Nasıl Önlenir? (Token Sızıntısı, Replay Saldırıları, Algorithm Confusion ve Daha Fazlası)

1️⃣ Token Sızıntısı 📄

  • Token’lar log dosyalarında, URL parametrelerinde veya frontend localStorage’da açıkça görünüyor.
  • Nasıl Önlenir?
    • Access token’ı Authorization header (Bearer) ile gönder, query string’e koyma.
    • Loglama kütüphanelerinde masking (ör. ****) uygula.
    • Frontend’de token’ı httpOnly, Secure, SameSite=Strict cookie’de tut; localStorage kullanma.
    • Kısa ömürlü access token + uzun ömürlü refresh token stratejisi uygula.

2️⃣ Replay Saldırısı 🔁

  • Aynı JWT birden fazla istekte tekrar kullanılıyor (nonce/timestamp yok).
  • Nasıl Önlenir?
    • Her istekte nonce (benzersiz rastgele değer) veya timestamp (ör. iat + exp window) kontrol et.
    • Sunucu tarafında Redis/In‑memory set ile kullanılmış nonce’ları sakla ve TTL ver.
    • mTLS ile istemci kimliğini doğrula, replay riskini azalt.

Örnek: Nonce / Timestamp Kontrolü (TypeScript)

// middleware/replayGuard.ts
import { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);
const REPLAY_WINDOW_MS = 5 * 60 * 1000; // 5 dakika

export async function replayGuard(req: Request, res: Response, next: NextFunction) {
  const auth = req.headers.authorization?.split(' ')[1];
  if (!auth) return res.status(401).json({ error: 'Token eksik' });

  // JWT payload’ı decode et (doğrulama yok, sadece claim okuma)
  const payload = JSON.parse(Buffer.from(auth.split('.')[1], 'base64').toString());

  const now = Date.now();
  const iat = (payload.iat ?? 0) * 1000;
  const nonce = payload.nonce as string | undefined;

  // 1️⃣ Timestamp window kontrolü
  if (now - iat > REPLAY_WINDOW_MS) {
    return res.status(401).json({ error: 'Token süresi doldu / replay window' });
  }

  // 2️⃣ Nonce varsa kullanılmış mı?
  if (nonce) {
    const used = await redis.set(`nonce:${nonce}`, '1', 'PX', REPLAY_WINDOW_MS, 'NX');
    if (!used) return res.status(401).json({ error: 'Replay tespit edildi' });
  }

  next();
}

Ne oluyor?

  • iat ile token yaşı 5 dk’ya sınırlanır.
  • nonce claim’i varsa Redis’te SET NX ile tek kullanımlık hale getirilir.

3️⃣ Algorithm Confusion 🛠

  • Saldırgan alg header’ını none veya HS256 yaparak imza doğrulamayı atlar.
  • Nasıl Önlenir?
    • Kabul edilecek algoritmaları (ör. RS256, ES256) beyaz listeye al.
    • alg header’ını ignore et, sadece config’deki algoritmayla doğrula.
    • none algoritmasını kesinlikle reddet.

Örnek: JWT Doğrulama Middleware – Alg Kontrolü (TypeScript)

// middleware/jwtVerify.ts
import { Request, Response, NextFunction } from 'express';
import * as jose from 'jose';

const ALLOWED_ALGS = ['RS256', 'ES256']; // sadece bu ikisi
const PUBLIC_KEY = process.env.JWT_PUBLIC_KEY!; // PEM formatında

export async function jwtVerify(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'Token eksik' });

  try {
    // jose kütüphanesi header’daki alg’ı **doğrulamaz**, biz zorunlu kılar
    const { payload, protectedHeader } = await jose.jwtVerify(token, PUBLIC_KEY, {
      algorithms: ALLOWED_ALGS, // sadece beyaz listeli alg’ler
    });

    // Ekstra: header’daki alg’ın beyaz listede olup olmadığını tekrar kontrol et
    if (!ALLOWED_ALGS.includes(protectedHeader.alg ?? '')) {
      throw new Error('Yasak algoritma');
    }

    (req as any).user = payload; // downstream kullanım için
    next();
  } catch (e) {
    return res.status(401).json({ error: 'Geçersiz token', detail: (e as Error).message });
  }
}

Nasıl çalışıyor?

  • jose.jwtVerify içine algorithms dizisi vererek sadece RS256/ES256 kabul edilir.
  • Header’daki alg alanı özellikle kontrol edilerek none veya HS256 engellenir.

4️⃣ Zayıf İmza Anahtarı / Anahtar Rotasyonu Yok 🔐

  • Kısa, tahmin edilebilir secret kullanılıyor; yıllarca aynı anahtar kalıyor.
  • Nasıl Önlenir?
    • RSA/ECDSA (asimetrik) anahtar çifti kullan; private key sadece imzalama sunucusunda.
    • Key ID (kid) claim’i ile birden fazla public key’i destekle.
    • Rotasyon: 90 günde bir yeni key pair üret, eski public key’i JWKS endpoint’inde tut, eski token’lar exp dolana kadar geçerli kalsın.

5️⃣ Refresh Token Rotasyonu ve Reuse Detection Eksikliği 🔄

  • Aynı refresh token tekrar kullanılıyor; çalınırsa saldırgan sürekli yeni access token alır.
  • Nasıl Önlenir?
    • Her refresh isteğinde yeni refresh token üret (rotasyon).
    • Eski refresh token’ı revoke et ve reuse detection: tekrar gelirse tüm token zincirini iptal et (kullanıcıyı logout yap).
    • Refresh token’ı httpOnly, Secure, SameSite=Strict cookie’de sakla.

Örnek: Refresh Token Reuse Detection (TypeScript)

// services/tokenService.ts
import { SignJWT, jwtVerify } from 'jose';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);
const REFRESH_TTL_SEC = 30 * 24 * 60 * 60; // 30 gün

export async function rotateRefreshToken(oldToken: string, userId: string) {
  // 1️⃣ Eski token’ı doğrula
  const { payload } = await jwtVerify(oldToken, getPublicKey(), {
    algorithms: ['RS256'],
  });

  // 2️⃣ Redis’te bu token’ın kullanılıp kullanılmadığını kontrol et
  const used = await redis.get(`rt_used:${oldToken}`);
  if (used) {
    // 🚨 Reuse tespit edildi → kullanıcının tüm tokenlarını sil
    await revokeAllUserTokens(userId);
    throw new Error('Refresh token reuse – güvenlik ihlali');
  }

  // 3️⃣ Eski token’ı "kullanıldı" olarak işaretle (TTL = refresh ömrü)
  await redis.set(`rt_used:${oldToken}`, '1', 'EX', REFRESH_TTL_SEC);

  // 4️⃣ Yeni access + refresh token üret
  const newAccess = await new SignJWT({ sub: userId })
    .setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
    .setExpirationTime('15m')
    .sign(getPrivateKey());

  const newRefresh = await new SignJWT({ sub: userId, typ: 'refresh' })
    .setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
    .setExpirationTime('30d')
    .sign(getPrivateKey());

  // 5️⃣ Yeni refresh token’ı Redis’te "geçerli" setine ekle
  await redis.sadd(`rt_valid:${userId}`, newRefresh);
  await redis.expire(`rt_valid:${userId}`, REFRESH_TTL_SEC);

  return { accessToken: newAccess, refreshToken: newRefresh };
}

async function revokeAllUserTokens(userId: string) {
  const tokens = await redis.smembers(`rt_valid:${userId}`);
  for (const t of tokens) await redis.set(`rt_used:${t}`, '1', 'EX', REFRESH_TTL_SEC);
  await redis.del(`rt_valid:${userId}`);
}

// yardımcı: private/public key yükleme (örnek)
function getPrivateKey() { /* ... */ }
function getPublicKey() { /* ... */ }

Ne sağlar?

  • Rotaton: her refresh’te yeni token.
  • Reuse detection: eski token tekrar gelirse revokeAllUserTokens ile tüm oturum kapatılır.

6️⃣ PKCE Kullanmamak 📱

  • Public client (SPA, mobil) Authorization Code Flow’da code_verifier / code_challenge yok.
  • Nasıl Önlenir?
    • PKCE (RFC 7636) zorunlu kıl: code_challenge=S256 + code_verifier gönder.
    • Backend’de code_verifier doğrulaması yap; yoksa invalid_grant dön.

7️⃣ mTLS Sertifika Yönetimi Hataları 🔒

  • İstemci/sunucu sertifikaları süresi dolmuş, self-signed veya revoked durumda.
  • Nasıl Önlenir?
    • Otomatik yenileme (cert-manager, Let’s Encrypt, HashiCorp Vault).
    • OCSP/CRL kontrolü aktif et; revoked sertifikayı reddet.
    • Sertifika pinning yerine trust store güncelleme stratejisi izle.
    • Test ortamında mutual TLS’i devre dışı bırakma; prod’da strict mod.

✅ Özet:

  • Token’ları gizli tut, kısa ömürlü yap.
  • Replay için nonce/timestamp + Redis.
  • Algorithm confusion için beyaz liste ve none reddi.
  • Güçlü asimetrik anahtar + rotasyon.
  • Refresh token rotasyonu + reuse detection.
  • PKCE zorunlu.
  • mTLS sertifika yaşam döngüsünü otomatikleştir.

Bu önlemler bir arada uygulandığında JWT tabanlı kimlik doğrulamanız çok daha dayanıklı hale gelir 🚀.


🎯 Mimarilere Göre Seçim Rehberi: SPA, Mikroservisler, Mobil ve M2M İçin Hangisi?

Hangi mimariyi kullanıyorsanız, kimlik doğrulama ve yetkilendirme stratejiniz de buna göre şekillenir.
Aşağıda karar ağacı / checklist formatında, yaygın 6 senaryo için önerileri, nedenlerini ve dikkat edilmesi gerekenleri topladım.


1️⃣ Classic Server‑Side Rendered App

Öneri: Session / Cookie (HttpOnly, Secure, SameSite=Lax)

Neden?

  • Sunucu tarafında state tutmak basittir, CSRF koruması SameSite ile kolay sağlanır.
  • Tarayıcı otomatik olarak cookie’leri gönderir, ekstra token yönetimi gerekmez.

Dikkat Edilecekler ⚠️

  • CSRF koruması için SameSite=Lax ya da Strict + anti‑CSRF token kullanın.
  • Cookie’lerin HttpOnly ve Secure bayrakları mutlaka açık olmalı.
  • Çoklu domain/subdomain senaryolarında Domain attribute’ı dikkatli ayarlayın.

2️⃣ SPA (React / Vue) + Backend‑for‑Frontend (BFF)

Öneri: OAuth2 / OIDC Authorization Code + PKCE + HttpOnly Cookie (BFF tarafında)

Neden?

  • PKCE (Proof Key for Code Exchange) public client’larda code‑exchange saldırılarını engeller.
  • Token’ları HttpOnly Cookie içinde tutarak XSS riskini minimize edersiniz.
  • BFF, access/refresh token’ları güvenli bir şekilde depolar ve SPA’ya sadece session cookie gönderir.

Dikkat Edilecekler ⚠️

  • Redirect URI tam olarak kaydedilmiş olmalı, wildcard kullanmayın.
  • Refresh token rotation + reuse detection uygulayın.
  • Cookie’ler SameSite=Lax (veya None; Secure cross‑site gerekiyorsa) olmalı.
  • BFF stateless olmalı; token’ları short‑lived tutun, uzun ömürlü refresh token’ları güvenli store’da saklayın.

3️⃣ Mobil Uygulama (iOS / Android)

Öneri: OAuth2 / OIDC Authorization Code + PKCE + Secure Storage (Keychain / Keystore)

Neden?

  • Mobil cihazlarda custom scheme ya da App Links ile güvenli redirect mümkündür.
  • PKCE public client (mobil) için zorunludur.
  • Access/refresh token’ları Secure Enclave / Keystore’ta saklayarak cihaz çalınsa bile token güvenli kalır.

Dikkat Edilecekler ⚠️

  • Redirect URI için myapp://callback gibi benzersiz scheme kullanın, http://localhost kullanmayın.
  • Biometric / PIN koruması ile token erişimini kısıtlayın.
  • App‑to‑Web (WebView) akışlarında ASWebAuthenticationSession / Chrome Custom Tab kullanın, embedding yapmayın.
  • Token yenileme arka planda (background) çalışmalı, kullanıcı deneyimini bozmamalı.

4️⃣ Mikroservisler Arası (East‑West)

Öneri: mTLS (Service Mesh – örn. Istio, Linkerd) veya JWT (Gateway validation + short‑lived)

Neden?

  • mTLS: Her servis kimliğini sertifika ile kanıtlar, ağ seviyesinde şifreleme ve kimlik doğrulama sağlar.
  • JWT: Gateway (API Gateway / Sidecar) token’ı doğrular, servisler arası çağrıda Authorization: Bearer <jwt> gönderilir.

Dikkat Edilecekler ⚠️

  • mTLS için SPIFFE/SPIRE ile otomatik sertifika rotasyonu kurun; manuel cert yönetimi hata yapar.
  • JWT kullanılıyorsa: kid header’ı ile key rotation destekleyin, aud ve iss claim’lerini doğrulayın.
  • Token TTL çok kısa tutun (≤ 5 dk) ve refresh mekanizması yerine mTLS tercih edin.
  • Service Mesh sidecar’ları kaynak tüketimine dikkat edin; küçük clusterdelerde JWT + Gateway daha hafif olabilir.

5️⃣ Partner / Third‑Party API (M2M)

Öneri: API Key + Rate Limit veya OAuth2 Client Credentials Flow

Neden?

  • API Key: Basit, hızlı entegrasyon; rate‑limit ile kötüye kullanım engellenir.
  • Client Credentials: Daha güçlü kimlik doğrulama, token tabanlı, scope bazlı yetkilendirme sunar.

Dikkat Edilecekler ⚠️

  • API Key’leri hashleyerek saklayın, loglama yapmayın.
  • Rate Limit hem IP hem key bazında uygulayın; burst için token bucket algoritması tercih edin.
  • Client Credentials için short‑lived access token (≤ 15 dk) ve scope kısıtlaması zorunlu.
  • Partner’a JWKS endpoint sağlayın, public key rotation sorunsuz olsun.
  • Audit log tutun: kim, ne zaman, hangi endpoint’e çağrı yaptı.

6️⃣ High‑Security / Zero Trust Ortamı

Öneri: mTLS + SPIFFE/SPIRE (veya Zero Trust Network Access – ZTNA çözümü)

Neden?

  • Zero Trust prensibi: “Hiçbir şeyi güvenme, her şeyi doğrula”.
  • SPIFFE/SPIRE her workload’a SPIFFE ID verir, sertifika yaşam döngüsünü otomatikleştirir.
  • mTLS hem kimlik doğrulama hem şifreleme sağlar, ağ seviyesinde lateral movement’u engeller.

Dikkat Edilecekler ⚠️

  • SPIRE Agent’ları her node’da çalıştırın, attestation (node identity) mekanizmasını doğru yapılandırın.
  • Certificate TTL çok kısa (≤ 1 saat) tutun, auto‑rotation testi yapın.
  • Policy Engine (OPA, Kyverno) ile SPIFFE ID bazlı fine‑grained yetkilendirme yazın.
  • Observability: mTLS handshake metriklerini (success/failure, latency) izleyin, anomali alarmı kurun.
  • Fallback planı: acil durumda break‑glass erişim için ayrı, audit‑loglu bir mekanizma bulundurun.

📋 Hızlı Kontrol Listesi (Checklist)

Senaryo Yöntem ✅ Temel Kontroller
Classic SSR Session/Cookie HttpOnly, Secure, SameSite, CSRF token
SPA + BFF Auth Code + PKCE + HttpOnly Cookie PKCE, Redirect URI, Refresh rotation, Cookie flags
Mobil Auth Code + PKCE + Secure Storage Custom scheme, Biometric, ASWebAuthSession
Mikroservis (East‑West) mTLS (SPIFFE) veya JWT (Gateway) Cert rotation, short TTL, aud/iss validation
Partner API API Key + Rate Limit veya Client Credentials Key hashing, rate‑limit, scope, JWKS
Zero Trust mTLS + SPIFFE/SPIRE SPIRE agent, short cert TTL, OPA policies, monitoring

Kullanım ipucu:
Projenizin mimari tipini belirleyin → Yukarıdaki satırdan yöntemi seçin → Kontrol listesini sprint planınıza ekleyin.
Böylece “hangi auth yöntemini kullanmalıyım?” sorusu artık bir karar ağacı değil, checklist halinde elinizde olur 🎯.


🎯 Bonus Tavsiye: Güvenliği Sürekli Bir Süreç Olarak Yönetmek ve Son Söz

Hazırsan bu yolculuğu bir alışkanlık haline getirelim 🚀
Güvenlik, "yapıp bitirdim" diyerek kenara bırakılan bir özellik değil; sürekli izleme, güncelleme ve iyileştirme gerektiren bir süreçtir.

Küçük adımlarla başla, büyük etki yaratıyorsun ✨

  • Güvenlik başlıkları – Helmet veya CSP ile HTTP başlıklarınızı sıkılaştırın 🔒
  • Bağımlılık taraması – Dependabot, Snyk veya npm audit ile her PR’de otomatik kontrol ✅
  • Pen‑test / Bug bounty – Düzenli dış denetimler ve ödül programlarıyla kör noktaları aydınlatın 🔍
  • Logging & Monitoring – Audit log, merkezi loglama (ELK, Loki) ve anomali alarmları kurun 📊
  • Incident Response Planı – "Ne olursa olsun" senaryosu için hazırlıklı olun, runbook’larınızı canlı tutun 🛠
// Örnek: Express + Helmet ile temel başlık koruması
const helmet = require('helmet');
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "trusted.cdn.com"],
      styleSrc: ["'self'", "'unsafe-inline'"]
    }
  }
}));

Bu sayede tarayıcı yalnızca izin verdiğiniz kaynakları yükler, XSS riskini büyük ölçüde azaltırsınız.

Kendi mimarinizi bu çerçevede değerlendirin, her sprint’te bir güvenlik öğesi ekleyin.
Unutmayın: Güvenli kod yazmak bir yetenek değil, bir alışkanlıktır. İyi kodlamalar! 🚀


Bu içerik tamamen yapay zeka destekli otomasyon sistemi ile üretilmiştir.

Burak Sağlık

Burak Saglik

©2024 Desing and Developed by @Burak Sağlık

All rights reserved