Session menempatkan seluruh status login di server — tepat sampai aplikasi kita tumbuh menjadi beberapa server yang berdiri di belakang load balancer, dan session store tiba-tiba harus dibagi atau direplikasi. JSON Web Token (JWT) membalik arsitektur ini: identitas pengguna dituliskan langsung di dalam token, lalu token itu ditandatangani server dengan kunci rahasia. Server penerima cukup memeriksa tanda tangan untuk mempercayai isi token, tanpa mencari-cari apa pun di penyimpanan. Pada chapter ini kita membangun JWT versi mini dari nol memakai node:crypto — bukan untuk dipakai di produksi, melainkan agar mekanismenya tidak lagi terasa seperti kotak tertutup.
Tiga Bagian yang Dipisah Titik #
JWT berbentuk string header.payload.signature. Ketiganya dikodekan base64url — variasi base64 yang mengganti + menjadi - dan / menjadi _ agar aman berada di dalam URL dan header HTTP. Isinya bukan rahasia: siapa pun bisa mendekode dan membacanya. Tanda tangan di bagian ketiga berperan sebagai segel keaslian, bukan penyembunyi isi.
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJidWRpIiwicGVyYW4iOiJlZGl0b3IiLCJleHAiOjE3OTg3NjE2MDB9.IDhqpJRzq6UZap6H1ldWSQtmAPOS3OjNoAsLvcN_lBM';
const [header, payload] = token.split('.');
console.log('header :', Buffer.from(header, 'base64url').toString('utf8'));
console.log('payload :', Buffer.from(payload, 'base64url').toString('utf8'));Output yang Dihasilkan:
header : {"alg":"HS256","typ":"JWT"}
payload : {"sub":"budi","peran":"editor","exp":1798761600}Bagian header menyatakan algoritma (HS256 berarti HMAC dengan SHA-256), sedangkan payload membawa claims — pasangan kunci-nilai tentang subjek token. Dua klaim yang hampir selalu ada: sub (subject, identitas pemilik token) dan exp (expiration, batas waktu berlakunya dalam format Unix time detik).
Menandatangani dengan HMAC-SHA256 #
Tanda tangan dihitung dari gabungan header.payload dengan kunci rahasia yang hanya diketahui server. crypto.createHmac('sha256', kunci) membuat objek penghitung HMAC; hasil digest()-nya dikodekan base64url lalu menjadi bagian ketiga. Verifikasi melakukan hal yang sama di arah sebaliknya: hitung ulang tanda tangan dari header.payload yang diterima, bandingkan dengan bagian ketiga memakai timingSafeEqual — teknik yang sama dengan verifikasi hash sandi pada Chapter Password Hashing dengan scrypt.
Bila seseorang mengubah isi payload (misalnya menaikkan peran dari editor ke admin) tanpa memiliki kunci, tanda tangan hasil hitungan ulang tidak akan cocok, dan token ditolak. Kekuatan skema ini sepenuhnya bergantung pada kerahasiaan kunci: kunci yang bocor membuat siapa pun bisa menerbitkan token sah palsu.
Satu Berkas: Terbitkan, Verifikasi, Palsukan #
Program berikut menerbitkan token untuk Budi, memverifikasi token asli, lalu mencoba dua jenis penyerangan: mengganti klaim tanpa menandatangani ulang, dan memakai token yang tanda tangannya sah tetapi sudah kedaluwarsa. Waktu “sekarang” dibekukan agar hasil contoh dapat dibandingkan antar pembaca — di produksi nilainya Math.floor(Date.now() / 1000).
import { createHmac, timingSafeEqual } from 'node:crypto';
// Kunci demo pendek supaya contoh mudah dibaca.
// Produksi: kunci panjang dan acak dari environment variable (mis. AUTH_SECRET).
const RAHASIA = 'kunci-demo-belajar-jwt';
// Waktu "sekarang" dibekukan agar output contoh bisa direproduksi.
// Produksi: Math.floor(Date.now() / 1000)
const WAKTU_DEMO = 1767225600; // = 2026-01-01T00:00:00Z
function base64url(data) {
return Buffer.from(data).toString('base64url');
}
function tandaTangan(header, payload) {
const hmac = createHmac('sha256', RAHASIA);
hmac.update(`${header}.${payload}`);
return base64url(hmac.digest());
}
function buatJwt(claims) {
const header = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
const payload = base64url(JSON.stringify(claims));
return `${header}.${payload}.${tandaTangan(header, payload)}`;
}
function verifikasiJwt(token) {
const bagian = token.split('.');
if (bagian.length !== 3) {
return { valid: false, alasan: 'struktur harus 3 bagian' };
}
const [header, payload, tanda] = bagian;
const tandaBaru = tandaTangan(header, payload);
const a = Buffer.from(tanda);
const b = Buffer.from(tandaBaru);
const cocok = a.length === b.length && timingSafeEqual(a, b);
if (!cocok) {
return { valid: false, alasan: 'tanda tangan tidak cocok' };
}
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
if (claims.exp <= WAKTU_DEMO) {
return { valid: false, alasan: 'token kedaluwarsa' };
}
return { valid: true, claims };
}
const token = buatJwt({ sub: 'budi', peran: 'editor', exp: 1798761600 });
console.log('JWT :', token);
console.log('Bagian :', token.split('.').length);
const hasil = verifikasiJwt(token);
console.log('Asli : valid =', hasil.valid, '| claims =', JSON.stringify(hasil.claims));
// Pemalsuan: ganti klaim peran tanpa menandatangani ulang token.
const [headerAsli, , tandaAsli] = token.split('.');
const payloadPalsu = base64url(JSON.stringify({ sub: 'budi', peran: 'admin', exp: 1798761600 }));
const tokenPalsu = `${headerAsli}.${payloadPalsu}.${tandaAsli}`;
const hasilPalsu = verifikasiJwt(tokenPalsu);
console.log('Palsu : valid =', hasilPalsu.valid, '| alasan =', hasilPalsu.alasan);
// Tanda tangan valid tetapi klaim exp sudah lewat: tetap ditolak.
const tokenLama = buatJwt({ sub: 'budi', peran: 'editor', exp: WAKTU_DEMO });
const hasilLama = verifikasiJwt(tokenLama);
console.log('Kedaluwarsa : valid =', hasilLama.valid, '| alasan =', hasilLama.alasan);Output yang Dihasilkan:
JWT : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJidWRpIiwicGVyYW4iOiJlZGl0b3IiLCJleHAiOjE3OTg3NjE2MDB9.IDhqpJRzq6UZap6H1ldWSQtmAPOS3OjNoAsLvcN_lBM
Bagian : 3
Asli : valid = true | claims = {"sub":"budi","peran":"editor","exp":1798761600}
Palsu : valid = false | alasan = tanda tangan tidak cocok
Kedaluwarsa : valid = false | alasan = token kedaluwarsaMari kita bedah baris-per-baris bagian intinya:
- Baris 15-19 (
tandaTangan): HMAC dihitung atas string${header}.${payload}— tanda tangan melindungi kedua bagian pertama sekaligus.digest()menghasilkanBufferyang lalu dikodekan base64url. - Baris 21-25 (
buatJwt): header dibangun dengan klaim wajibalgdantyp, payload berisi claims aplikasi. Urutan ketiga bagian diikat dengan titik. - Baris 27-45 (
verifikasiJwt): pemeriksaan berlapis — struktur tiga bagian, kecocokan tanda tangan, lalu klaimexp. Baris 36 memakaitimingSafeEqualdengan pemeriksaan panjang lebih dulu, persis pola verifikasi sandi. - Baris 54-59 (pemalsuan): payload baru diketik dengan peran
admin, tetapi tanda tangan lama dipakai ulang. Karenaheader.payloadsudah berubah, HMAC hasil hitungan ulang berbeda — token ditolak meski strukturnya sempurna. - Baris 61-64 (kedaluwarsa): token ditandatangani dengan kunci yang sah, namun
exp-nya sama dengan waktu pemeriksaan sehingga lolos pemeriksaan tanda tangan lalu tertahan di pemeriksaan klaim. Masa berlaku pendek adalah kompensasi utama dari sulitnya mencabut token.
Konsekuensi Tanpa Penyimpanan #
Sifat stateless JWT adalah pedang bermata dua, dan tabel ini merangkum dua sisinya:
| Aspek | Dampak |
|---|---|
| Verifikasi | Tanpa akses database atau Redis — cukup kunci rahasia yang sama di semua server |
| Penskalaan | Server baru cukup membawa kunci; tidak ada session store yang harus direplikasi |
| Pencabutan segera | Sulit — token tetap sah sampai kedaluwarsa; solusi umumnya daftar tolak (denylist) atau umur token pendek |
| Ukuran permintaan | Setiap permintaan membawa seluruh claims, jadi token membesar seiring banyaknya klaim |
Di sisi klien, token umumnya disimpan di localStorage (mudah dipakai, tetapi terbaca oleh JavaScript sehingga rentan XSS) atau di cookie HttpOnly (terlindung dari JavaScript, tetapi memerlukan penanganan CSRF — kembali ke pembahasan Chapter Session dan Cookie). Tidak ada pilihan bebas risiko; pilihan yang tepat bergantung pada ancaman yang paling relevan bagi aplikasi kita.
Token yang bisa diverifikasi sendiri ini tinggal satu langkah lagi menjadi sistem yang utuh: sebuah lapisan pemeriksa yang berdiri di depan setiap endpoint. Chapter Middleware Autentikasi membangun lapisan itu di atas pola header Authorization: Bearer.
Pengenalan resmi format JWT lengkap dengan contoh tiap klaim tersedia di jwt.io/introduction
; rincian createHmac ada di dokumentasi crypto Node.js
.