JWT (JSON Web Token), stateless kimlik doğrulamanın standart çözümüdür. Sunucu taraflı session saklama gerektirmez; token'ın kendisi tüm bilgiyi taşır. Bu derste Node.js + Express ile JWT tabanlı tam bir auth sistemi, refresh token mekanizması ve güvenlik önlemlerini öğreneceksin.
JWT Nasıl Çalışır?
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkZW0iLCJpYXQiOjE3NTA1MDIzMzEsImV4cCI6MTc1MDUwNTkzMX0
.SdKkUjp4Sq1yMJ7YP6HtF81oYGiEG_XH3kBM-X9FKKU
Payload decode edildiğinde:
{
"sub": "1234567890",
"name": "Adem",
"iat": 1750502331, // issued at
"exp": 1750505931, // expires (1 saat sonra)
"role": "user"
}Token imzalanmış ama şifrelenmemiş — payload herkes okuyabilir, ama imzayı sadece gizli anahtar sahibi doğrulayabilir.
Proje Kurulumu
npm init -y
npm install express jsonwebtoken bcryptjs cookie-parser
npm install -D typescript @types/express @types/jsonwebtoken @types/bcryptjs ts-node nodemon// src/config/env.ts
export const config = {
JWT_SECRET: process.env.JWT_SECRET!,
JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET!,
ACCESS_TOKEN_EXPIRY: '15m', // kısa ömürlü
REFRESH_TOKEN_EXPIRY: '30d', // uzun ömürlü
BCRYPT_ROUNDS: 12,
} as const;
if (!config.JWT_SECRET || !config.JWT_REFRESH_SECRET) {
throw new Error('JWT_SECRET ve JWT_REFRESH_SECRET zorunlu');
}Token Üretme ve Doğrulama
// src/lib/jwt.ts
import jwt, { JwtPayload } from 'jsonwebtoken';
import { config } from '../config/env';
interface TokenPayload {
userId: number;
email: string;
role: 'user' | 'admin';
}
export function signAccessToken(payload: TokenPayload): string {
return jwt.sign(payload, config.JWT_SECRET, {
expiresIn: config.ACCESS_TOKEN_EXPIRY,
issuer: 'codeforge-api',
audience: 'codeforge-web',
});
}
export function signRefreshToken(payload: Pick<TokenPayload, 'userId'>): string {
return jwt.sign(payload, config.JWT_REFRESH_SECRET, {
expiresIn: config.JWT_REFRESH_SECRET,
});
}
export function verifyAccessToken(token: string): TokenPayload {
return jwt.verify(token, config.JWT_SECRET, {
issuer: 'codeforge-api',
audience: 'codeforge-web',
}) as TokenPayload;
}
export function verifyRefreshToken(token: string): Pick<TokenPayload, 'userId'> {
return jwt.verify(token, config.JWT_REFRESH_SECRET) as Pick<TokenPayload, 'userId'>;
}Auth Controller
// src/controllers/auth.controller.ts
import { Request, Response, NextFunction } from 'express';
import bcrypt from 'bcryptjs';
import { prisma } from '../lib/prisma';
import { signAccessToken, signRefreshToken, verifyRefreshToken } from '../lib/jwt';
import { config } from '../config/env';
export async function register(req: Request, res: Response, next: NextFunction) {
try {
const { name, email, password } = req.body;
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) {
return res.status(409).json({ error: 'Bu e-posta zaten kullanılıyor.' });
}
const hash = await bcrypt.hash(password, config.BCRYPT_ROUNDS);
const user = await prisma.user.create({
data: { name, email, password: hash },
select: { id: true, name: true, email: true, role: true },
});
const accessToken = signAccessToken({ userId: user.id, email: user.email, role: user.role });
const refreshToken = signRefreshToken({ userId: user.id });
// Refresh token'ı DB'de sakla (iptal kontrolü için)
await prisma.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
},
});
// Refresh token'ı HttpOnly cookie olarak gönder
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 gün
});
res.status(201).json({ user, accessToken });
} catch (err) {
next(err);
}
}
export async function login(req: Request, res: Response, next: NextFunction) {
try {
const { email, password } = req.body;
const user = await prisma.user.findUnique({ where: { email } });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'E-posta veya şifre hatalı.' });
}
const accessToken = signAccessToken({ userId: user.id, email: user.email, role: user.role });
const refreshToken = signRefreshToken({ userId: user.id });
await prisma.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
},
});
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000,
});
res.json({
user: { id: user.id, name: user.name, email: user.email, role: user.role },
accessToken,
});
} catch (err) {
next(err);
}
}
export async function refresh(req: Request, res: Response, next: NextFunction) {
try {
const token = req.cookies?.refreshToken;
if (!token) return res.status(401).json({ error: 'Refresh token bulunamadı.' });
// Token DB'de var mı ve iptal edilmedi mi?
const stored = await prisma.refreshToken.findUnique({
where: { token },
include: { user: { select: { id: true, email: true, role: true } } },
});
if (!stored || stored.expiresAt < new Date()) {
res.clearCookie('refreshToken');
return res.status(401).json({ error: 'Geçersiz veya süresi dolmuş refresh token.' });
}
const payload = verifyRefreshToken(token);
const newAccessToken = signAccessToken({
userId: stored.user.id,
email: stored.user.email,
role: stored.user.role,
});
res.json({ accessToken: newAccessToken });
} catch (err) {
next(err);
}
}
export async function logout(req: Request, res: Response, next: NextFunction) {
try {
const token = req.cookies?.refreshToken;
if (token) {
await prisma.refreshToken.deleteMany({ where: { token } });
}
res.clearCookie('refreshToken');
res.json({ message: 'Başarıyla çıkış yapıldı.' });
} catch (err) {
next(err);
}
}Middleware: Token Doğrulama
// src/middlewares/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { verifyAccessToken } from '../lib/jwt';
declare global {
namespace Express {
interface Request {
user?: { userId: number; email: string; role: string };
}
}
}
export function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Token gerekli.' });
}
const token = authHeader.slice(7);
try {
req.user = verifyAccessToken(token);
next();
} catch (err: any) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token süresi doldu.', code: 'TOKEN_EXPIRED' });
}
return res.status(401).json({ error: 'Geçersiz token.' });
}
}
export function authorize(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Bu işlem için yetkiniz yok.' });
}
next();
};
}Route Tanımları
// src/routes/auth.routes.ts
import { Router } from 'express';
import * as auth from '../controllers/auth.controller';
import { authenticate, authorize } from '../middlewares/auth.middleware';
import { rateLimiter } from '../middlewares/rate-limit.middleware';
const router = Router();
router.post('/register', rateLimiter(5, 15 * 60), auth.register);
router.post('/login', rateLimiter(10, 15 * 60), auth.login);
router.post('/refresh', auth.refresh);
router.post('/logout', auth.logout);
router.get('/me', authenticate, (req, res) => res.json(req.user));
// Admin route
router.get('/admin/users', authenticate, authorize('admin'), adminController.listUsers);
export default router;Client Tarafı: Token Yenileme
// Axios interceptor — access token expire olursa refresh dene
import axios from 'axios';
const api = axios.create({ baseURL: '/api', withCredentials: true });
api.interceptors.response.use(
(response) => response,
async (error) => {
const original = error.config;
if (error.response?.status === 401 &&
error.response?.data?.code === 'TOKEN_EXPIRED' &&
!original._retry) {
original._retry = true;
const { data } = await api.post('/auth/refresh');
original.headers['Authorization'] = `Bearer ${data.accessToken}`;
return api(original);
}
return Promise.reject(error);
}
);Güvenlik Kontrol Listesi
✓ Access token kısa ömürlü (15 dakika)
✓ Refresh token HttpOnly cookie (XSS engeli)
✓ Refresh token DB'de saklanıyor (iptal desteği)
✓ HTTPS zorunlu (production)
✓ Rate limiting (brute force engeli)
✓ bcrypt rounds >= 12
✓ issuer + audience claim doğrulama
✗ localStorage'da token SAKLAMA (XSS riski)
✗ Zayıf JWT_SECRET (minimum 256 bit / 32 karakter)Özet
JWT auth sisteminin üç temeli: kısa ömürlü access token (15dk, Authorization header), uzun ömürlü refresh token (30gün, HttpOnly cookie), ve refresh token'ı DB'de saklayarak iptal mekanizması. verifyAccessToken middleware'i korunan route'lara, authorize() yetki kontrolüne eklenir. Client tarafında Axios interceptor access token expire olduğunda otomatik yeniler.