Ajie Logo Ajie.
Navigation

© 2026 Ajie Kusumadhany.

Back to Articles

Why Your JWT Implementation Is Probably Insecure Kenapa Implementasi JWT Kamu Kemungkinan Tidak Aman

Ajie Ajie Kusumadhany
Jul 02, 2026 9 min read
Why Your JWT Implementation Is Probably Insecure Kenapa Implementasi JWT Kamu Kemungkinan Tidak Aman

I reviewed a authentication system last month that made me physically wince. The developer had implemented JWT with such confidence, yet every single token was effectively a master key to the entire application. No expiration. No rotation. Stored in localStorage like candy in an open jar. When I pointed out the vulnerabilities, they looked at me like I'd just told them their baby was ugly.

Here's the uncomfortable truth about JWT that nobody wants to admit. Most implementations are ticking time bombs waiting to explode. Not because JWT is fundamentally broken, but because developers treat it like a drop-in replacement for sessions without understanding the tradeoffs.

Let's dissect why your JWT implementation is probably insecure, and more importantly, how to fix it before someone with bad intentions discovers your mistakes the hard way.

The Illusion of Simplicity

JWT looks deceptively simple on the surface. You encode some data, sign it with a secret, and send it to the client. The client sends it back, you verify the signature, and boom—you have authentication. What could possibly go wrong?

Everything. Absolutely everything can go wrong.

The simplicity is a trap. JWT offloads state to the client, which means you're trusting data you don't control. That trust requires careful boundaries, and most developers never establish them properly.

What JWT Actually Gives You

A JWT has three parts: header, payload, and signature. The header specifies the algorithm. The payload contains your claims (user ID, roles, expiration). The signature proves the token wasn't tampered with.

Notice what's missing? Revocation. You cannot invalidate a JWT before it expires without additional infrastructure. This single fact causes more security headaches than any other JWT property.

Feature Session Cookies JWT
Server-side Storage Required Not required
Instant Revocation Yes No (without extra work)
Scalability Harder (requires shared storage) Easier (stateless)
Cross-domain Usage Complex (CORS, CSRF) Simpler
Token Size Small (just session ID) Larger (full payload)

Mistake #1: Storing JWT in localStorage

This is the single most common mistake I see. Developers store JWT in localStorage because it's convenient. The token persists across page reloads, it's easy to access from JavaScript, and everything seems to work perfectly.

Until an attacker injects one line of JavaScript into your page.

Any XSS vulnerability becomes a complete authentication bypass. An attacker can read localStorage, extract your token, and impersonate your users indefinitely. No phishing required. No man-in-the-middle attacks needed. Just one vulnerable npm package or one unsanitized user input field.

The fix is straightforward but often ignored. Store JWT in httpOnly cookies. These cookies cannot be accessed by JavaScript, which means XSS attacks cannot steal them. Yes, this introduces CSRF concerns, but CSRF tokens are a solved problem. XSS protection through httpOnly cookies is your first line of defense.

Not all cookies are created equal. A JWT cookie needs specific flags to provide real security:

  • httpOnly: Prevents JavaScript access (non-negotiable)
  • secure: Only sent over HTTPS (mandatory in production)
  • sameSite: Set to 'strict' or 'lax' for CSRF protection
  • path: Restrict to authentication endpoints when possible

Configure all of these. Not some. All.

Mistake #2: Ignoring Token Expiration

I've seen JWT implementations with expiration times measured in months. One memorable codebase had tokens valid for an entire year. The developer's reasoning? Users don't like logging in repeatedly.

Users also don't like having their accounts compromised.

A long-lived JWT is functionally equivalent to a password that cannot be changed. If that token leaks, the attacker has access for the entire validity period. There's no "log out all sessions" button for JWT. The token lives until it expires.

Short-lived access tokens combined with refresh tokens solve this problem. Your access token might expire in 15 minutes, but a refresh token (stored securely) allows obtaining a new access token without user interaction. When you need to revoke access, you invalidate the refresh token.

The Access Token vs Refresh Token Pattern

This pattern requires more work, but the security benefits are substantial:

Access tokens are short-lived (5-30 minutes). They contain the claims needed to authenticate API requests. If compromised, the window of exposure is limited.

Refresh tokens are long-lived but stored server-side. They're single-use or have rotation. When a refresh token is used, the old one is invalidated and a new one is issued. If someone tries to use an invalidated refresh token, you know there's been a breach.

Mistake #3: Algorithm Confusion Attacks

Here's an attack vector most developers have never heard of. JWT libraries verify tokens by checking the algorithm specified in the token header. But what happens when an attacker changes that algorithm?

The classic attack changes the algorithm from RS256 (asymmetric) to HS256 (symmetric). The attacker then signs the token using the public key (which is publicly available) as the HMAC secret. Some JWT libraries, when they see HS256, will verify the signature using the public key as the secret. The verification succeeds, and the attacker has a forged token.

This sounds theoretical, but it's been found in real applications. The fix is simple: explicitly specify which algorithm you expect during verification. Never trust the algorithm in the token header.

// WRONG: Trusts algorithm from token jwt.verify(token, publicKey); // CORRECT: Enforces expected algorithm jwt.verify(token, publicKey, { algorithms: ['RS256'] });

Mistake #4: Putting Sensitive Data in Payloads

A JWT payload is encoded, not encrypted. Anyone with the token can read its contents by simply base64-decoding the payload section. That means sensitive information—passwords, API keys, personal data—should never go in a JWT.

I once found a JWT containing a user's home address and phone number. The developer thought because it was "signed," the data was protected. Signed means tamper-evident, not confidential. There's a massive difference.

If you need to include sensitive references, use opaque identifiers. Store the actual data server-side and include only a user ID or session reference in the token.

Mistake #5: Not Implementing Token Rotation

Static JWT secrets are a risk. If your secret leaks, every token ever issued becomes compromised. You have no way to know which tokens are legitimate and which are forged.

Token rotation means periodically changing your signing secret. With rotation, you maintain a set of valid secrets (current and previous). New tokens are signed with the current secret. Verification checks against all valid secrets. Old secrets eventually expire and are removed.

This limits the blast radius if a secret leaks. Old tokens signed with a compromised secret will eventually expire. You're not stuck with a permanently compromised system.

Building a Secure JWT Implementation

Let's put all these principles together into a practical implementation checklist:

First, choose the right algorithm. RS256 is generally preferred for distributed systems because you can share the public key freely. HS256 is simpler but requires all services to share the secret.

Second, implement short-lived access tokens. Fifteen minutes is a reasonable default. Users won't notice the expiration if refresh tokens work properly.

Third, store tokens in httpOnly cookies with proper security flags. This protects against XSS-based token theft.

Fourth, implement refresh token rotation. Each use generates a new refresh token and invalidates the old one.

Fifth, maintain a token blacklist for immediate revocation when needed (password changes, account deletion, suspicious activity).

Sixth, log token events. Know when tokens are issued, refreshed, and revoked. Anomaly detection becomes possible when you have visibility.

When to Actually Use JWT

JWT isn't always the right choice. For a simple monolithic application with one domain, session cookies are often simpler and more secure. JWT shines in specific scenarios:

  • Microservices architectures where services need to verify tokens independently
  • Single Sign-On (SSO) across multiple applications
  • Mobile applications where cookie management is awkward
  • Third-party API authentication

Don't use JWT just because it's trendy. Use it because your architecture demands its specific benefits.

Key Takeaways

  • Store JWT in httpOnly cookies, never in localStorage or accessible JavaScript storage
  • Use short-lived access tokens paired with refresh tokens for security without usability pain
  • Explicitly specify expected algorithms during verification to prevent algorithm confusion attacks
  • Never put sensitive data in JWT payloads—they're encoded, not encrypted
  • Implement token rotation and blacklisting for proper lifecycle management
  • Consider whether JWT is actually necessary for your use case—sessions are often simpler and safer

JWT security isn't about getting one thing right. It's about not getting a dozen things wrong. Each mistake compounds the others. But with careful implementation, JWT provides a powerful authentication mechanism that scales beautifully across modern distributed systems.

Your users trust you with their authentication. Make sure that trust is justified.

Bulan lalu saya mereview sistem autentikasi yang membuat saya merasa ngeri secara fisik. Developer tersebut mengimplementasikan JWT dengan penuh percaya diri, namun setiap token efektif menjadi kunci master untuk seluruh aplikasi. Tidak ada expiration. Tidak ada rotasi. Disimpan di localStorage seperti permen dalam toples terbuka. Ketika saya tunjukkan kerentanannya, mereka menatap saya seperti baru saja saya mengatakan kalau bayi mereka jelek.

Inilah kebenaran tidak nyaman tentang JWT yang tidak ada yang ingin akui. Sebagian besar implementasi adalah bom waktu yang menunggu untuk meledak. Bukan karena JWT rusak secara fundamental, tapi karena developer memperlakukannya seperti pengganti drop-in untuk session tanpa memahami tradeoffs-nya.

Mari kita bedah kenapa implementasi JWT kamu kemungkinan tidak aman, dan yang lebih penting, cara memperbaikinya sebelum seseorang dengan niat buruk menemukan kesalahanmu dengan cara yang sulit.

Ilusi Kesederhanaan

JWT terlihat sangat sederhana di permukaan. Kamu encode beberapa data, tandatangani dengan secret, dan kirim ke client. Client mengirimkannya kembali, kamu verifikasi signature, dan boom—kamu punya autentikasi. Apa yang bisa salah?

Semua hal. Benar-benar semua hal bisa salah.

Kesederhanaan itu adalah jebakan. JWT melimpahkan state ke client, yang berarti kamu mempercayai data yang tidak kamu kontrol. Kepercayaan itu membutuhkan batasan yang hati-hati, dan sebagian besar developer tidak pernah membangunnya dengan benar.

Apa yang Sebenarnya Diberikan JWT

JWT memiliki tiga bagian: header, payload, dan signature. Header menentukan algoritma. Payload berisi claims kamu (user ID, roles, expiration). Signature membuktikan token tidak diubah.

Perhatikan apa yang hilang? Revocation. Kamu tidak bisa membatalkan JWT sebelum expired tanpa infrastruktur tambahan. Fakta tunggal ini menyebabkan lebih banyak masalah keamanan daripada properti JWT lainnya.

Fitur Session Cookies JWT
Server-side Storage Dibutuhkan Tidak dibutuhkan
Revokasi Instan Ya Tidak (tanpa kerja extra)
Skalabilitas Lebih sulit (butuh shared storage) Lebih mudah (stateless)
Penggunaan Cross-domain Kompleks (CORS, CSRF) Lebih simpel
Ukuran Token Kecil (hanya session ID) Lebih besar (full payload)

Kesalahan #1: Menyimpan JWT di localStorage

Ini adalah kesalahan paling umum yang saya temui. Developer menyimpan JWT di localStorage karena praktis. Token bertahan across page reload, mudah diakses dari JavaScript, dan semuanya tampak bekerja dengan sempurna.

Sampai attacker menyuntikkan satu baris JavaScript ke halamanmu.

Setiap kerentanan XSS menjadi bypass autentikasi total. Attacker bisa membaca localStorage, mengekstrak tokenmu, dan menyamar sebagai usermu tanpa batas waktu. Tidak perlu phishing. Tidak perlu serangan man-in-the-middle. Cukup satu package npm yang rentan atau satu field input user yang tidak di-sanitize.

Perbaikannya sederhana tapi sering diabaikan. Simpan JWT di httpOnly cookies. Cookie ini tidak bisa diakses oleh JavaScript, yang berarti serangan XSS tidak bisa mencurinya. Ya, ini memperkenalkan kekhawatiran CSRF, tapi CSRF token adalah masalah yang sudah terpecahkan. Proteksi XSS melalui httpOnly cookies adalah garis pertahanan pertamamu.

Tidak semua cookie dibuat sama. Cookie JWT membutuhkan flag spesifik untuk memberikan keamanan nyata:

  • httpOnly: Mencegah akses JavaScript (non-negotiable)
  • secure: Hanya dikirim via HTTPS (wajib di production)
  • sameSite: Set ke 'strict' atau 'lax' untuk proteksi CSRF
  • path: Batasi ke endpoint autentikasi bila memungkinkan

Konfigurasikan semuanya. Bukan sebagian. Semua.

Kesalahan #2: Mengabaikan Token Expiration

Saya pernah melihat implementasi JWT dengan waktu expiration diukur dalam bulan. Satu codebase yang memorable punya token valid selama setahun penuh. Alasan developernya? User tidak suka login berulang kali.

User juga tidak suka akun mereka dibajak.

JWT yang berumur panjang secara fungsional setara dengan password yang tidak bisa diganti. Jika token itu bocor, attacker punya akses selama periode validitas penuh. Tidak ada tombol "log out semua session" untuk JWT. Token hidup sampai expired.

Access token berumur pendek dikombinasikan dengan refresh token menyelesaikan masalah ini. Access token kamu mungkin expired dalam 15 menit, tapi refresh token (yang disimpan dengan aman) memungkinkan memperoleh access token baru tanpa interaksi user. Ketika kamu perlu mencabut akses, kamu invalidasi refresh token.

Pola Access Token vs Refresh Token

Pola ini membutuhkan lebih banyak kerja, tapi benefit keamanannya substansial:

Access token berumur pendek (5-30 menit). Mereka berisi claims yang dibutuhkan untuk mengautentikasi request API. Jika dikompromikan, window exposure terbatas.

Refresh token berumur panjang tapi disimpan server-side. Mereka single-use atau punya rotasi. Ketika refresh token digunakan, yang lama diinvalidasi dan yang baru diterbitkan. Jika seseorang mencoba menggunakan refresh token yang sudah diinvalidasi, kamu tahu ada pelanggaran keamanan.

Kesalahan #3: Serangan Algorithm Confusion

Inilah vektor serangan yang tidak pernah didengar sebagian besar developer. Library JWT memverifikasi token dengan memeriksa algoritma yang ditentukan di header token. Tapi apa yang terjadi ketika attacker mengubah algoritma itu?

Serangan klasik mengubah algoritma dari RS256 (asymmetric) ke HS256 (symmetric). Attacker kemudian menandatangani token menggunakan public key (yang tersedia publik) sebagai secret HMAC. Beberapa library JWT, ketika melihat HS256, akan memverifikasi signature menggunakan public key sebagai secret. Verifikasi berhasil, dan attacker punya token palsu.

Ini terdengar teoretis, tapi sudah ditemukan di aplikasi nyata. Perbaikannya sederhana: secara eksplisit tentukan algoritma yang kamu harapkan saat verifikasi. Jangan pernah percaya algoritma di header token.

// SALAH: Percaya algoritma dari token jwt.verify(token, publicKey); // BENAR: Memaksakan algoritma yang diharapkan jwt.verify(token, publicKey, { algorithms: ['RS256'] });

Kesalahan #4: Menaruh Data Sensitif di Payloads

Payload JWT di-encode, bukan di-encrypt. Siapa pun dengan token bisa membaca isinya dengan cukup base64-decode bagian payload. Itu berarti informasi sensitif—password, API key, data pribadi—tidak boleh masuk ke JWT.

Saya pernah menemukan JWT berisi alamat rumah dan nomor telepon user. Developernya berpikir karena "ditandatangani," data tersebut terlindungi. Signed berarti tamper-evident, bukan confidential. Ada perbedaan massive.

Jika kamu perlu menyertakan referensi sensitif, gunakan identifier opaque. Simpan data sebenarnya server-side dan sertakan hanya user ID atau referensi session di token.

Kesalahan #5: Tidak Mengimplementasikan Token Rotation

Secret JWT yang statis adalah risiko. Jika secretmu bocor, setiap token yang pernah diterbitkan menjadi dikompromikan. Kamu tidak punya cara untuk mengetahui token mana yang legit dan mana yang palsu.

Token rotation berarti secara berkala mengubah signing secret. Dengan rotasi, kamu mempertahankan set secret valid (current dan previous). Token baru ditandatangani dengan secret saat ini. Verifikasi memeriksa terhadap semua secret valid. Secret lama akhirnya expired dan dihapus.

Ini membatasi blast radius jika secret bocor. Token lama yang ditandatangani dengan secret yang dikompromikan akan akhirnya expired. Kamu tidak terjebak dengan sistem yang dikompromikan secara permanen.

Membangun Implementasi JWT yang Aman

Mari gabungkan semua prinsip ini menjadi checklist implementasi praktis:

Pertama, pilih algoritma yang tepat. RS256 umumnya lebih disukai untuk sistem terdistribusi karena kamu bisa membagikan public key secara bebas. HS256 lebih simpel tapi membutuhkan semua service untuk berbagi secret.

Kedua, implementasikan access token berumur pendek. Lima belas menit adalah default yang masuk akal. User tidak akan menyadari expiration jika refresh token bekerja dengan benar.

Ketiga, simpan token di httpOnly cookies dengan flag keamanan yang tepat. Ini melindungi dari pencurian token berbasis XSS.

Keempat, implementasikan refresh token rotation. Setiap penggunaan menghasilkan refresh token baru dan menginvalidasi yang lama.

Kelima, pertahankan token blacklist untuk revokasi instan saat dibutuhkan (perubahan password, penghapusan akun, aktivitas mencurigakan).

Keenam, log event token. Ketahui kapan token diterbitkan, di-refresh, dan di-revoke. Deteksi anomali menjadi mungkin ketika kamu punya visibility.

Kapan Sebenarnya Menggunakan JWT

JWT tidak selalu pilihan yang tepat. Untuk aplikasi monolithic sederhana dengan satu domain, session cookies sering lebih simpel dan lebih aman. JWT bersinar di skenario spesifik:

  • Arsitektur microservices di mana service perlu memverifikasi token secara independen
  • Single Sign-On (SSO) across multiple applications
  • Aplikasi mobile di mana manajemen cookie canggung
  • Autentikasi API pihak ketiga

Jangan gunakan JWT hanya karena trend. Gunakan karena arsitekturmu membutuhkan benefit spesifiknya.

Kesimpulan Utama

  • Simpan JWT di httpOnly cookies, jangan pernah di localStorage atau JavaScript storage yang accessible
  • Gunakan access token berumur pendek dipasangkan dengan refresh token untuk keamanan tanpa rasa sakit usability
  • Secara eksplisit tentukan algoritma yang diharapkan saat verifikasi untuk mencegah serangan algorithm confusion
  • Jangan pernah menaruh data sensitif di payload JWT—mereka di-encode, bukan di-encrypt
  • Implementasikan token rotation dan blacklisting untuk manajemen lifecycle yang tepat
  • Pertimbangkan apakah JWT benar-benar diperlukan untuk use case kamu—session sering lebih simpel dan lebih aman

Keamanan JWT bukan tentang mendapatkan satu hal dengan benar. Ini tentang tidak mendapatkan lusinan hal dengan salah. Setiap kesalahan memperparah yang lain. Tapi dengan implementasi yang hati-hati, JWT menyediakan mekanisme autentikasi powerful yang scale beautifully across sistem terdistribusi modern.

Usermu mempercayaimu dengan autentikasi mereka. Pastikan kepercayaan itu terbuktikan.

#JWT #Security #Authentication #Backend #Best Practices