Express.js ve Node.js’te JWT Kimlik Doğrulama: Kapsamlı Rehber
Modern web uygulamalarında kullanıcı kimlik doğrulama ve yetkilendirme süreçleri, güvenliğin temelini oluşturur. Bu bağlamda JSON Web Token (JWT), sunucu tarafında oturum bilgisi tutma ihtiyacını ortadan kaldıran, hafif ve esnek bir yöntem olarak öne çıkar. Bu makalede, Node.js ve Express.js kullanarak JWT tabanlı kimlik doğrulamanın nasıl adım adım uygulanacağını, temel kavramlardan en iyi güvenlik uygulamalarına kadar tüm detaylarıyla ele alacağız.
JWT Nedir ve Neden Kullanılır?
JSON Web Token (JWT), taraflar arasında güvenli bir şekilde bilgi alışverişini sağlayan kompakt, URL güvenli bir yöntemdir. Özellikle API tabanlı uygulamalarda, mobil ve tek sayfa uygulamalarında (SPA) kullanıcı kimlik doğrulama ve yetkilendirme için sıklıkla tercih edilir. JWT’nin temel amacı, bir kullanıcının kimliğini doğruladıktan sonra, sunucunun bu kullanıcıya belirli kaynaklara erişim yetkisi vermesini sağlamaktır.
Kimlik Doğrulama ve Yetkilendirme Farkı
- Kimlik Doğrulama (Authentication): Kullanıcının kim olduğunu kanıtlama sürecidir. Örneğin, kullanıcı adı ve şifre ile giriş yapma.
- Yetkilendirme (Authorization): Kimliği doğrulanmış bir kullanıcının belirli bir kaynağa veya eyleme erişim izni olup olmadığını belirleme sürecidir. Örneğin, bir yöneticinin diğer kullanıcıların profillerini düzenleyebilmesi.
JWT, hem kimlik doğrulama sonrasında kullanıcıya bir token vererek kimliğini kanıtlamasına yardımcı olur hem de bu token içindeki bilgilerle yetkilendirme kararları alınmasını kolaylaştırır.
Geleneksel Oturum Yönetimine Alternatif
Geleneksel oturum yönetiminde, kullanıcı giriş yaptığında sunucu tarafında bir oturum kimliği (session ID) oluşturulur ve bu kimlik bir çerez (cookie) aracılığıyla istemciye gönderilir. Her istekte istemci bu çerezi sunucuya geri gönderir ve sunucu, oturum kimliğini veritabanında veya bellekte tuttuğu oturum bilgileriyle karşılaştırır. JWT ise bu duruma bir alternatif sunar.
Durumsuz (Stateless) Yapının Avantajları
JWT’nin en büyük avantajlarından biri, sunucu tarafında oturum durumu (state) tutma ihtiyacını ortadan kaldırmasıdır. Her JWT, kullanıcının kimliği ve yetkileri hakkında gerekli tüm bilgiyi kendi içinde barındırır. Bu durum, özellikle mikroservis mimarilerinde ve yatay ölçeklenebilirlik (horizontal scaling) gerektiren uygulamalarda büyük avantaj sağlar:
- Ölçeklenebilirlik: Her sunucu token’ı bağımsız olarak doğrulayabilir, bu da yük dengeleyicilerle birden fazla sunucu arasında kolayca dağıtım yapılmasına olanak tanır.
- Basitlik: Sunucu tarafında oturum veritabanı yönetimi veya çerez depolama gibi karmaşık işlemleri azaltır.
- Çapraz Alan Erişimi (CORS): JWT’ler HTTP başlıklarında taşındığı için, farklı alan adları arasında kimlik doğrulama yapmak daha kolaydır.
JWT’nin Faydaları
- Güvenlik: İmza sayesinde token’ın içeriğinin değiştirilmediği garanti edilir.
- Hafiflik: JSON formatında olduğu için kompakt ve kolayca iletilir.
- Esneklik: Herhangi bir programlama dili veya platformda kullanılabilir.
- Standartlaşma: RFC 7519 ile tanımlanmış açık bir standarttır.
JWT Yapısı ve Bileşenleri
Bir JWT, üç ana bölümden oluşur ve bu bölümler noktalarla (.) ayrılır:
header.payload.signature
Her bölüm Base64Url ile kodlanmıştır.
Header (Başlık)
Header, token'ın türünü (JWT) ve kullanılan imzalama algoritmasını (örneğin, HS256 veya RS256) belirtir. Örnek bir başlık:
{
"alg": "HS256",
"typ": "JWT"
}
Bu JSON nesnesi Base64Url ile kodlanır ve JWT'nin ilk kısmını oluşturur.
Payload (Yük)
Payload, token'ın asıl verilerini (claim'leri) içerir. Claim'ler, kullanıcı hakkında bilgiler (örneğin, kullanıcı ID'si, adı, rolleri) veya token'ın geçerlilik süresi gibi meta veriler olabilir. Üç tür claim vardır:
- Kayıtlı Claim'ler (Registered Claims): JWT standartları tarafından tanımlanmış, ancak zorunlu olmayan claim'lerdir. Örnekler:
iss(issuer),exp(expiration time),sub(subject),aud(audience). - Genel Claim'ler (Public Claims): Çakışmaları önlemek için IANA JWT Kayıt Defteri'nde tanımlanmış veya kendi ad alanınızda tanımlanmış özel claim'lerdir.
- Özel Claim'ler (Private Claims): Uygulamanızın ihtiyaçlarına göre tanımladığınız özel claim'lerdir. Örneğin,
userIdveyauserRole.
Örnek bir payload:
{
"userId": "12345",
"username": "exampleuser",
"role": "admin",
"iat": 1678886400, // Issued At (token oluşturulma zamanı)
"exp": 1678890000 // Expiration Time (token bitiş zamanı)
}
Bu JSON nesnesi de Base64Url ile kodlanır ve JWT'nin ikinci kısmını oluşturur.
Signature (İmza)
İmza, JWT'nin bütünlüğünü ve orijinalliğini doğrulamak için kullanılır. Header ve Payload'ın Base64Url kodlanmış halleri, bir gizli anahtar (secret key) kullanılarak Header'da belirtilen algoritma ile şifrelenir. İmza şu şekilde oluşturulur:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret_key
)
İmza, token'ın değiştirilmediğini garanti eder. Eğer header veya payload değiştirilirse, imza geçersiz olur ve token reddedilir.
Örnek Bir JWT
Yukarıdaki header ve payload kullanılarak oluşturulmuş bir JWT şu şekilde görünebilir:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NSIsInVzZXJuYW1lIjoiZXhhbXBsZXVzZXIiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE2Nzg4ODY0MDAsImV4cCI6MTY3ODg5MDAwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Express.js Projesi Kurulumu
JWT kimlik doğrulamasını uygulamak için öncelikle temel bir Express.js projesi kurmamız gerekiyor.
Gerekli Paketlerin Kurulumu
Proje dizininizde aşağıdaki komutları çalıştırarak gerekli paketleri kurun:
mkdir jwt-auth-app
cd jwt-auth-app
npm init -y
npm install express jsonwebtoken dotenv bcryptjs
express: Web sunucumuzu oluşturmak için.jsonwebtoken: JWT oluşturma ve doğrulama için.dotenv: Ortam değişkenlerini yönetmek için.bcryptjs: Şifreleri güvenli bir şekilde hashlemek için.
Temel Express Uygulaması
Proje kök dizininde app.js adında bir dosya oluşturun ve aşağıdaki temel Express uygulamasını ekleyin:
// app.js
require('dotenv').config(); // Ortam değişkenlerini yükle
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json()); // JSON body'leri parse etmek için
app.get('/', (req, res) => {
res.send('JWT Kimlik Doğrulama Uygulamasına Hoş Geldiniz!');
});
app.listen(PORT, () => {
console.log(Sunucu http://localhost:${PORT} adresinde çalışıyor.);
});
Ortam Değişkenlerinin Yönetimi (.env)
Güvenlik anahtarları gibi hassas bilgileri kod içinde tutmak yerine ortam değişkenleri olarak saklamak en iyi uygulamadır. Proje kök dizininde .env adında bir dosya oluşturun:
// .env
JWT_SECRET=cok_gizli_anahtariniz_burada_olmali
PORT=5000
JWT_SECRET değerini güçlü ve tahmin edilemez bir dizeyle değiştirin.
JWT Kimlik Doğrulama Akışı
JWT tabanlı kimlik doğrulama genellikle aşağıdaki adımları içerir:
- Kullanıcı Kaydı (Register): Kullanıcı, kullanıcı adı ve şifre gibi bilgilerle kayıt olur. Şifre hashlenerek veritabanına kaydedilir.
- Kullanıcı Girişi (Login): Kullanıcı, kullanıcı adı ve şifresiyle giriş yapmaya çalışır.
- Token Oluşturma: Sunucu, kullanıcının kimliğini doğruladıktan sonra, kullanıcı bilgilerini içeren bir JWT oluşturur ve imzalar.
- Token Gönderme: Sunucu, oluşturulan JWT'yi istemciye yanıt olarak gönderir (genellikle HTTP başlığında veya yanıt gövdesinde).
- Token Depolama: İstemci (tarayıcı, mobil uygulama), token'ı güvenli bir şekilde saklar (örneğin, localStorage, sessionStorage veya HTTP Only çerezleri).
- Korunan Rotalara Erişim: İstemci, korunan bir rotaya erişmek istediğinde, sakladığı JWT'yi her istekle birlikte HTTP
Authorizationbaşlığında (Bearer Tokenformatında) sunucuya gönderir. - Token Doğrulama: Sunucu, gelen JWT'yi doğrular (imzasını kontrol eder, süresinin dolup dolmadığını kontrol eder).
- Yetkilendirme: Token geçerliyse, sunucu token içindeki bilgilere göre kullanıcının istenen kaynağa erişim yetkisi olup olmadığını kontrol eder.
- Yanıt: Sunucu, yetkilendirme durumuna göre isteği işler ve yanıt döner.
JWT Oluşturma ve İmzalama
Şimdi bir kullanıcı kayıt ve giriş endpoint'i oluşturalım ve başarılı girişte JWT üretelim.
Kullanıcı Kaydı (Register) Endpoint'i
app.js dosyanıza aşağıdaki kodu ekleyin. Basitlik adına kullanıcıları bellekte tutacağız, ancak gerçek bir uygulamada veritabanı kullanmalısınız.
// app.js (devamı)
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
// Geçici kullanıcı veritabanı
const users = []; // Gerçek uygulamada MongoDB, PostgreSQL vb. kullanılmalı
app.post('/register', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ message: 'Kullanıcı adı ve şifre gerekli.' });
}
// Kullanıcı zaten var mı kontrol et
if (users.find(u => u.username === username)) {
return res.status(409).json({ message: 'Bu kullanıcı adı zaten mevcut.' });
}
try {
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = { id: users.length + 1, username, password: hashedPassword };
users.push(newUser);
res.status(201).json({ message: 'Kullanıcı başarıyla kaydedildi.', user: { id: newUser.id, username: newUser.username } });
} catch (error) {
res.status(500).json({ message: 'Kayıt sırasında bir hata oluştu.', error: error.message });
}
});
Kullanıcı Girişi (Login) Endpoint'i
Giriş başarılı olduğunda, jsonwebtoken.sign() metodunu kullanarak bir JWT oluşturacağız.
// app.js (devamı)
app.post('/login', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ message: 'Kullanıcı adı ve şifre gerekli.' });
}
const user = users.find(u => u.username === username);
if (!user) {
return res.status(400).json({ message: 'Kullanıcı adı veya şifre hatalı.' });
}
try {
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ message: 'Kullanıcı adı veya şifre hatalı.' });
}
// JWT oluştur
const token = jwt.sign(
{ userId: user.id, username: user.username, role: 'user' }, // Payload
process.env.JWT_SECRET, // Gizli anahtar
{ expiresIn: '1h' } // Token süresi
);
res.status(200).json({ message: 'Giriş başarılı.', token });
} catch (error) {
res.status(500).json({ message: 'Giriş sırasında bir hata oluştu.', error: error.message });
}
});
jsonwebtoken Kütüphanesinin Kullanımı
Yukarıdaki örnekte jwt.sign() metodunu kullandık:
- İlk argüman: Token'ın payload'ı olacak JavaScript nesnesi.
- İkinci argüman: Token'ı imzalamak için kullanılacak gizli anahtar (
JWT_SECRET). - Üçüncü argüman (isteğe bağlı): Token'ın seçenekleri, örneğin
expiresIn(token'ın ne kadar süre geçerli olacağı).
JWT Doğrulama ve Koruma
Bir kez token oluşturulduğunda, istemci bu token'ı korunan rotalara erişmek için kullanacaktır. Sunucu tarafında ise bu token'ın geçerliliğini doğrulamamız gerekir.
Middleware Oluşturma
Express.js'te middleware'ler, isteklere yanıt vermeden önce veya sonra çalıştırılan fonksiyonlardır. JWT doğrulaması için bir middleware oluşturacağız.
middleware/auth.js adında bir dosya oluşturun:
// middleware/auth.js
const jwt = require('jsonwebtoken');
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN_STRING
if (token == null) {
return res.status(401).json({ message: 'Erişim reddedildi. Token bulunamadı.' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
// Token geçersiz veya süresi dolmuş
return res.status(403).json({ message: 'Geçersiz veya süresi dolmuş token.' });
}
req.user = user; // Doğrulanmış kullanıcı bilgilerini isteğe ekle
next(); // Bir sonraki middleware'e veya route handler'a geç
});
};
module.exports = authenticateToken;
Token'ı Header'dan Çekme
Yukarıdaki middleware'de, token'ı HTTP Authorization başlığından çekiyoruz. İstemciler token'ı genellikle Bearer formatında gönderirler.
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN_STRING" -> ["Bearer", "TOKEN_STRING"]
Token'ı Doğrulama
jwt.verify() metodu, token'ı ve gizli anahtarı kullanarak token'ın geçerliliğini kontrol eder. Eğer token geçerliyse, payload'ı içeren bir user nesnesi döner. Geçersizse veya süresi dolmuşsa bir hata fırlatır.
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
// ...
});
Korunan Rotalara Uygulama
Şimdi bu middleware'i korumak istediğimiz rotalara uygulayalım. app.js dosyanıza aşağıdaki kodu ekleyin:
// app.js (devamı)
const authenticateToken = require('./middleware/auth');
app.get('/protected', authenticateToken, (req, res) => {
res.json({
message: 'Korunan rotaya hoş geldiniz!',
user: req.user // Middleware'den gelen kullanıcı bilgileri
});
});
app.get('/admin', authenticateToken, (req, res) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ message: 'Bu rotaya erişim yetkiniz yok.' });
}
res.json({ message: 'Admin paneline hoş geldiniz!', user: req.user });
});
Artık /protected rotasına erişmek için geçerli bir JWT'ye ihtiyacınız var. /admin rotası ise hem geçerli bir JWT hem de token içindeki role değerinin 'admin' olmasını gerektirir.
Güvenlik En İyi Uygulamaları ve Dikkat Edilmesi Gerekenler
JWT kullanırken güvenliği artırmak için bazı önemli noktalara dikkat etmek gerekir.
Secret Key Güvenliği
JWT_SECRET kesinlikle gizli tutulmalıdır. Asla versiyon kontrol sistemlerine (Git) yüklenmemeli ve güçlü, rastgele bir dize olmalıdır. Ortam değişkenleri veya özel bir anahtar yönetim sistemi kullanın.
Token Süresi ve Yenileme
expiresIn değeri çok uzun olmamalıdır (örneğin, 15 dakika - 1 saat arası). Kısa süreli token'lar, çalınmaları durumunda oluşturabilecekleri zararı sınırlar. Uzun süreli oturumlar için token yenileme (refresh token) mekanizması kullanılabilir. Refresh token'lar genellikle daha uzun ömürlüdür ve yeni bir access token almak için kullanılırlar. Refresh token'lar veritabanında saklanmalı ve tek kullanımlık olmalıdır.
Token Kara Listeleme (Blacklisting)
JWT'ler doğaları gereği durumsuz olduklarından, bir token'ı iptal etmek (örneğin, kullanıcı çıkış yaptığında veya bir güvenlik ihlali durumunda) zordur. Token'ın süresi dolana kadar geçerli kalır. Bu sorunu çözmek için, iptal edilen token'ları bir kara listeye (veritabanı, Redis gibi hızlı bir önbellek) kaydedebilirsiniz. Her istekte, gelen token'ın kara listede olup olmadığını kontrol edersiniz.
HTTPS Kullanımı
Tüm iletişim kanalları (istemci-sunucu arası) HTTPS ile şifrelenmelidir. Aksi takdirde, JWT'ler ağ üzerinden düz metin olarak iletilir ve dinleme saldırılarına (eavesdropping) açık hale gelir.
XSS ve CSRF Koruması
- XSS (Cross-Site Scripting): JWT'leri
localStorage'da saklamak XSS saldırılarına karşı savunmasız hale getirebilir, çünkü kötü amaçlı komut dosyaları token'a erişebilir. HTTP Only çerezleri bu riski azaltır çünkü JavaScript bu çerezlere erişemez. - CSRF (Cross-Site Request Forgery): HTTP Only çerezleri CSRF'ye karşı savunmasız olabilir. CSRF koruması için genellikle anti-CSRF token'lar veya SameSite çerez niteliği kullanılır.
Payload'da Hassas Bilgi Saklamaktan Kaçının
JWT payload'ı Base64Url ile kodlandığı için herkes tarafından okunabilir. Bu nedenle, şifreler, kredi kartı numaraları gibi hassas bilgileri payload içinde saklamayın. Sadece kullanıcı kimliği ve yetkilendirme için gerekli minimum bilgiyi ekleyin.
Sonuç
JWT kimlik doğrulama, modern web uygulamaları için güçlü, esnek ve ölçeklenebilir bir çözümdür. Node.js ve Express.js ile entegrasyonu oldukça basittir ve geliştiricilere stateless API'ler oluşturma imkanı sunar. Ancak, güvenlik en iyi uygulamalarına dikkat etmek, özellikle secret key'in korunması, token sürelerinin yönetimi ve uygun depolama stratejilerinin kullanılması kritik öneme sahiptir. Bu rehber sayesinde, JWT tabanlı kimlik doğrulama sistemlerini güvenli ve etkili bir şekilde uygulayabilecek bilgiye sahip olduğunuzu umuyoruz.
SSS (Sık Sorulan Sorular)
JWT ile oturum tabanlı kimlik doğrulama arasındaki temel fark nedir?
JWT, sunucu tarafında oturum durumu (state) tutmaz; her token kendi içinde gerekli tüm bilgiyi barındırır. Oturum tabanlı sistemler ise sunucuda oturum kimliğini ve ilgili verileri saklar. JWT'nin durumsuz yapısı, özellikle dağıtık sistemlerde ve ölçeklenebilirlik açısından avantaj sağlar.
JWT'yi istemci tarafında nerede saklamalıyım?
Bu, bir tartışma konusudur. Seçenekler arasında localStorage, sessionStorage ve HTTP Only çerezleri bulunur. localStorage ve sessionStorage XSS saldırılarına karşı savunmasızken, HTTP Only çerezleri JavaScript erişimine kapalı olduğu için XSS riskini azaltır. Ancak HTTP Only çerezleri de CSRF'ye karşı dikkatli olunmasını gerektirir.
Token'ın süresi dolduğunda ne yapmalıyım?
Token'ın süresi dolduğunda, istemci genellikle bir hata alır (403 Forbidden). Bu durumda, istemci yeni bir access token almak için refresh token kullanabilir (eğer bir refresh token mekanizması uygulandıysa) veya kullanıcıdan tekrar giriş yapmasını isteyebilir.
JWT'ler neden şifrelenmez?
JWT'ler varsayılan olarak şifrelenmez, sadece imzalanır. İmzalama, token içeriğinin değiştirilmediğini garanti ederken, şifreleme token içeriğinin okunmasını engeller. Eğer token içindeki bilgilerin gizli kalması gerekiyorsa, JSON Web Encryption (JWE) kullanılabilir, ancak bu, JWT'nin temel kullanım durumundan daha karmaşıktır.
Bir JWT'yi nasıl iptal edebilirim (logout veya güvenlik ihlali durumunda)?
JWT'ler durumsuz olduğu için doğrudan iptal edilemezler; süresi dolana kadar geçerli kalırlar. İptal mekanizması için genellikle bir kara liste (blacklist) kullanılır. İptal edilmek istenen token'lar, süresi dolana kadar bir veritabanına veya önbelleğe kaydedilir ve her istekte token'ın kara listede olup olmadığı kontrol edilir.