JWT (JSON Web Token) untuk Autentikasi
Di artikel HTTP & Siklus Request-Response kita melihat bahwa setiap request datang dengan header Authorization: Bearer <token>. Sekarang pertanyaannya: apa isi token itu, dan kenapa server begitu percaya padanya? Jawabannya ada di JWT — salah satu format token paling populer untuk autentikasi API di aplikasi web dan mobile.
JWT (JSON Web Token) adalah compact token yang membawa identitas pengguna di dalam dirinya sendiri, dan didefinisikan secara resmi di RFC 7519. Karena informasinya dibawa sendiri oleh token, server tidak perlu menyimpan state login di memory — inilah yang membuat JWT disebut stateless. Mari kita bedah dari nol.
Apa itu JWT
Bayangkan JWT sebagai tiket masuk konser: tiket itu berisi nama Anda, tanggal acara, dan nomor kursi, ditandatangani oleh panitia dengan segel khusus. Ketika Anda menunjukkannya ke penjaga pintu, penjaga cukup mengecek segelnya — tidak perlu menelepon panitia untuk memastikan Anda benar-benar sudah membayar.
Secara teknis, JWT adalah string terenkode (bukan terenkripsi) yang berisi klaim dalam format JSON. Klaim (claims) adalah pernyataan terstruktur tentang pengguna, misalnya "saya adalah user id 42" atau "role saya ADMIN". Token ini ditandatangani dengan kunci yang hanya diketahui server, sehingga isinya tidak bisa diubah oleh pihak lain tanpa merusak tanda tangan.
Struktur JWT selalu terdiri dari tiga bagian yang dipisahkan titik:
xxxxx.yyyyy.zzzzz
───── ───── ─────
header payload signature
Kenapa backend memakai JWT
Sistem autentikasi berbasis session klasik bekerja seperti ini: server membuat session id, menyimpannya di memory/database, lalu memberikannya ke klien dalam bentuk cookie. Masalah muncul ketika aplikasi Anda tumbuh besar:
| Situasi | Session klasik | JWT |
|---|---|---|
| Tiga server backend berbagi beban | Session harus disinkronkan (atau pakai Redis/sticky session) | Tidak perlu, token berdiri sendiri |
| Aplikasi mobile & desktop | Cookie lebih rumit di-handle lintas platform | Token dikirim manual via header, mudah |
| Satu backend dipakai banyak klien (web, mobile, IoT) | Tiap klien butuh mekanisme session sendiri | Mekanisme identik untuk semua klien |
| Skala horizontal (server ditambah) | Butuh infra penyimpanan session ekstra | Cukup bagikan secret yang sama |
| Logout instan | Mudah, cukup hapus session di server | Sulit (perlu blacklist, dibahas nanti) |
Ingat kuncinya: session menyimpan "siapa saya" di server. JWT menyimpan "siapa saya" di dalam token yang dibawa klien, dan server cukup mengecek tanda tangannya. Tidak ada yang lebih baik secara mutlak — keduanya punya trade-off yang akan kita bahas lebih dalam di Session vs JWT.
Perbandingan singkat session vs JWT
| Aspek | Session-based | JWT |
|---|---|---|
| Lokasi data | Di server (memory/DB/Redis) | Di token, dibawa klien |
| State di server | Ada (serverful) | Tidak ada (stateless) |
| Skala horizontal | Butuh session store bersama | Cukup secret/keyset yang sama |
| Revocation / logout | Langsung efektif | Butuh blacklist atau token kedaluwarsa |
| Muatan | Hanya session id (kecil) | Seluruh klaim ikut (besar) |
Tiga Bagian JWT
Setiap JWT dibangun dari tiga bagian. Diagram berikut merangkumnya sekaligus:
| Bagian | Isi | Peran |
|---|---|---|
Header | Algoritma + tipe token | Memberi tahu server cara memverifikasi |
Payload | Klaim (identitas & metadata) | Membawa data pengguna |
Signature | Hasil hash dari header + payload dengan kunci | Menjamin token tidak dipalsukan |
Header
Header adalah objek JSON yang mendeskripsikan bagaimana token ditandatangani. Dua field yang wajib dipahami: alg (algoritma) dan typ (tipe token).
{
"alg": "HS256",
"typ": "JWT"
}
Header ini kemudian di-encode dengan Base64URL — tanpa karakter =, +, dan / supaya aman di URL. Hasilnya adalah potongan pertama token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload
Payload berisi klaim. Ada tiga kategori klaim menurut RFC 7519:
- Registered claims — klaim standar yang sudah punya makna baku.
- Public claims — klaim yang terdaftar di IANA agar tidak bertabrakan namanya.
- Private claims — klaim buatan aplikasi Anda sendiri.
Klaim standar yang paling sering dipakai:
| Klaim | Kepanjangan | Arti |
|---|---|---|
sub | subject | Identitas utama token — biasanya user id |
iat | issued at | Kapan token diterbitkan (epoch seconds) |
exp | expiration time | Kapan token kedaluwarsa — wajib dicek server |
iss | issuer | Siapa yang menerbitkan token (mis. https://auth.app.com) |
aud | audience | Untuk siapa token ini — biasanya nama resource/API |
{
"sub": "42",
"name": "Hendro",
"role": "ADMIN",
"iat": 1754342400,
"exp": 1754428800,
"iss": "https://api.endsite.dev"
}
Di contoh di atas, selain klaim standar ada klaim kustom name dan role. Klaim kustom inilah yang biasa dipakai aplikasi untuk autentikasi otorisasi tanpa memanggil database di setiap request. Bagian payload yang di-encode adalah potongan kedua token:
eyJzdWIiOiI0MiIsIm5hbWUiOiJIZW5kcm8iLCJyb2xlIjoiQURNSU4iLCJpYXQiOjE3NTQzNDI0MDAsImV4cCI6MTc1NDQyODgwMCwiaXNzIjoiaHR0cHM6Ly9hcGkuZW5kc2l0ZS5kZXYifQ
Signature
Signature adalah bagian yang membuat JWT aman. Rumusnya sederhana tapi kuat:
signature = HMAC-SHA256(
base64url(header) + "." + base64url(payload),
secret_key
)
Garis besar prosesnya:
- Gabungkan
base64url(header)danbase64url(payload)dengan tanda titik. - Hash gabungan itu bersama kunci rahasia dengan algoritma yang tertulis di
alg. - Hasil hash di-encode Base64URL menjadi potongan ketiga token.
Kenapa bagian ini membuat token tidak bisa dipalsukan? Karena untuk menghitung signature yang benar, penyerang harus tahu kunci rahasia. Kalau penyerang mengubah role dari USER menjadi ADMIN di payload, signature otomatis tidak cocok lagi — dan verifikasi akan gagal. Anda bisa membuktikannya langsung: tempel token asli di bawah ke jwt.io debugger, ubah satu huruf di payload, dan lihat tulisan Invalid Signature muncul.
Contoh JWT lengkap
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiI0MiIsIm5hbWUiOiJIZW5kcm8iLCJyb2xlIjoiQURNSU4iLCJpYXQiOjE3NTQzNDI0MDAsImV4cCI6MTc1NDQyODgwMCwiaXNzIjoiaHR0cHM6Ly9hcGkuZW5kc2l0ZS5kZXYifQ.
4rKx0...8zLQ
Salin ketiga baris itu (gabung jadi satu baris tanpa baris baru), tempel di bagian Encoded di jwt.io, lalu masukkan secret apa pun. Anda akan melihat header dan payload ter-decoding dengan mudah — bukti bahwa isi JWT bukan rahasia, siapa pun bisa membacanya. Yang tidak bisa siapa pun lakukan tanpa kunci adalah membuat signature yang valid.
Poin penting yang sering disalahpahami: JWT di-encode dengan Base64URL, bukan dienkripsi. Payload bisa dibaca siapa saja yang memegang token. Jangan pernah menaruh password, nomor kartu, atau data sensitif lain di dalam payload.
Alur Login dan Verifikasi
Sekarang kita rangkai semuanya ke dalam alur nyata di aplikasi:
Klien (Frontend/Mobile) Server Backend
| |
| 1. POST /api/auth/login |
| {email, password} |
|----------------------------------------->|
| 2. Validasi kredensial
| 3. Buat & tanda tangani JWT
| 4. 200 OK {accessToken} |
|<-----------------------------------------|
| |
| 5. Simpan token (memory/cookie) |
| |
| 6. GET /api/users |
| Authorization: Bearer <token> |
|----------------------------------------->|
| 7. Verifikasi signature
| 8. Cek exp (belum kedaluwarsa?)
| 9. Proses request
| 10. 200 OK {data} |
|<-----------------------------------------|
Tahapannya satu per satu:
- Login — klien mengirim kredensial (
email+password) ke endpoint/api/auth/login. - Validasi kredensial — server mengecek ke database. Jangan pernah menyimpan password polos; bandingkan dengan hash, misalnya pakai BCrypt.
- Terbitkan token — server membuat JWT berisi
sub,iat,exp, dan klaim kustom, lalu menandatanganinya. - Kirim ke klien — token dikembalikan lewat body response atau httpOnly cookie.
- Klien menyimpan token — untuk dipakai di setiap request berikutnya.
- Setiap request — klien mengirim header
Authorization: Bearer <token>. - Server verifikasi signature — jika signature tidak cocok, token dianggap palsu dan request ditolak dengan
401 Unauthorized. - Server cek
exp— jika sudah lewat waktu kedaluwarsa, token ditolak. - Proses request — jika valid, klaim di payload (misal
role) bisa langsung dipakai untuk autentikasi vs otorisasi. - Response — data dikembalikan ke klien.
// Skema yang dilakukan server di setiap request yang butuh autentikasi
public boolean isValidToken(String token) {
try {
Jws<Claims> jws = Jwts.parser()
.verifyWith(SECRET_KEY)
.build()
.parseSignedClaims(token);
return jws.getPayload().getExpiration().after(new Date());
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
HS256 vs RS256
Dua algoritma penandatanganan yang paling umum, keduanya disebutkan di jwt.io introduction. Perbedaan intinya adalah simetris vs asimetris:
| Aspek | HS256 (HMAC) | RS256 (RSA) |
|---|---|---|
| Jenis kunci | Simetris — satu secret | Asimetris — pasangan private/public key |
| Penandatanganan | Satu secret yang sama | Private key (dirahasiakan) |
| Verifikasi | Secret yang sama | Public key (boleh disebar) |
| Siapa bisa memverifikasi | Hanya yang tahu secret | Siapa pun yang punya public key |
| Cocok untuk | Aplikasi Anda sendiri, satu service | Pihak ketiga / banyak service penerima |
| Contoh pemakaian | Backend internal, monolith | Okta, Auth0, Keycloak, microservices |
Aturan praktis: kalau token diterbitkan dan diverifikasi oleh satu server milik Anda sendiri,
HS256dengan secret yang kuat sudah cukup. Kalau token diterbitkan oleh pihak ketiga (misalnya Okta atau Auth0) dan banyak service Anda yang harus memverifikasinya, pakaiRS256— service cukup memegang public key, tidak perlu berbagi rahasia. Spring Security mendukung keduanya lewat JWT resource server.
Klaim exp & iat dan Refresh Token
Dua klaim ini adalah pasangan yang wajib dipahami bersama:
iat(issued at) — mencatat kapan token dibuat. Berguna untuk audit dan untuk menolak token yang terlalu tua.exp(expiration time) — mencatat kapan token tidak berlaku lagi. Server wajib mengecek ini.
Kenapa token harus kedaluwarsa? Karena sekali token bocor, penyerang bisa memakainya seperti pengguna asli sampai token itu tidak berlaku. Waktu kedaluwarsa yang pendek (misal 15–30 menit untuk access token) memperkecil "jendela bahaya". Namun token yang sering kedaluwarsa juga membuat pengguna harus login terus-menerus — solusinya adalah refresh token:
- Saat login, server menerbitkan dua token: access token (berumur pendek) dan refresh token (berumur panjang, misal 7–30 hari).
- Ketika access token kedaluwarsa, klien mengirim refresh token ke endpoint khusus untuk mendapatkan access token baru.
- Refresh token sebaiknya disimpan dengan aman dan bisa dicabut (misal disimpan di database sehingga bisa di-blacklist saat logout).
Analoginya: access token itu seperti "tiket masuk hari ini" — cepat kedaluwarsa supaya aman. Refresh token itu seperti "KTP" — jarang dipakai, tapi dipakai untuk memperbarui tiket ketika masuk ke tempat Anda bekerja setiap hari.
Implementasi di Spring Security
Mari kita praktikkan. Di Spring Security, verifikasi JWT adalah fitur built-in melalui JwtDecoder. Anda tidak perlu menulis verifikasi manual — cukup daftarkan decoder.
1. Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
2. Konfigurasi SecurityFilterChain
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
@Bean
JwtDecoder jwtDecoder() {
SecretKey key = Keys.hmacShaKeyFor("rahasia-super-kuat-yang-panjang".getBytes());
return NimbusJwtDecoder.withSecretKey(key).build();
}
}
Dengan konfigurasi ini, Spring Security otomatis membaca header Authorization: Bearer <token>, memverifikasi signature dan exp, lalu mengisi Authentication di dalam request. Endpoint yang butuh ROLE_ADMIN otomatis menolak pengguna lain.
3. Mengambil klaim di controller
@RestController
public class UserController {
@GetMapping("/api/users/me")
public Map<String, Object> me(@AuthenticationPrincipal Jwt jwt) {
return Map.of(
"id", jwt.getSubject(),
"email", jwt.getClaimAsString("email"),
"role", jwt.getClaimAsString("role")
);
}
}
4. Generate token dengan JwtService
@Service
public class JwtService {
private final SecretKey key = Keys.hmacShaKeyFor("rahasia-super-kuat-yang-panjang".getBytes());
private static final long EXPIRATION_MS = 30 * 60 * 1000; // 30 menit
public String generateToken(UserDetails user) {
Instant now = Instant.now();
return Jwts.builder()
.subject(user.getUsername())
.claim("role", user.getAuthorities())
.issuedAt(Date.from(now))
.expiration(Date.from(now.plusMillis(EXPIRATION_MS)))
.issuer("https://api.endsite.dev")
.signWith(key)
.compact();
}
}
Jwts.builder() berasal dari library jjwt — yang sama dengan yang dipakai contoh isValidToken di atas. Perhatikan issuer yang konsisten dengan nilai iss yang kita verifikasi di decoder.
Risiko & Best Practice
JWT praktis, tapi ada beberapa jebakan keamanan yang wajib Anda kenali.
Di mana menyimpan token?
Ini perdebatan klasik:
| Penyimpanan | Kelebihan | Risiko |
|---|---|---|
localStorage | Simpel, mudah dibaca JavaScript | Rentan XSS — script jahat bisa mencuri token |
httpOnly cookie | Tidak bisa dibaca JavaScript, tahan XSS | Rentan CSRF — perlu proteksi token CSRF tambahan |
Intinya: token yang disimpan di localStorage hanya aman jika aplikasi Anda benar-benar bebas XSS (yang sulit dijamin 100%). Banyak praktisi menyarankan httpOnly cookie untuk web, sementara aplikasi mobile biasanya menyimpan token di penyimpanan aman bawaan OS (Keychain/Keystore).
Jangan taruh data sensitif di payload
Sudah disebutkan beberapa kali karena ini penting: payload JWT hanya di-base64, bukan dienkripsi. Siapa pun yang memegang token bisa membacanya. Jadi:
- Boleh:
sub,email,role,name. - Jangan pernah: password, nomor kartu kredit, nomor KTP, token internal.
Selalu verifikasi di server
- Verifikasi signature dengan kunci yang tepat.
- Cek
exp(daniat,iss,audkalau relevan). - Jangan pernah percaya payload tanpa verifikasi — attacker bisa membuat token sendiri kalau Anda hanya mem-parsing base64 tanpa mengecek tanda tangan.
Penanganan logout
Karena JWT stateless, server tidak otomatis tahu token sudah "dicabut". Dua pendekatan umum:
- Blacklist/denylist — simpan
jti(JWT ID) dari token yang dicabut ke Redis sampai token itu kedaluwarsa secara alami; verifikasi menolak token yang ada di daftar. - Rotasi refresh token — saat refresh, token lama diganti yang baru; token yang dipakai ulang dianggap bocor dan seluruh sesi dicabut.
Kesalahan Umum
Ringkasan jebakan yang paling sering ditemui di lapangan:
| Kesalahan | Kenapa berbahaya | Solusi |
|---|---|---|
| Menaruh password di payload | Payload bisa dibaca siapa pun | Hanya kirim sub/email/role |
Tidak mengecek exp | Token bekas bisa dipakai selamanya | Wajib cek exp di decoder |
| Memakai token tanpa verifikasi signature | Attacker bisa memalsukan role jadi ADMIN | Selalu verifikasi sebelum percaya klaim |
| Secret terlalu pendek/sederhana | Brute-force HMAC jadi mudah | Gunakan secret ≥ 256 bit acak |
| Menyimpan token di localStorage tanpa mitigasi XSS | Token bisa dicuri script jahat | Pertimbangkan httpOnly cookie + CSRF |
HS256 untuk verifikasi publik | Secret ikut bocor jika public key dibagikan | Pakai RS256 untuk verifikasi lintas service |
// Contoh anti-pattern: membaca klaim TANPA verifikasi signature
Claims claims = Jwts.parser() // salah: tanpa key, tanpa verifyWith
.build()
.parseClaimsJwt(token) // parseClaimsJwt TIDAK memverifikasi signature
.getPayload();
String role = claims.get("role", String.class); // bisa saja palsu
Baris kode di atas terlihat polos, tapi parseClaimsJwt hanya mendecode, tidak memverifikasi. Attacker bisa mengganti role menjadi ADMIN, meng-encode ulang dengan secret apa pun, dan server akan percaya. Selalu gunakan Jwts.parser().verifyWith(key)... seperti di bagian alur verifikasi.
Ringkasan
JWT adalah token stateless berisi header, payload, dan signature. Header menentukan algoritma, payload membawa klaim identitas, dan signature menjamin token tidak bisa dipalsukan. Server memverifikasi signature dan exp di setiap request tanpa perlu menyimpan session — inilah yang membuat JWT cocok untuk API yang melayani banyak klien dan server yang ber-skala horizontal. Pilih HS256 untuk aplikasi mandiri dan RS256 saat melibatkan pihak ketiga. Ingat tiga peringatan besarnya: payload bukanlah enkripsi, verifikasi adalah harga mati, dan token harus selalu punya masa kedaluwarsa.
Lanjut membaca
- Autentikasi vs Otorisasi — perbedaan identitas dan hak akses.
- Session vs JWT — perbandingan mendalam dua pendekatan autentikasi.
- HTTP & Siklus Request-Response — tempat
Authorizationheader pertama kali muncul. - JWT Introduction — dokumentasi resmi dengan debugger interaktif.
- RFC 7519: JSON Web Token — spesifikasi resmi JWT.
- Spring Security: JWT Resource Server — integrasi JWT di Spring.
- Auth0: JSON Web Tokens — penjelasan konsep dan praktik dari vendor identitas.
- Okta Developer Docs — panduan praktis autentikasi dan JWT.