OpenID Connect Nedir: OAuth 2.0 ile Kimlik Katmanı

Kimlik doğrulama dünyasında en çok karıştırılan konulardan biri OAuth 2.0 ile OpenID Connect arasındaki fark. Çoğu geliştirici ve sysadmin “OAuth ile login yapıyorum” diyor, ama aslında yaptıkları şey OpenID Connect. Bu ikisini birbirinden ayırt etmek, güvenli ve doğru çalışan bir sistem kurmanın ilk adımı. OAuth 2.0 bir yetkilendirme protokolü, yani “bu uygulama şu kaynağa erişebilir” sorusunu çözüyor. OpenID Connect ise bunun üstüne oturan bir kimlik katmanı, yani “bu kullanıcı kim?” sorusunu yanıtlıyor. Gelin bu ikisini, aralarındaki ilişkiyi ve gerçek dünya senaryolarında nasıl kullanıldığını inceleyelim.

OAuth 2.0’ı Tekrar Hatırlayalım

OAuth 2.0, bir uygulamanın kullanıcı adına başka bir servise erişmesini sağlayan yetkilendirme çerçevesi. Klasik örnek: bir uygulama Google Drive’ınıza erişmek istiyor. Siz Google’a gidip “evet, bu uygulamaya Drive erişimi ver” diyorsunuz. OAuth bu akışı yönetiyor.

Temel kavramlar şunlar:

  • Resource Owner: Kaynağa sahip olan kullanıcı
  • Client: Erişim isteyen uygulama
  • Authorization Server: Token dağıtan sunucu (Google, GitHub, Keycloak vs.)
  • Resource Server: API veya korunan kaynak
  • Access Token: Kaynağa erişim için kullanılan token
  • Refresh Token: Yeni access token almak için kullanılan uzun ömürlü token

OAuth 2.0’ın en büyük eksikliği şu: size bir access token veriyor ama bu token “kim” olduğunuzu söylemiyor. Sadece “bu token şu izinlere sahip” diyor. Kimlik doğrulama için başka bir katmana ihtiyaç var, işte OpenID Connect burada devreye giriyor.

OpenID Connect Nedir?

OpenID Connect (OIDC), OAuth 2.0 üzerine inşa edilmiş bir kimlik katmanı. OAuth 2.0’ın yetkilendirme akışını kullanıyor ama üstüne bir şey ekliyor: ID Token. Bu token, kullanıcının kim olduğunu kriptografik olarak doğrulanmış bir şekilde taşıyor.

OIDC’nin eklediği temel kavramlar:

  • ID Token: JWT formatında, kullanıcı kimliğini içeren token
  • UserInfo Endpoint: Kullanıcı bilgilerini döndüren standart endpoint
  • Scope: openid: OAuth flow’a eklenen ve OIDC’yi aktive eden scope
  • Claims: Token içindeki kullanıcı bilgileri (sub, email, name vs.)
  • Discovery Document: .well-known/openid-configuration endpoint’i

Kısaca: OAuth 2.0 ile bir kapıyı açıyorsunuz, OIDC ile o kapıdan geçen kişinin kimlik kartını inceliyorsunuz.

ID Token Yapısı

ID Token aslında bir JWT (JSON Web Token). Üç parçadan oluşuyor: header, payload ve signature. Payload kısmında claims denen bilgiler var.

Standart OIDC claims:

  • sub: Subject identifier, kullanıcının benzersiz ID’si
  • iss: Issuer, token’ı kim yayınladı
  • aud: Audience, token kimin için
  • exp: Expiration, token ne zaman sona eriyor
  • iat: Issued at, token ne zaman oluşturuldu
  • nonce: Replay attack’ları önlemek için
  • email: Kullanıcının email adresi
  • email_verified: Email doğrulanmış mı
  • name: Kullanıcının tam adı
  • given_name: İsim
  • family_name: Soyisim
  • picture: Profil fotoğrafı URL’i

Bir ID Token decode edelim:

# JWT token'ı decode etmek için jq kullanabiliriz
TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFobWV0IFlpbG1heiIsImVtYWlsIjoiYWhtZXRAZXhhbXBsZS5jb20iLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJteWFwcCIsImV4cCI6MTcwMDAwMDAwMCwiaWF0IjoxNjk5OTk2NDAwfQ.signature"

# Payload kısmını decode et (ikinci parça)
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .

# Çıktı:
# {
#   "sub": "1234567890",
#   "name": "Ahmet Yilmaz",
#   "email": "[email protected]",
#   "iss": "https://auth.example.com",
#   "aud": "myapp",
#   "exp": 1700000000,
#   "iat": 1699996400
# }

Discovery Document

OIDC’nin en güzel özelliklerinden biri standardize edilmiş bir discovery endpoint’i olması. Bir identity provider’ın desteklediği her şeyi tek bir yerden öğrenebiliyorsunuz.

# Google'ın OIDC discovery document'ini çekelim
curl -s https://accounts.google.com/.well-known/openid-configuration | jq '{
  issuer: .issuer,
  authorization_endpoint: .authorization_endpoint,
  token_endpoint: .token_endpoint,
  userinfo_endpoint: .userinfo_endpoint,
  jwks_uri: .jwks_uri,
  scopes_supported: .scopes_supported
}'

# Keycloak için
curl -s https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration | jq .

Bu document size şunları söylüyor: authorization endpoint nerede, token endpoint nerede, hangi scope’lar destekleniyor, hangi signing algoritmaları kullanılıyor, UserInfo endpoint nerede.

OIDC Authorization Code Flow

En yaygın ve en güvenli akış Authorization Code Flow. Özellikle web uygulamaları için ideal. Adım adım nasıl çalıştığını görelim.

# Adım 1: Authorization isteği oluştur
# Kullanıcıyı şu URL'e yönlendir:

CLIENT_ID="myapp"
REDIRECT_URI="https://myapp.example.com/callback"
STATE=$(openssl rand -hex 16)
NONCE=$(openssl rand -hex 16)
SCOPE="openid email profile"
AUTH_ENDPOINT="https://auth.example.com/authorize"

AUTH_URL="${AUTH_ENDPOINT}?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=${SCOPE}&state=${STATE}&nonce=${NONCE}"

echo "Kullanici su URL'e yonlendirilecek:"
echo $AUTH_URL

Kullanıcı giriş yapıp onayladıktan sonra authorization server, redirect_uri‘ye bir code parametresi ile yönlendiriyor. Bu code’u access token ve ID token ile değiştiriyorsunuz.

# Adım 2: Code'u token ile değiştir
CODE="authorization_code_buraya"
CLIENT_SECRET="supersecret"
TOKEN_ENDPOINT="https://auth.example.com/token"

curl -X POST $TOKEN_ENDPOINT 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "grant_type=authorization_code" 
  -d "code=${CODE}" 
  -d "redirect_uri=${REDIRECT_URI}" 
  -d "client_id=${CLIENT_ID}" 
  -d "client_secret=${CLIENT_SECRET}"

# Yanıt:
# {
#   "access_token": "eyJ...",
#   "id_token": "eyJ...",
#   "token_type": "Bearer",
#   "expires_in": 3600,
#   "refresh_token": "eyJ..."
# }

UserInfo Endpoint Kullanımı

Access token alındıktan sonra, daha fazla kullanıcı bilgisine ihtiyaç duyarsanız UserInfo endpoint’ini kullanabilirsiniz.

# Access token ile UserInfo endpoint'ini çağır
ACCESS_TOKEN="eyJhbGciOiJSUzI1NiJ9..."
USERINFO_ENDPOINT="https://auth.example.com/userinfo"

curl -H "Authorization: Bearer ${ACCESS_TOKEN}" 
     -H "Accept: application/json" 
     $USERINFO_ENDPOINT

# Yanıt:
# {
#   "sub": "1234567890",
#   "name": "Ahmet Yilmaz",
#   "given_name": "Ahmet",
#   "family_name": "Yilmaz",
#   "email": "[email protected]",
#   "email_verified": true,
#   "picture": "https://example.com/photo.jpg",
#   "locale": "tr-TR"
# }

Keycloak ile OIDC Kurulumu

Gerçek dünyada en çok kullanılan self-hosted identity provider Keycloak. Docker ile hızlıca ayağa kaldıralım.

# Keycloak'u Docker ile başlat
docker run -d 
  --name keycloak 
  -p 8080:8080 
  -e KEYCLOAK_ADMIN=admin 
  -e KEYCLOAK_ADMIN_PASSWORD=admin123 
  quay.io/keycloak/keycloak:23.0.0 start-dev

# Realm ve client oluşturmak için Keycloak Admin CLI kullanabiliriz
# Önce token alalım
ADMIN_TOKEN=$(curl -s -X POST 
  http://localhost:8080/realms/master/protocol/openid-connect/token 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "username=admin" 
  -d "password=admin123" 
  -d "grant_type=password" 
  -d "client_id=admin-cli" | jq -r '.access_token')

# Yeni realm oluştur
curl -X POST http://localhost:8080/admin/realms 
  -H "Authorization: Bearer $ADMIN_TOKEN" 
  -H "Content-Type: application/json" 
  -d '{
    "realm": "mycompany",
    "enabled": true,
    "displayName": "My Company"
  }'

# Client oluştur
curl -X POST http://localhost:8080/admin/realms/mycompany/clients 
  -H "Authorization: Bearer $ADMIN_TOKEN" 
  -H "Content-Type: application/json" 
  -d '{
    "clientId": "myapp",
    "enabled": true,
    "protocol": "openid-connect",
    "publicClient": false,
    "redirectUris": ["https://myapp.example.com/*"],
    "standardFlowEnabled": true,
    "directAccessGrantsEnabled": false
  }'

Python ile OIDC Entegrasyonu

Uygulamalarınıza OIDC entegrasyonu eklemek için Python’da authlib kütüphanesi çok kullanışlı.

# Bağımlılıkları yükle
pip install authlib flask requests

# Flask uygulaması için OIDC entegrasyonu
cat > app.py << 'EOF'
from flask import Flask, redirect, url_for, session, request, jsonify
from authlib.integrations.flask_client import OAuth
import os

app = Flask(__name__)
app.secret_key = os.urandom(24)

oauth = OAuth(app)

# OIDC provider kaydet
oauth.register(
    name='myidp',
    client_id='myapp',
    client_secret='myclientsecret',
    server_metadata_url='https://auth.example.com/.well-known/openid-configuration',
    client_kwargs={
        'scope': 'openid email profile'
    }
)

@app.route('/login')
def login():
    # Kullaniciyi IDP'ye yonlendir
    redirect_uri = url_for('callback', _external=True)
    return oauth.myidp.authorize_redirect(redirect_uri)

@app.route('/callback')
def callback():
    # Token al ve dogrula
    token = oauth.myidp.authorize_access_token()
    
    # ID token otomatik dogrulanir
    id_token = token.get('id_token')
    userinfo = token.get('userinfo')
    
    # Session'a kaydet
    session['user'] = {
        'sub': userinfo['sub'],
        'email': userinfo.get('email'),
        'name': userinfo.get('name')
    }
    
    return redirect(url_for('profile'))

@app.route('/profile')
def profile():
    user = session.get('user')
    if not user:
        return redirect(url_for('login'))
    return jsonify(user)

@app.route('/logout')
def logout():
    session.clear()
    # IDP'de de logout yap (backchannel logout)
    return redirect('https://auth.example.com/logout?redirect_uri=https://myapp.example.com')

if __name__ == '__main__':
    app.run(debug=True)
EOF

python app.py

Nginx ile OIDC Proxy Kurulumu

Mevcut uygulamaları değiştirmeden önlerine bir OIDC proxy koymak yaygın bir pattern. oauth2-proxy bu iş için mükemmel.

# oauth2-proxy kurulumu
wget https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.6.0/oauth2-proxy-v7.6.0.linux-amd64.tar.gz
tar xzf oauth2-proxy-v7.6.0.linux-amd64.tar.gz
sudo mv oauth2-proxy-v7.6.0.linux-amd64/oauth2-proxy /usr/local/bin/

# Keycloak ile kullanmak için konfigürasyon
cat > /etc/oauth2-proxy/oauth2-proxy.cfg << 'EOF'
provider = "oidc"
provider_display_name = "Keycloak"
oidc_issuer_url = "https://keycloak.example.com/realms/mycompany"

client_id = "oauth2-proxy"
client_secret = "proxy-secret-buraya"

cookie_secret = "zorlu-cookie-secret-32-byte"
cookie_secure = true
cookie_domain = ".example.com"

email_domains = ["example.com"]
upstreams = ["http://localhost:3000"]

redirect_url = "https://app.example.com/oauth2/callback"

http_address = "0.0.0.0:4180"

# ID token'dan claim'leri header olarak geç
pass_user_headers = true
set_xauthrequest = true
EOF

# Servisi başlat
oauth2-proxy --config=/etc/oauth2-proxy/oauth2-proxy.cfg &

# Nginx konfigürasyonu
cat > /etc/nginx/sites-available/myapp << 'EOF'
server {
    listen 443 ssl;
    server_name app.example.com;

    location /oauth2/ {
        proxy_pass http://127.0.0.1:4180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location @error401 {
        return 302 /oauth2/sign_in?rd=$scheme://$host$request_uri;
    }

    location / {
        auth_request /oauth2/auth;
        error_page 401 = @error401;

        # Kullanıcı bilgilerini backend'e geç
        auth_request_set $user $upstream_http_x_auth_request_user;
        auth_request_set $email $upstream_http_x_auth_request_email;
        proxy_set_header X-User $user;
        proxy_set_header X-Email $email;

        proxy_pass http://localhost:3000;
    }
}
EOF

nginx -t && systemctl reload nginx

Token Doğrulama ve Güvenlik Kontrolleri

Token aldınız, güzel. Peki bunu doğru doğruluyor musunuz? Bu kısım çok kritik.

# JWKS endpoint'inden public key'leri çek
JWKS_URI="https://auth.example.com/.well-known/jwks.json"
curl -s $JWKS_URI | jq .

# Python ile token doğrulama scripti
cat > verify_token.py << 'EOF'
import jwt
import requests
from jwt.algorithms import RSAAlgorithm
import json

def get_public_keys(jwks_uri):
    """JWKS endpoint'inden public key'leri al"""
    response = requests.get(jwks_uri)
    jwks = response.json()
    
    keys = {}
    for key_data in jwks['keys']:
        kid = key_data['kid']
        public_key = RSAAlgorithm.from_jwk(json.dumps(key_data))
        keys[kid] = public_key
    
    return keys

def verify_id_token(token, client_id, issuer, jwks_uri):
    """ID Token'ı doğrula"""
    # Token header'ından kid al
    header = jwt.get_unverified_header(token)
    kid = header.get('kid')
    
    # Public key'leri al
    public_keys = get_public_keys(jwks_uri)
    
    if kid not in public_keys:
        raise ValueError(f"Bilinmeyen kid: {kid}")
    
    public_key = public_keys[kid]
    
    # Token'ı doğrula - tüm kritik kontroller burada yapılıyor
    claims = jwt.decode(
        token,
        public_key,
        algorithms=['RS256'],
        audience=client_id,  # aud kontrolü
        issuer=issuer,        # iss kontrolü
        options={
            'verify_exp': True,    # exp kontrolü
            'verify_iat': True,    # iat kontrolü
        }
    )
    
    return claims

# Kullanım
try:
    claims = verify_id_token(
        token="eyJ...",
        client_id="myapp",
        issuer="https://auth.example.com",
        jwks_uri="https://auth.example.com/.well-known/jwks.json"
    )
    print(f"Token gecerli. Kullanici: {claims['email']}")
except jwt.ExpiredSignatureError:
    print("Token suresi dolmus!")
except jwt.InvalidAudienceError:
    print("Token bu uygulama icin degil!")
except Exception as e:
    print(f"Token gecersiz: {e}")
EOF

python3 verify_token.py

Gerçek Dünya Senaryosu: Microservice Mimarisi

Bir e-ticaret şirketinde çalıştığınızı düşünün. Onlarca microservice var: ürün servisi, sipariş servisi, ödeme servisi vs. Her servisin kullanıcı kimliğini doğrulaması gerekiyor.

Yanlış yaklaşım: Her servis kendi login sayfasına sahip, her biri ayrı kullanıcı veritabanı tutuyor. Kullanıcı her servise ayrı login yapıyor.

Doğru yaklaşım: Merkezi bir identity provider (Keycloak), tüm servisler OIDC ile entegre.

Akış şöyle işliyor:

  • Kullanıcı frontend’e giriş yapıyor, Keycloak’tan ID token ve access token alıyor
  • Frontend her API isteğinde access token’ı Authorization header’ında gönderiyor
  • Her microservice token’ı doğruluyor, sub claim’inden kullanıcıyı tanıyor
  • Servisler arası iletişimde Client Credentials flow kullanılıyor
# Servisler arası iletişim için Client Credentials Flow
# Bu flow'da kullanıcı yok, servis kendi kimliğiyle token alıyor

curl -X POST https://auth.example.com/token 
  -H "Content-Type: application/x-www-form-urlencoded" 
  -d "grant_type=client_credentials" 
  -d "client_id=order-service" 
  -d "client_secret=order-service-secret" 
  -d "scope=product-service:read"

# Alınan token ile product service'i çağır
SERVICE_TOKEN="eyJ..."
curl -H "Authorization: Bearer $SERVICE_TOKEN" 
     https://product-service.internal/api/products/123

Bu mimaride önemli noktalar:

  • Token cache: Her istek için token almak yerine, token expire olmadan önce yeniliyorsunuz
  • Scope kontrolü: Her servis sadece ihtiyacı olan scope’ları talep ediyor
  • Token introspection: Bazı durumlarda token’ı authorization server’a doğrulatmak gerekebilir (opaque token kullanıyorsanız)
  • Backchannel logout: Kullanıcı çıkış yaptığında tüm servislerin bilgilenmesi

Yaygın Hatalar ve Çözümleri

Sysadmin olarak bu hataları kaçınılmaz görürsünüz:

Nonce doğrulamasını atlama: Authorization isteğinde gönderdiğiniz nonce’u, ID token’dan validate etmeyi unutmak replay attack’lara kapı açar.

State parametresini kontrol etmeme: CSRF saldırılarına karşı state parametresini mutlaka doğrulayın.

Token’ı sadece decode edip doğrulamamak: Token’ı decode etmek ve doğrulamak çok farklı şey. İmza, exp, iss, aud kontrollerinin hepsini yapın.

Clock skew problemleri: Token exp kontrolünde sunucular arası saat farkı sorun çıkarır. Az da olsa bir tolerans (genellikle 5 dakika) bırakın.

HTTP üzerinden OIDC: Token’lar hassas bilgi. HTTPS zorunlu, istisnasız.

Sonuç

OpenID Connect, modern kimlik doğrulama dünyasının omurgası haline geldi. OAuth 2.0’ın yetkilendirme gücünü alıp üstüne standartlaştırılmış bir kimlik katmanı ekliyor. Tek Sign-On (SSO) kurmak istiyorsanız, microservice mimarisinde merkezi kimlik yönetimi yapacaksanız, ya da “Google ile giriş yap” özelliği ekleyecekseniz, OIDC öğrenmek zorundasınız.

Keycloak gibi self-hosted çözümler kurumsal ortamlar için mükemmel başlangıç noktası. Production’a çıkmadan önce discovery document’ini inceleyin, token doğrulama mantığını sıkı tutun, nonce ve state parametrelerini asla atlamayın. JWKS rotation’ı da göz ardı etmeyin, public key’ler değişebilir ve uygulamanız buna adapte olabilmeli.

Kimlik doğrulama her zaman “şimdilik çalışıyor, sonra düzeltirim” yaklaşımıyla yapılacak bir şey değil. Temeli sağlam atmak, ileride saatlerce debug süresini ve potansiyel güvenlik açıklarını önlüyor.

Bir yanıt yazın

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