Kimlik Doğrulama ve Oturum Yönetimi← İçindekiler Güvenli Kodlama Eğitimi

Kimlik Doğrulama ve Oturum Yönetimi

B

2009’da RockYou adlı sosyal medya sitesi 32 milyon kullanıcının parolasını sızdırdı. Parolalar düz metin olarak veritabanında duruyordu. Hiç şifreleme yoktu. Bu sızıntı, dünyanın en büyük parola sözlüğüne kaynak oldu: sızan parolalar, bugün hâlâ brute force saldırılarında kullanılıyor.

Özlü kural: Parola asla düz metin olarak saklanmaz. Hash’lenir, tuzlanır ve yavaş bir algoritmayla işlenir. “Veritabanım güvende” diye düşünme; veritabanı bir gün sızar, o gün elinde ne kalırsa ona bakacaksın.

Hızlı ipucu: Veritabanındaki parola kolonuna bak. Düz metin veya MD5 görüyorsan, bu gece düzelt.

53.1 Parola Saklama

Parola saklamanın tek doğru yolu vardır: yavaş, tuzlu, parametreli bir hash fonksiyonu kullanmak. Hızlı algoritmalar (MD5, SHA-1, SHA-256) parola saklamak için YANLIŞ’tır. Neden? Çünkü bu algoritmalar saniyede milyonlarca deneme yapabilir. Saldırgan bir GPU ile saniyede 100 milyar MD5 hash üretebilir. 8 karakterlik bir parola dakikalar içinde kırılır.

Kötü Yaklaşımlar

import hashlib

# YANLIS 1: Duz metin saklama
def save_password_bad_v1(password):
    db.execute("INSERT INTO users (password) VALUES (?)", (password,))
    # Veritabani sizarsa tum parolalar acikta

# YANLIS 2: MD5 (kirilmis, cok hizli)
def save_password_bad_v2(password):
    hashed = hashlib.md5(password.encode()).hexdigest()
    db.execute("INSERT INTO users (password) VALUES (?)", (hashed,))
    # MD5 saniyede 100 milyar deneme yapilabilir
    # Rainbow table ile aninda kirilir

# YANLIS 3: SHA-256 tuzsuz (hizli, rainbow table acigi)
def save_password_bad_v3(password):
    hashed = hashlib.sha256(password.encode()).hexdigest()
    db.execute("INSERT INTO users (password) VALUES (?)", (hashed,))
    # SHA-256 saniyede milyarlarca deneme
    # Tuz yok, ayni parola ayni hash uretir

Bu üç yöntem de yanlış çünkü hepsi HIZLI. Parola hash’lemek için hız istemezsin. Tam tersine, yavaş olmasını istersin. Saldırganın her denemesi 0.5 saniye sürerse, 8 karakterlik bir parolayı kırmak binlerce yıl alır.

Doğru Yaklaşım: bcrypt

import bcrypt

def hash_password(password: str) -> bytes:
    """Parolayi bcrypt ile guvenli sekilde hash'le"""
    # 1. Parolayi bytes'a cevir
    password_bytes = password.encode('utf-8')
    
    # 2. bcrypt otomatik olarak tuz (salt) uretir
    # gensalt(12) icindeki 12, maliyet faktorudur (cost factor)
    # Her artis, hashleme suresini 2 katina cikarir
    # 12 = yaklasik 0.3 saniye (varsayilan)
    salt = bcrypt.gensalt(rounds=12)
    
    # 3. Hash'le
    hashed = bcrypt.hashpw(password_bytes, salt)
    return hashed

def verify_password(password: str, hashed: bytes) -> bool:
    """Kullanici girisini dogrula"""
    password_bytes = password.encode('utf-8')
    # checkpw, hash ve girisi karsilastirir
    # timing attack'e karsi sabit sureli karsilastirma yapar
    return bcrypt.checkpw(password_bytes, hashed)

# Kullanim
hashed_pw = hash_password("KullanicininParola123")
# Veritabanina hashed_pw kaydet

# Giris dogrulama
if verify_password(input_password, stored_hashed_pw):
    print("Parola dogru")
else:
    print("Parola yanlis")

bcrypt’in özelliği: hash’in içinde tuz (salt) gömülüdür. Aynı parola her seferinde farklı hash üretir. Maliyet faktörü (rounds=12) zamanla artırılabilir, böylece donanım geliştikçe güvenlik seviyesi korunur.

Alternatif: Argon2

# Argon2, bcrypt'in halefidir, daha modern bir parola hash algoritmasidir
# 2015 Parola Hashleme Yarismasi kazananidir
from argon2 import PasswordHasher

ph = PasswordHasher(
    time_cost=3,       # Iterasyon sayisi (yuksek = yavas = guvenli)
    memory_cost=65536, # 64 MB bellek kullanimi (GPU saldirilarini engeller)
    parallelism=4,     # Es zamanli is parcacigi sayisi
)

def hash_password_argon2(password: str) -> str:
    return ph.hash(password)

def verify_password_argon2(password: str, hash_str: str) -> bool:
    try:
        return ph.verify(hash_str, password)
    except argon2.exceptions.VerifyMismatchError:
        return False  # Parola yanlis
    except argon2.exceptions.InvalidHashError:
        return False  # Hash formati bozuk

Argon2, GPU ve ASIC saldırılarına karşı daha dirençli çünkü bellek gereksinimi yüksektir. Bir GPU 100 milyar MD5 hesaplayabilir ama sadece birkaç Argon2 hesaplayabilir çünkü her hesap 64 MB bellek ister.

Pepper (Biber) kavramı: Salt her kullanıcı için farklıdır ve hash’in içinde saklanır. Pepper ise tüm kullanıcılar için aynıdır ve hash’in DIŞINDA, uygulamanın kodunda veya ayrı bir yapılandırmada saklanır. Saldırgan veritabanını çalsa bile pepper’ı bilemez, bu yüzden hash’leri kıramaz.

import os
import hmac
import hashlib
import bcrypt

# Pepper uygulamanin environment variable'inda saklanir
# Veritabaninda DEGIL
PEPPER = os.environ['PASSWORD_PEPPER']  # ornegin: 32 byte rastgele string

def hash_with_pepper(password: str) -> bytes:
    """Once pepper ile HMAC uygula, sonra bcrypt ile hash'le"""
    # 1. Parola + pepper -> HMAC-SHA256
    peppered = hmac.new(
        PEPPER.encode('utf-8'),
        password.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    # 2. Pepper'lanmis degeri bcrypt ile hash'le
    return bcrypt.hashpw(peppered.encode('utf-8'), bcrypt.gensalt(12))

def verify_with_pepper(password: str, hashed: bytes) -> bool:
    peppered = hmac.new(
        PEPPER.encode('utf-8'),
        password.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    return bcrypt.checkpw(peppered.encode('utf-8'), hashed)
Bunu Yap
  • bcrypt (rounds=12+) veya Argon2 kullan
  • Her kullanıcı için ayrı tuz (salt) kullan
  • Pepper uygulama kodunda sakla, veritabanında değil
  • Maliyet faktörünü yıllık gözden geçir, gerekirse artır
Bunu Yapma
  • MD5, SHA-1, SHA-256 ile parola hash'le
  • Aynı tuzu tüm kullanıcılar için kullan
  • Parolayı logla veya hata mesajına ekle
  • Kendi hash fonksiyonunu yaz

53.2 Oturum Yönetimi

Kullanıcı giriş yaptı. Şimdi “bu kullanıcı hâlâ giriş yapmış mı?” sorusunu her istekte yanıtlaman gerek. Bunu oturum (session) yönetimi ile yaparsın.

Güvenli Oturum ID’si Üretimi

import secrets

def generate_session_id() -> str:
    """ Kriptografik olarak guvenli oturum ID'si uret"""
    # secrets module, random'dan farkli olarak
    # isletim sisteminin entropy kaynagini kullanir
    # 32 byte = 256 bit = 43 karakterlik URL-safe string
    return secrets.token_urlsafe(32)

# KOTU: random veya uuid kullanma
import random
bad_id = str(random.randint(100000, 999999))  # Tahmin edilebilir!
import uuid
bad_id2 = str(uuid.uuid4())  # uuid4 guvenlidir ama secrets daha kisadir
from flask import Flask, make_response

app = Flask(__name__)

@app.route('/login', methods=['POST'])
def login():
    # ... parola dogrulama ...
    
    session_id = generate_session_id()
    save_session(session_id, user_id)
    
    response = make_response("Giris basarili")
    response.set_cookie(
        'session_id',
        session_id,
        httponly=True,       # JavaScript erisemez (XSS korumasi)
        secure=True,         # Sadece HTTPS uzerinden gonderilir
        samesite='Lax',      # CSRF korumasi
        max_age=3600,        # 1 saat sonra cookie biter
        path='/',            # Tum site gecerli
    )
    return response
Cookie bayrağıNe yaparNeden önemli
HttpOnlyJavaScript cookie’ye erişemezXSS ile cookie çalınmasını engeller
SecureCookie sadece HTTPS’te gönderilirDinleme (sniffing) saldırılarını engeller
SameSite=LaxBaska siteden gelen istekte cookie gitmezCSRF koruması sağlar
max_ageCookie’nin ömrüSonsuz oturum riskini azaltır

Oturum sabitleme (session fixation) saldırısı: Saldırgan, kullanıcıya kendi belirlediği bir oturum ID’si verir (örneğin URL ile). Kullanıcı bu ID ile giriş yapar. Saldırgan aynı ID’yi kullanarak kullanıcı hesabına girer.

def login_secure(user_id):
    # ESKI oturum ID'sini hemen Invalidate et
    old_session = request.cookies.get('session_id')
    if old_session:
        delete_session(old_session)
    
    # HER ZAMAN yeni oturum ID'si uret
    # Giris sirasinda oturum ID degismezse, fixation acigi var demektir
    new_session_id = generate_session_id()
    save_session(new_session_id, user_id)
    return new_session_id

53.3 JWT Güvenliği

JWT (JSON Web Token), oturum bilgilerini JSON olarak taşıyan, dijital imzalı bir tokendır. Tarayıcıdan sunucuya her istekte gönderilir. Oturum bilgisi sunucuda değil, token’ın içinde taşınır (stateless).

JWT üç bölümden oluşur: header.payload.signature

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjMsImV4cCI6MTcwMA==.aBcDeFgHiJkL
|--- header ---||-------- payload --------||----- signature -----|

Doğru Kullanım

import jwt
import datetime
import os

SECRET_KEY = os.environ['JWT_SECRET']  # Cok uzun, rastgele bir anahtar

def create_jwt(user_id: int) -> str:
    """Giris yapmis kullanici icin JWT uret"""
    payload = {
        'user_id': user_id,
        'iat': datetime.datetime.now(datetime.timezone.utc),  # Olusturma zamani
        'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=15),
        'iss': 'myapp',  # Token'i kim yayinladi
        'aud': 'myapp-users',  # Token kimin icin
    }
    # HS256 ile imzala
    return jwt.encode(payload, SECRET_KEY, algorithm='HS256')

def verify_jwt(token: str) -> dict:
    """Token'i dogrula ve payload'u dondur"""
    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=['HS256'],  # SADECE beklenen algoritma
            issuer='myapp',
            audience='myapp-users',
        )
        return payload
    except jwt.ExpiredSignatureError:
        raise Exception("Token suresi dolmus")
    except jwt.InvalidTokenError:
        raise Exception("Gecersiz token")

Kritik Güvenlik Açıkları

1. alg: none açığı: En tehlikeli JWT açığı. Saldırgan token’ın header’ını {"alg": "none"} olarak değiştirir. Zayıf implementasyonlar imzayı kontrol etmeden token’ı kabul eder.

# KOTU KUTUPHANE: imzalamayi tamamen kapatir (alg=none dahil tum ataklara acik)
payload = jwt.decode(token, verify=False)  # ASLA boyle yapma!

# IYI KUTUPHANE: sadece HS256 kabul eder
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
# algorithms parametresi ZORUNLU, eksik olamaz

2. Symmetric vs Asymmetric karışıklığı: Token RS256 (asymmetric) ile imzalanmış ama sunucu HS256 (symmetric) bekliyorsa, saldırgan public key’i kendi imzası olarak kullanabilir.

3. Hassas veri payload’da: JWT payload Base64 ile encode edilir, şifrelenmez. “password”, “credit_card” gibi verileri payload’a KOYMA. Token herkes tarafından okunabilir.

53.4 OAuth 2.0 ve OIDC

OAuth 2.0, bir uygulamanın başka bir uygulama adına hareket etmesine izin veren bir yetki protokolüdür. “Bu uygulamaya kendi adına tweet atma izni veriyor musun?” mantığı. OIDC (OpenID Connect), OAuth 2.0’ın üzerine inşa edilmiş bir kimlik doğrulama katmanıdır.

En yaygın akış: Authorization Code Flow + PKCE

# OAuth 2.0 Authorization Code Flow (kabaca)
# 1. Kullaniciyi yetki sunucusuna yonlendir
auth_url = (
    f"https://auth.example.com/authorize?"
    f"response_type=code"
    f"&client_id={CLIENT_ID}"
    f"&redirect_uri={REDIRECT_URI}"
    f"&scope=openid profile email"
    f"&state={random_state}"  # CSRF korumasi
    f"&code_challenge={pkce_challenge}"  # PKCE
)
# Tarayici auth_url'e yonlenir

# 2. Yetki sunucusu kod ile geri doner
# https://app.example.com/callback?code=xyz123&state=xxx

# 3. Kodu access token ile takas et
response = requests.post("https://auth.example.com/token", data={
    "grant_type": "authorization_code",
    "code": auth_code,
    "client_id": CLIENT_ID,
    "code_verifier": pkce_verifier,  # PKCE
    "redirect_uri": REDIRECT_URI,
})
access_token = response.json()["access_token"]

PKCE (Proof Key for Code Exchange): Mobil ve tek sayfa uygulamalarında client_secret güvenli saklanamaz. PKCE, her yetki isteği için dinamik bir secret üretir. 2024’ten beri tüm OAuth istemcilerinde PKCE zorunludur.

Implicit flow neden önerilmiyor?

Eski OAuth 2.0 uygulamalarında “implicit flow” kullanılırdı. Bu akışta access token doğrudan URL fragment’ında (#token=...) gönderilir. Sorun: token tarayıcı geçmişinde kalır, JavaScript’e sızar, loglanabilir. Modern uygulamalarda implicit flow yerine authorization code flow + PKCE kullanılır. Token URL’de değil, sunucudan sunucuya POST ile alınır.

53.5 MFA Mimarisi

Multi-Factor Authentication (MFA), kullanıcının kim olduğunu kanıtlamak için iki veya daha fazla faktör ister. Faktörler:

  • Bildiği bir şey (parola, PIN)
  • Sahip olduğu bir şey (telefon, hardware token)
  • Olduğu bir şey (parmak izi, yüz tanıma)

TOTP (Time-based One-Time Password)

import pyotp

# 1. Kullanimci icin benzersiz secret uret
secret = pyotp.random_base32()  # 32 karakter base32
# Bu secret'i veritabaninda kullanici ile birlikte sakla
# Kullaniciya QR kod olarak goster (Google Authenticator ile tara)

# 2. 6 haneli kod uret (30 saniyede bir degisir)
totp = pyotp.TOTP(secret)
current_code = totp.now()  # ornegin: "123456"

# 3. Giris sirasinda kullanicinin girdigi kodu dogrula
user_code = request.form['mfa_code']
if totp.verify(user_code):
    print("MFA dogrulandi")
else:
    print("Yanlis kod")
    # Burada rate limiting yap: 3 yanlistan sonra kilitle

Backup codes: Telefonunu kaybeden kullanıcı hesaba giremez. Bunun için kayıt sırasında tek kullanımlık yedek kodlar üret ve kullanıcıya yazdır:

import secrets

def generate_backup_codes(count=10) -> list:
    """Tek kullanimlik yedek kodlar uret"""
    codes = []
    for _ in range(count):
        # Her kod 16 haneli, tire ile ayrik (ornegin: ABCD-1234-EFGH-5678)
        raw = secrets.token_hex(8).upper()
        formatted = '-'.join([raw[i:i+4] for i in range(0, 16, 4)])
        codes.append(formatted)
    return codes

MFA uygularken rate limiting şarttır. Saldırgan 6 haneli bir TOTP kodunu denemek için en fazla 1.000.000 kombinasyon dener. 30 saniyede sınırsız deneme yapılmasına izin verirsen, otomatik bir araç dakikalar içinde kodu bulur. Çözüm: 3 yanlış denemeden sonra hesabı 15 dakika kilitle veya denemeler arası 1 saniye bekleme süresi koy.

Senaryo: JWT Token Çalındı

Bir kullanıcının JWT token’ı XSS açığı ile çalındı. Saldırgan bu token’ı kullanarak API’ye istek gönderiyor. JWT stateless olduğu için sunucu token’ın çalındığını bilemez. Token’ın süresi 1 saat. Bu 1 saat boyunca saldırgan istediği işlemi yapabilir. Çözüm: access token süresini kısa tut (15 dakika). Daha uzun süreli oturum için refresh token kullan ve refresh token’ı veritabanında tut. Çalınma şüphesi olduğunda refresh token’ı veritabanından sil, kullanıcı yeniden giriş yapmak zorunda kalsın.

SMS tabanlı MFA neden önerilmiyor?

SMS ile gönderilen doğrulama kodu, SIM swap saldırısına açıktır. Saldırgan telefon sağlayıcısını kandırıp kullanıcının numarasını kendi SIM kartına aktarır. Artık doğrulama SMS’leri saldırgana gider. Ayrıca SMS trafiği şifresiz protokollerle iletilebilir (SS7 açığı). NIST, SMS tabanlı MFA’yı “kısıtlı kullanım” kategorisine aldı. TOTP (Google Authenticator) veya hardware token (YubiKey) her zaman SMS’den daha güvenlidir.

Kimlik Doğrulama Güvenlik Kontrol Listesi
  • Parolalar bcrypt (rounds=12+) veya Argon2 ile hash’leniyor
  • Her kullanıcı için ayrı salt + pepper kullanılıyor
  • Oturum ID’leri secrets.token_urlsafe ile üretiliyor
  • Cookie’lerde HttpOnly + Secure + SameSite bayrakları açık
  • Girişte yeni oturum ID üretiliyor (fixation koruması)
  • JWT’de algorithms parametresi zorunlu, alg:none kabul edilmiyor
  • JWT payload’da hassas veri yok
  • Access token kısa (15 dk), refresh token veritabanında
  • MFA için TOTP kullanılıyor, rate limiting açık
  • Backup codes üretiliyor ve tek kullanımlık

Gerçek olay: 2012’de LinkedIn, 165 milyon kullanıcının parolasını sızdırdı. Parolalar SHA-1 ile hash’lenmişti ama tuzsuz (unsalted). Saldırganlar, hazır tablolarla milyonlarca parolayı kırdı. Sonuç: parolalar darknet’te satıldı, kullanıcıların diğer hesapları da ele geçirildi. Ders: hash yetmez, tuz (salt) ve yavaş algoritma şarttır.

Benzetme: Kimlik doğrulama, bir binanın kapısındaki güvenlik görevlisidir. Parolanı söylersin, içeri alır. Oturum yönetimi, içeride dolaşırken “hâlâ sen misin?” diye biletini kontrol eden görevlidir. JWT ise kapıda biletini yırtıp cebine koyduğun sistemdir: içeride kimse seni tekrar kontrol etmez. Bilet çalınırsa, başkası senin yerine gezer.

İlgili Bölümler