Autentikasi & JWT — Node.js

Autentikasi memverifikasi identitas user. JWT (JSON Web Token) adalah cara populer untuk autentikasi API. Alur autentikasi: 1. User login dengan email +…

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-api dan 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

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 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

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.

Yang akan kamu pelajari