JWT Security — Security

JSON Web Token (JWT) JWT adalah token berbasis JSON yang sering digunakan untuk autentikasi API. JWT terdiri dari tiga bagian yang di-encode Base64: Header…

JWT (JSON Web Token) adalah token login berbentuk teks yang membawa data user dan sebuah tanda tangan dari server. Bayangin surat keterangan dari kantor kelurahan: isinya bisa dibaca siapa pun yang memegangnya, tapi cap stempelnya cuma bisa dibuat kantor itu. Kalau satu kata di suratnya diganti, capnya gak cocok lagi dan suratnya ditolak. Di JWT, isi surat itu payload, capnya signature, dan stempelnya secret yang cuma disimpan server.

🧪 Tebakan cepat: Rina punya token dengan "role": "user". Dia mengubah payload-nya jadi "role": "admin", lalu mengirim token itu lagi ke server. Server menerima atau menolak? Jawabannya ada di bagian "Signature: cap yang ikut berubah".

Tiga bagian JWT

JWT terdiri dari tiga bagian yang dipisahkan titik: header, payload, dan signature. Ini token milik Rina dari Lab, persis:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJSaW5hIiwicm9sZSI6InVzZXIiLCJpYXQiOjE3NjcyMjU2MDAsImV4cCI6MTc2NzIyNjUwMH0.ctar5r1m2MZbZEFi6gdjLTyKDInvOPTFZlGknQ0YieU
Bagian Isinya setelah di-decode Gunanya
Header (pertama) {"alg":"HS256","typ":"JWT"} algoritma tanda tangan dan jenis token
Payload (kedua) {"sub":"42","name":"Rina","role":"user","iat":1767225600,"exp":1767226500} data, disebut claims
Signature (ketiga) ctar5r1m2MZbZEFi6gdjLTyKDInvOPTFZlGknQ0YieU bukti bahwa header dan payload gak diubah

Claim yang sering dipakai: sub (subject, id user), iat (issued at, kapan token dibuat), dan exp (expiration, kapan token kedaluwarsa). Waktunya ditulis sebagai Unix timestamp, yaitu jumlah detik sejak 1 Januari 1970. Di token ini iat 1767225600 = 1 Januari 2026 pukul 00.00 UTC, dan exp 900 detik sesudahnya, jadi token berlaku 15 menit.

Payload bisa dibaca siapa pun

Header dan payload cuma di-encode dengan base64url: base64 yang memakai - dan _ supaya aman ditaruh di URL. Encode itu cuma ganti bentuk, bukan enkripsi. Gak perlu secret untuk membacanya:

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJSaW5hIiwicm9sZSI6InVzZXIiLCJpYXQiOjE3NjcyMjU2MDAsImV4cCI6MTc2NzIyNjUwMH0.ctar5r1m2MZbZEFi6gdjLTyKDInvOPTFZlGknQ0YieU";

function decodePart(part) {
  const b64 = part.replace(/-/g, "+").replace(/_/g, "/"); // base64url jadi base64 biasa
  const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
  return JSON.parse(new TextDecoder().decode(bytes));
}

const [header, payload] = token.split(".");
console.log(decodePart(header));  // { alg: "HS256", typ: "JWT" }
console.log(decodePart(payload)); // { sub: "42", name: "Rina", role: "user", iat: 1767225600, exp: 1767226500 }

Jadi jangan pernah taruh password, nomor KTP, atau data rahasia lain di payload. Signature menjamin data gak diubah, bukan merahasiakannya.

Signature: cap yang ikut berubah

Dengan alg HS256, server membuat signature begini: HMAC-SHA256 dari teks header.payload, memakai secret. HMAC adalah cara menghitung "sidik jari" dari sebuah teks dengan sebuah kunci. Teksnya berubah satu huruf saja, sidik jarinya berubah total, dan tanpa kunci yang sama gak ada yang bisa menghitung sidik jari yang benar.

Waktu Rina mengganti "user" jadi "admin", payload-nya berubah, tapi signature-nya masih yang lama, ctar5r1m.... Server menghitung ulang dari payload baru dan mendapat WjEImYyq.... Beda, jadi server menolak dengan 401. Itu jawaban tebakannya. Rina gak bisa membuat signature baru karena dia gak punya secret.

Verifier yang benar

Verifier adalah kode server yang memeriksa token sebelum percaya isinya. Ini versi yang dipakai Lab, dengan Web Crypto yang ada di browser dan Node:

const enc = new TextEncoder();
const bytesOf = (part) => Uint8Array.from(atob(part.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
const jsonOf = (part) => JSON.parse(new TextDecoder().decode(bytesOf(part)));

async function verifyJwt(token, secret, now) {
  if (enc.encode(secret).length < 32) throw new Error("JWT secret terlalu pendek, minimal 32 byte");
  const [h, p, s] = token.split(".");
  const header = jsonOf(h);
  if (header.alg !== "HS256") return { ok: false, error: `alg ${header.alg} gak diizinkan` };

  const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
  const valid = await crypto.subtle.verify("HMAC", key, bytesOf(s), enc.encode(`${h}.${p}`));
  if (!valid) return { ok: false, error: "signature gak cocok" };

  const payload = jsonOf(p);
  if (typeof payload.exp !== "number" || now >= payload.exp) return { ok: false, error: "token kedaluwarsa" };
  return { ok: true, payload };
}

Di Node, library jsonwebtoken melakukan hal yang sama lewat jwt.verify(token, secret, { algorithms: ["HS256"] }). Opsi algorithms itu wajib kamu isi.

Dua jebakan klasik: alg none dan secret lemah

⚠️ Jebakan yang sering ketemu

TL;DR: JWT = header.payload.signature. Bagian pertama header (alg, typ), kedua payload (claims seperti sub, iat, exp), ketiga signature. Header dan payload cuma base64url, jadi bisa dibaca siapa pun: jangan isi data rahasia. Signature HS256 adalah HMAC-SHA256 dari header.payload dengan secret, jadi payload yang diubah membuat signature-nya gak cocok dan server menolak. Verifier yang benar menentukan algoritmanya sendiri (cuma HS256, alg: none ditolak), memeriksa signature, dan menolak token pada atau setelah exp. Secret minimal 32 byte acak, dan access token sebaiknya berumur pendek.

Yang akan kamu pelajari