API Güvenliği: OAuth 2.0, JWT ve Rate Limiting Stratejileri
API Güvenliğine Giriş
Modern yazılım dünyasında API'ler, uygulamalar arasındaki iletişimin temel yapı taşlarını oluşturur. Mobil uygulamalar, web servisleri, IoT cihazları ve mikroservis mimarileri; hepsi API'ler aracılığıyla veri alışverişi yapar. Ancak bu yaygın kullanım, API'leri siber saldırganlar için cazip hedefler haline getirir. OWASP API Security Top 10 listesine baktığımızda, kimlik doğrulama eksiklikleri, yetkilendirme hataları ve aşırı veri ifşası gibi güvenlik açıklarının ne kadar yaygın olduğunu görürüz.
Bu yazıda, API güvenliğinin üç temel direğini derinlemesine inceleyeceğiz: OAuth 2.0 ile yetkilendirme, JWT (JSON Web Token) ile kimlik doğrulama ve Rate Limiting ile kötüye kullanımın önlenmesi.
OAuth 2.0: Yetkilendirme Standardı
OAuth 2.0 Nedir?
OAuth 2.0, bir kullanıcının kimlik bilgilerini paylaşmadan üçüncü taraf uygulamalara sınırlı erişim izni vermesini sağlayan bir yetkilendirme çerçevesidir. Örneğin, bir kullanıcı Google hesap şifresini vermeden bir uygulamaya Google Drive dosyalarına erişim izni verebilir.
OAuth 2.0'ın temel aktörleri şunlardır:
- Resource Owner: Kaynağın sahibi, genellikle son kullanıcı
- Client: Kaynağa erişmek isteyen uygulama
- Authorization Server: Yetkilendirme tokenlarını veren sunucu
- Resource Server: Korunan kaynakları barındıran sunucu
OAuth 2.0 Grant Türleri
OAuth 2.0, farklı senaryolar için farklı yetkilendirme akışları (grant types) sunar:
- Authorization Code Grant: Web uygulamaları için en güvenli yöntemdir. Kullanıcı, yetkilendirme sunucusuna yönlendirilir, onay verdikten sonra bir yetkilendirme kodu döner ve bu kod sunucu tarafında access token ile değiştirilir.
- Authorization Code with PKCE: Mobil ve SPA (Single Page Application) uygulamaları için Authorization Code akışının güvenli versiyonudur. Client secret kullanılamayan ortamlarda code_verifier ve code_challenge mekanizmasıyla güvenlik sağlanır.
- Client Credentials Grant: Makine-makine (M2M) iletişiminde kullanılır. Kullanıcı etkileşimi yoktur; istemci kendi kimlik bilgileriyle doğrudan token alır.
- Refresh Token: Access token süresi dolduğunda, kullanıcıyı tekrar yetkilendirme sürecine sokmadan yeni bir access token almayı sağlar.
OAuth 2.0 Uygulama Örneği (Node.js)
const express = require('express');
const axios = require('axios');
const app = express();
const CLIENT_ID = process.env.OAUTH_CLIENT_ID;
const CLIENT_SECRET = process.env.OAUTH_CLIENT_SECRET;
const REDIRECT_URI = 'https://app.example.com/callback';
// Kullanıcıyı yetkilendirme sunucusuna yönlendir
app.get('/auth/login', (req, res) => {
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('scope', 'read write');
authUrl.searchParams.set('state', generateRandomState());
res.redirect(authUrl.toString());
});
// Callback: Yetkilendirme kodunu access token ile değiştir
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
if (!validateState(state)) {
return res.status(403).json({ error: 'Geçersiz state parametresi' });
}
try {
const tokenResponse = await axios.post('https://auth.example.com/token', {
grant_type: 'authorization_code',
code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
});
const { access_token, refresh_token, expires_in } = tokenResponse.data;
// Token'ları güvenli şekilde saklayın
res.json({ message: 'Yetkilendirme başarılı', expires_in });
} catch (error) {
res.status(401).json({ error: 'Token alınamadı' });
}
});
OAuth 2.0 Güvenlik İpuçları
- state parametresini mutlaka kullanın; CSRF saldırılarını önler
- Access token ömrünü kısa tutun (15-30 dakika)
- Refresh token'ları döndürme (rotation) mekanizmasıyla kullanın
- Scope'ları mümkün olduğunca dar tanımlayın (en az yetki prensibi)
- Token'ları asla URL parametresi olarak taşımayın
- Implicit Grant kullanmayın; bunun yerine PKCE ile Authorization Code kullanın
JWT: JSON Web Token ile Kimlik Doğrulama
JWT'nin Yapısı
JWT, üç bölümden oluşan, Base64URL ile kodlanmış ve nokta (.) ile ayrılmış bir token formatıdır:
- Header: Token tipi ve kullanılan imzalama algoritması bilgisini içerir
- Payload: Claim adı verilen anahtar-değer çiftlerini taşır (kullanıcı bilgileri, yetkiler, son kullanma tarihi vb.)
- Signature: Header ve payload'ın bütünlüğünü doğrulayan kriptografik imza
JWT Oluşturma ve Doğrulama
const jwt = require('jsonwebtoken');
const ACCESS_TOKEN_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_TOKEN_SECRET = process.env.JWT_REFRESH_SECRET;
// JWT oluşturma
function generateTokens(user) {
const accessToken = jwt.sign(
{
sub: user.id,
email: user.email,
roles: user.roles,
},
ACCESS_TOKEN_SECRET,
{
expiresIn: '15m',
issuer: 'api.example.com',
audience: 'example.com',
}
);
const refreshToken = jwt.sign(
{ sub: user.id },
REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// JWT doğrulama middleware'i
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.startsWith('Bearer ')
? authHeader.slice(7)
: null;
if (!token) {
return res.status(401).json({ error: 'Token bulunamadı' });
}
try {
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET, {
issuer: 'api.example.com',
audience: 'example.com',
});
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token süresi dolmuş' });
}
return res.status(403).json({ error: 'Geçersiz token' });
}
}
JWT Güvenlik Önlemleri
- Algoritmayı doğrulayın: Token doğrularken beklenen algoritmayı açıkça belirtin.
alg: "none"saldırılarına karşı dikkatli olun. - Asimetrik anahtarlar kullanın: Mümkünse HS256 yerine RS256 veya ES256 tercih edin. Bu sayede token'ı doğrulayan tarafın imzalama yeteneğine sahip olması gerekmez.
- Hassas veri taşımayın: JWT payload'ı şifrelenmez, yalnızca Base64URL ile kodlanır. Şifre, kredi kartı numarası gibi hassas verileri asla payload'a koymayın.
- Kısa ömürlü token'lar kullanın: Access token'lar için 15-30 dakika, refresh token'lar için 7-30 gün uygun sürelerdir.
- Token iptal mekanizması kurun: JWT'ler stateless olduğu için iptal etmek zordur. Kritik senaryolar için bir kara liste (blacklist) veya token versiyonlama mekanizması uygulayın.
Rate Limiting: API'yi Aşırı Kullanımdan Koruma
Neden Rate Limiting?
Rate limiting, belirli bir zaman diliminde bir istemcinin yapabileceği istek sayısını sınırlandırma mekanizmasıdır. Bu mekanizma olmadan API'niz şu risklere açık kalır:
- DDoS saldırıları: Aşırı istek yüküyle servisin çökmesi
- Brute-force saldırıları: Giriş endpoint'lerine tekrarlanan deneme
- Kaynak tükenmesi: Tek bir istemcinin tüm sunucu kaynaklarını tüketmesi
- Veri kazıma (scraping): Otomatik araçlarla verilerin toplu çekilmesi
- API kötüye kullanımı: Ücretsiz katman limitlerinin aşılması
Popüler Rate Limiting Algoritmaları
1. Fixed Window (Sabit Pencere): Belirli zaman aralıklarında (örneğin her dakika) sabit bir istek limiti uygulanır. Uygulaması kolaydır ancak pencere sınırlarında ani yük artışlarına (burst) izin verebilir.
2. Sliding Window (Kayan Pencere): Fixed window'un geliştirilmiş halidir. Önceki pencerenin ağırlıklı ortalaması hesaba katılarak daha düzgün bir dağılım sağlanır.
3. Token Bucket: Belirli hızla doldurulan bir token havuzu vardır. Her istek bir token harcar. Havuz boşsa istekler reddedilir. Kısa süreli burst trafiğe izin verirken uzun vadede ortalama hızı korur.
4. Leaky Bucket: İstekler bir kuyruğa eklenir ve sabit hızda işlenir. Kuyruk dolduğunda yeni istekler reddedilir. Çıkış hızını tamamen sabitler.
Redis Tabanlı Rate Limiting Uygulaması
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
// Sliding Window Rate Limiter
async function slidingWindowRateLimiter(req, res, next) {
const identifier = req.user?.id || req.ip;
const endpoint = req.route?.path || req.path;
const key = `rate_limit:${identifier}:${endpoint}`;
const WINDOW_SIZE = 60; // saniye
const MAX_REQUESTS = 100;
const now = Date.now();
const windowStart = now - WINDOW_SIZE * 1000;
const multi = redis.multi();
// Eski kayıtları temizle
multi.zremrangebyscore(key, 0, windowStart);
// Yeni isteği ekle
multi.zadd(key, now, `${now}:${Math.random()}`);
// Penceredeki toplam istek sayısını al
multi.zcard(key);
// TTL ayarla
multi.expire(key, WINDOW_SIZE);
const results = await multi.exec();
const requestCount = results[2][1];
// Rate limit başlıklarını ayarla
res.set({
'X-RateLimit-Limit': MAX_REQUESTS,
'X-RateLimit-Remaining': Math.max(0, MAX_REQUESTS - requestCount),
'X-RateLimit-Reset': Math.ceil((now + WINDOW_SIZE * 1000) / 1000),
});
if (requestCount > MAX_REQUESTS) {
const retryAfter = Math.ceil(WINDOW_SIZE / 2);
res.set('Retry-After', retryAfter);
return res.status(429).json({
error: 'Çok fazla istek gönderildi',
message: `Lütfen ${retryAfter} saniye sonra tekrar deneyin`,
});
}
next();
}
Katmanlı Rate Limiting Stratejisi
Profesyonel API'lerde tek bir rate limit kuralı yeterli değildir. Katmanlı bir yaklaşım çok daha etkilidir:
const rateLimitConfig = {
// Global limit: Tüm istemciler için toplam
global: { windowMs: 60000, max: 10000 },
// IP bazlı limit
perIP: { windowMs: 60000, max: 200 },
// Kullanıcı bazlı limit
perUser: { windowMs: 60000, max: 500 },
// Endpoint bazlı özel kurallar
endpoints: {
'POST /auth/login': { windowMs: 900000, max: 10 },
'POST /auth/forgot-password': { windowMs: 3600000, max: 3 },
'POST /api/upload': { windowMs: 60000, max: 5 },
'GET /api/search': { windowMs: 60000, max: 30 },
},
// Abonelik planına göre limit
plans: {
free: { windowMs: 3600000, max: 100 },
basic: { windowMs: 3600000, max: 1000 },
pro: { windowMs: 3600000, max: 10000 },
enterprise: { windowMs: 3600000, max: 100000 },
},
};
Üç Katmanı Bir Arada Kullanma
Bu üç mekanizma birbirini tamamlar ve birlikte kullanıldığında güçlü bir güvenlik duvarı oluşturur:
const app = express();
// 1. Rate limiting: İlk savunma hattı
app.use(slidingWindowRateLimiter);
// 2. OAuth 2.0: Yetkilendirme kontrolü
app.use('/api', authenticateToken);
// 3. Yetki kontrolü: Rol bazlı erişim
app.get('/api/admin/users', authorize(['admin']), (req, res) => {
// Sadece admin rolüne sahip, rate limit aşmamış,
// geçerli JWT token'ı olan kullanıcılar buraya ulaşır
res.json({ users: [] });
});
Ek Güvenlik Önlemleri
OAuth 2.0, JWT ve rate limiting temel yapı taşları olmakla birlikte, kapsamlı bir API güvenlik stratejisi için şu ek önlemleri de uygulamanız gerekir:
- HTTPS zorunluluğu: Tüm API trafiğini TLS üzerinden şifreleyin. HTTP isteklerini HTTPS'e yönlendirin ve HSTS başlığını kullanın.
- Input validation: Gelen tüm verileri katı şekilde doğrulayın. SQL injection, XSS ve komut enjeksiyonu saldırılarına karşı parametrize sorgular ve çıktı kodlama kullanın.
- CORS yapılandırması: İzin verilen origin'leri açıkça belirtin. Wildcard (*) kullanmaktan kaçının.
- API versiyonlama: Güvenlik güncellemelerini mevcut istemcileri bozmadan uygulamanıza olanak tanır.
- Loglama ve izleme: Tüm API çağrılarını loglamak, anormallikleri tespit etmek ve güvenlik olaylarına hızlı müdahale etmek için merkezi bir loglama sistemi kurun.
- API Gateway kullanımı: Kong, AWS API Gateway veya Apigee gibi çözümler, kimlik doğrulama, rate limiting, loglama ve dönüşüm gibi ortak güvenlik katmanlarını merkezi olarak yönetmenizi sağlar.
Sonuç
API güvenliği, tek seferlik bir yapılandırma değil, sürekli evrim geçiren bir süreçtir. OAuth 2.0 ile sağlam bir yetkilendirme altyapısı kurmak, JWT ile verimli ve güvenli kimlik doğrulama sağlamak ve rate limiting ile kötüye kullanımı engellemek, bu sürecin temel adımlarıdır. Ancak güvenlik, yalnızca araçlardan ibaret değildir; düzenli güvenlik denetimleri yapmak, bağımlılıkları güncel tutmak, güvenlik açıklarını proaktif olarak taramak ve ekibinizi güvenli kodlama pratikleri konusunda eğitmek de en az teknik önlemler kadar önemlidir.
Unutmayın: bir API'nin güvenliği, en zayıf halkası kadar güçlüdür. Bu üç temel stratejiyi doğru uygulayarak, API'lerinizi yaygın saldırı vektörlerine karşı korumanın sağlam bir temelini atabilirsiniz.