1. Pengenalan Authentication & Authorization
Dalam pengembangan web, authentication dan authorization adalah dua konsep keamanan yang berbeda namun saling melengkapi. Memahami perbedaannya sangat penting untuk membangun aplikasi web yang aman.
Authentication vs Authorization
| Aspek | Authentication (Autentikasi) | Authorization (Otorisasi) |
|---|---|---|
| Pertanyaan | "Siapa kamu?" | "Apa yang boleh kamu lakukan?" |
| Tujuan | Memverifikasi identitas pengguna | Menentukan hak akses pengguna |
| Kapan | Sebelum authorization | Setelah authentication |
| Contoh | Login dengan email & password | Admin bisa hapus user, user biasa tidak |
| Data | Credential (password, token, biometric) | Permissions, roles, policies |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β ALUR AUTHENTICATION & AUTHORIZATION β β β β ββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β β β User βββββΊβ AuthenticationβββββΊβ Authorization β β β β Request β β (Siapa?) β β (Boleh apa?) β β β ββββββββββββ ββββββββ¬ββββββββ ββββββββββββ¬ββββββββββββ β β β β β β ββββββΌβββββ ββββββΌβββββ β β β Verifikasiβ β Cek Roleβ β β β Credentialβ β & Perm. β β β ββββββ¬βββββ ββββββ¬βββββ β β β β β β ββββββββΌβββββββ ββββββββΌβββββββ β β β β Berhasil β β β Diizinkan β β β β β Gagal β β β Ditolak β β β βββββββββββββββ βββββββββββββββ β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Metode Authentication yang Umum
| Metode | Cara Kerja | Cocok Untuk |
|---|---|---|
| Session-based | Server menyimpan session, client menyimpan cookie | Web tradisional, monolith app |
| Token-based (JWT) | Server mengeluarkan token, client menyimpannya | SPA, mobile app, API |
| OAuth 2.0 | Delegasi autentikasi ke provider (Google, GitHub) | Login sosial, integrasi pihak ketiga |
| API Key | Kode unik untuk mengidentifikasi aplikasi | API publik, server-to-server |
| Biometric | Sidik jari, Face ID, iris scan | Aplikasi mobile high-security |
2. Session-based Authentication
Session-based authentication adalah metode autentikasi tradisional di mana server menyimpan informasi session pengguna. Saat pengguna login, server membuat session dan mengirim session ID ke browser melalui cookie.
Cara Kerja Session
βββββββββββββββ βββββββββββββββ ββββββββββββββββ β Browser β β Server β β Database β β β 1. POST β β β β β Login Form βββββββββββΊβ /login ββββββββββΊβ Cek user β β email+pass β β β β di database β β β β βββββββββββ β β ββββββββββββ Set-Cookie β β β β β 2. β session_id β β β β β Cookie β =abc123 β β β β β β β β β β β 3. GET β β β β β βββββββββββΊβ /dashboard ββββββββββΊβ Ambil data β β Cookie: β Cookie β Cek sessionβ β berdasarkan β β session=abc β β abc123 β β β user_id β β ββββββββββββ Response βββββββββββ β βββββββββββββββ βββββββββββββββ ββββββββββββββββ
Implementasi Session dengan Express.js
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());
// Setup Redis client untuk session store
const redisClient = createClient({
url: 'redis://localhost:6379'
});
redisClient.connect();
// Konfigurasi session
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET, // Gunakan secret yang kuat
name: 'sessionId', // Jangan gunakan default 'connect.sid'
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Tidak bisa diakses JavaScript
secure: true, // Hanya dikirim via HTTPS
sameSite: 'strict', // Mencegah CSRF
maxAge: 15 * 60 * 1000, // 15 menit
path: '/',
}
}));
// Middleware: cek autentikasi
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Silakan login terlebih dahulu' });
}
next();
}
// Middleware: cek role
function requireRole(...roles) {
return (req, res, next) => {
if (!roles.includes(req.session.role)) {
return res.status(403).json({ error: 'Akses ditolak' });
}
next();
};
}
// Endpoint: Login
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
// Cari user di database
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
if (!user.rows[0]) {
return res.status(401).json({ error: 'Email atau password salah' });
}
// Verifikasi password
const validPassword = await bcrypt.compare(password, user.rows[0].password_hash);
if (!validPassword) {
return res.status(401).json({ error: 'Email atau password salah' });
}
// Simpan data di session
req.session.userId = user.rows[0].id;
req.session.email = user.rows[0].email;
req.session.role = user.rows[0].role;
res.json({ message: 'Login berhasil', user: { email: user.rows[0].email } });
});
// Endpoint: Logout
app.post('/api/logout', (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: 'Gagal logout' });
res.clearCookie('sessionId');
res.json({ message: 'Logout berhasil' });
});
});
// Endpoint terproteksi
app.get('/api/dashboard', requireAuth, (req, res) => {
res.json({ message: `Selamat datang, user ${req.session.userId}` });
});
// Endpoint dengan role check
app.delete('/api/users/:id', requireAuth, requireRole('admin'), async (req, res) => {
await db.query('DELETE FROM users WHERE id = $1', [req.params.id]);
res.json({ message: 'User berhasil dihapus' });
});
app.listen(3000);
Jangan menyimpan session di memory server (default Express). Gunakan Redis, PostgreSQL, atau MongoDB sebagai session store. Memory server akan hilang saat restart dan tidak scalable untuk multiple server instances.
3. JSON Web Token (JWT)
JWT (JSON Web Token) adalah standar terbuka (RFC 7519) untuk membuat token autentikasi yang aman dan self-contained. JWT berisi informasi terkait pengguna yang dapat diverifikasi tanpa mengakses database.
Struktur JWT
// JWT terdiri dari 3 bagian yang dipisahkan oleh titik (.)
// HEADER.PAYLOAD.SIGNATURE
// 1. HEADER β Algoritma dan tipe token
{
"alg": "HS256", // Algoritma signing (HS256 atau RS256)
"typ": "JWT" // Tipe token
}
// Base64Url: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
// 2. PAYLOAD β Data (claims) yang disimpan
{
"sub": "1234567890", // Subject (user ID)
"name": "Budi Santoso", // Nama pengguna
"email": "budi@mail.com", // Email
"role": "admin", // Role pengguna
"iat": 1719340800, // Issued At (waktu dibuat)
"exp": 1719344400 // Expiration (waktu kedaluwarsa)
}
// Base64Url: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I...
// 3. SIGNATURE β Tanda tangan digital untuk verifikasi
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
SECRET_KEY
)
// Base64Url: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// Hasil akhir:
// eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature
Session vs JWT
| Aspek | Session | JWT |
|---|---|---|
| Penyimpanan | Server-side (database/Redis) | Client-side (localStorage/cookie) |
| Scalability | Perlu shared session store | Stateless, mudah di-scale |
| Revocation | Hapus session di server | Sulit (perlu blacklist) |
| Ukuran | Hanya session ID di cookie | Token bisa besar (payload panjang) |
| CORS | Otomatis dengan cookie | Perlu header manual |
| Cocok untuk | Monolith, web tradisional | SPA, mobile, microservices |
Implementasi JWT dengan Node.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
// Secret keys β simpan di environment variables!
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
// Generate access token (berlaku 15 menit)
function generateAccessToken(user) {
return jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role,
},
ACCESS_SECRET,
{ expiresIn: '15m' }
);
}
// Generate refresh token (berlaku 7 hari)
function generateRefreshToken(user) {
return jwt.sign(
{ sub: user.id },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
}
// Middleware: verifikasi access token
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: 'Token tidak ditemukan' });
}
try {
const decoded = jwt.verify(token, ACCESS_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token kedaluwarsa', code: 'TOKEN_EXPIRED' });
}
return res.status(403).json({ error: 'Token tidak valid' });
}
}
// Endpoint: Login β mengembalikan kedua token
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await findUserByEmail(email);
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
return res.status(401).json({ error: 'Kredensial salah' });
}
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// Simpan refresh token di database
await db.query(
'UPDATE users SET refresh_token = $1 WHERE id = $2',
[refreshToken, user.id]
);
// Kirim refresh token sebagai httpOnly cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari
});
res.json({
accessToken,
user: { id: user.id, email: user.email, role: user.role }
});
});
// Endpoint: Refresh token
app.post('/api/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token tidak ditemukan' });
}
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
const user = await db.query(
'SELECT * FROM users WHERE id = $1 AND refresh_token = $2',
[decoded.sub, refreshToken]
);
if (!user.rows[0]) {
return res.status(403).json({ error: 'Refresh token tidak valid' });
}
const newAccessToken = generateAccessToken(user.rows[0]);
res.json({ accessToken: newAccessToken });
} catch (error) {
return res.status(403).json({ error: 'Refresh token tidak valid' });
}
});
// Endpoint terproteksi
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({
message: 'Data profil',
userId: req.user.sub,
email: req.user.email,
role: req.user.role
});
});
Jangan menyimpan data sensitif di payload JWT β payload hanya di-encode (Base64), bukan dienkripsi. Siapa pun bisa membaca isinya. Juga, simpan secret key yang sangat kuat dan jangan pernah commit ke repository. Gunakan environment variables atau secret manager.
4. OAuth 2.0
OAuth 2.0 adalah protokol standar industri untuk delegasi akses. OAuth memungkinkan pengguna memberikan akses terbatas ke resource mereka di satu aplikasi ke aplikasi lain, tanpa membagikan password.
Alur OAuth 2.0 β Authorization Code Flow
ββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββ β User β β Aplikasi β β Auth Server β β Resource β β (Browser)β β (Client) β β (Google) β β Server β β β β β β β β β β 1. Klik ββββββΊβ β β β β β β "Login β β 2. Redirect ββββββΊβ 3. User β β β β Google" β β ke Google β β Login & β β β β β β β β Grant Accessβ β β β βββββββ βββββββ 4. Redirect β β β β β 5. β Auth Code β β + Auth Code β β β β β β β β β β β β β β 6. Tukar ββββββΊβ 7. Verifikasiβ β β β β β Code + β β & kirim β β β β β β Client Secretβ β Access Tokenβ β β β β β βββββββ + Refresh β β β β β β 8. Simpan β β β β β β β β Token β β β β β β β β 9. Akses APIβββββββββββββββββββββββββββΊβ 10. Data β β βββββββ 11. Responseβββββββββββββββββββββββββββ β ββββββββββββ ββββββββββββββββ ββββββββββββββββ ββββββββββββββ
Implementasi OAuth 2.0 dengan Passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// Konfigurasi Google OAuth Strategy
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/api/auth/google/callback',
scope: ['profile', 'email']
},
async (accessToken, refreshToken, profile, done) => {
try {
// Cek apakah user sudah ada di database
let user = await db.query(
'SELECT * FROM users WHERE google_id = $1',
[profile.id]
);
if (user.rows[0]) {
// User sudah ada β update last login
await db.query(
'UPDATE users SET last_login = NOW() WHERE google_id = $1',
[profile.id]
);
return done(null, user.rows[0]);
}
// User baru β buat akun
const newUser = await db.query(
`INSERT INTO users (google_id, email, name, avatar, provider)
VALUES ($1, $2, $3, $4, 'google')
RETURNING *`,
[profile.id, profile.emails[0].value, profile.displayName, profile.photos[0].value]
);
return done(null, newUser.rows[0]);
} catch (error) {
return done(error, null);
}
}
));
// Serialize/Deserialize user untuk session
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
done(null, user.rows[0]);
});
// Routes
app.get('/api/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/api/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// Autentikasi berhasil
res.redirect('/dashboard');
}
);
app.get('/api/auth/logout', (req, res) => {
req.logout(() => {
res.redirect('/');
});
});
// Cek status autentikasi
app.get('/api/auth/status', (req, res) => {
if (req.isAuthenticated()) {
res.json({ authenticated: true, user: req.user });
} else {
res.json({ authenticated: false });
}
});
Untuk membuat Google OAuth credentials, kunjungi Google Cloud Console β APIs & Services β Credentials β Create OAuth 2.0 Client ID. Pastikan redirect URI yang dikonfigurasi sesuai dengan callback URL di aplikasi Anda.
5. Role-Based Access Control (RBAC)
RBAC adalah metode authorization yang mengelompokkan pengguna ke dalam roles dan memberikan permissions berdasarkan role tersebut. Ini menyederhanakan manajemen akses dibanding memberikan izin ke setiap pengguna secara individual.
Struktur RBAC
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β RBAC STRUCTURE β β β β USERS ROLES PERMISSIONS β β ββββββββββ ββββββββββ βββββββββββββββ β β β Budi ββββββββΊβ Admin ββββββββΊβ users:read β β β β β β β β users:write β β β ββββββββββ β β β users:delete β β β ββββββββββ β β β posts:manage β β β β Sari ββββββββΊβ β β settings:all β β β β β ββββββββββ βββββββββββββββ β β ββββββββββ ββββββββββ βββββββββββββββ β β ββββββββββ β Editor ββββββββΊβ posts:write β β β β Andi ββββββββΊβ β β posts:read β β β β β ββββββββββ β media:upload β β β ββββββββββ ββββββββββ βββββββββββββββ β β β Viewer β βββββββββββββββ β β β ββββββββΊβ posts:read β β β ββββββββββ β profile:edit β β β βββββββββββββββ β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Implementasi RBAC
// Definisi roles dan permissions
const ROLES = {
admin: {
permissions: [
'users:read', 'users:write', 'users:delete',
'posts:read', 'posts:write', 'posts:delete', 'posts:publish',
'media:read', 'media:upload', 'media:delete',
'settings:read', 'settings:write',
'analytics:read',
]
},
editor: {
permissions: [
'posts:read', 'posts:write', 'posts:publish',
'media:read', 'media:upload',
'analytics:read',
]
},
author: {
permissions: [
'posts:read', 'posts:write',
'media:read', 'media:upload',
]
},
viewer: {
permissions: [
'posts:read',
'profile:read', 'profile:write',
]
}
};
// Middleware: cek permission
function checkPermission(requiredPermission) {
return (req, res, next) => {
const userRole = req.user.role;
const roleConfig = ROLES[userRole];
if (!roleConfig) {
return res.status(403).json({ error: 'Role tidak dikenal' });
}
if (!roleConfig.permissions.includes(requiredPermission)) {
return res.status(403).json({
error: 'Akses ditolak',
required: requiredPermission,
yourRole: userRole
});
}
next();
};
}
// Middleware: cek multiple permissions (harus punya semua)
function checkAllPermissions(...permissions) {
return (req, res, next) => {
const roleConfig = ROLES[req.user.role];
const hasAll = permissions.every(p =>
roleConfig.permissions.includes(p)
);
if (!hasAll) {
return res.status(403).json({ error: 'Akses ditolak' });
}
next();
};
}
// Menggunakan middleware
app.get('/api/users', checkPermission('users:read'), getUsers);
app.post('/api/users', checkPermission('users:write'), createUser);
app.delete('/api/users/:id', checkPermission('users:delete'), deleteUser);
app.put('/api/posts/:id', checkPermission('posts:write'), updatePost);
app.post('/api/posts/:id/publish',
checkAllPermissions('posts:write', 'posts:publish'),
publishPost
);
6. Password Hashing
Menyimpan password dalam bentuk plaintext adalah kesalahan fatal. Password harus selalu di-hash menggunakan algoritma yang aman sebelum disimpan ke database. Hashing adalah proses satu arah β tidak bisa dikembalikan ke password asli.
Algoritma Hashing yang Aman
| Algoritma | Keamanan | Rekomendasi |
|---|---|---|
| bcrypt | β Sangat baik β adaptive cost | β Rekomendasi utama untuk web |
| argon2id | β Terbaik β pemenang PHC | β Pilihan modern terbaik |
| scrypt | β Baik β memory-hard | Alternatif yang aman |
| SHA-256 | β Buruk β terlalu cepat | JANGAN gunakan untuk password |
| MD5 | β Sangat buruk β sudah dipecahkan | JANGAN PERNAH gunakan |
const bcrypt = require('bcrypt');
// Konfigurasi
const SALT_ROUNDS = 12; // Semakin tinggi semakin aman, tapi lebih lambat
// Hash password saat registrasi
async function registerUser(email, plainPassword) {
// Validasi password strength
if (plainPassword.length < 8) {
throw new Error('Password minimal 8 karakter');
}
// Hash password
const passwordHash = await bcrypt.hash(plainPassword, SALT_ROUNDS);
// Hasil: $2b$12$LJ3m4ys1Ly...
// Simpan ke database (JANGAN simpan plainPassword!)
const result = await db.query(
'INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id',
[email, passwordHash]
);
return result.rows[0];
}
// Verifikasi password saat login
async function loginUser(email, plainPassword) {
const user = await db.query(
'SELECT id, email, password_hash FROM users WHERE email = $1',
[email]
);
if (!user.rows[0]) {
// Gunakan pesan error yang sama untuk mencegah user enumeration
throw new Error('Email atau password salah');
}
// bcrypt.compare menangani salt secara otomatis
const isValid = await bcrypt.compare(plainPassword, user.rows[0].password_hash);
if (!isValid) {
throw new Error('Email atau password salah');
}
return user.rows[0];
}
// Password strength checker
function checkPasswordStrength(password) {
const checks = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /[0-9]/.test(password),
special: /[!@#$%^&*(),.?":{}|<>]/.test(password),
};
const score = Object.values(checks).filter(Boolean).length;
return {
score, // 0-5
checks,
strong: score >= 4,
message: score >= 4 ? 'Password kuat β
' : 'Password lemah β gunakan kombinasi huruf, angka, simbol'
};
}
- Menyimpan password dalam bentuk plaintext
- Menggunakan MD5 atau SHA-1 untuk hashing password
- Mengirim password melalui URL query parameters
- Menyimpan password di log server atau error messages
- Menggunakan salt yang sama untuk semua password (bcrypt sudah handle ini)
7. Two-Factor Authentication (2FA)
Two-Factor Authentication (2FA) menambahkan lapisan keamanan ekstra dengan meminta dua bentuk verifikasi: sesuatu yang Anda tahu (password) dan sesuatu yang Anda miliki (kode dari aplikasi authenticator atau SMS).
Jenis 2FA
| Jenis | Cara Kerja | Keamanan |
|---|---|---|
| TOTP (Time-based OTP) | Kode 6 digit dari Google Authenticator / Authy | ββββ Sangat baik |
| SMS OTP | Kode dikirim via SMS | ββ Sedang (bisa di-SS7 attack) |
| Email OTP | Kode dikirim via email | ββ Sedang |
| WebAuthn / Passkey | Hardware key atau biometric | βββββ Terbaik |
Implementasi TOTP 2FA
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Setup 2FA untuk user baru
async function setup2FA(userId) {
// Generate secret unik untuk user
const secret = speakeasy.generateSecret({
name: 'BeebaneLabs', // Nama aplikasi
account: 'user@email.com', // Email user
length: 32
});
// Simpan secret di database (encrypted!)
await db.query(
'UPDATE users SET twofa_secret = $1, twofa_enabled = false WHERE id = $2',
[encrypt(secret.base32), userId]
);
// Generate QR code untuk di-scan
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
return {
secret: secret.base32, // Backup: user bisa simpan manual
qrCode: qrCodeUrl, // QR code untuk Google Authenticator
message: 'Scan QR code dengan Google Authenticator'
};
}
// Verifikasi kode 2FA saat login
function verify2FA(secret, token) {
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
window: 1 // Izinkan 1 step tolerance (30 detik)
});
}
// Endpoint: Setup 2FA
app.post('/api/2fa/setup', authenticateToken, async (req, res) => {
const result = await setup2FA(req.user.sub);
res.json(result);
});
// Endpoint: Verifikasi & aktifkan 2FA
app.post('/api/2fa/verify', authenticateToken, async (req, res) => {
const { token } = req.body;
const user = await db.query('SELECT * FROM users WHERE id = $1', [req.user.sub]);
const secret = decrypt(user.rows[0].twofa_secret);
const isValid = verify2FA(secret, token);
if (!isValid) {
return res.status(400).json({ error: 'Kode 2FA tidak valid' });
}
// Aktifkan 2FA
await db.query('UPDATE users SET twofa_enabled = true WHERE id = $1', [req.user.sub]);
// Generate backup codes
const backupCodes = Array.from({ length: 8 }, () =>
speakeasy.generateSecret({ length: 10 }).base32.slice(0, 8)
);
await db.query(
'UPDATE users SET twofa_backup_codes = $1 WHERE id = $2',
[JSON.stringify(backupCodes.map(c => hash(c))), req.user.sub]
);
res.json({
message: '2FA berhasil diaktifkan β
',
backupCodes,
warning: 'Simpan backup codes ini β Anda hanya bisa melihatnya sekali!'
});
});
// Login dengan 2FA
app.post('/api/login-2fa', async (req, res) => {
const { email, password, totpToken } = req.body;
// Step 1: Verifikasi email & password
const user = await loginUser(email, password);
// Step 2: Jika 2FA aktif, verifikasi TOTP
if (user.twofa_enabled) {
if (!totpToken) {
return res.status(200).json({ require2FA: true, message: 'Masukkan kode 2FA' });
}
const secret = decrypt(user.twofa_secret);
if (!verify2FA(secret, totpToken)) {
return res.status(401).json({ error: 'Kode 2FA salah' });
}
}
// Step 3: Generate token
const accessToken = generateAccessToken(user);
res.json({ accessToken, user: { id: user.id, email: user.email } });
});
8. Security Best Practices
Berikut best practices keamanan yang harus diterapkan di setiap aplikasi web production:
HTTP Security Headers
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
// Security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
}
},
hsts: { maxAge: 31536000, includeSubDomains: true },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));
// CORS configuration
app.use(cors({
origin: ['https://beebanelabs.pages.dev', 'http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// Rate limiting β mencegah brute force
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 menit
max: 5, // Maks 5 attempt
message: { error: 'Terlalu banyak percobaan login. Coba lagi dalam 15 menit.' },
standardHeaders: true,
});
app.use('/api/login', loginLimiter);
// General API rate limit
const apiLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 menit
max: 100, // Maks 100 request per menit
});
app.use('/api/', apiLimiter);
- Selalu gunakan HTTPS di production
- Terapkan rate limiting pada endpoint login dan API
- Gunakan httpOnly cookies untuk token sensitif
- Validasi dan sanitize semua input pengguna
- Gunakan CSP headers untuk mencegah XSS
- Implementasikan CSRF protection untuk form
- Enkripsi data sensitif at rest dan in transit
- Lakukan security audit secara berkala
- Log semua aktivitas autentikasi untuk monitoring
- Gunakan dependency scanning (npm audit, Snyk)
9. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Authentication & Authorization: