Merhaba arkadaşlar, bugün başımı çok ağrıtan bir konudan bahsedeceğim: JWT token güvenliği. Bir projede, kullanıcı giriş yaptıktan sonra gelen JWT'yi nereye koyacağım konusunda kafayı yemiştim. LocalStorage mı, sessionStorage mı, cookies mi? Hangisi daha güvenli? Üstüne bir de token'ın süresi dolunca kullanıcıyı tekrar login sayfasına atmak istemiyordum. İşte tüm bu dertlere bulduğum, benim için en temiz çözümü anlatacağım.
Karşılaştığım Büyük İkilem
İlk başta herkes gibi ben de token'ı localStorage'a atmıştım. Çok pratikti ama bir XSS (Cross-Site Scripting) açığı durumunda token'ın çalınabileceğini öğrenince içim rahat etmedi. sessionStorage biraz daha iyiydi ama sekme kapanınca token uçuyordu, kullanıcı deneyimi kötüydü. HTTP-only cookies ise XSS'e karşı daha dayanıklı ama CSRF (Cross-Site Request Forgery) riski taşıyordu. "Yok artık, bu işin içinden nasıl çıkacağım?" dediğim anda araştırmalarım beni Access Token + Refresh Token ikilisine ve HTTP-only cookies'e yöneltti.
Benim Tercih Ettiğim Güvenli Mimari
Sonunda oturttuğum sistem şu şekilde:
1. Access Token (JWT): Kısa ömürlü (5-15 dakika). İsteklerin Authorization header'ı ile gönderilir.
2. Refresh Token: Uzun ömürlü (7 gün, 1 ay gibi). Sadece yeni access token almak için kullanılır. MUTLAKA HTTP-only, Secure, SameSite=Strict cookie olarak saklanır.
Bu sayede access token ele geçirilse bile kısa sürede geçersiz hale geliyor. Refresh token ise JavaScript'ten erişilemediği için XSS'ten nispeten korunaklı.
Backend Tarafında Refresh Mantığı (Node.js Örneği)
İşte login endpoint'inde ve token refresh işleminde kullandığım temel yapı. Önce kullanıcı giriş yaptığında hem access token hem de refresh token üretip, refresh token'ı cookie'ye koyuyoruz.
Şimdi de access token süresi dolduğunda, istemci tarafından özel bir endpoint'e yapılan istekle yeni bir token alalım.
Frontend Tarafında İstekleri Yönetmek (Axios Interceptor)
Frontend'de, her istekte token'ı header'a eklemek ve 401 hatası alındığında otomatik olarak yenilemek için Axios Interceptor kullanıyorum. Bu, kullanıcıyı hiç hissettirmeden token'ı tazeler.
Sonuç ve Tavsiyelerim
Bu yapıyı kurduktan sonra hem güvenlik konusunda içim rahat etti hem de kullanıcı deneyimi mükemmel oldu. Kullanıcı, access token'ı süresi dolsa bile arka planda yenilendiği için uygulamadan atılmıyor. Tabii ki mutlak güvenlik diye bir şey yok. Bu yapının yanında sunucu tarafında refresh token'ları bir veritabanında veya Redis'te tutup, iptal etme (logout all devices) özelliği de ekleyebilirsiniz.
Siz JWT token'larınızı nasıl saklıyorsunuz? Bu refresh token stratejisini kullanıyor musunuz? Ya da "Ben refresh token'ı da veritabanında tutuyorum, daha güvenli" diyenler var mı? Fikirlerinizi merakla bekliyorum, aşağıdaki yorumlarda buluşalım!
İlk başta herkes gibi ben de token'ı localStorage'a atmıştım. Çok pratikti ama bir XSS (Cross-Site Scripting) açığı durumunda token'ın çalınabileceğini öğrenince içim rahat etmedi. sessionStorage biraz daha iyiydi ama sekme kapanınca token uçuyordu, kullanıcı deneyimi kötüydü. HTTP-only cookies ise XSS'e karşı daha dayanıklı ama CSRF (Cross-Site Request Forgery) riski taşıyordu. "Yok artık, bu işin içinden nasıl çıkacağım?" dediğim anda araştırmalarım beni Access Token + Refresh Token ikilisine ve HTTP-only cookies'e yöneltti.
Sonunda oturttuğum sistem şu şekilde:
1. Access Token (JWT): Kısa ömürlü (5-15 dakika). İsteklerin Authorization header'ı ile gönderilir.
2. Refresh Token: Uzun ömürlü (7 gün, 1 ay gibi). Sadece yeni access token almak için kullanılır. MUTLAKA HTTP-only, Secure, SameSite=Strict cookie olarak saklanır.
Bu sayede access token ele geçirilse bile kısa sürede geçersiz hale geliyor. Refresh token ise JavaScript'ten erişilemediği için XSS'ten nispeten korunaklı.
İşte login endpoint'inde ve token refresh işleminde kullandığım temel yapı. Önce kullanıcı giriş yaptığında hem access token hem de refresh token üretip, refresh token'ı cookie'ye koyuyoruz.
JavaScript:
// Kullanıcı Login Olduğunda
app.post('/api/login', async (req, res) => {
// ... Kullanıcı doğrulama işlemleri ...
const user = await User.findOne({ email: req.body.email });
const accessToken = jwt.sign(
{ userId: user._id },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user._id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
// Refresh Token'ı HTTP-only Cookie'ye kaydet
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // HTTPS'te çalışıyor
sameSite: 'strict',
maxAge: 7 24 60 60 1000 // 7 gün
});
// Access Token'ı response body'si ile gönder
res.json({ accessToken });
});
Şimdi de access token süresi dolduğunda, istemci tarafından özel bir endpoint'e yapılan istekle yeni bir token alalım.
JavaScript:
// Yeni Access Token Alma (Refresh) Endpoint'i
app.post('/api/refresh-token', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.sendStatus(401); // Unauthorized
}
// Cookie'den gelen token geçerli mi?
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
if (err) {
return res.sendStatus(403); // Forbidden (Token geçersiz/yanlış)
}
// Refresh token geçerliyse yeni bir access token üret
const newAccessToken = jwt.sign(
{ userId: user.userId },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
});
});
Frontend'de, her istekte token'ı header'a eklemek ve 401 hatası alındığında otomatik olarak yenilemek için Axios Interceptor kullanıyorum. Bu, kullanıcıyı hiç hissettirmeden token'ı tazeler.
JavaScript:
import axios from 'axios';
const api = axios.create({ baseURL: '/api' });
// Her istekten önce token'ı header'a ekle
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken'); // Access token'ı burada saklıyorum
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Yanıtları dinle, 401 hatasında token'ı yenile
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Refresh token endpoint'ine istek yap
const response = await axios.post('/api/refresh-token', {}, { withCredentials: true });
const newAccessToken = response.data.accessToken;
localStorage.setItem('accessToken', newAccessToken);
// Yeni token ile başarısız olan isteği tekrarla
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return api(originalRequest);
} catch (refreshError) {
// Refresh token da geçersizse, kullanıcıyı login sayfasına yönlendir
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
Bu yapıyı kurduktan sonra hem güvenlik konusunda içim rahat etti hem de kullanıcı deneyimi mükemmel oldu. Kullanıcı, access token'ı süresi dolsa bile arka planda yenilendiği için uygulamadan atılmıyor. Tabii ki mutlak güvenlik diye bir şey yok. Bu yapının yanında sunucu tarafında refresh token'ları bir veritabanında veya Redis'te tutup, iptal etme (logout all devices) özelliği de ekleyebilirsiniz.
Siz JWT token'larınızı nasıl saklıyorsunuz? Bu refresh token stratejisini kullanıyor musunuz? Ya da "Ben refresh token'ı da veritabanında tutuyorum, daha güvenli" diyenler var mı? Fikirlerinizi merakla bekliyorum, aşağıdaki yorumlarda buluşalım!