Autentikasi adalah membuktikan siapa kamu; otorisasi adalah menentukan apa yang boleh kamu lakukan. Bayangin konser. Di gerbang kamu menunjukkan KTP dan tiket (login: email dan password), lalu petugas memasang gelang di tanganmu (token). Setelah itu kamu cukup menunjukkan gelang: petugas tiap area mengecek gelangnya asli dan belum kedaluwarsa. Area VIP mengecek satu hal lagi, jenis gelangnya. Gak punya gelang: "kamu siapa?", itu 401. Gelangnya asli tapi bukan VIP: "kamu dikenal, tapi gak boleh masuk sini", itu 403.
🧪 Tebakan cepat: Budi login di
catatan-apidan dapat token JWT. Dia membuka token itu, mengubah"role":"user"jadi"role":"admin", lalu mengirimnya untuk menghapus catatan. Apa jawaban server? Tebak dulu, jawabannya ada di bagian "JWT: header, payload, signature".
Simpan hash password, bukan password-nya
Database yang bocor gak boleh membocorkan password. Jadi yang disimpan adalah hash: hasil fungsi satu arah yang gak bisa dibalik. Untuk password, pakai fungsi yang sengaja lambat dan memakai salt (tambahan acak per user), seperti bcrypt (npm install bcrypt):
const bcrypt = require("bcrypt");
const hash = await bcrypt.hash("pass123", 10);
console.log(hash); // $2b$10$nFW9NnGRP2xZa25TT767P.S3MJI3ptEVuxVoeJI.thycUJ92dHchi
console.log(await bcrypt.compare("pass123", hash)); // true
console.log(await bcrypt.compare("pass124", hash)); // false
- Hasilnya 60 karakter:
$2b$jenis algoritmanya,10cost-nya, 22 karakter berikutnya salt, sisanya hash. - Panggil
bcrypt.hash("pass123", 10)dua kali, hasilnya beda, karena salt-nya acak.comparemembaca salt dari hash yang tersimpan. - Cost 10 berarti 2^10 putaran: sekitar 70 ms di laptop kami. Cost 12 sekitar 270 ms. Lambat untuk satu login gak terasa, tapi penyerang yang mencoba miliaran tebakan jadi kewalahan.
SHA-256 biasa gak cocok: pass123 selalu jadi hash yang sama, dan GPU bisa menghitung miliaran per detik. Alternatif modern bcrypt adalah Argon2id (paket argon2).
Login: cocokkan, lalu beri tanda pengenal
const users = [
{ id: 1, email: "[email protected]", role: "user", passwordHash: "$2b$10$nFW9NnGRP2xZa25TT767P.S3MJI3ptEVuxVoeJI.thycUJ92dHchi" },
{ id: 2, email: "[email protected]", role: "admin", passwordHash: "$2b$10$ZMcOs6hGaflkr9wqNFSKyuH04ed0dFEu4bXjrwhmUYjSxAOD5VUW2" },
];
app.post("/login", async (req, res) => {
const { email, password } = req.body;
const user = users.find((u) => u.email === email);
const cocok = user ? await bcrypt.compare(password, user.passwordHash) : false;
if (!cocok) return res.status(401).json({ error: "Email atau password salah" });
const token = jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: "1h" });
res.json({ token });
});
Email yang gak terdaftar dan password yang salah dijawab dengan pesan yang sama. Kalau beda, penyerang bisa mengecek email mana yang punya akun.
Session cookie atau JWT
Ada dua cara memberi "gelang":
| Session cookie | JWT | |
|---|---|---|
| Yang dipegang browser | id acak, misalnya sid=7f3a... |
token berisi data yang ditandatangani |
| Dikirim lewat | header Cookie, otomatis oleh browser |
header Authorization: Bearer <token> |
| Server menyimpan | daftar session (memori atau database) | gak ada, cukup kunci rahasianya |
| Logout | hapus session-nya di server, langsung berlaku | token tetap sah sampai exp, jadi buat umurnya pendek |
Cookie session sebaiknya diberi HttpOnly (gak bisa dibaca JavaScript), Secure (cuma lewat HTTPS), dan SameSite=Lax. JWT cocok untuk API yang dipanggil aplikasi mobile atau server lain.
JWT: header, payload, signature
JWT singkatan dari JSON Web Token. Ini token Budi, dibuat jwt.sign dengan kunci contoh contoh-rahasia-yang-panjangnya-32-karakter-lebih:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzkxNDI4NDAwLCJleHAiOjE3OTE0MzIwMDB9.bptuA6-76kBynG1cjXouDy8ZR8vXA1-Ywr0MeCvi8e4
Tiga bagian dipisah titik:
- Header
{"alg":"HS256","typ":"JWT"}: algoritma tanda tangannya. - Payload
{"userId":1,"role":"user","iat":1791428400,"exp":1791432000}: datanya.iatwaktu dibuat,expwaktu kedaluwarsa (1 jam kemudian), dalam detik. - Signature: HMAC-SHA256 dari
header.payloaddengan kunci rahasia server.
Header dan payload cuma di-encode base64url, bukan dienkripsi. Siapa pun bisa membacanya, jadi jangan taruh password di situ. Yang melindungi token adalah signature-nya: tanpa kunci rahasia, gak ada yang bisa membuat signature yang cocok. Saat Budi mengganti role jadi admin, payload-nya berubah, signature lamanya gak cocok lagi, dan jwt.verify melempar JsonWebTokenError: invalid signature. Server menjawab 401 {"error":"Token tidak valid"}. Itu jawaban tebakannya.
jwt.sign(payload, rahasia) membuat token; jwt.verify(token, rahasia) mengecek signature dan exp, lalu mengembalikan payload-nya. Token kedaluwarsa memberi TokenExpiredError: jwt expired.
Middleware auth: 401 atau 403
function wajibLogin(req, res, next) {
const [jenis, token] = (req.headers.authorization || "").split(" ");
if (jenis !== "Bearer" || !token) {
return res.status(401).json({ error: "Butuh login" });
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
res.status(401).json({ error: "Token tidak valid" });
}
}
function wajibAdmin(req, res, next) {
if (req.user.role !== "admin") {
return res.status(403).json({ error: "Khusus admin" });
}
next();
}
app.get("/catatan", wajibLogin, (req, res) => res.json(catatan));
app.delete("/catatan/:id", wajibLogin, wajibAdmin, (req, res) => {
catatan = catatan.filter((c) => c.id !== Number(req.params.id));
res.status(204).end();
});
| Request | Jawaban |
|---|---|
GET /catatan tanpa header |
401 {"error":"Butuh login"} |
GET /catatan dengan token Budi |
200, daftar catatan |
DELETE /catatan/2 dengan token Budi |
403 {"error":"Khusus admin"} |
DELETE /catatan/2 dengan token Siti |
204, terhapus |
DELETE /catatan/1 dengan token Budi yang diubah jadi admin |
401 {"error":"Token tidak valid"} |
Header-nya berbentuk Bearer <token>, jadi .split(" ") memisahkan kata Bearer dari token-nya. jwt.verify(req.headers.authorization, ...) tanpa memotongnya gagal dengan JsonWebTokenError: invalid token.
Di latihan: login sederhana
Latihannya fungsi biasa, tanpa bcrypt dan JWT sungguhan, supaya alurnya kelihatan. users berisi Budi ([email protected], pass123) dan Siti ([email protected], pass456). generateToken(userId) mengembalikan "token-" + userId. login(email, password) mencari user yang email dan password-nya cocok dengan users.find(...), lalu mengembalikan { success: true, token: generateToken(user.id) }, atau { success: false, error: "Email atau password salah" } tanpa token. Password teks biasa di sini cuma untuk latihan; aplikasi sungguhan menyimpan hash bcrypt.
⚠️ Jebakan yang sering ketemu
- Menyimpan password asli, atau hash cepat seperti MD5 dan SHA-256. Pakai bcrypt atau Argon2id.
- Menaruh data rahasia di payload JWT. Payload bisa dibaca siapa pun yang memegang token-nya.
- Lupa memotong
Bearer.jwt.verifymenerima"Bearer eyJ..."dan gagal. - Kunci rahasia pendek atau ditulis di kode. Simpan di environment variable (
JWT_SECRET), minimal 32 karakter acak. Kunci yang bocor berarti siapa pun bisa membuat token admin. - Tertukar 401 dan 403. 401: belum terbukti siapa kamu. 403: sudah dikenal, tapi gak punya izin.
TL;DR: Autentikasi membuktikan siapa kamu, otorisasi menentukan apa yang boleh. Simpan password sebagai hash bcrypt (bcrypt.hash(password, 10), cek dengan bcrypt.compare), bukan teks asli atau SHA-256. Setelah login, beri tanda pengenal: session cookie (id acak, server menyimpan session-nya) atau JWT (JSON Web Token: header, payload, signature HMAC-SHA256). Payload JWT bisa dibaca siapa pun; mengubahnya merusak signature, jadi jwt.verify menolaknya. jwt.sign membuat token, jwt.verify mengeceknya. Middleware membaca Authorization: Bearer <token>, memotong Bearer dengan .split(" ")[1], menjawab 401 kalau token gak ada atau gak valid, dan 403 kalau user-nya dikenal tapi gak punya izin.