JWT (JSON Web Token), modern API’larda ve dağıtık sistemlerde kimlik doğrulama için sıkça kullandığımız, kompakt ve kendi içinde doğrulanabilir bir token formatı. Bir süredir birçok projemde JWT’leri kullanıyorum, özellikle mikroservis mimarilerinde veya mobil uygulamaların backend’leriyle iletişiminde büyük kolaylık sağlıyorlar. Ancak JWT’nin en büyük avantajı olan “stateless” yapısı, beraberinde token yenileme ve özellikle iptal mekanizmaları konusunda bazı karmaşıklıkları da getiriyor. Bu karmaşıklıklar, doğru yönetilmediğinde ciddi güvenlik açıkları yaratabilir.
Bir üretim ERP’sinde, operatör ekranları için kullandığımız API’larda JWT’yi ilk uygulamaya başladığımda, başlangıçta her şey sorunsuz görünüyordu. Ancak token’ların süresi dolduğunda kullanıcıların aniden sistemden atılması, ya da acil bir durumda bir kullanıcının erişimini anında kesemememiz gibi durumlar, bu mekanizmaların ne kadar kritik olduğunu bana gösterdi. Bu yazıda, JWT yenileme ve iptal stratejilerini, bunların güvenlik üzerindeki etkilerini ve kendi deneyimlerimden yola çıkarak hangi yaklaşımları tercih ettiğimi detaylıca anlatacağım.
JWT’nin Temelleri ve Stateless Yapının Zorlukları
JWT, aslında üç bölümden oluşan basit bir string: header.payload.signature. Header kısmında token tipi ve kullanılan imzalama algoritması bulunur. Payload kısmında ise kullanıcı kimliği, yetkiler gibi iddialar (claims) yer alır. Signature ise header ve payload’ın bir sır anahtarı ile imzalanmasıyla oluşturulur ve token’ın bütünlüğünü sağlar. Bu yapısı sayesinde sunucu tarafında herhangi bir oturum bilgisi tutmaya gerek kalmaz; her istek geldiğinde token’ın geçerliliği kriptografik olarak doğrulanabilir.
Bu stateless yapı, özellikle yatay ölçeklenebilirlik açısından büyük avantajlar sunar. Yüzlerce sunucunun olduğu bir ortamda her sunucunun oturum durumunu yönetmesi yerine, her sunucunun sadece token’ı doğrulayabilmesi işleri çok basitleştirir. Ancak bu durumun bir de madalyonun diğer yüzü var: Bir token’ı verdikten sonra, süresi dolana kadar onu iptal etmek oldukça zordur. Örneğin, bir kullanıcının şifresi çalındı veya hesabı ele geçirildi. Eğer o kullanıcının aktif bir JWT’si varsa, süresi bitene kadar sisteme erişmeye devam edebilir. Bu da anında müdahale gerektiren durumlarda bizi çaresiz bırakır.
Peki bu sorunu nasıl çözüyoruz? Genellikle access token ve refresh token ikilisini kullanarak bir denge kurmaya çalışırız. Bir access token genellikle kısa ömürlü (örneğin 15 dakika ile 1 saat arası) tutulurken, refresh token daha uzun ömürlü (örneğin birkaç gün veya hafta) olur.
Access Token ve Refresh Token Modeliyle Yönetim
Uygulamalarımda genellikle access token ve refresh token modelini tercih ediyorum. Bu model, JWT’nin stateless yapısının getirdiği iptal zorluğunu hafifletmek için yaygın olarak kullanılan bir yöntemdir. Access token, doğrudan API kaynaklarına erişmek için kullanılır ve kısa bir yaşam süresine sahiptir. Bu, token çalınsa bile kötü niyetli kullanıcının erişim süresinin kısıtlı olacağı anlamına gelir. Örneğin, ben genellikle access token ömrünü kısa (birkaç on dakika ile bir saat arası) tutarım.
Bir access token süresi dolduğunda, istemci (mobil uygulama veya web frontend) refresh token’ı kullanarak yeni bir access token talep eder. Refresh token ise daha uzun bir yaşam süresine sahiptir ve yalnızca yeni access token almak için kullanılır. Bu sayede kullanıcı, sık sık yeniden giriş yapmak zorunda kalmaz, ancak access token’ın kısa ömrü sayesinde olası bir sızıntı durumunda risk minimize edilmiş olur. Bir mobil uygulamada, kullanıcıların sürekli şifre girmek zorunda kalmamasını bu yöntemle sağlıyorum.
// Örnek bir Access Token Payload'ı
{
"sub": "1234567890",
"name": "Mustafa Erbay",
"iat": 1716384000, // Issued At (2024-05-22 00:00:00 UTC)
"exp": 1716385800, // Expiration (2024-05-22 00:30:00 UTC) - 30 dakika sonra
"aud": "my-api",
"iss": "auth-service",
"jti": "a1b2c3d4e5f6g7h8i9j0" // JWT ID
}
Bu modelde, güvenlik açısından kritik olan parça refresh token’dır. Access token çalınsa bile, kısa sürede geçersiz hale gelir. Ancak refresh token çalınırsa, saldırgan sürekli olarak yeni access token’lar üretebilir ve sisteme uzun süre erişebilir. Bu yüzden refresh token’ın güvenliği, uygulamanın genel güvenliği için hayati önem taşır.
Refresh token’ı sadece belirli IP aralıklarından ve belirli cihaz parmak iziyle kullanıma izin verecek şekilde kısıtlamak, çalınan bir token’ın farklı bir ortamda kullanılmasını ciddi şekilde zorlaştırır.
Refresh Token Güvenliği ve Depolama Stratejileri
Refresh token’ların güvenli bir şekilde depolanması, sistem güvenliği için en kritik adımlardan biridir. Yanlış depolama, token’ın çalınmasına ve yetkisiz erişime yol açabilir. Genellikle iki ana depolama alanı tartışılır: istemci tarafı (client-side) ve sunucu tarafı (server-side).
İstemci Tarafı Depolama:
- Local Storage/Session Storage: Bu yöntem, XSS (Cross-Site Scripting) saldırılarına karşı son derece savunmasızdır. Tarayıcıda çalışan kötü amaçlı bir JavaScript kodu, bu depolama alanlarına kolayca erişebilir ve
refresh token’ı çalabilir. Bir yan ürünümün eski versiyonunda bu hatayı yapmış, bir XSS zafiyeti tespit ettiğimde token’ların ne kadar kolay çalınabileceğini görmüştüm. Hemen bu yaklaşımı terk ettim. - HTTP-Only Cookies:
refresh token’ı bir HTTP-Only cookie olarak ayarlamak, JavaScript erişimini engellediği için XSS riskini azaltır. AyrıcaSecurebayrağı ile sadece HTTPS üzerinden gönderilmesini sağlayarak man-in-the-middle saldırılarına karşı koruma sağlarız. Ancak bu yaklaşım CSRF (Cross-Site Request Forgery) saldırılarına karşı savunmasız kalabilir. CSRF koruması içinSameSite=LaxveyaStrictattribute’ları ileCSRF tokengibi ek önlemler almak gerekir.
Sunucu Tarafı Depolama:
Benim üretim ortamlarındaki tercihim genellikle refresh token’ları sunucu tarafında yönetmektir. Bu, token’ın kendisinin istemcide saklanmaması anlamına gelir. Bunun yerine, istemciye sadece bir session ID veya bir defalık kullanılan refresh token hash’i verilir. Gerçek refresh token ve onunla ilişkili kullanıcı bilgileri, sunucu tarafındaki güvenli bir veritabanında (genellikle Redis veya PostgreSQL) saklanır.
Örneğin, bir refresh token oluşturduğumda, bu token’ı bir UUID ile birlikte Redis’e SET komutuyla kaydederim:
import uuid
import redis
# Redis bağlantısı
r = redis.Redis(host='localhost', port=6379, db=0)
def generate_and_store_refresh_token(user_id: int):
refresh_token_id = str(uuid.uuid4())
# Refresh token'ı Redis'te user_id ile ilişkilendirerek sakla
# Ömrü 7 gün (604800 saniye) olsun
r.setex(f"refresh_token:{refresh_token_id}", 604800, user_id)
return refresh_token_id
# Kullanım örneği
user_id = 123
token_id = generate_and_store_refresh_token(user_id)
print(f"Oluşturulan refresh_token_id: {token_id}")
Bu senaryoda, istemciye refresh_token_id gönderilir. İstemci yeni access token talep ettiğinde, bu refresh_token_id’yi sunucuya gönderir. Sunucu, Redis’ten refresh_token_id’ye karşılık gelen user_id’yi kontrol eder. Eğer token geçerliyse, yeni access token üretir ve belki de eski refresh_token_id’yi geçersiz kılarak yenisini oluşturur (refresh token rotation). Bu yöntem, refresh token’ın çalınması riskini önemli ölçüde azaltır çünkü token’ın kendisi hassas bir depolama alanında değil, doğrudan sunucu kontrolündedir.
Token İptali (Revocation) Mekanizmaları
Stateless JWT’lerin doğası gereği, bir kez imzalanan bir token’ı süresi dolmadan iptal etmek doğrudan mümkün değildir. Ancak, yukarıda bahsettiğim access token ve refresh token modelini kullanarak veya ek mekanizmalar geliştirerek bu sorunu çözebiliriz.
1. Blacklisting (Kara Liste)
En yaygın ve basit yöntemlerden biri blacklisting’dir. Bir kullanıcı oturumunu kapattığında veya şifresi değiştirildiğinde, aktif olan access token’ının JTI’sını (JWT ID) hızlı erişimli bir depoya (genellikle Redis) kaydederiz. Her API isteğinde, gelen access token’ın JTI’sının bu kara listede olup olmadığını kontrol ederiz. Eğer listedeyse, token geçersiz sayılır.
# Redis'e kara listeye ekleme
def blacklist_jwt(jti: str, exp_timestamp: int):
# Token'ın kalan ömrü kadar Redis'te tut
now = int(time.time())
ttl = exp_timestamp - now
if ttl > 0:
r.setex(f"blacklist:{jti}", ttl, "revoked")
print(f"Token {jti} kara listeye eklendi, {ttl} saniye sonra silinecek.")
# Kara listede olup olmadığını kontrol etme
def is_jwt_blacklisted(jti: str) -> bool:
return r.exists(f"blacklist:{jti}")
Trade-off’lar:
- Depolama Yükü: Çok sayıda
access token’ı kara listeye almak, Redis’te ek bellek kullanımı anlamına gelir. Ancak token’ların kısa ömürlü olması, bu yükü yönetilebilir kılar. - Gecikme (Latency): Her istekte Redis kontrolü ek bir ağ çağrısı ve gecikme yaratır. Ancak Redis’in hızı sayesinde bu gecikme genellikle milisaniyeler düzeyindedir.
- Eventual Consistency: Dağıtık sistemlerde kara liste güncellemelerinin tüm sunuculara yayılması anlık olmayabilir, bu da kısa bir süre için geçersiz bir token’ın hala kabul edilebileceği anlamına gelir. Pratikte bu, anlık geçersiz kılmanın kritik olduğu sistemlerde bile genelde kabul edilebilir bir risk seviyesinde kalır; yayılma gecikmesi yeterince kısadır.
2. Refresh Token Rotation (Yenileme Tokenı Döngüsü)
Bu yöntem, refresh token’ların her kullanımdan sonra yeni bir token ile değiştirilmesini içerir. Bir istemci, süresi dolan access token’ı yerine yeni bir access token almak için mevcut refresh token’ını kullandığında, sunucu sadece yeni bir access token değil, aynı zamanda yeni bir refresh token da gönderir. Eski refresh token ise anında geçersiz kılınır.
Eğer bir saldırgan eski refresh token’ı ele geçirip kullanmaya çalışırsa, sunucu bu token’ın zaten kullanılmış ve geçersiz kılınmış olduğunu fark eder. Bu durumda, sadece saldırganın isteğini reddetmekle kalmayıp, aynı zamanda daha önce verilen tüm refresh token’ları (hem kullanıcının hem de saldırganın elindeki) geçersiz kılarak olası bir çalınma durumunu tespit edebilir ve oturumu tamamen sonlandırabiliriz.
Trade-off’lar:
- Karmaşıklık: İstemci ve sunucu tarafında daha karmaşık bir state yönetimi gerektirir.
- Senkronizasyon: Bu yöntemde başlangıçta istemci tarafında senkronizasyon sorunları çıkabilir. Kullanıcı, ağ gecikmesi nedeniyle yeni
refresh token’ı alamadan eski token ile tekrar denediğinde hatalar oluşur. Bu durum idempotent istekler ve retry mekanizmalarıyla çözülür.
3. Hibrit Yaklaşım (Session Management ile Entegrasyon)
Büyük ölçekli kurumsal uygulamalarda, saf JWT stateless modelinin iptal zorlukları nedeniyle, geleneksel oturum yönetimi ile JWT’yi birleştiren hibrit yaklaşımları da değerlendirdim. Bu modelde, kullanıcı ilk kimlik doğrulamasını yaptığında sunucu tarafında bir oturum oluşturulur ve bu oturuma bağlı bir oturum ID’si (session ID) istemciye verilir. JWT ise bu oturum ID’sini içerir.
Her istekte, JWT doğrulanır ve içindeki oturum ID’si ile sunucu tarafındaki oturumun hala aktif olup olmadığı kontrol edilir. Bir kullanıcı oturumu kapattığında veya yönetici tarafından devre dışı bırakıldığında, sunucu tarafındaki oturum anında sonlandırılabilir. Bu, JWT’nin hızlı doğrulaması ile geleneksel oturum yönetiminin güçlü iptal yeteneklerini birleştirir.
Trade-off’lar:
- Statefulness: Sunucu tarafında oturum durumu tutulduğu için “tamamen stateless” olmaktan uzaklaşılır.
- Performans: Her istekte oturum kontrolü ek bir veritabanı sorgusu gerektirebilir, bu da performansa etki edebilir. Ancak bu, Redis gibi in-memory bir cache sistemiyle minimize edilebilir.
Güvenlik Pratiklerinde Dikkat Edilmesi Gerekenler
JWT ve token yenileme/iptal mekanizmalarını tasarlarken göz önünde bulundurmamız gereken genel güvenlik pratikleri de var. Bunlar, token’ların kendisi kadar, onları kullanan sistemin genel direncini artırır.
1. Rate Limiting ve DDoS Koruması
Yetkisiz erişim denemelerini veya refresh token’ın brute force saldırılarını engellemek için rate limiting kritik öneme sahiptir. Login endpoint’ine ve refresh token endpoint’ine makul bir deneme sınırı koymak iyi bir pratiktir. Bu, Nginx üzerinde aşağıdaki gibi bir konfigürasyonla sağlanabilir:
http {
# ...
limit_req_zone $binary_remote_addr zone=login_req:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=refresh_req:10m rate=5r/m;
# ...
server {
# ...
location /api/auth/login {
limit_req zone=login_req burst=10 nodelay;
proxy_pass http://backend_service;
}
location /api/auth/refresh {
limit_req zone=refresh_req burst=5 nodelay;
proxy_pass http://backend_service;
}
}
}
Bu konfigürasyon, her IP adresinden /api/auth/login ve /api/auth/refresh endpoint’lerine dakikada 5 istekle sınırlama getirir. Ayrıca, genel DDoS saldırılarına karşı Cloudflare gibi servisler kullanarak Layer 7 seviyesinde koruma sağlamak, sunuculara ulaşan kötü niyetli trafiği azaltır.
2. Doğru JWT Claim’leri ve İmzalama Algoritması
JWT’lerin içinde doğru claims’lerin bulunması ve güvenli bir imzalama algoritması kullanılması şarttır:
iss(Issuer): Token’ı veren kuruluş.aud(Audience): Token’ın hangi hedef kitle için olduğu.exp(Expiration Time): Token’ın son kullanma zamanı.nbf(Not Before): Token’ın geçerli olmaya başlayacağı zaman.iat(Issued At): Token’ın oluşturulduğu zaman.jti(JWT ID): Token’a benzersiz bir kimlik sağlar, kara listeleme için kullanılır.
İmzalama algoritması olarak HS256 (HMAC with SHA-256) gibi simetrik algoritmalar genellikle yeterlidir ancak çoklu servislerin olduğu ve token’ı imzalayan servisin, doğrulayan servisten farklı olduğu durumlarda RS256 (RSA with SHA-256) gibi asimetrik algoritmalar tercih edilebilir. Asimetrik algoritmalar, token’ı imzalayan özel anahtarın (private key) ifşa edilmesi riskini azaltırken, herkesin genel anahtar (public key) ile doğrulama yapabilmesini sağlar. Benim genelde tercih ettiğim, eğer birden fazla servis token’ı doğrulayacaksa RS256’dır.
3. Gelişmiş Güvenlik Önlemleri
- Çok Faktörlü Kimlik Doğrulama (MFA): Özellikle hassas işlemler için MFA uygulamak, token çalınsa bile ek bir güvenlik katmanı sağlar.
- Oturum İzleme ve Anomali Tespiti: Kullanıcı davranışlarını izleyerek, normal dışı erişim kalıplarını (örneğin, aynı kullanıcının kısa sürede farklı coğrafi konumlardan giriş yapması) tespit edip şüpheli oturumları otomatik olarak sonlandırmak faydalıdır. Anormal giriş denemeleri veya yüksek sayıda başarısız işlem tespit edildiğinde ilgili
refresh token’ları otomatik olarak iptal etmek iyi bir önlemdir. - Güvenli İletişim: Tüm token alışverişi ve API iletişiminin sadece HTTPS üzerinden yapılması elzemdir.
Benim Yaklaşımım ve Gelecek Perspektifi
Yıllar içinde JWT’lerle ilgili birçok farklı senaryo ve sorunu gördüm. Özellikle güvenlik ve kullanılabilirlik dengesini kurmak çok kritik. Bu yüzden, refresh token rotation ve sunucu tarafında refresh token depolama kombinasyonunu genelde tercih ediyorum. Bu, access token’ların kısa ömürlü kalmasını sağlarken, refresh token’ların çalınması durumunda anında tespit ve iptal imkanı sunuyor.
Token iptali konusunda, Redis üzerinde JTI ile blacklisting yapmak, mevcut access token’ları anında geçersiz kılmak için yeterli bir çözüm sunuyor. Bu, özellikle bir kullanıcının acilen sistemden çıkarılması gerektiğinde veya şifre değişikliği gibi durumlarda hızlı aksiyon almamızı sağlıyor.
Gelecekte, AI destekli operasyonel izleme ve anomali tespiti, token güvenliğini bir sonraki seviyeye taşıyacak. AI modelleri, normal kullanıcı davranışlarını öğrenerek şüpheli token kullanımlarını çok daha hızlı ve doğru bir şekilde tespit edebilir. Örneğin, bir kullanıcının API çağrı frekansı veya tipindeki ani değişiklikler, bir AI agent tarafından incelenerek potansiyel bir token sızıntısı veya kötüye kullanım sinyali olarak yorumlanabilir. Hatta LLM’ler ile logları analiz edip, şüpheli token kullanım paternlerini bulmak üzerine denemeler yapıyorum.