Membangun autentikasi dan otorisasi: hashing password dengan bcrypt, JWT dan session cookie, middleware protect untuk route terproteksi, serta RBAC berbasis peran untuk authorization.

Setelah di episode 9-10 kalian punya data dan di episode 11 halaman, episode 12 menjawab pertanyaan keamanan pertama: siapa kalian (authentication) dan apa yang boleh kalian lakukan (authorization). Dua konsep ini sering dicampur, padahal berbeda: autentikasi membuktikan identitas, otorisasi menentukan hak.
Mengapa ini bagian terpenting? Karena autentikasi yang salah adalah celah yang memicu sebagian besar kebocoran data. Di episode ini kita membangun hashing password yang benar, dua mekanisme sesi (JWT dan cookie), middleware protect, dan RBAC — pola yang sama dipakai aplikasi produksi nyata.
Menyimpan password mentah berarti satu kebocoran database = semua akun terbongkar. Yang disimpan adalah hash — hasil fungsi satu arah yang tidak bisa dibalik. bcrypt adalah pilihan standar karena punya salt otomatis dan cost factor yang bisa dinaikkan seiring waktu:
npm install bcryptimport bcrypt from "bcrypt"
const SALT_ROUNDS = 12
export async function hashPassword(plain) {
return bcrypt.hash(plain, SALT_ROUNDS)
}
export async function verifyPassword(plain, hashed) {
return bcrypt.compare(plain, hashed)
}SALT_ROUNDS = 12 menentukan beban komputasi hash — di tahun 2026, 12 adalah titik seimbang antara keamanan dan kecepatan. Jangan memakai round di bawah 10 untuk kredensial pengguna.
Important
Password tidak pernah boleh masuk ke schema User yang di-return ke client. Saat mengirim user, selalu seleksi field — contoh: { id, name, email, role, createdAt } — tanpa password. Kebocoran hash pun tetap berbahaya, apalagi hash yang berisi password.
JWT adalah token yang ditandatangani server; client menyimpannya dan mengirimnya di header Authorization. Stateless — server tidak perlu menyimpan sesi:
npm install jsonwebtokenimport jwt from "jsonwebtoken"
const JWT_SECRET = process.env.JWT_SECRET
export function signToken(payload) {
return jwt.sign(payload, JWT_SECRET, { expiresIn: "15m" })
}
export function verifyToken(token) {
return jwt.verify(token, JWT_SECRET)
}expiresIn: "15m" membuat akses token berumur pendek — pola standar: akses token singkat, refresh token panjang (tidak dibahas penuh di series ini, tapi konsepnya sama).
Kebalikan dari JWT: data sesi disimpan di server, client hanya membawa identifier acak di cookie HttpOnly:
npm install express-sessionimport session from "express-session"
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
maxAge: 24 * 60 * 60 * 1000,
},
})
)httpOnly: true mencegah JavaScript client membaca cookie (perlindungan XSS), secure: true hanya mengirim lewat HTTPS. Session store default menyimpan di memori — untuk produksi ganti ke store Redis (episode 21-22) agar sesi bertahan lintas instance.
Endpoint login memverifikasi kredensial dan menerbitkan token:
import { verifyPassword } from "../utils/password.js"
import { signToken } from "../utils/jwt.js"
export const login = async (req, res) => {
const { email, password } = req.body || {}
const user = await User.findOne({ email })
if (!user) {
throw new AppError(401, "INVALID_CREDENTIALS", "Email atau password salah")
}
const ok = await verifyPassword(password, user.password)
if (!ok) {
throw new AppError(401, "INVALID_CREDENTIALS", "Email atau password salah")
}
const token = signToken({ sub: user.id, role: user.role })
res.json({ token, user: { id: user.id, email: user.email, role: user.role } })
}Pesan error yang sama untuk "user tidak ada" dan "password salah" mencegah penyerang memetakan email yang terdaftar — kebocoran informasi kecil yang sering dilupakan.
Middleware protect memvalidasi token dan menempelkan user ke req — pola yang sama sekali lagi kita lihat di episode 5:
import { verifyToken } from "../utils/jwt.js"
export const protect = async (req, res, next) => {
const header = req.get("Authorization") || ""
if (!header.startsWith("Bearer ")) {
throw new AppError(401, "UNAUTHORIZED", "Token tidak ada")
}
const token = header.slice(7)
try {
const payload = verifyToken(token)
req.user = { id: payload.sub, role: payload.role }
next()
} catch {
throw new AppError(401, "INVALID_TOKEN", "Token tidak valid atau kedaluwarsa")
}
}Route dilindungi tinggal memasang middleware di posisi kedua:
router.get("/me", protect, (req, res) => {
res.json({ user: req.user })
})req.user yang diisi middleware bisa dibaca handler di bawahnya — pola memperkaya req yang kita kenalkan sejak episode 5 kini membuahkan hasil.
RBAC (Role-Based Access Control) membatasi aksi berdasarkan peran. Middleware requireRole dibangun sebagai factory (pola episode 5):
export const requireRole = (...roles) => {
return (req, res, next) => {
if (!req.user) {
throw new AppError(401, "UNAUTHORIZED", "Silakan login dulu")
}
if (!roles.includes(req.user.role)) {
throw new AppError(403, "FORBIDDEN", "Akses ditolak untuk peran ini")
}
next()
}
}Pemakaian berantai dengan protect:
router.delete("/users/:id", protect, requireRole("admin"), deleteUser)
router.get("/users/:id", protect, (req, res) => res.json({ ok: true }))protect.requireRole("admin").const ROLES = {
admin: ["read:all", "write:all", "delete:all"],
editor: ["read:all", "write:own"],
viewer: ["read:own"],
}
export const can = (role, permission) => ROLES[role]?.includes(permission)Pendekatan permission-based ini lebih fleksibel daripada hardcode nama peran di tiap route — kita pakai pola ini sebagai bahan diskusi di episode 13 dan 23.
Selalu eksklusi password dari response. Salah satu cara: method toSafeJSON() di model, atau seleksi field eksplisit di tiap query.
JWT_SECRET harus random dan panjang (minimal 32 byte). Jangan pernah hardcode di source atau commit — dikelola lewat env (episode 16).
express-session default memakai MemoryStore — hilang saat server restart dan tidak bisa dipakai lintas instance. Untuk produksi, gunakan Redis store (episode 22).
Warning
Mengirim JWT di body response untuk disimpan localStorage rentan XSS. Alternatif yang lebih aman: cookie HttpOnly dengan secure. Pilihannya bergantung arsitektur, tetapi token di localStorage + aplikasi dengan XSS = akun bisa dibajak. Kita perkuat lapisan ini di episode 18.
Episode 12 membangun fondasi keamanan identitas: hashing bcrypt dengan salt, dua mekanisme sesi (JWT stateless vs session cookie), middleware protect yang mengisi req.user, dan RBAC berbasis peran dengan status 401/403 yang tepat.
Inti yang harus dibawa pulang:
protect memvalidasi token dan mengisi req.user.requireRole(...roles) membatasi aksi; 403 = peran salah, 401 = belum login.password ke client dalam bentuk apa pun.Di episode 13 selanjutnya kita akan memvalidasi input: validation & sanitization — zod dan express-validator, sanitasi input, dan pemetaan error validasi ke response 400 yang konsisten. Sampai jumpa di episode 13!