Refresh Token Stratejisi: Uzun Süreli Oturum Yönetimi

Uzun süreli oturum yönetimi, API entegrasyonlarında en çok baş ağrıtan konulardan biri. Access token’ın 15 dakikada expire olduğunu fark eden bir kullanıcının “neden sürekli çıkış yapıyorum?” diye müşteri desteğine yazması… Klasik senaryo. İşte tam bu noktada refresh token stratejisi devreye giriyor ve doğru uygulandığında hem güvenliği hem de kullanıcı deneyimini aynı anda çözüyor.

Refresh Token Nedir ve Neden İhtiyaç Duyarız

OAuth 2.0 ve modern JWT tabanlı sistemlerde iki temel token tipi var: access token ve refresh token. Access token, korunan kaynaklara erişmek için kullanılan, kısa ömürlü bir kimlik bilgisi. Refresh token ise bu access token’ı yenilemek için kullanılan, çok daha uzun ömürlü ve güvenli saklanan bir token.

Neden iki ayrı token kullanıyoruz? Çünkü güvenlik ile kullanılabilirlik arasında ciddi bir gerilim var.

  • Kısa ömürlü access token: Çalınsa bile kısa sürede geçersiz kalır. 15 dakika, 1 saat gibi süreler. Ama kullanıcı her saat başı login olmak zorunda kalır.
  • Uzun ömürlü access token: Kullanıcı deneyimi iyi ama çalındığında günlerce geçerli kalabilir. Disaster senaryosu.
  • Access token + Refresh token kombinasyonu: Access token kısa ömürlü, refresh token uzun ömürlü ama sadece yeni token almak için kullanılır ve güvenli kanalda saklanır.

Bu yaklaşımın güzelliği şu: Refresh token, Authorization Server ile doğrudan iletişim gerektiriyor. Yani bir saldırgan access token’ı ele geçirse bile, kısa süre içinde geçersiz kalacak. Refresh token’ı ele geçirmek ise çok daha zor çünkü bu token API çağrılarında gönderilmiyor, sadece token yenileme işleminde kullanılıyor.

Token Ömürleri ve Strateji Belirleme

Doğru strateji için önce senaryonu net belirlemen gerekiyor. Her uygulama aynı gereksinimlere sahip değil.

Tipik token ömrü konfigürasyonları:

  • Access token süresi: 15 dakika ile 1 saat arası (güvenlik odaklı sistemler için 15 dk önerilen)
  • Refresh token süresi: 7 gün ile 90 gün arası (kullanım sıklığına göre değişir)
  • Sliding expiration: Her refresh işleminde süre sıfırlanır, aktif kullanıcılar hiç logout olmaz
  • Absolute expiration: Ne olursa olsun belirli bir süre sonra zorla logout, bankacılık uygulamaları için şart

Bir e-ticaret platformunda sliding expiration mantıklı. Kullanıcı her gün alışveriş yapıyorsa login olmadan devam etsin. Ama bir fintech uygulamasında absolute expiration zorunlu, regülasyon gereği her 24 saatte bir re-authentication isteyebilirsin.

Temel Refresh Token Akışı

Önce basit bir Python örneği ile temel akışı görelim:

# Basit token yenileme akışını test etmek için curl ile simüle edelim

# 1. Initial login - access ve refresh token al
curl -X POST https://api.example.com/auth/login 
  -H "Content-Type: application/json" 
  -d '{"username": "[email protected]", "password": "SecurePass123!"}' 
  | jq '{access_token, refresh_token, expires_in}'

# 2. Access token ile korunan endpoint'e istek
curl -X GET https://api.example.com/api/v1/users/profile 
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9..."

# 3. Token expire olduktan sonra refresh et
curl -X POST https://api.example.com/auth/refresh 
  -H "Content-Type: application/json" 
  -d '{"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..."}'

Şimdi bunu gerçek dünya senaryosunda nasıl otomatize ederiz, bir Python client yazalım:

# Python ortamı hazırlayalım
pip install requests python-jose cryptography

# Token yönetim modülü için dizin oluştur
mkdir -p /opt/api-client/auth
touch /opt/api-client/auth/__init__.py
touch /opt/api-client/auth/token_manager.py
# token_manager.py içeriği - production-ready refresh token yönetimi
cat << 'EOF' > /opt/api-client/auth/token_manager.py
import time
import json
import logging
import requests
from pathlib import Path
from threading import Lock

logger = logging.getLogger(__name__)

class TokenManager:
    def __init__(self, auth_url, client_id, client_secret, token_file="/tmp/.token_cache"):
        self.auth_url = auth_url
        self.client_id = client_id
        self.client_secret = client_secret
        self.token_file = Path(token_file)
        self._lock = Lock()
        self._tokens = self._load_tokens()
    
    def _load_tokens(self):
        """Disk'ten token cache yükle"""
        if self.token_file.exists():
            try:
                with open(self.token_file, 'r') as f:
                    return json.load(f)
            except (json.JSONDecodeError, KeyError):
                logger.warning("Token cache bozuk, temizleniyor")
                self.token_file.unlink()
        return {}
    
    def _save_tokens(self, tokens):
        """Token'ları güvenli şekilde diske kaydet"""
        # 600 permission - sadece owner okuyabilir
        self.token_file.touch(mode=0o600)
        with open(self.token_file, 'w') as f:
            json.dump(tokens, f)
        self._tokens = tokens
    
    def _is_token_expired(self, buffer_seconds=60):
        """Token süresi dolmadan buffer_seconds önce expired say"""
        if 'expires_at' not in self._tokens:
            return True
        return time.time() >= (self._tokens['expires_at'] - buffer_seconds)
    
    def get_valid_token(self):
        """Her zaman geçerli bir access token döner"""
        with self._lock:
            if self._is_token_expired():
                logger.info("Access token expired veya yakında expire, yenileniyor...")
                self._refresh_or_reauth()
            return self._tokens.get('access_token')
    
    def _refresh_or_reauth(self):
        """Refresh token varsa kullan, yoksa yeniden authenticate et"""
        if 'refresh_token' in self._tokens:
            try:
                self._do_refresh()
                return
            except RefreshTokenExpiredError:
                logger.warning("Refresh token da geçersiz, yeniden giriş gerekiyor")
        raise AuthenticationRequiredError("Yeniden kimlik doğrulama gerekli")
    
    def _do_refresh(self):
        response = requests.post(
            f"{self.auth_url}/token",
            data={
                "grant_type": "refresh_token",
                "refresh_token": self._tokens['refresh_token'],
                "client_id": self.client_id,
                "client_secret": self.client_secret,
            },
            timeout=10
        )
        
        if response.status_code == 401:
            del self._tokens['refresh_token']
            raise RefreshTokenExpiredError("Refresh token geçersiz")
        
        response.raise_for_status()
        token_data = response.json()
        
        self._save_tokens({
            "access_token": token_data['access_token'],
            "refresh_token": token_data.get('refresh_token', self._tokens.get('refresh_token')),
            "expires_at": time.time() + token_data.get('expires_in', 3600)
        })
        logger.info("Token başarıyla yenilendi")

class RefreshTokenExpiredError(Exception):
    pass

class AuthenticationRequiredError(Exception):
    pass
EOF

Rotation Stratejisi: Tek Kullanımlık Refresh Token

Modern sistemlerin çoğu refresh token rotation uyguluyor. Her token yenileme işleminde eski refresh token geçersiz kılınıyor ve yeni bir refresh token veriliyor. Bu sayede bir refresh token çalınsa bile, bir kez kullanıldıktan sonra geçersiz kalıyor.

# Nginx ile token endpoint'ini reverse proxy arkasına al
# ve rate limiting ekle - brute force koruması

cat << 'EOF' > /etc/nginx/conf.d/auth-ratelimit.conf
# Refresh token endpoint için agresif rate limiting
limit_req_zone $binary_remote_addr zone=refresh_limit:10m rate=10r/m;
limit_req_zone $http_x_device_id zone=device_limit:10m rate=30r/m;

server {
    listen 443 ssl;
    server_name api.example.com;

    location /auth/refresh {
        # IP başına dakikada 10 istek, burst 5
        limit_req zone=refresh_limit burst=5 nodelay;
        limit_req_status 429;
        
        # Device ID bazlı limit (mobile app için)
        limit_req zone=device_limit burst=10;
        
        proxy_pass http://auth-service:8080;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # Refresh token endpoint'ini loglama
        access_log /var/log/nginx/token-refresh.log combined;
    }
}
EOF

nginx -t && systemctl reload nginx

Rotation senaryosunda dikkat edilmesi gereken kritik bir nokta var: concurrent refresh istekleri. İki sekme aynı anda token yenilemeye çalışırsa ne olur? Birincisi başarılı olur, ikincisi invalid token hatası alır. Bu durumu handle etmek için locking mekanizması şart.

# Redis ile distributed lock - çoklu instance senaryosu için
# Token yenileme sırasında race condition önleme

cat << 'EOF' > /opt/api-client/auth/distributed_lock.py
import redis
import uuid
import time

class RedisRefreshLock:
    def __init__(self, redis_url="redis://localhost:6379"):
        self.redis = redis.from_url(redis_url)
        self.lock_timeout = 10  # saniye
    
    def acquire_refresh_lock(self, user_id):
        """Kullanıcı bazlı refresh lock al"""
        lock_key = f"token_refresh_lock:{user_id}"
        lock_value = str(uuid.uuid4())
        
        # SET NX EX - sadece key yoksa set et, 10 saniye TTL
        acquired = self.redis.set(
            lock_key, 
            lock_value, 
            nx=True, 
            ex=self.lock_timeout
        )
        
        if acquired:
            return lock_value
        return None
    
    def release_lock(self, user_id, lock_value):
        """Sadece lock'u alanın release edebilmesi için Lua script"""
        lua_script = """
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        """
        lock_key = f"token_refresh_lock:{user_id}"
        self.redis.eval(lua_script, 1, lock_key, lock_value)
    
    def wait_and_get_new_token(self, user_id, max_wait=5):
        """Lock tutuluyorsa bekle, diğer instance refresh etmiş olabilir"""
        start = time.time()
        while time.time() - start < max_wait:
            # Redis'ten fresh token oku
            token_key = f"access_token:{user_id}"
            token = self.redis.get(token_key)
            if token:
                return token.decode()
            time.sleep(0.1)
        raise TimeoutError("Token yenileme timeout")
EOF

Güvenli Token Saklama Stratejileri

Token’ı nerede sakladığın, refresh stratejin kadar önemli. Yanlış saklama tüm güvenlik çabanı boşa çıkarıyor.

Server-side uygulamalar için:

  • Refresh token’ları veritabanında hash’lenmiş şekilde sakla, bcrypt kullan
  • Token’ları sadece HTTPS üzerinden ilet
  • Access token’ları memory’de tut, diske yazma
  • Disk’e yazmak zorundaysan dosya permission’larını 600 olarak ayarla

Browser tabanlı uygulamalar için:

  • Refresh token’ı HttpOnly, Secure, SameSite=Strict cookie’de sakla
  • localStorage kullanma, XSS saldırısına açık
  • Access token memory’de tut, sayfa yenilenince refresh token cookie’den yenile
# Refresh token'ları veritabanında güvenli saklama
# PostgreSQL örneği - token rotation ile birlikte

cat << 'EOF' > /opt/auth-service/migrations/create_refresh_tokens.sql
CREATE TABLE refresh_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token_hash VARCHAR(255) NOT NULL UNIQUE,
    device_info JSONB,
    ip_address INET,
    created_at TIMESTAMP DEFAULT NOW(),
    expires_at TIMESTAMP NOT NULL,
    last_used_at TIMESTAMP,
    is_revoked BOOLEAN DEFAULT FALSE,
    family_id UUID NOT NULL,  -- Token rotation family tracking
    
    INDEX idx_refresh_token_hash (token_hash),
    INDEX idx_user_id_active (user_id) WHERE is_revoked = FALSE
);

-- Token family reuse detection için trigger
CREATE OR REPLACE FUNCTION detect_token_reuse()
RETURNS TRIGGER AS $$
BEGIN
    -- Revoke edilmiş token kullanılmaya çalışılıyorsa, tüm family'yi revoke et
    IF OLD.is_revoked = TRUE THEN
        UPDATE refresh_tokens 
        SET is_revoked = TRUE 
        WHERE family_id = OLD.family_id;
        
        RAISE EXCEPTION 'Token reuse detected for family: %', OLD.family_id;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;
EOF

psql -U postgres -d authdb -f /opt/auth-service/migrations/create_refresh_tokens.sql

Token Revocation ve Oturum Yönetimi

Bir kullanıcı logout yaptığında veya şifre değiştirdiğinde, refresh token’ların anında geçersiz kılınması gerekiyor. Bu, token blacklisting veya token revocation list ile yapılıyor.

# Redis tabanlı token blacklist - hızlı revocation
# Token'ın kalan geçerlilik süresi kadar Redis'te tut

cat << 'EOF' >> /opt/api-client/auth/token_manager.py

class TokenBlacklist:
    def __init__(self, redis_client):
        self.redis = redis_client
    
    def revoke_token(self, jti, expires_at):
        """JTI (JWT ID) bazlı blacklist - süresi dolana kadar tut"""
        ttl = int(expires_at - time.time())
        if ttl > 0:
            self.redis.setex(f"blacklist:{jti}", ttl, "1")
            logger.info(f"Token {jti} blacklist'e eklendi, TTL: {ttl}s")
    
    def is_revoked(self, jti):
        return self.redis.exists(f"blacklist:{jti}") > 0
    
    def revoke_all_user_tokens(self, user_id):
        """Kullanıcının tüm token'larını geçersiz kıl - şifre değişimi senaryosu"""
        # DB'deki tüm refresh token'ları revoke et
        # Redis'e user bazlı invalidation timestamp yaz
        invalidation_key = f"user_invalidated_at:{user_id}"
        self.redis.set(invalidation_key, str(time.time()), ex=86400 * 90)
        logger.warning(f"Kullanıcı {user_id} için tüm token'lar geçersiz kılındı")
EOF

Monitoring ve Alerting

Refresh token sisteminizin sağlığını izlemek, güvenlik açıklarını erkenden fark etmek için kritik.

# Token metriklerini Prometheus formatında expose eden script
# Anormal refresh oranları, failed refresh spikes gibi durumları izle

cat << 'EOF' > /opt/monitoring/token_metrics.sh
#!/bin/bash

LOG_FILE="/var/log/nginx/token-refresh.log"
METRICS_FILE="/var/lib/prometheus/node-exporter/token_metrics.prom"

# Son 5 dakikadaki refresh isteklerini say
REFRESH_COUNT=$(awk -v start="$(date -d '5 minutes ago' '+%d/%b/%Y:%H:%M')" 
  '$4 > "["start' "$LOG_FILE" | wc -l)

# 429 (rate limited) sayısı
RATE_LIMITED=$(grep " 429 " "$LOG_FILE" | 
  awk -v start="$(date -d '5 minutes ago' '+%d/%b/%Y:%H:%M')" 
  '$4 > "["start' | wc -l)

# 401 (invalid refresh token) sayısı  
INVALID_REFRESH=$(grep "POST /auth/refresh" "$LOG_FILE" | 
  grep " 401 " | 
  awk -v start="$(date -d '5 minutes ago' '+%d/%b/%Y:%H:%M')" 
  '$4 > "["start' | wc -l)

cat > "$METRICS_FILE" << METRICS
# HELP token_refresh_requests_total Son 5 dakikadaki refresh istekleri
# TYPE token_refresh_requests_total gauge
token_refresh_requests_total $REFRESH_COUNT

# HELP token_refresh_rate_limited_total Rate limited refresh istekleri
# TYPE token_refresh_rate_limited_total gauge
token_refresh_rate_limited_total $RATE_LIMITED

# HELP token_refresh_invalid_total Gecersiz refresh token istekleri
# TYPE token_refresh_invalid_total gauge
token_refresh_invalid_total $INVALID_REFRESH
METRICS

echo "Metrikler guncellendi: Refresh=$REFRESH_COUNT, RateLimited=$RATE_LIMITED, Invalid=$INVALID_REFRESH"
EOF

chmod +x /opt/monitoring/token_metrics.sh

# Cron ile her 5 dakikada çalıştır
echo "*/5 * * * * root /opt/monitoring/token_metrics.sh" > /etc/cron.d/token-metrics

İzlenmesi gereken kritik metrikler:

  • Refresh başarı/hata oranı: Ani artış, token sızdırma işaretçisi olabilir
  • Unique IP başına refresh sayısı: Tek IP’den çok fazla farklı kullanıcı token’ı yenileniyorsa alarm
  • Token reuse detection sayısı: Çalınmış token kullanım girişimi
  • Coğrafi anomali: Türkiye’den giriş yapan kullanıcının tokenı Brezilya’dan yenilenmeye çalışılıyor

Gerçek Dünya Senaryosu: Microservice Ortamında Token Yönetimi

Birden fazla microservice olan bir sistemde her servis ayrı token yönetimi yapmak yerine merkezi bir auth middleware kullanmak daha mantıklı.

# Kubernetes'te sidecar proxy pattern ile token yönetimi
# Her pod'a auth-proxy container ekle

cat << 'EOF' > /opt/k8s/auth-sidecar-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: auth-proxy-config
data:
  config.yaml: |
    auth_service_url: "https://auth.internal:8080"
    token_refresh_buffer: 120  # expire'dan 2 dk önce yenile
    cache_backend: "redis"
    redis_url: "redis://redis-service:6379/1"
    
    # Token rotation endpoint'i
    refresh_endpoint: "/oauth/token"
    grant_type: "refresh_token"
    
    # Retry politikası
    refresh_retry:
      max_attempts: 3
      backoff_ms: 500
      max_backoff_ms: 5000
    
    # Downstream servise inject edilecek header
    inject_header: "X-Internal-Auth-Token"
EOF

kubectl apply -f /opt/k8s/auth-sidecar-config.yaml

Bu yapıda uygulama kodu token yönetiminden tamamen habersiz oluyor. Sidecar proxy, gelen isteği yakalıyor, token geçerliyse forward ediyor, değilse yeniliyor ve sonra forward ediyor. Uygulama geliştiricisi sadece business logic’e odaklanıyor.

Güvenlik Kontrol Listesi

Refresh token stratejinizi production’a almadan önce şunları kontrol edin:

  • HTTPS zorunluluğu: Token endpoint’i asla plain HTTP üzerinden çalışmasın
  • Token entropy: Refresh token en az 256 bit rastgele veri içermeli, UUID4 kullanma (sadece 122 bit)
  • Database index: token_hash kolonunda mutlaka index olsun, her istekte full table scan felakete davet
  • Expiry check: Token süresini sunucu tarafında kontrol et, client’a güvenme
  • IP binding: Risk toleransına göre, refresh token’ı ilk alınan IP’ye bind etmeyi değerlendir
  • Device fingerprint: Mobile uygulamalarda device ID ile token’ı ilişkilendir
  • Audit log: Her refresh işlemini, başarılı veya başarısız, logla. Compliance gereksinimi de olabilir
  • Token limit per user: Bir kullanıcının aktif olabilecek maksimum refresh token sayısını sınırla (örneğin 5 cihaz)

Sonuç

Refresh token stratejisi, “kullanıcı sürekli login olmak zorunda kalmasın ama güvenlik de sağlam olsun” probleminin zarif çözümü. Ancak bu eleganlığın arkasında dikkat edilmesi gereken onlarca detay var.

En yaygın hatalar şunlar: Refresh token’ı localStorage’da saklamak, rotation uygulamadan uzun ömürlü refresh token kullanmak, token reuse detection’ı ihmal etmek ve revocation mekanizması kurmamak. Bu hataların herhangi biri, güvenli zannettiğin sistemi açık kapıya çeviriyor.

Doğru uygulandığında elde ettiğin şey ise kullanıcıların haftalarca, hatta aylarca sorunsuz oturum açık tutabildiği, güvenlik olayı yaşandığında anında müdahale edebildiğin ve tüm token aktivitesini gözlemleyebildiğin sağlam bir altyapı. Monitoring’i ihmal etme, token sistemlerindeki anormallikler çoğu zaman büyük bir güvenlik olayının erken habercisidir. Metrikleri kur, alert’leri ayarla ve gece sakin uyu.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir