JWT & Session Management Cheat Sheet - BelajarKoding | BelajarKodingJWT & Session Management Cheat Sheet
Referensi cepat JWT authentication, session management, cookies security, dan best practices. Must-have buat backend developer.
JavaScript10 min read1.989 kata Lanjutkan Membaca
Daftar gratis untuk akses penuh ke semua artikel dan cheat sheet. Cepat, mudah, dan tanpa biaya!
Akses Tanpa Batas
Baca semua artikel dan cheat sheet kapan pun kamu mau
Bookmark Konten
Simpan artikel dan roadmap favoritmu untuk dibaca nanti
Gratis Selamanya
Tidak ada biaya tersembunyi, 100% gratis
Dengan mendaftar, kamu setuju dengan syarat dan ketentuan kami
#JWT Basics
Dasar-dasar penggunaan JSON Web Token untuk authentication dan authorization.
const jwt = require('jsonwebtoken');
// Generate JWT
const token = jwt.sign(
{ userId: 123, username:
'john'
},
// Payload
process.env.JWT_SECRET, // Secret
{ expiresIn: '1h' } // Options
);
// Verify JWT
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
// Invalid or expired token
}
console.log(decoded); // { userId: 123, username: 'john', iat: ..., exp: ... }
});
// Decode without verification (for debugging)
const decoded = jwt.decode(token);
Konfigurasi tambahan untuk JWT seperti expiration, audience, dan algoritma.
jwt.sign(payload, secret, {
expiresIn: '1h', // '1h', '7d', '30m', 60 (seconds)
notBefore: '30s', // Token not valid before
audience: 'api.example.com',
issuer: 'auth.example.com',
subject: 'user@example.com',
jwtid: 'unique-id',
algorithm:
Cara mengelola session di server untuk menyimpan state user.
Implementasi session menggunakan express-session middleware.
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
Praktik keamanan penting untuk cookies yang menyimpan data sensitif.
// Set cookie
res.cookie('name', 'value', {
httpOnly: true, // JavaScript can't access (XSS protection)
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 3600000,
Atribut-atribut penting pada cookie untuk keamanan dan kontrol.
// httpOnly - CRITICAL for auth cookies
httpOnly: true // JavaScript ga bisa akses
// secure - HTTPS only
secure: true // Only send over HTTPS
secure: process.env.NODE_ENV === 'production'
// sameSite - CSRF protection
#Access Token vs Refresh Token
Perbedaan antara access token (pendek) dan refresh token (panjang) untuk keamanan yang lebih baik.
// Generate both tokens
function generateTokens(user) {
const
#Authentication Middleware
Middleware untuk memverifikasi authentication sebelum mengakses route yang dilindungi.
// JWT middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader
Cara meng-hash password dengan aman menggunakan bcrypt atau argon2.
const bcrypt = require('bcrypt');
// Hash password
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Verify password
const isValid
Proteksi terhadap Cross-Site Request Forgery attacks.
const csrf = require('csurf');
// Setup CSRF
const csrfProtection = csrf({ cookie: true });
app.use(csrfProtection);
// Get CSRF token
app.
Membatasi jumlah request per pengguna untuk mencegah abuse dan serangan.
const rateLimit = require('express-rate-limit');
// General limiter
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
const { body, validationResult } = require('express-validator');
app.post('/register',
// Validation rules
body('email').isEmail
#OAuth 2.0 / Social Login
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// Configure strategy
passport.use(new GoogleStrategy
#Token Blacklist (JWT Revocation)
const redis = require('redis');
const client = redis.createClient();
// Blacklist token on logout
app.post(
#Passwordless Authentication
const crypto = require('crypto');
// Send magic link
app.post('/login/email', async (req, res)
#2FA (Two-Factor Authentication)
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate secret
app.post
const helmet = require('helmet');
// Use helmet (sets multiple security headers)
app.use(helmet());
// Or manually
app.use((req, res,
// Store access token in memory (React)
const [accessToken, setAccessToken] = useState(null);
// DO
- Use HTTPS in production
- httpOnly cookies for refresh tokens
- Short-lived access tokens (5-15 min)
- Long-
// Login flow
POST /login
→ Verify credentials
→ Generate access + refresh tokens
→ Store refresh token in DB
→ Send refresh token as httpOnly cookie
→ Return access token in response body
# .env file
JWT_SECRET=your-256-bit-secret-here
REFRESH_TOKEN_SECRET=different-256-bit-secret
SESSION_SECRET=session-secret-256-bit
ACCESS_TOKEN_EXPIRES=15m
REFRESH_TOKEN_EXPIRES=7d
BCRYPT_ROUNDS=10
NODE_ENV=production
// Jest example
describe('Authentication', () => {
let token;
test('should register new user', async
'HS256'
// HS256, RS256, ES256
});
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
},
store: new RedisStore({ client: redisClient }) // Use Redis in production
}));
// Access session
req.session.userId = user.id;
req.session.save(); // Explicit save (if needed)
req.session.destroy(); // Logout
req.session.regenerate(); // Regenerate session ID
// Expire time (ms)
domain: '.example.com',
path: '/',
signed: true // Cryptographically signed
});
// Get cookie
req.cookies.name // Unsigned
req.signedCookies.name // Signed
// Clear cookie
res.clearCookie('name', { path: '/' });
sameSite: 'strict' // Never sent cross-site
sameSite: 'lax' // Sent on top-level navigation (default)
sameSite: 'none' // Always sent (requires secure: true)
// maxAge vs expires
maxAge: 3600000 // Relative (ms from now)
expires: new Date('2025-12-31') // Absolute date
// domain & path
domain: '.example.com' // Available to subdomains
path: '/api' // Only for /api/* routes
// Cookie prefixes
__Secure-name // Must have secure: true
__Host-name // Must have secure: true, no domain, path: '/'
accessToken
=
jwt.
sign
(
{ userId: user.id, username: user.username },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' } // Short-lived
);
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' } // Long-lived
);
return { accessToken, refreshToken };
}
// Login
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body);
const { accessToken, refreshToken } = generateTokens(user);
// Store refresh token in database
await db.saveRefreshToken(user.id, refreshToken);
// Send refresh token as httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken });
});
// Refresh endpoint
app.post('/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
const isValid = await db.validateRefreshToken(decoded.userId, refreshToken);
if (!isValid) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
const accessToken = jwt.sign(
{ userId: decoded.userId },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken });
});
// Logout
app.post('/logout', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
await db.deleteRefreshToken(refreshToken);
res.clearCookie('refreshToken');
res.json({ message: 'Logged out' });
});
&&
authHeader.
split
(
' '
)[
1
];
// Bearer TOKEN
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
}
// Session middleware
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
next();
}
// Usage
app.get('/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
=
await
bcrypt.
compare
(password, hashedPassword);
// Argon2 (recommended, more secure)
const argon2 = require('argon2');
const hash = await argon2.hash(password);
const isValid = await argon2.verify(hash, password);
get
(
'/form'
, (
req
,
res
)
=>
{
res.json({ csrfToken: req.csrfToken() });
});
// Validate CSRF (automatic)
app.post('/submit', (req, res) => {
// CSRF middleware validates automatically
res.json({ message: 'Success' });
});
// Client-side (send in header or body)
fetch('/submit', {
method: 'POST',
headers: {
'CSRF-Token': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
// 15 minutes
max: 100, // Max requests per windowMs
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
// Login limiter (stricter)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true // Only count failed attempts
});
app.use('/api/', limiter);
app.post('/login', loginLimiter, loginHandler);
// Redis store (for distributed systems)
const RedisStore = require('rate-limit-redis');
const limiter = rateLimit({
store: new RedisStore({
client: redisClient
}),
max: 100
});
().
normalizeEmail
(),
body('password').isLength({ min: 8 })
.matches(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/)
.withMessage('Must contain uppercase, lowercase, and number'),
body('username').isAlphanumeric().isLength({ min: 3, max: 20 }),
// Handler
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process registration
}
);
({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
const user = await db.findOrCreateUser(profile);
done(null, user);
}
));
// Routes
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard');
}
);
'/logout'
,
async
(
req
,
res
)
=>
{
const token = req.headers.authorization?.split(' ')[1];
const decoded = jwt.decode(token);
// Store in Redis with expiration
const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
await client.setex(`blacklist:${token}`, expiresIn, 'true');
res.json({ message: 'Logged out' });
});
// Check blacklist in middleware
async function authenticateToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
// Check blacklist
const isBlacklisted = await client.get(`blacklist:${token}`);
if (isBlacklisted) {
return res.status(401).json({ error: 'Token revoked' });
}
// Verify token
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
}
=>
{
const { email } = req.body;
const token = crypto.randomBytes(32).toString('hex');
const expires = Date.now() + 15 * 60 * 1000; // 15 minutes
await db.saveLoginToken(email, token, expires);
const magicLink = `https://app.com/verify?token=${token}`;
await sendEmail(email, magicLink);
res.json({ message: 'Check your email' });
});
// Verify token
app.get('/verify', async (req, res) => {
const { token } = req.query;
const record = await db.findLoginToken(token);
if (!record || record.expires < Date.now()) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
// Create session
req.session.userId = record.userId;
await db.deleteLoginToken(token);
res.redirect('/dashboard');
});
(
'/2fa/setup'
,
async
(
req
,
res
)
=>
{
const secret = speakeasy.generateSecret({
name: `MyApp (${req.user.email})`
});
// Save secret to database
await db.save2FASecret(req.user.id, secret.base32);
// Generate QR code
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
res.json({ secret: secret.base32, qrCode: qrCodeUrl });
});
// Verify 2FA code
app.post('/2fa/verify', async (req, res) => {
const { token } = req.body;
const user = await db.findUser(req.user.id);
const verified = speakeasy.totp.verify({
secret: user.twoFactorSecret,
encoding: 'base32',
token: token,
window: 2 // Allow 2 time steps (±60 seconds)
});
if (!verified) {
return res.status(401).json({ error: 'Invalid code' });
}
// Enable 2FA
await db.enable2FA(req.user.id);
res.json({ message: '2FA enabled' });
});
next
)
=>
{
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// HTTPS only
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// Content Security Policy
res.setHeader('Content-Security-Policy', "default-src 'self'");
next();
});
// Login
async function login(credentials) {
const response = await fetch('/api/login', {
method: 'POST',
credentials: 'include', // Send cookies
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const { accessToken } = await response.json();
setAccessToken(accessToken); // Store in memory
}
// Use token in requests
async function fetchProfile() {
const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
return response.json();
}
// Refresh token automatically
async function refreshAccessToken() {
const response = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include' // Send refresh token cookie
});
const { accessToken } = await response.json();
setAccessToken(accessToken);
return accessToken;
}
// Axios interceptor (auto refresh)
axios.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
const newToken = await refreshAccessToken();
error.config.headers.Authorization = `Bearer ${newToken}`;
return axios.request(error.config);
}
return Promise.reject(error);
}
);
lived refresh
tokens
(
7
-
30
days)
- Strong, random secrets (crypto.randomBytes(64))
- Hash passwords with bcrypt/argon2
- Validate all inputs
- Rate limit auth endpoints
- Use CSRF protection
- Implement 2FA for sensitive operations
// DON'T
- Store JWT in localStorage (XSS vulnerable)
- Put sensitive data in JWT payload
- Use weak secrets (e.g., "secret")
- Skip token expiration
- Trust client-side data
- Use GET for logout
- Store passwords in plain text
- Expose detailed error messages
- Allow unlimited login attempts
- Use cookies without httpOnly/secure/sameSite
// Protected request
GET /api/resource
Headers: Authorization: Bearer <access-token>
→ Verify access token
→ If valid, process request
→ If expired, return 401
// Refresh flow
POST /refresh
Cookies: refreshToken=<refresh-token>
→ Verify refresh token
→ Check DB for validity
→ Generate new access token
→ Return new access token
// Logout flow
POST /logout
→ Delete refresh token from DB
→ Clear refresh token cookie
→ (Optional) Blacklist access token
()
=>
{
const res = await request(app)
.post('/register')
.send({ email: 'test@test.com', password: 'Password123!' });
expect(res.status).toBe(201);
});
test('should login', async () => {
const res = await request(app)
.post('/login')
.send({ email: 'test@test.com', password: 'Password123!' });
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('accessToken');
token = res.body.accessToken;
});
test('should access protected route with token', async () => {
const res = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
});
test('should reject without token', async () => {
const res = await request(app).get('/api/profile');
expect(res.status).toBe(401);
});
test('should logout', async () => {
const res = await request(app)
.post('/logout')
.set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
});
});