Referensi cepat JWT authentication, session management, cookies security, dan best practices. Must-have buat backend developer.
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: 'HS256' // HS256, RS256, ES256
});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,
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 IDPraktik 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, // 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: '/' });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
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: '/'Perbedaan antara access token (pendek) dan refresh token (panjang) untuk keamanan yang lebih baik.
// Generate both tokens
function generateTokens(user) {
const 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' });
});Middleware untuk memverifikasi authentication sebelum mengakses route yang dilindungi.
// JWT middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && 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 });
});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 = 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);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.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)
});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, // 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
});const { body, validationResult } = require('express-validator');
app.post('/register',
// Validation rules
body('email').isEmail().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
}
);const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// Configure strategy
passport.use(new GoogleStrategy({
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');
}
);const redis = require('redis');
const client = redis.createClient();
// Blacklist token on logout
app.post('/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 crypto = require('crypto');
// Send magic link
app.post('/login/email', async (req, res) => {
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');
});const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate secret
app.post('/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' });
});const helmet = require('helmet');
// Use helmet (sets multiple security headers)
app.use(helmet());
// Or manually
app.use((req, res, 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();
});// Store access token in memory (React)
const [accessToken, setAccessToken] = useState(null);
// 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);
}
);// DO
- Use HTTPS in production
- httpOnly cookies for refresh tokens
- Short-lived access tokens (5-15 min)
- Long-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// 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
// 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# .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 () => {
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);
});
});