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 }
token.split(".")memecah token jadi tiga bagian.replacemengubah-jadi+dan_jadi/, laluatobmembaca base64-nya.TextDecodermengubah byte jadi teks UTF-8, laluJSON.parsejadi object.
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 };
}
- Cek panjang secret dulu: server menolak jalan dengan secret pendek.
header.alg !== "HS256": algoritma ditentukan server, bukan dibaca dari token. Cuma HS256 yang diterima.crypto.subtle.verifymenghitung ulang signature darih.pdan membandingkannya dengan aman.now >= payload.exp: token gak boleh dipakai pada atau setelah waktuexp. Token tanpaexpjuga ditolak.
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
{"alg":"none","typ":"JWT"}: spesifikasi JWT mengenalalg: none, artinya token tanpa signature (bagian ketiganya kosong). Verifier yang membaca algoritma dari header akan "memeriksa" dengan algoritma none, yaitu tanpa memeriksa apa pun, lalu menerima payload palsu. Verifier di atas menolaknya karena cuma HS256 yang diizinkan.- Secret lemah: secret seperti
"secret123"(9 byte) bisa ditebak dengan mencoba daftar kata satu per satu. Siapa pun yang tahu secret-nya bisa membuat token apa saja. Pakai 32 byte acak, misalnyacrypto.randomBytes(32).toString("hex")di Node (64 karakter hex), simpan di environment variable, dan jangan pernah taruh di kode.
⚠️ Jebakan yang sering ketemu
- Data rahasia di payload. Siapa pun yang memegang token bisa membacanya dengan
atob. - Token tanpa
exp, atauexpberhari-hari. Token yang bocor berlaku selama itu. Pakai access token pendek (misalnya 15 menit) dan refresh token untuk minta yang baru. - Logout dianggap mencabut token. Server gak menyimpan daftar token, jadi token tetap valid sampai
exp. Kalau butuh cabut seketika, pakai daftar token yang diblokir (denylist) atau session biasa. - Menyimpan token di
localStorage. Script XSS bisa membacanya. Simpan di cookieHttpOnlykalau bisa.
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.