İçeriğe geç
Node.js

Node.js Authentication: JWT Token Sistemi

Tarık Tunç
Node.js Authentication: JWT Token Sistemi

Node.js Authentication: JWT Token Sistemi

Modern web uygulamalarında kullanıcı kimlik doğrulama (authentication) ve yetkilendirme (authorization), güvenliğin temel taşlarıdır. JWT (JSON Web Token), stateless authentication sağlayan, kompakt ve self-contained bir token formatıdır. Bu rehberde Node.js ve Express.js ile JWT tabanlı bir authentication sistemi kuracak, access ve refresh token mekanizmasını, güvenli şifre yönetimini ve rol bazlı erişim kontrolünü detaylı olarak inceleyeceğiz.

1. JWT Nedir ve Nasıl Çalışır?

JWT, üç bölümden oluşan Base64 ile kodlanmış bir string'dir: Header (algoritma ve token türü), Payload (kullanıcı bilgileri ve claims) ve Signature (doğrulama imzası). Sunucu, JWT'yi oluştururken bir secret key ile imzalar; her istekte istemci bu token'ı gönderir ve sunucu imzayı doğrulayarak kullanıcıyı tanır. Session tabanlı authentication'dan farklı olarak, sunucu tarafında herhangi bir state tutulmaz.

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
require('dotenv').config();

const app = express();
app.use(express.json());

// Yapılandırma
const config = {
  accessTokenSecret: process.env.JWT_ACCESS_SECRET,
  refreshTokenSecret: process.env.JWT_REFRESH_SECRET,
  accessTokenExpiry: '15m',
  refreshTokenExpiry: '7d',
  saltRounds: 12
};

// Basit in-memory store (production'da veritabanı kullanın)
const users = [];
const refreshTokens = new Set();

// Token oluşturma fonksiyonları
function generateAccessToken(user) {
  return jwt.sign(
    { userId: user.id, email: user.email, role: user.role },
    config.accessTokenSecret,
    { expiresIn: config.accessTokenExpiry, issuer: 'my-app' }
  );
}

function generateRefreshToken(user) {
  const token = jwt.sign(
    { userId: user.id, tokenVersion: user.tokenVersion },
    config.refreshTokenSecret,
    { expiresIn: config.refreshTokenExpiry }
  );
  refreshTokens.add(token);
  return token;
}

function generateTokenPair(user) {
  return {
    accessToken: generateAccessToken(user),
    refreshToken: generateRefreshToken(user),
    expiresIn: 900 // 15 dakika (saniye cinsinden)
  };
}
Bilgi: Access token kısa ömürlü (15-60 dakika), refresh token uzun ömürlü (7-30 gün) olmalıdır. Access token süresi dolduğunda, refresh token ile yeni bir access token alınır. Bu yaklaşım, güvenlik ve kullanıcı deneyimi arasında iyi bir denge sağlar.

2. Kayıt ve Giriş Endpoint'leri

Kullanıcı kaydında şifreler bcrypt ile hash'lenerek saklanır. Giriş işleminde şifre doğrulaması yapılır ve başarılı ise token çifti döndürülür. Her aşamada güvenlik kontrolleri uygulamak kritik öneme sahiptir.

// Kayıt (Register)
app.post('/api/auth/register', async (req, res) => {
  try {
    const { name, email, password } = req.body;

    // Email kontrolü
    if (users.find(u => u.email === email)) {
      return res.status(409).json({
        success: false,
        message: 'Bu email adresi zaten kayıtlı'
      });
    }

    // Şifre gücü kontrolü
    if (password.length < 8 || !/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) {
      return res.status(400).json({
        success: false,
        message: 'Şifre en az 8 karakter, büyük/küçük harf ve rakam içermelidir'
      });
    }

    // Şifreyi hash'le
    const hashedPassword = await bcrypt.hash(password, config.saltRounds);

    const user = {
      id: crypto.randomUUID(),
      name,
      email: email.toLowerCase(),
      password: hashedPassword,
      role: 'user',
      tokenVersion: 0,
      createdAt: new Date().toISOString()
    };

    users.push(user);
    const tokens = generateTokenPair(user);

    res.status(201).json({
      success: true,
      message: 'Kayıt başarılı',
      data: {
        user: { id: user.id, name: user.name, email: user.email, role: user.role },
        ...tokens
      }
    });
  } catch (error) {
    res.status(500).json({ success: false, message: 'Sunucu hatası' });
  }
});

// Giriş (Login)
app.post('/api/auth/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    const user = users.find(u => u.email === email.toLowerCase());
    if (!user) {
      return res.status(401).json({
        success: false,
        message: 'Geçersiz email veya şifre'
      });
    }

    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      return res.status(401).json({
        success: false,
        message: 'Geçersiz email veya şifre'
      });
    }

    const tokens = generateTokenPair(user);

    res.json({
      success: true,
      message: 'Giriş başarılı',
      data: {
        user: { id: user.id, name: user.name, email: user.email, role: user.role },
        ...tokens
      }
    });
  } catch (error) {
    res.status(500).json({ success: false, message: 'Sunucu hatası' });
  }
});
Uyarı: Login hata mesajlarında "Email bulunamadı" veya "Şifre yanlış" gibi spesifik bilgiler vermeyin. Bu, saldırganların geçerli email adreslerini tespit etmesine (user enumeration) yol açar. Her zaman genel bir "Geçersiz email veya şifre" mesajı kullanın.

3. Token Yenileme ve Çıkış İşlemi

Refresh token mekanizması, kullanıcının sürekli yeniden giriş yapmasını önler. Access token süresi dolduğunda, istemci refresh token ile yeni bir access token alır. Çıkış işleminde refresh token invalidate edilmelidir.

// Token yenileme
app.post('/api/auth/refresh', (req, res) => {
  const { refreshToken } = req.body;

  if (!refreshToken || !refreshTokens.has(refreshToken)) {
    return res.status(401).json({
      success: false,
      message: 'Geçersiz refresh token'
    });
  }

  try {
    const decoded = jwt.verify(refreshToken, config.refreshTokenSecret);
    const user = users.find(u => u.id === decoded.userId);

    if (!user || user.tokenVersion !== decoded.tokenVersion) {
      refreshTokens.delete(refreshToken);
      return res.status(401).json({
        success: false,
        message: 'Token geçersiz kılınmış'
      });
    }

    // Eski refresh token'ı sil, yeni çift oluştur
    refreshTokens.delete(refreshToken);
    const tokens = generateTokenPair(user);

    res.json({
      success: true,
      data: tokens
    });
  } catch (error) {
    refreshTokens.delete(refreshToken);
    res.status(401).json({ success: false, message: 'Token süresi dolmuş' });
  }
});

// Çıkış (Logout)
app.post('/api/auth/logout', authenticate, (req, res) => {
  const { refreshToken } = req.body;

  if (refreshToken) {
    refreshTokens.delete(refreshToken);
  }

  // Tüm oturumları kapatmak için tokenVersion artır
  const user = users.find(u => u.id === req.user.id);
  if (user) {
    user.tokenVersion += 1;
  }

  res.json({
    success: true,
    message: 'Çıkış başarılı'
  });
});

4. Authentication ve Authorization Middleware

Authentication middleware, korunan endpoint'lere erişimi kontrol eder. Authorization middleware ise kullanıcının belirli bir işlemi yapmaya yetkisi olup olmadığını denetler. Bu iki kavram birbirinden farklıdır: authentication "kim olduğunuzu", authorization "ne yapabileceğinizi" belirler.

// Authentication middleware
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({
      success: false,
      message: 'Authorization header gerekli (Bearer token)'
    });
  }

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

  try {
    const decoded = jwt.verify(token, config.accessTokenSecret);
    req.user = decoded;
    next();
  } catch (error) {
    const message = error.name === 'TokenExpiredError'
      ? 'Token süresi dolmuş, lütfen yenileyin'
      : 'Geçersiz token';
    return res.status(401).json({ success: false, message });
  }
}

// Role-based authorization
function authorize(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        message: `Bu işlem '${roles.join("' veya '")}' rolü gerektirir`
      });
    }
    next();
  };
}

// Kullanım örnekleri
app.get('/api/profile', authenticate, (req, res) => {
  const user = users.find(u => u.id === req.user.userId);
  res.json({ success: true, data: { name: user.name, email: user.email } });
});

app.get('/api/admin/users', authenticate, authorize('admin'), (req, res) => {
  const safeUsers = users.map(({ password, ...u }) => u);
  res.json({ success: true, data: safeUsers });
});

app.delete('/api/admin/users/:id',
  authenticate,
  authorize('admin'),
  (req, res) => {
    // Admin kullanıcı silme işlemi
    res.json({ success: true, message: 'Kullanıcı silindi' });
  }
);
İpucu: Production ortamında refresh token'ları veritabanında saklayın ve her kullanımda rotate edin (eski token'ı sil, yenisini oluştur). Ayrıca refresh token'ı HttpOnly cookie olarak göndermek, XSS saldırılarına karşı ek güvenlik sağlar.

5. Güvenlik En İyi Pratikleri

JWT tabanlı authentication sisteminizin güvenliğini artırmak için şu önlemleri alın: şifreleri her zaman bcrypt veya argon2 ile hash'leyin, token'larda hassas bilgi (şifre, kredi kartı) saklamayın, HTTPS kullanın, rate limiting uygulayın ve brute force koruması ekleyin. Ayrıca token'ların sadece gerekli bilgileri içermesine dikkat edin; payload ne kadar küçük olursa token o kadar hızlı işlenir ve her istekte daha az bant genişliği kullanılır.

CORS yapılandırmanızı sıkılaştırın, Content Security Policy header'larını ekleyin ve düzenli olarak bağımlılıklarınızı güvenlik açıklarına karşı tarayın. npm audit komutu, bilinen güvenlik açıklarını tespit etmenize yardımcı olur.

Sonuç

JWT tabanlı authentication sistemi, doğru uygulandığında güvenli ve ölçeklenebilir bir kimlik doğrulama çözümü sunar. Bu rehberde ele aldığımız konular — token oluşturma, kayıt/giriş akışları, token yenileme ve rol bazlı erişim kontrolü — modern bir web uygulamasının authentication altyapısının temellerini oluşturur. Güvenlik konusunda hiçbir zaman taviz vermemeli ve sisteminizi düzenli olarak güvenlik açıklarına karşı denetlemelisiniz.

Kaynaklar

İlgili Yazılar