
Sat Oct 10 2026

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:
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 ☕🚀
Hazırsan bu iki kavramı, günlük hayattan bir benzetmeyle kafanızdan silinmeyecek hale getirelim 🎯
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:
⚠️ 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).
Bu protokoller konuşurken sürekli duyacaksın terimler. Hadi tek tek tanıyalım 👇
id_token'ı imzalayan taraf budur. Token'ın güvenilirliğini IdP sağlar.access_token'ı doğrular → Scope/claim'lere bakar → İzin ver/reddet.GET /api/users/123/profile isteğini alan backend servisin.client_id ve client_secret ile tanınır.read:users, write:posts, openid profile email gibi boşlukla ayrılmış string'ler.{
"sub": "user-123",
"email": "ayse@ornek.com",
"role": "admin",
"scope": "read:users write:posts"
}
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?
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... 🚀
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 🎯
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:
session oluştururSet-Cookie header'ı ile session ID'yi tarayıcıya verirCookie'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'kullanacaksanSecure: truezorunlu — aksi takdirde tarayıcı cookie'yi reddeder.
SameSite + CSRF token (double submit cookie pattern) ile korunmalısınSameSite ve CORS politikalarıyla kısıtlı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.
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`);
});
cookie objesinde HttpOnly, Secure, SameSite bayrakları set edilmiş — bu production standartlarıreq.session.userId yazdığında express-session arka planda session'ı güncelliyor ve cookie'yi tarayıcıya Set-Cookie ile yolluyor/logout route'unda req.session.destroy() ile sunucu tarafı state siliniyor VE res.clearCookie() ile tarayıcıdaki cookie de temizleniyor — çift güvencerequireAuth middleware'i ile korumalı rotalar kontrol ediliyor — session yoksa 401Geliş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 ✅
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, nokta (.) ile birleştirilmiş Base64URL encoded üç kısımdan oluşur:
alg, typ)sub, exp, iat, custom)eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9 // Header
. // ayırıcı
eyJzdWIiOiIxMjM0IiwibmFtZSI6IkpvaG4iLCJleHAiOjE3MDAwMDAwMDB9 // Payload
. // ayırıcı
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c // Signature
Ne oluyor burada?
| Ö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. 🔐
alg: none → İmza doğrulaması yok! Eski kütüphanelerde verify(token, '', { algorithms: ['none'] }) çalışır. Asla none kabul etme.algorithms: ['RS256'] (veya ES256) belirt.jsonwebtoken ile RS256 Imzalama & Doğrulama 🛠Not: Private/public key çiftini
opensslile üretipPRIVATE_KEY/PUBLIC_KEYenv 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?
generateAccessToken → Kısa ömürlü, RS256 imzalı access token döner.generateRefreshToken → Uzun ömürlü, tokenVersion ile rotasyon sağlar.verifyAccessToken → Her istekte Authorization header’ından token alır, public key ile doğrular, req.user’a koyar.| 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:
Bu yapıyı projenize entegre edince stateless kimlik doğrulamanın hem performanslı hem de güvenli olduğunu göreceksiniz. ✅
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 🎯.
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.| 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 |
Ö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?
generatePKCE() → verifier (gizli) + challenge (herkese açık).buildAuthUrl() → kullanıcıyı yetkilendirme sunucusuna gönderir, challenge query’de gider.code ile redirect_uri’ne döner.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…
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.
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.
| 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çığı |
sk_live_, sk_test_, pk_ gibi. Hangi ortam, hangi tip anahtar anında belli olur.bcrypt / argon2 / scrypt kullanın. Key sızsa bile saldırgan hash'i geri çeviremez.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;
| 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-limityerine Redis-backed limiter (rate-limiter-flexibleveya custom Redis lua script) kullanın. Memory store cluster'da paylaşımaz.
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! 🚀
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 🎯.
| 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: STRICTayarını açarsınız ✅.
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.
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
}
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?
ClientAuth: RequireAndVerifyClientCert ile istemci sertifikasını zorunlu kılar.RootCAs ile sunucu sertifikasını doğrular, Certificates ile kendi kimliğini sunar.| 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 ✅.
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 |
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.
****) uygula.iat + exp window) kontrol et.// 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.alg header’ını none veya HS256 yaparak imza doğrulamayı atlar.RS256, ES256) beyaz listeye al.alg header’ını ignore et, sadece config’deki algoritmayla doğrula.none algoritmasını kesinlikle reddet.// 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.alg alanı özellikle kontrol edilerek none veya HS256 engellenir.exp dolana kadar geçerli kalsın.// 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?
revokeAllUserTokens ile tüm oturum kapatılır.code_verifier / code_challenge yok.code_challenge=S256 + code_verifier gönder.code_verifier doğrulaması yap; yoksa invalid_grant dön.✅ Özet:
none reddi.Bu önlemler bir arada uygulandığında JWT tabanlı kimlik doğrulamanız çok daha dayanıklı hale gelir 🚀.
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.
Öneri: Session / Cookie (HttpOnly, Secure, SameSite=Lax)
Neden?
Dikkat Edilecekler ⚠️
SameSite=Lax ya da Strict + anti‑CSRF token kullanın.Öneri: OAuth2 / OIDC Authorization Code + PKCE + HttpOnly Cookie (BFF tarafında)
Neden?
Dikkat Edilecekler ⚠️
None; Secure cross‑site gerekiyorsa) olmalı.Öneri: OAuth2 / OIDC Authorization Code + PKCE + Secure Storage (Keychain / Keystore)
Neden?
Dikkat Edilecekler ⚠️
myapp://callback gibi benzersiz scheme kullanın, http://localhost kullanmayın.Öneri: mTLS (Service Mesh – örn. Istio, Linkerd) veya JWT (Gateway validation + short‑lived)
Neden?
Authorization: Bearer <jwt> gönderilir.Dikkat Edilecekler ⚠️
Öneri: API Key + Rate Limit veya OAuth2 Client Credentials Flow
Neden?
Dikkat Edilecekler ⚠️
Öneri: mTLS + SPIFFE/SPIRE (veya Zero Trust Network Access – ZTNA çözümü)
Neden?
Dikkat Edilecekler ⚠️
| 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 🎯.
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 ✨
// Ö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.
All rights reserved