OAuth 2.0 ile Mobil Uygulama Kimlik Doğrulama

Mobil uygulama geliştirirken en çok baş ağrıtan konulardan biri kimlik doğrulama. “Kullanıcı adı ve şifre yeter” diyenler var, ama modern mobil ekosistemde bu yaklaşım hem güvensiz hem de kullanıcı deneyimi açısından berbat. OAuth 2.0, özellikle mobil uygulamalar için tasarlanmış flow’larıyla bu sorunu çok daha zarif bir şekilde çözüyor. Gelin, bir sistem yöneticisi ve API entegrasyon uzmanı gözünden OAuth 2.0’ın mobil uygulamalarda nasıl çalıştığını, hangi akışların kullanılması gerektiğini ve production ortamında nelere dikkat etmeniz gerektiğini inceleyelim.

OAuth 2.0 Nedir ve Mobil İçin Neden Önemli?

OAuth 2.0, yetkilendirme için bir framework. Kimlik doğrulama (authentication) ile yetkilendirme (authorization) arasındaki farkı anlamak burada kritik. OAuth 2.0 temelde bir yetkilendirme protokolü, ama OpenID Connect (OIDC) ile birleştiğinde tam bir kimlik doğrulama çözümüne dönüşüyor.

Mobil uygulamalarda klasik “backend’e username/password gönder, token al” yaklaşımının sorunları:

  • Kullanıcı şifresini mobil uygulamaya teslim ediyor, bu ciddi bir güvenlik riski
  • Şifre değiştiğinde tüm session’lar geçersiz oluyor
  • Çoklu cihaz yönetimi karmaşık
  • Third-party servislerle entegrasyon için kullanıcının şifresini paylaşmak gerekiyor
  • Token revoke işlemi granular değil

OAuth 2.0 ile bu sorunların tamamı ortadan kalkıyor. Uygulamanız asla kullanıcı şifresine erişmiyor, sadece yetkilendirilmiş bir token alıyor.

Mobil Uygulamalar İçin Doğru Flow: PKCE

Mobil uygulamalar için Authorization Code Flow with PKCE (Proof Key for Code Exchange) kullanmak zorundasınız. Neden? Çünkü mobil uygulamalar “public client” olarak sınıflandırılıyor, yani client_secret‘ı güvenle saklayamıyorlar. APK/IPA dosyasını tersine mühendislikle analiz eden biri, içine gömmüş olduğunuz client_secret’ı bulabilir.

PKCE bu sorunu şu şekilde çözüyor:

  • Her authorization isteği için rastgele bir code_verifier oluşturuyorsunuz
  • Bu verifier’ın SHA-256 hash’ini code_challenge olarak authorization sunucusuna gönderiyorsunuz
  • Token exchange sırasında orijinal code_verifier‘ı sunucu doğruluyor
  • Böylece authorization code intercept edilse bile kullanılamıyor

PKCE Flow Adım Adım

Önce teorik akışı anlayalım, sonra koda geçelim:

  1. Uygulama rastgele code_verifier üretiyor (43-128 karakter)
  2. code_verifier‘dan code_challenge türetiliyor (SHA-256 + base64url)
  3. Browser/in-app browser açılıyor, authorization endpoint’e gidiliyor
  4. Kullanıcı giriş yapıyor ve izin veriyor
  5. Authorization server code ile redirect yapıyor
  6. Uygulama code + code_verifier ile token endpoint’i çağırıyor
  7. Access token ve refresh token alınıyor

Backend Tarafında Authorization Server Kurulumu

Production ortamında genellikle Keycloak, Auth0, Okta veya kendinizin yönettiği bir OAuth server kullanıyorsunuz. Ben bu örnekte Keycloak üzerinden gideceğim çünkü open-source ve tam kontrol sizde.

Docker ile hızlı Keycloak kurulumu:

# Keycloak 23.x ile production-ready kurulum
docker run -d 
  --name keycloak 
  -p 8080:8080 
  -e KEYCLOAK_ADMIN=admin 
  -e KEYCLOAK_ADMIN_PASSWORD=supersecretpassword 
  -e KC_DB=postgres 
  -e KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak 
  -e KC_DB_USERNAME=keycloak 
  -e KC_DB_PASSWORD=dbpassword 
  -e KC_HOSTNAME=auth.sirketiniz.com 
  -e KC_HTTPS_CERTIFICATE_FILE=/opt/keycloak/conf/tls.crt 
  -e KC_HTTPS_CERTIFICATE_KEY_FILE=/opt/keycloak/conf/tls.key 
  quay.io/keycloak/keycloak:23.0.0 start

Keycloak’ta mobil uygulama için client oluşturmak:

# Keycloak Admin CLI ile client oluşturma
# Önce admin token al
ADMIN_TOKEN=$(curl -s -X POST 
  "https://auth.sirketiniz.com/realms/master/protocol/openid-connect/token" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "username=admin&password=supersecretpassword&grant_type=password&client_id=admin-cli" 
  | jq -r '.access_token')

# Mobil uygulama için public client oluştur
curl -s -X POST 
  "https://auth.sirketiniz.com/admin/realms/myapp/clients" 
  -H "Authorization: Bearer $ADMIN_TOKEN" 
  -H "Content-Type: application/json" 
  -d '{
    "clientId": "myapp-mobile",
    "name": "MyApp Mobile Client",
    "publicClient": true,
    "standardFlowEnabled": true,
    "directAccessGrantsEnabled": false,
    "redirectUris": [
      "myapp://callback",
      "https://myapp.com/auth/callback"
    ],
    "webOrigins": ["*"],
    "attributes": {
      "pkce.code.challenge.method": "S256"
    }
  }'

Burada dikkat edilmesi gereken nokta: publicClient: true ile directAccessGrantsEnabled: false. Direct access grants (resource owner password flow) mobil uygulamalar için kapatılmalı.

PKCE İmplementasyonu: Sunucu Tarafı Token Doğrulama

Backend API’nizin token doğrulamasını nasıl yapacağını görelim. Python/FastAPI örneği:

# Gerekli paketleri kur
pip install fastapi python-jose[cryptography] httpx python-multipart

# JWKS endpoint'ten public key'leri çek ve cache'le
cat > token_validator.py << 'EOF'
import httpx
import json
from jose import jwt, JWTError
from fastapi import HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

KEYCLOAK_URL = "https://auth.sirketiniz.com"
REALM = "myapp"
JWKS_URL = f"{KEYCLOAK_URL}/realms/{REALM}/protocol/openid-connect/certs"

security = HTTPBearer()
jwks_cache = None

async def get_jwks():
    global jwks_cache
    if jwks_cache is None:
        async with httpx.AsyncClient() as client:
            response = await client.get(JWKS_URL)
            jwks_cache = response.json()
    return jwks_cache

async def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
    token = credentials.credentials
    try:
        jwks = await get_jwks()
        # Token header'dan kid al
        unverified_header = jwt.get_unverified_header(token)
        
        # Uygun key'i bul
        rsa_key = {}
        for key in jwks["keys"]:
            if key["kid"] == unverified_header["kid"]:
                rsa_key = {
                    "kty": key["kty"],
                    "kid": key["kid"],
                    "use": key["use"],
                    "n": key["n"],
                    "e": key["e"]
                }
        
        if not rsa_key:
            raise HTTPException(status_code=401, detail="Public key bulunamadi")
        
        payload = jwt.decode(
            token,
            rsa_key,
            algorithms=["RS256"],
            audience="myapp-mobile",
            issuer=f"{KEYCLOAK_URL}/realms/{REALM}"
        )
        return payload
    except JWTError as e:
        raise HTTPException(status_code=401, detail=f"Token gecersiz: {str(e)}")
EOF

Token Refresh Mekanizması

Mobil uygulamalarda en kritik konulardan biri refresh token yönetimi. Access token süresi dolduğunda kullanıcıyı tekrar login ekranına atmak kabul edilemez bir UX.

# Refresh token ile yeni access token alma
# Bu script mobil uygulama backend'inde veya test amaçlı kullanılabilir

#!/bin/bash
KEYCLOAK_URL="https://auth.sirketiniz.com"
REALM="myapp"
CLIENT_ID="myapp-mobile"
REFRESH_TOKEN="mevcut_refresh_token_buraya"

# Token yenile
RESPONSE=$(curl -s -X POST 
  "${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "grant_type=refresh_token" 
  -d "client_id=${CLIENT_ID}" 
  -d "refresh_token=${REFRESH_TOKEN}")

# Yeni token'ları parse et
NEW_ACCESS_TOKEN=$(echo $RESPONSE | jq -r '.access_token')
NEW_REFRESH_TOKEN=$(echo $RESPONSE | jq -r '.refresh_token')
EXPIRES_IN=$(echo $RESPONSE | jq -r '.expires_in')

echo "Yeni access token alindi, ${EXPIRES_IN} saniye gecerli"
echo "Access Token: ${NEW_ACCESS_TOKEN:0:50}..."

# Hata kontrolü
if [ "$NEW_ACCESS_TOKEN" == "null" ]; then
  ERROR=$(echo $RESPONSE | jq -r '.error_description')
  echo "HATA: Refresh token gecersiz veya suresi dolmus: $ERROR"
  echo "Kullanici yeniden login olmali"
  exit 1
fi

Refresh token rotation’ı mutlaka etkinleştirin. Her refresh işleminde yeni bir refresh token alınmalı ve eskisi geçersiz olmalı. Bu sayede çalınan bir refresh token’ın tekrar kullanılması engellenebiliyor.

Token Güvenli Saklama

Mobil cihazlarda token’ları nerede sakladığınız son derece önemli. Yanlış saklama, cihaz rootlanmış/jailbreak yapılmış olsa bile risk oluşturuyor.

iOS için: Keychain kullanın, UserDefaults’a kesinlikle token saklamayın.

Android için: EncryptedSharedPreferences veya Android Keystore kullanın, düz SharedPreferences kullanmayın.

Backend tarafında ise token blacklist mekanizması kurmak gerekiyor:

# Redis ile token blacklist implementasyonu
# Token revoke edildiğinde JTI (JWT ID) Redis'e yazılıyor

#!/bin/bash
# Token blacklist kontrol scripti (test/debug amaçlı)

REDIS_HOST="localhost"
REDIS_PORT="6379"
TOKEN_JTI="token-jti-degeri-buraya"

# Token blacklist'te mi kontrol et
IS_BLACKLISTED=$(redis-cli -h $REDIS_HOST -p $REDIS_PORT 
  GET "blacklist:${TOKEN_JTI}")

if [ -n "$IS_BLACKLISTED" ]; then
  echo "UYARI: Bu token blacklist'te, erişim reddedilmeli"
else
  echo "Token gecerli, blacklist'te degil"
fi

# Token'ı blacklist'e ekle (logout işleminde)
TOKEN_EXPIRY=3600  # saniye cinsinden token TTL
redis-cli -h $REDIS_HOST -p $REDIS_PORT 
  SETEX "blacklist:${TOKEN_JTI}" $TOKEN_EXPIRY "revoked"

echo "Token blacklist'e eklendi, ${TOKEN_EXPIRY} saniye sonra otomatik silinecek"

Introspection Endpoint ile Token Doğrulama

JWKS tabanlı yerel doğrulama yerine bazen server-side introspection tercih edilebilir. Özellikle token’ın aktif olup olmadığını real-time kontrol etmek istediğinizde:

#!/bin/bash
# OAuth 2.0 Token Introspection
# RFC 7662 standardına uygun

KEYCLOAK_URL="https://auth.sirketiniz.com"
REALM="myapp"
# Introspection için confidential client gerekiyor (bu backend servisi)
CLIENT_ID="myapp-backend"
CLIENT_SECRET="backend_client_secret_buraya"
ACCESS_TOKEN="dogrulanacak_token_buraya"

INTROSPECT_RESPONSE=$(curl -s -X POST 
  "${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token/introspect" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -u "${CLIENT_ID}:${CLIENT_SECRET}" 
  -d "token=${ACCESS_TOKEN}")

IS_ACTIVE=$(echo $INTROSPECT_RESPONSE | jq -r '.active')
USERNAME=$(echo $INTROSPECT_RESPONSE | jq -r '.preferred_username')
SCOPE=$(echo $INTROSPECT_RESPONSE | jq -r '.scope')
EXPIRY=$(echo $INTROSPECT_RESPONSE | jq -r '.exp')

if [ "$IS_ACTIVE" == "true" ]; then
  echo "Token gecerli"
  echo "Kullanici: $USERNAME"
  echo "Scope: $SCOPE"
  echo "Gecerlilik suresi (Unix timestamp): $EXPIRY"
else
  echo "Token gecersiz veya suresi dolmus"
  echo "Tam yanit: $INTROSPECT_RESPONSE"
fi

Introspection’ın dezavantajı her istek için bir network call yapılması. Yüksek trafikli sistemlerde JWKS tabanlı yerel doğrulama daha performanslı.

Gerçek Dünya Senaryosu: Çoklu Cihaz Yönetimi

Bir kullanıcı hem telefon hem tabletinden uygulamayı kullanıyor. Güvenlik ihlali olduğunu düşünüyor ve “tüm cihazlardan çıkış yap” istiyor. Bu senaryoyu nasıl yönetirsiniz?

#!/bin/bash
# Kullanicinin tum aktif session'larini sonlandirma
# Keycloak Admin API kullanarak

KEYCLOAK_URL="https://auth.sirketiniz.com"
REALM="myapp"
TARGET_USERNAME="[email protected]"

# Admin token al (service account ile)
ADMIN_TOKEN=$(curl -s -X POST 
  "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "grant_type=client_credentials" 
  -d "client_id=admin-service-account" 
  -d "client_secret=service_account_secret")

TOKEN=$(echo $ADMIN_TOKEN | jq -r '.access_token')

# Kullanicinin user ID'sini al
USER_INFO=$(curl -s 
  "${KEYCLOAK_URL}/admin/realms/${REALM}/users?username=${TARGET_USERNAME}" 
  -H "Authorization: Bearer $TOKEN")

USER_ID=$(echo $USER_INFO | jq -r '.[0].id')
echo "Kullanici ID: $USER_ID"

# Tum session'lari listele
SESSIONS=$(curl -s 
  "${KEYCLOAK_URL}/admin/realms/${REALM}/users/${USER_ID}/sessions" 
  -H "Authorization: Bearer $TOKEN")

SESSION_COUNT=$(echo $SESSIONS | jq length)
echo "Aktif session sayisi: $SESSION_COUNT"

# Tum session'lari sonlandir
curl -s -X DELETE 
  "${KEYCLOAK_URL}/admin/realms/${REALM}/users/${USER_ID}/sessions" 
  -H "Authorization: Bearer $TOKEN"

echo "Kullanicinin tum session'lari sonlandirildi"

# Opsiyonel: Kullaniciyi geçici olarak devre disi bırak
curl -s -X PUT 
  "${KEYCLOAK_URL}/admin/realms/${REALM}/users/${USER_ID}" 
  -H "Authorization: Bearer $TOKEN" 
  -H "Content-Type: application/json" 
  -d '{"enabled": false}'

echo "Kullanici hesabi devre disi birakildi, guvenlik incelemesi sonrasi tekrar aktif edilebilir"

Rate Limiting ve Brute Force Koruması

Token endpoint’inizin brute force saldırılarına karşı korunması gerekiyor. Nginx düzeyinde rate limiting:

# Nginx konfigürasyonu - /etc/nginx/sites-available/auth-proxy

cat > /etc/nginx/sites-available/auth-proxy << 'EOF'
# Rate limiting zone tanımla
limit_req_zone $binary_remote_addr zone=token_endpoint:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=auth_endpoint:10m rate=30r/m;

server {
    listen 443 ssl http2;
    server_name auth.sirketiniz.com;

    ssl_certificate /etc/ssl/certs/auth.crt;
    ssl_certificate_key /etc/ssl/private/auth.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;

    # Token endpoint'e rate limiting uygula
    location ~* /protocol/openid-connect/token {
        limit_req zone=token_endpoint burst=5 nodelay;
        limit_req_status 429;
        
        # Brute force logla
        access_log /var/log/nginx/token_access.log;
        
        proxy_pass http://keycloak_backend;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
    }

    # Authorization endpoint
    location ~* /protocol/openid-connect/auth {
        limit_req zone=auth_endpoint burst=10 nodelay;
        
        proxy_pass http://keycloak_backend;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
EOF

nginx -t && systemctl reload nginx
echo "Nginx konfigürasyonu guncellendi"

Monitoring ve Alerting

Production ortamında OAuth sistemini izlemek kritik. Başarısız authentication denemeleri, token anomalileri gibi durumları yakalamak gerekiyor:

#!/bin/bash
# OAuth metriklerini topla ve Prometheus formatında sun
# /usr/local/bin/oauth-metrics-collector.sh

KEYCLOAK_URL="https://auth.sirketiniz.com"
REALM="myapp"
METRICS_FILE="/var/lib/node_exporter/oauth_metrics.prom"
ADMIN_TOKEN=$(cat /etc/keycloak/admin_token)  # Güvenli saklanan token

# Aktif session sayısını al
SESSION_COUNT=$(curl -s 
  "${KEYCLOAK_URL}/admin/realms/${REALM}/users/count?enabled=true" 
  -H "Authorization: Bearer $ADMIN_TOKEN")

# Son 1 saatteki başarısız login sayısı (Keycloak events API)
FAILED_LOGINS=$(curl -s 
  "${KEYCLOAK_URL}/admin/realms/${REALM}/events?type=LOGIN_ERROR&max=1000" 
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq length)

TIMESTAMP=$(date +%s)

# Prometheus formatında yaz
cat > $METRICS_FILE << METRICS
# HELP keycloak_active_sessions Aktif kullanici session sayisi
# TYPE keycloak_active_sessions gauge
keycloak_active_sessions{realm="${REALM}"} ${SESSION_COUNT} ${TIMESTAMP}000

# HELP keycloak_failed_logins_last_hour Son 1 saatteki basarisiz login
# TYPE keycloak_failed_logins_last_hour gauge  
keycloak_failed_logins_last_hour{realm="${REALM}"} ${FAILED_LOGINS} ${TIMESTAMP}000
METRICS

echo "Metrikler guncellendi: Session=${SESSION_COUNT}, FailedLogins=${FAILED_LOGINS}"

# Kritik eşiği aşıldıysa alert gönder
if [ "$FAILED_LOGINS" -gt 100 ]; then
  curl -s -X POST "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK" 
    -H "Content-Type: application/json" 
    -d "{"text": "UYARI: Son 1 saatte ${FAILED_LOGINS} basarisiz login denemesi tespit edildi!"}"
fi

Yaygın Hatalar ve Çözümleri

Prodüksiyon ortamında en sık karşılaştığım sorunları ve çözümlerini paylaşayım:

Token süre farkı sorunları: Sunucu saati ile mobil cihaz saatinin farklı olması JWT doğrulamasını bozuyor. Her zaman NTP senkronizasyonu yapın ve JWT kütüphanenizdeki clock_skew toleransını makul bir değere (örneğin 60 saniye) ayarlayın.

Redirect URI mismatch hataları: Geliştirme, staging ve production için ayrı client tanımlamaları yapın. myapp://callback deep link’inin tüm platformlarda tutarlı olduğundan emin olun.

JWKS cache sorunları: Keycloak’ta key rotation yapıldığında eski JWKS cache’i olan API’ler token’ları reddediyor. Cache’in TTL’ini makul tutun (15-30 dakika) ve kid eşleşmezse cache’i temizleyip yeniden çekin.

Refresh token süresi dolduğunda kullanıcı deneyimi: Refresh token da expire olmuşsa kullanıcının sessiz bir şekilde login sayfasına yönlendirilmesi gerekiyor. Bunu app başlangıcında token validasyonu yaparak önleyebilirsiniz.

Scope yönetimi: Her endpoint için gereken minimum scope’u talep edin. “Tüm izinleri bir seferde al” yaklaşımı yerine incremental consent tercih edin. Kullanıcı bir özelliği kullandığında o özellik için gereken ek izinleri isteyin.

Sonuç

OAuth 2.0 ile PKCE kombinasyonu, mobil uygulama kimlik doğrulama için bugün itibarıyla en güvenli ve ölçeklenebilir yaklaşım. Özellikle şu noktalara dikkat etmek gerekiyor:

  • Mobil istemciler için her zaman PKCE kullanın, client_secret tabanlı flow’lardan uzak durun
  • Token’ları platforma özel güvenli depolama alanlarında saklayın (Keychain, Android Keystore)
  • Refresh token rotation’ı mutlaka etkinleştirin
  • Token endpoint’lerinize rate limiting uygulayın
  • JWKS tabanlı yerel doğrulama ile introspection arasındaki trade-off’ları değerlendirin
  • Çoklu cihaz senaryolarını ve “tüm cihazlardan çıkış” işlemini önceden planlayın
  • Monitoring ve anomali tespitini ihmal etmeyin

Bir sistem yöneticisi olarak söyleyebileceğim en önemli şey şu: OAuth 2.0’ı kurmak bir kez yapılan iş değil. Key rotation, token policy güncelleme, yeni client ekleme gibi operasyonel süreçleri de planlayın. Keycloak gibi bir çözüm kullanıyorsanız admin API’yi scriptleyin, manuel işlemleri minimuma indirin. Güvenlik ve kullanıcı deneyimi burada çelişmiyor, ikisini birlikte elde edebilirsiniz.

Bir yanıt yazın

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