Logo
HOMEABOUTPROJECTSBLOGCONTACT

Luthfi A. Pratama

Enterprise integration specialist building scalable, mission-critical solutions for modern businesses.

LINKS

  • About
  • Projects
  • Blog
  • Contact

CONNECT

GitHubLinkedInEmail

© 2026 Luthfi A. Pratama. All rights reserved.

Designed and built with passion.

Back to all articles
Security

JWT Security: Kenapa Token yang Diutak-atik Pasti Ketahuan

Catatan pembelajaran tentang keamanan JWT - gimana server bisa tahu kalau token diubah, kelebihan & kekurangan JWT, penerapan di microservice, dan konsep phantom token.

Luthfi A. Pratama

Luthfi A. Pratama

Software Engineer

2026-03-07
12 min read
JWT Security: Kenapa Token yang Diutak-atik Pasti Ketahuan

Apa itu JWT?

JWT (JSON Web Token) itu standar terbuka (RFC 7519) buat ngirim informasi antar pihak secara aman dalam bentuk JSON. JWT paling sering dipakai buat authentication dan authorization.

Bentuknya kalau dilihat kayak string random panjang yang dipisah titik:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikx1dGhmaSIsInJvbGUiOiJ1c2VyIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Kelihatan kayak encrypted? Sebenernya engga. JWT itu cuma di-encode, bukan di-encrypt. Bedanya jauh.

Struktur JWT: Header, Payload, Signature

JWT terdiri dari tiga bagian, dipisah titik (.):

HEADER.PAYLOAD.SIGNATURE

1. Header

Header berisi info tentang tipe token dan algoritma yang dipakai buat bikin signature.

{
  "alg": "HS256",
  "typ": "JWT"
}

Ini di-encode pakai Base64URL jadi: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. Payload

Payload berisi claims - data yang kita mau kirim. Misalnya:

{
  "sub": "1234567890",
  "name": "Luthfi",
  "role": "user",
  "iat": 1709784000,
  "exp": 1709787600
}

Juga di-encode pakai Base64URL. Bukan encrypted. Siapa aja bisa decode ini dan baca isinya.

Coba aja buka jwt.io, paste token di atas, langsung keliatan isinya.

3. Signature

Nah, ini bagian kunci keamanannya.

Signature dibuat dengan cara:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret_key
)

secret_key ini cuma server yang tahu. Client ga punya, ga bisa lihat, ga bisa nebak.

Kenapa User Ga Bisa Ngubah Isi JWT?

Ini pertanyaan yang bagus dan sering bikin bingung. Karena kalau JWT cuma di-encode (bukan encrypted), berarti siapa aja bisa decode → ubah isinya → encode ulang dong?

Bisa. Tapi sia-sia.

Skenario: user jahat mau upgrade role

Katakanlah user "Budi" dapet JWT setelah login:

{
  "sub": "123",
  "name": "Budi",
  "role": "user"
}

Budi pengen jadi admin. Dia decode payload, ubah "role": "user" jadi "role": "admin", encode ulang pakai Base64URL.

Sekarang dia punya header dan payload baru. Tapi dia ga bisa bikin signature baru yang valid. Kenapa?

Karena signature itu dihitung dari:

HMACSHA256(header + "." + payload, SECRET_KEY)

Budi ga punya SECRET_KEY. Cuma server yang punya.

Jadi yang terjadi:

  1. Budi kirim JWT dengan payload yang diubah
  2. Server terima JWT
  3. Server ambil header + payload dari JWT yang diterima
  4. Server hitung ulang signature pakai SECRET_KEY miliknya
  5. Server bandingin signature yang dia hitung vs signature di JWT
  6. Ga cocok → Token invalid → Request ditolak
Signature dari Budi:    SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Signature yang dihitung: Xk9a2PQ7vN3Rt5YmBw1FgH8jK0LzUe4Wd6Ax9Cy3Ds
(Beda! Token ditolak.)

Analogi simpel

Bayangin JWT itu kayak surat resmi dari kantor pemerintah. Isinya bisa dibaca siapa aja (ga dikunci). Tapi di bawah ada stempel resmi yang cuma kantor itu yang punya.

Kamu bisa fotokopi surat, ubah isinya. Tapi kamu ga bisa bikin stempel yang sama. Pas dicek, stempelnya ga cocok = surat palsu.

Signature JWT = stempel itu.

Dua Jenis Algoritma Signing

Symmetric: HMAC (HS256, HS384, HS512)

Satu kunci untuk sign DAN verify.
  • Server sign pakai SECRET_KEY
  • Server verify juga pakai SECRET_KEY yang sama
  • Cocok kalau cuma satu server yang handle semuanya
  • Masalah: kalau banyak service perlu verify, semua harus punya SECRET_KEY. Satu bocor = semua bocor

Asymmetric: RSA / ECDSA (RS256, ES256, dll)

Dua kunci: Private Key untuk sign, Public Key untuk verify.
  • Auth server sign pakai Private Key (rahasia, cuma auth server yang punya)
  • Service lain verify pakai Public Key (boleh dibagi ke siapa aja)
  • Public key bocor? Ga masalah. Ga bisa dipake buat sign token baru
  • Ideal untuk microservice architecture

Kelebihan JWT

1. Stateless — ga perlu nyimpan session di server

Ini keunggulan utama JWT dibanding session-based auth.

Session based: user login → server bikin session → simpan di database/Redis → kasih session ID ke client. Setiap request, server lookup session ID di database.

JWT: user login → server bikin token → kasih ke client. Setiap request, server cuma verify signature. Ga perlu lookup ke mana-mana.

graph LR
    subgraph Session-Based
        A1[Request] --> B1[Lookup DB/Redis] --> C1[Response]
    end
    subgraph JWT
        A2[Request] --> B2[Verify Signature - CPU only] --> C2[Response]
    end

Hasilnya? Lebih cepat, lebih scalable.

2. Self-contained — semua info ada di token

Token JWT udah berisi semua informasi yang dibutuhkan. User ID, role, permissions, expiry — semuanya ada di payload.

Server ga perlu query database buat tahu siapa user ini dan boleh ngapain.

3. Cross-domain / Cross-service friendly

JWT bisa dipake lintas domain dan lintas service. Tinggal taro di header Authorization: Bearer <token>.

Ga kayak cookie yang ribet sama domain restriction dan CORS.

4. Scalable secara horizontal

Mau nambah 10 server? Tinggal kasih semua server kunci yang sama (symmetric) atau public key yang sama (asymmetric).

Ga perlu shared session store. Ga perlu sticky sessions. Ga perlu sinkronisasi antar server.

Kekurangan JWT

1. Ga bisa di-revoke dengan gampang

Ini kelemahan terbesar JWT.

Session based: mau kick user? Hapus session dari database. Selesai. Request berikutnya langsung ditolak.

JWT: token udah di tangan client. Server ga nyimpan apa-apa. Gimana caranya invalidate token yang udah beredar?

Ga bisa. Selama token belum expired, token itu valid.

Solusi yang biasa dipake:

  • Blacklist: simpan daftar token yang di-revoke di database/Redis. Tapi ini ngalahin purpose JWT yang stateless
  • Short expiry + refresh token: bikin access token yang umurnya pendek (misal 15 menit), dan pakai refresh token buat minta yang baru
  • Token versioning: simpan versi token di database, kalau mau revoke tinggal naikin versi

Semua solusi di atas nambah complexity. Trade-off.

2. Token size bisa gede

JWT itu self-contained, jadi makin banyak data di payload, makin gede tokennya.

Session ID biasanya cuma ~32 bytes. JWT bisa ratusan bytes, bahkan kilobytes.

Ini dikirim di setiap request. Kalau mobile app dengan koneksi lambat, ini bisa kerasa.

3. Payload bisa dibaca siapa aja

Ingat, JWT cuma di-encode, bukan di-encrypt. Siapa aja bisa decode dan baca isinya.

Jangan pernah taro data sensitif di JWT payload. Jangan taro password, nomor kartu kredit, atau data personal yang sensitif.

Kalau emang harus, pakai JWE (JSON Web Encryption). Tapi ini nambah complexity lagi.

4. Rentan kalau implementasi salah

  • alg: "none" attack — beberapa library lama terima token tanpa signature kalau header bilang alg none
  • Secret key yang lemah — kalau pakai HS256 dan secret key-nya "secret123", bisa di-brute force
  • Ga validasi issuer dan audience — token dari service A bisa dipake di service B
  • Ga cek expiry — token expired tapi tetep diterima

JWT di Microservice Architecture

Di arsitektur monolith, JWT relatif straightforward. Satu server yang sign dan verify. Simple.

Di microservice, ceritanya beda. Ada banyak service yang perlu tahu siapa user ini dan boleh ngapain.

Pendekatan 1: Setiap service verify sendiri

graph LR
    Client --> GW[API Gateway]
    GW --> A["Service A (verify JWT)"]
    GW --> B["Service B (verify JWT)"]
    GW --> C["Service C (verify JWT)"]

Pakai asymmetric key (RSA/ECDSA):

  • Auth service sign pakai private key
  • Semua service verify pakai public key
  • Public key bisa dishare lewat JWKS endpoint (JSON Web Key Set)

Kelebihan: setiap service independen, ga perlu call auth service
Kekurangan: revocation tetep susah, token besar jalan-jalan di internal network

Pendekatan 2: API Gateway verify, service trust gateway

graph LR
    Client --> GW["API Gateway (verify JWT)"]
    GW --> A["Service A (trust gateway)"]
    GW --> B["Service B (trust gateway)"]
    GW --> C["Service C (trust gateway)"]

Gateway verify JWT sekali, lalu forward request ke service dengan header yang udah di-trust (misal X-User-Id, X-User-Role).

Kelebihan: service di belakang lebih simple, ga perlu logic JWT
Kekurangan: gateway jadi single point of trust. Kalau gateway di-bypass, service ga punya proteksi

Masalah: JWT yang jalan-jalan di internal network

JWT itu self-contained. Di dalamnya ada user info, roles, permissions, claims.

Kalau JWT ini jalan-jalan antar service di internal network:

  1. Attack surface besar — kalau satu service compromised, attacker dapet JWT lengkap dengan semua claims
  2. Data leakage — service yang sebenernya ga perlu tahu user details, tetep bisa baca semuanya dari JWT
  3. Token size — payload gede yang terus-terusan dikirim antar service

Ini yang bikin orang mikirin solusi yang lebih elegan: Phantom Token Pattern.

Phantom Token Pattern

Phantom token itu pattern di mana client dapet opaque token (token yang ga bisa dibaca, kayak random string), tapi di dalam sistem, opaque token itu di-tuker sama JWT yang berisi claims lengkap.

Gimana cara kerjanya?

sequenceDiagram
    participant C as Client
    participant GW as API Gateway
    participant Auth as Auth Server
    participant S as Service A

    C->>GW: Request + Opaque Token
    GW->>Auth: Introspect opaque token
    Auth-->>GW: Return JWT
    GW->>S: Forward request + JWT
    S-->>GW: Response
    GW-->>C: Response

Step by step:

  1. Client login → Auth server kasih opaque token (random string, ga bisa di-decode, ga ada info di dalamnya)
  2. Client kirim request dengan opaque token di header Authorization
  3. API Gateway terima opaque token
  4. Gateway introspect opaque token ke Auth Server: "ini token valid ga? Kalau valid, kasih JWT-nya"
  5. Auth Server cek di database/cache: "token ini valid, ini JWT-nya"
  6. Gateway ganti opaque token dengan JWT
  7. Gateway forward request ke internal service dengan JWT
  8. Internal service verify dan baca JWT seperti biasa

Kenapa ini bagus?

1. Client ga bisa baca token

Opaque token itu cuma random string. Client ga bisa decode, ga bisa baca claims, ga bisa tahu isinya.

Opaque token: "dGhpcyBpcyBhIHJhbmRvbSBvcGFxdWUgdG9rZW4="
JWT:          "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM..."

Client cuma pegang opaque token. Ga ada info yang bisa di-extract.

2. Revocation jadi gampang

Opaque token itu reference token — disimpan di auth server. Mau revoke? Hapus dari database. Selesai.

Pas gateway introspect, auth server bilang "token ini invalid" → request ditolak.

Ini menggabungkan kelebihan session-based (easy revocation) dengan kelebihan JWT (self-contained di internal network).

3. Attack surface berkurang

JWT yang berisi claims sensitif cuma jalan di internal network (antara gateway dan service). Ga pernah keluar ke client atau internet publik.

Kalau ada man-in-the-middle di public network, yang didapet cuma opaque token. Ga ada info yang leaked.

4. Separation of concerns

  • Client cuma perlu urusan sama opaque token
  • API Gateway urusan introspection dan swap
  • Internal services urusan verify JWT
  • Auth server urusan manage token lifecycle

Tiap komponen punya tanggung jawab yang jelas.

Trade-off

Ga ada silver bullet. Phantom token juga punya kekurangan:

  1. Latency tambahan — setiap request, gateway harus introspect ke auth server. Bisa di-mitigate pakai caching, tapi nambah complexity
  2. Auth server jadi dependency — kalau auth server mati, gateway ga bisa introspect = semua request gagal. Harus high availability
  3. Complexity — lebih complex dari JWT biasa. Butuh gateway yang support, auth server yang capable, caching strategy yang bener
  4. Caching dilemma — kalau cache terlalu lama, revocation ga instan. Kalau ga di-cache, auth server kena beban berat tiap request

Kapan pakai phantom token?

  • Sistem yang butuh revocation kapan aja (financial, healthcare, enterprise)
  • Arsitektur yang udah pakai API Gateway
  • Ketika data di JWT sensitif dan ga boleh terekspos ke client
  • Organisasi yang butuh compliance ketat (SOC2, HIPAA, PCI-DSS)

Kalau simple app dengan 1-2 service? Overkill. Pakai JWT biasa aja.

Ringkasan

Aspek Session-Based JWT Phantom Token
Storage Server-side (DB/Redis) Client-side Opaque di client, JWT di internal
Scalability Butuh shared store Sangat scalable Tergantung auth server
Revocation Mudah Susah Mudah
Performance DB lookup tiap request CPU-only verify Introspection + verify
Token size Kecil (session ID) Bisa besar Kecil di client, besar di internal
Info leakage Tidak ada Payload readable Tidak ada ke client
Complexity Rendah Sedang Tinggi

Kesimpulan

JWT itu powerful dan elegant. Tapi bukan solusi untuk semua masalah.

Yang paling penting dipahami:

  1. JWT itu di-encode, bukan di-encrypt. Siapa aja bisa baca isinya. Yang bikin aman itu signature-nya
  2. Signature menjamin integritas. Ubah satu karakter di payload → signature ga cocok → server tahu token udah dimodifikasi
  3. Stateless itu double-edged sword. Scalable, tapi susah di-revoke
  4. Di microservice, pilih strategi yang sesuai. Ga harus phantom token, tapi pahami trade-off masing-masing
  5. Security itu layers. JWT cuma satu layer. Tetep butuh HTTPS, proper key management, input validation, dan security practices lainnya

Jangan cuma pakai JWT karena "semua orang pakai." Pahami cara kerjanya, pahami limitasinya, baru tentukan apa yang paling cocok buat sistem kamu.

Topics

SecuritySoftware Engineering

Related Articles

OAuth 2.0 & OpenID Connect: Gimana "Login dengan Google" Itu Bekerja

Catatan pembelajaran tentang OAuth 2.0 dan OpenID Connect - dari kenapa kita butuh authorization framework, Authorization Code Flow step-by-step, PKCE, ID token vs access token, sampai common mistakes di production.

Caching: Kenapa Sistem Cepat Itu Sistem yang Males Ngitung Ulang

Catatan pembelajaran tentang caching - dari kenapa cache itu penting, strategi caching, Redis vs Memcached, cache invalidation, CDN, sampai pitfalls yang sering kejadian di production.

Clustering: Gimana Banyak Server Kerja Bareng Jadi Satu Sistem

Catatan pembelajaran tentang konsep clustering - dari kenapa satu server ga cukup, gimana cluster milih leader, sampai consensus algorithm dan real-world implementations.