JWT ile Role-Based Access Control (RBAC) Uygulaması

Mikroservis mimarisine geçtiğinizde ya da birden fazla ekibin aynı API’yi kullandığı bir ortamda çalışmaya başladığınızda, “kim neye erişebilir?” sorusu hayatınızın merkezine oturur. Statik API anahtarları bir süre idare eder, session tabanlı auth eski monolitlerde gayet iyi çalışır, ama ölçeklenebilir ve durumsuz bir erişim kontrol sistemi istiyorsanız JWT üzerine kurulu bir RBAC (Role-Based Access Control) implementasyonu tam size göre.

Bu yazıda gerçek dünya senaryoları üzerinden JWT ile rol tabanlı erişim kontrolünü nasıl kuracağınızı, token yapısını nasıl tasarlayacağınızı ve yaygın güvenlik tuzaklarından nasıl kaçınacağınızı ele alacağız.

JWT ve RBAC: Temel Kavramları Netleştirelim

JWT (JSON Web Token), üç parçadan oluşan base64 ile encode edilmiş bir token formatıdır: header, payload ve signature. Buradaki kilit nokta şu: token içinde kullanıcıya ait bilgileri, özellikle rolleri, taşıyabilirsiniz. Sunucu her istekte veritabanına koşmak zorunda kalmaz, token’ı doğrular ve payload’dan rolleri okur.

RBAC ise kullanıcılara doğrudan izin vermek yerine rol atayıp o rollere izin bağlamak demektir. admin, editor, viewer, billing_manager gibi roller tanımlarsınız, kullanıcılar bu rollere sahip olur.

İkisini birleştirdiğinizde şu akışı elde edersiniz:

  • Kullanıcı login olur, sistem kullanıcının rollerini çeker
  • Roller JWT payload’una eklenir
  • Her API isteğinde token doğrulanır, roller kontrol edilir
  • Endpoint’e göre yeterli rol varsa istek geçer, yoksa 403 döner

Token Yapısını Doğru Tasarlamak

JWT payload’unu nasıl tasarladığınız, ilerleyen süreçte başınızın ağrıyıp ağrımayacağını belirler. Standart claim’lerin yanında özel claim’ler ekleyebilirsiniz.

# JWT payload örneği - decode edilmiş hali
# Header
{
  "alg": "RS256",
  "typ": "JWT"
}

# Payload
{
  "sub": "usr_8f3a2b1c",
  "email": "[email protected]",
  "roles": ["editor", "billing_viewer"],
  "permissions": ["post:write", "post:read", "invoice:read"],
  "tenant_id": "tenant_abc123",
  "iat": 1703001600,
  "exp": 1703005200,
  "jti": "tok_7d2e9f4a"
}

Burada dikkat etmeniz gereken birkaç nokta var:

  • sub: Kullanıcı ID’si, tahmin edilemez bir format kullanın
  • roles: Üst düzey roller listesi
  • permissions: İnce taneli izinler, rollerin flatten edilmiş hali de olabilir
  • tenant_id: Multi-tenant sistemlerde zorunlu, aksi halde bir tenant başkasının verisine erişebilir
  • jti: Token’a özgün ID, blacklist mekanizması için kritik
  • exp: Kısa tutun, access token için 1 saat yeterli

HS256 yerine RS256 kullanmanızı şiddetle tavsiye ederim. Asimetrik şifreleme ile token’ı imzalayan servis (private key) ile doğrulayan servis (public key) farklı olabilir. Microservice ortamında bu büyük avantaj.

Node.js ile JWT ve RBAC Middleware Uygulaması

Pratik koda geçelim. Express.js ile bir auth middleware yazalım.

# Gerekli paketlerin kurulumu
npm install jsonwebtoken express-jwt jwks-rsa

# RSA anahtar çifti oluşturma
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
# auth.middleware.js - Token doğrulama ve rol kontrolü
# Bu dosyayı Node.js projenizin middleware klasörüne koyun

const jwt = require('jsonwebtoken');
const fs = require('fs');

const publicKey = fs.readFileSync('./keys/public.pem');

// Token doğrulama middleware'i
const verifyToken = (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ 
      error: 'Token bulunamadi',
      code: 'MISSING_TOKEN'
    });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, publicKey, { 
      algorithms: ['RS256'],
      issuer: 'https://auth.sirketiniz.com'
    });
    
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ 
        error: 'Token suresi doldu',
        code: 'TOKEN_EXPIRED'
      });
    }
    return res.status(401).json({ 
      error: 'Gecersiz token',
      code: 'INVALID_TOKEN'
    });
  }
};

// Rol kontrolü middleware factory
const requireRoles = (...requiredRoles) => {
  return (req, res, next) => {
    if (!req.user || !req.user.roles) {
      return res.status(403).json({ 
        error: 'Rol bilgisi bulunamadi',
        code: 'NO_ROLES'
      });
    }

    const userRoles = req.user.roles;
    const hasRole = requiredRoles.some(role => userRoles.includes(role));

    if (!hasRole) {
      return res.status(403).json({ 
        error: 'Bu islemi yapma yetkiniz yok',
        code: 'INSUFFICIENT_PERMISSIONS',
        required: requiredRoles,
        current: userRoles
      });
    }

    next();
  };
};

// Granular permission kontrolü
const requirePermission = (permission) => {
  return (req, res, next) => {
    const userPermissions = req.user?.permissions || [];
    
    if (!userPermissions.includes(permission)) {
      return res.status(403).json({ 
        error: `'${permission}' iznine sahip degilsiniz`,
        code: 'PERMISSION_DENIED'
      });
    }
    next();
  };
};

module.exports = { verifyToken, requireRoles, requirePermission };
# routes/posts.js - Middleware'leri route'larda kullanmak
const express = require('express');
const router = express.Router();
const { verifyToken, requireRoles, requirePermission } = require('../middleware/auth');

# Herkese açık endpoint - sadece token doğrulama
router.get('/posts', verifyToken, async (req, res) => {
  # req.user.tenant_id ile sadece o tenant'ın postlarını getir
  const posts = await Post.find({ tenant_id: req.user.tenant_id });
  res.json(posts);
});

# Sadece editor ve admin yazabilir
router.post('/posts', 
  verifyToken, 
  requireRoles('editor', 'admin'), 
  async (req, res) => {
    const post = await Post.create({
      ...req.body,
      author_id: req.user.sub,
      tenant_id: req.user.tenant_id
    });
    res.status(201).json(post);
  }
);

# Admin-only endpoint
router.delete('/posts/:id',
  verifyToken,
  requireRoles('admin'),
  async (req, res) => {
    await Post.deleteOne({ 
      _id: req.params.id,
      tenant_id: req.user.tenant_id  # Tenant izolasyonu unutulmasin!
    });
    res.status(204).send();
  }
);

Token Üretim Servisi

Login servisinde token nasıl üretilir, rolleri nasıl dahil ederiz:

# auth.service.js - Login ve token üretimi
const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('./keys/private.pem');

const generateTokens = async (userId, db) => {
  # Kullanicinin rollerini veritabanindan cek
  const user = await db.users.findOne({ id: userId });
  const userRoles = await db.userRoles.findMany({ 
    userId,
    include: { role: { include: { permissions: true } } }
  });

  # Rolleri ve izinleri flatten et
  const roles = userRoles.map(ur => ur.role.name);
  const permissions = [...new Set(
    userRoles.flatMap(ur => ur.role.permissions.map(p => p.code))
  )];

  const accessTokenPayload = {
    sub: userId,
    email: user.email,
    roles,
    permissions,
    tenant_id: user.tenantId,
    type: 'access'
  };

  const refreshTokenPayload = {
    sub: userId,
    tenant_id: user.tenantId,
    type: 'refresh',
    jti: generateUUID()
  };

  const accessToken = jwt.sign(accessTokenPayload, privateKey, {
    algorithm: 'RS256',
    expiresIn: '1h',
    issuer: 'https://auth.sirketiniz.com',
    audience: 'https://api.sirketiniz.com'
  });

  const refreshToken = jwt.sign(refreshTokenPayload, privateKey, {
    algorithm: 'RS256',
    expiresIn: '30d',
    issuer: 'https://auth.sirketiniz.com'
  });

  # Refresh token'i DB'ye kaydet (blacklist/revoke icin)
  await db.refreshTokens.create({
    jti: refreshTokenPayload.jti,
    userId,
    expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
  });

  return { accessToken, refreshToken };
};

Token Revocation: Blacklist Mekanizması

JWT’nin stateless doğası bir güvenlik açığı barındırır: token expire olmadan önce nasıl geçersiz kılarsınız? Kullanıcı logout oldu, şifresi değişti ya da hesabı askıya alındı. Bu durumlar için Redis tabanlı bir blacklist mekanizması kurmanız gerekiyor.

# token.blacklist.js - Redis ile token blacklist
const redis = require('redis');
const client = redis.createClient({ url: process.env.REDIS_URL });

const revokeToken = async (jti, expiresAt) => {
  # Token'in kalan suresini hesapla
  const ttl = Math.floor((expiresAt * 1000 - Date.now()) / 1000);
  
  if (ttl > 0) {
    # Redis'e ekle, TTL ile otomatik temizlensin
    await client.setEx(`blacklist:${jti}`, ttl, '1');
  }
};

const isTokenRevoked = async (jti) => {
  const result = await client.get(`blacklist:${jti}`);
  return result !== null;
};

# Middleware'e entegre et
const verifyTokenWithBlacklist = async (req, res, next) => {
  # ... onceki token dogrulama kodu ...
  
  # Token gecerliyse blacklist kontrolu yap
  if (decoded.jti) {
    const revoked = await isTokenRevoked(decoded.jti);
    if (revoked) {
      return res.status(401).json({ 
        error: 'Token iptal edilmis',
        code: 'TOKEN_REVOKED'
      });
    }
  }
  
  req.user = decoded;
  next();
};

# Logout endpoint'inde kullanim
app.post('/logout', verifyToken, async (req, res) => {
  await revokeToken(req.user.jti, req.user.exp);
  res.json({ message: 'Basariyla cikis yapildi' });
});

Python/FastAPI ile RBAC Implementasyonu

Node.js ekibiniz yoksa, Python FastAPI ile de aynı sistemi kurabilirsiniz:

# Python FastAPI RBAC - dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError
from functools import wraps
from typing import List

security = HTTPBearer()
PUBLIC_KEY = open("keys/public.pem").read()

async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security)
):
    token = credentials.credentials
    
    try:
        payload = jwt.decode(
            token, 
            PUBLIC_KEY, 
            algorithms=["RS256"],
            audience="https://api.sirketiniz.com"
        )
        return payload
    except JWTError as e:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=f"Gecersiz token: {str(e)}",
            headers={"WWW-Authenticate": "Bearer"},
        )

def require_roles(roles: List[str]):
    async def role_checker(user: dict = Depends(get_current_user)):
        user_roles = user.get("roles", [])
        if not any(role in user_roles for role in roles):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail={
                    "error": "Yetkiniz bulunmuyor",
                    "required_roles": roles,
                    "your_roles": user_roles
                }
            )
        return user
    return role_checker

# Route kullanimi
from fastapi import FastAPI
app = FastAPI()

@app.get("/admin/users")
async def list_users(
    user: dict = Depends(require_roles(["admin", "super_admin"]))
):
    tenant_id = user["tenant_id"]
    # tenant_id ile filtrelenmiş kullanıcıları getir
    return {"users": [], "tenant": tenant_id}

@app.delete("/admin/users/{user_id}")
async def delete_user(
    user_id: str,
    current_user: dict = Depends(require_roles(["admin"]))
):
    return {"deleted": user_id}

Gerçek Dünya Senaryosu: SaaS Platformunda Multi-Tenant RBAC

Diyelim ki bir SaaS proje yönetim aracı geliştiriyorsunuz. Her müşteri bir tenant, her tenantın kendi admin’leri ve üyeleri var. Ayrıca platform genelinde sizin super admin’leriniz var.

Bu durumda rol hiyerarşiniz şöyle olmalı:

  • super_admin: Tüm tenantlara erişim, platform yönetimi
  • tenant_admin: Kendi tenant’ı içinde tam yetki, kullanıcı ekleyip çıkarabilir
  • project_manager: Proje oluşturabilir, üye atayabilir
  • member: Atandığı projelerde iş yapabilir
  • viewer: Sadece okuma

Token payload’unda tenant_id claim’i olmadan her endpoint’te tenant izolasyonunu manuel yapmak zorunda kalırsınız ve er ya da geç birisi bir hataya düşer. tenant_id‘yi token’a gömmek ve her sorguya otomatik eklemek bu riski ortadan kaldırır.

Dikkat etmeniz gereken kritik nokta: bir tenant_admin başka bir tenant’ın kaynaklarını görmeye çalışırsa, bu isteği sadece rol seviyesinde değil veri katmanında da reddetmeniz gerekir. Yani şöyle bir pattern kullanın:

# Tenant izolasyonu - her zaman token'dan tenant_id al, body/param'dan alma
const getProject = async (req, res) => {
  const project = await Project.findOne({
    _id: req.params.projectId,
    tenant_id: req.user.tenant_id  # Bu satir olmazsa olmaz
  });

  if (!project) {
    # 404 don, 403 degil - tenant bilgisi sizdirilmasin
    return res.status(404).json({ error: 'Proje bulunamadi' });
  }

  res.json(project);
};

404 dönmek 403 dönmekten daha güvenlidir çünkü 403 ile “bu kaynak var ama sana kapalı” bilgisini sızdırmış olursunuz.

Yaygın Hatalar ve Güvenlik Tuzakları

Pratikte sıkça karşılaştığım sorunları listeleyelim:

  • Token’da hassas veri saklamak: Şifre hash’i, kredi kartı bilgisi, TCK no gibi verileri JWT’ye koymayın. Payload sadece base64 encode edilmiştir, şifrelenmemiştir. İsteyen decode edip okuyabilir.
  • Uzun süreli access token: 24 saatlik ya da 7 günlük access token güvenlik açığı demektir. Access token 15-60 dakika, refresh token daha uzun olsun.
  • none algoritmasına karşı koruma eksikliği: algorithms: ['RS256'] şeklinde açıkça belirtin. Kütüphanelerin eski versiyonlarında algoritma none olarak manipüle edilebiliyordu.
  • Audience ve issuer doğrulaması yapmamak: Başka bir servis için üretilmiş token’ların sizin servisinizde çalışmasına izin vermiş olursunuz.
  • Rolleri sadece frontend’de kontrol etmek: Bu en büyük hata. Frontend’deki kontrol sadece UX için, gerçek güvenlik her zaman backend’de olmalı.
  • JTI olmadan revocation: Token’ı geçersiz kılmanız gerektiğinde tüm kullanıcıyı logout yapmak zorunda kalırsınız.

Token Yenileme Akışı

Refresh token mekanizmasını da doğru kurmak önemli:

# refresh.endpoint.js - Token yenileme
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;

  if (!refreshToken) {
    return res.status(400).json({ error: 'Refresh token gerekli' });
  }

  try {
    const decoded = jwt.verify(refreshToken, publicKey, {
      algorithms: ['RS256']
    });

    if (decoded.type !== 'refresh') {
      return res.status(400).json({ error: 'Gecersiz token tipi' });
    }

    # DB'de refresh token mevcut mu kontrol et
    const storedToken = await db.refreshTokens.findOne({
      jti: decoded.jti,
      userId: decoded.sub,
      revokedAt: null
    });

    if (!storedToken) {
      # Token rotation saldirisi olabilir - kullanicinin tum oturumlarini kapat
      await db.refreshTokens.updateMany(
        { userId: decoded.sub },
        { revokedAt: new Date() }
      );
      return res.status(401).json({ 
        error: 'Gecersiz refresh token - tum oturumlar kapatildi' 
      });
    }

    # Eski refresh token'i iptal et
    await db.refreshTokens.update(
      { jti: decoded.jti },
      { revokedAt: new Date() }
    );

    # Yeni token cifti olustur (token rotation)
    const newTokens = await generateTokens(decoded.sub, db);
    res.json(newTokens);

  } catch (err) {
    res.status(401).json({ error: 'Refresh token dogrulanamadi' });
  }
});

Burada token rotation uyguladık: her refresh isteğinde eski refresh token iptal edilip yeni bir tane verilir. Bir refresh token iki kez kullanılmaya çalışılırsa bu, token çalındığına işaret eder ve tüm aktif oturumlar kapatılır.

Monitoring ve Audit Log

Üretim ortamında kimin neye ne zaman eriştiğini kayıt altına almak hem güvenlik hem de hata ayıklama açısından hayati önem taşır:

# audit.middleware.js - Erişim logları
const auditLog = (action) => {
  return async (req, res, next) => {
    const start = Date.now();
    
    # Response bittikten sonra log at
    res.on('finish', async () => {
      await db.auditLogs.create({
        userId: req.user?.sub,
        tenantId: req.user?.tenant_id,
        action,
        resource: req.path,
        method: req.method,
        statusCode: res.statusCode,
        duration: Date.now() - start,
        ip: req.ip,
        userAgent: req.headers['user-agent'],
        timestamp: new Date()
      });
    });
    
    next();
  };
};

# Kullanim
router.delete('/users/:id',
  verifyToken,
  requireRoles('admin'),
  auditLog('user.delete'),
  deleteUserHandler
);

Bu logları düzenli olarak inceleyin. Anormal saatlerde toplu 403 hataları muhtemelen yetkisiz erişim denemesidir. Aynı IP’den farklı user_id’lerle gelen istekler credential stuffing işareti olabilir.

Sonuç

JWT tabanlı RBAC sistemi doğru kurulduğunda hem ölçeklenebilir hem de yönetilebilir bir erişim kontrol mekanizması sunar. Özetlemek gerekirse:

  • RS256 kullanın, HS256 değil; özellikle mikroservis ortamında public key dağıtımı çok daha güvenli.
  • Token payload’unu fazla şişirmeyin; roller ve temel kimlik bilgileri yeterli.
  • Her endpoint’te tenant izolasyonunu veri katmanında uygulayın, sadece role güvenmeyin.
  • Kısa ömürlü access token ile uzun ömürlü refresh token kombinasyonunu kullanın.
  • Token rotation ile refresh token güvenliğini artırın.
  • Audit log olmadan üretim ortamına çıkmayın.
  • jti claim’i her zaman ekleyin, revocation ihtiyacı mutlaka gelecektir.

Bu sistem bir kez sağlam kurulduğunda, yeni servisler eklemek sadece public key’i paylaşmak ve middleware’i entegre etmek kadar basit hale gelir. Başlangıçta biraz zahmetli görünse de teknik borcu olan bir auth sistemi yerine bunu seçmek, ilerleyen dönemde size saatler kazandırır.

Bir yanıt yazın

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