JWT bersifat stateless sehingga server tidak memiliki penyimpanan terpusat untuk memeriksa apakah token telah dibatalkan — selama tanda tangan valid dan belum kedaluwarsa, token diterima. Solusinya adalah menjaga kedaluwarsa access token tetap singkat (15 menit) dan memelihara blacklist Redis berisi ID token (JTI) yang dicabut selama jendela tersebut. Untuk refresh token, menyimpan hash yang valid di database memungkinkan revokasi cukup dengan menghapus catatannya.
Library JWT awal mengizinkan field algoritma di header token diatur ke 'none', yang berarti verifikasi tanda tangan tidak diperlukan — penyerang bisa memalsukan token dengan klaim apa pun. Library modern sudah memperbaiki ini, tetapi hanya jika dikonfigurasi dengan benar: Anda harus secara eksplisit menentukan daftar algoritma yang diizinkan (misalnya algorithms: ['HS256'] di jsonwebtoken) dan tidak membiarkan library membaca algoritma dari header token itu sendiri.
JavaScript apa pun yang berjalan di halaman Anda — termasuk skrip yang disuntikkan melalui XSS — dapat membaca localStorage dan mencuri token yang tersimpan di sana. Pola yang direkomendasikan adalah menyimpan access token berumur pendek dalam variabel JavaScript (di memori) dan refresh token dalam cookie httpOnly Secure SameSite=Strict. Saat halaman di-refresh, aplikasi memanggil endpoint refresh saat inisialisasi untuk mendapatkan access token baru menggunakan httpOnly cookie, sehingga memberikan ketahanan terhadap XSS sekaligus CSRF.
Implementasinya menggabungkan access token JWT dengan kedaluwarsa 15 menit (ditandatangani RS256, disimpan di memori), refresh token berkedaluwarsa 7 hari yang disimpan dalam cookie httpOnly Secure, rotasi refresh token dengan tabel revokasi berbasis PostgreSQL, dan blacklist Redis untuk jendela 15 menit saat ini. Pendekatan ini memberikan kecepatan verifikasi stateless dari JWT sekaligus kemampuan revokasi setara autentikasi sesi, dan mendukung fitur 'logout dari semua perangkat' dengan menghapus semua catatan refresh token milik satu user ID.
Autentikasi sesi lebih cocok ketika Anda membutuhkan revokasi instan tanpa harus membangun blacklist terpisah, ketika session store sisi server (Redis) sudah ada dalam stack Anda, atau ketika kemudahan implementasi yang benar menjadi prioritas — lebih sedikit jebakan dibanding JWT. JWT menjadi pilihan unggul untuk arsitektur API-first atau microservices di mana verifikasi stateless dan autentikasi lintas layanan lebih penting daripada kemampuan pencabutan instan.
JWT vs autentikasi sesi adalah salah satu debat developer yang menghasilkan lebih banyak panas daripada cahaya. Saya telah mengimplementasikan keduanya dalam produksi — autentikasi sesi untuk aplikasi server-rendered tradisional, JWT untuk sistem ERP API-first — dan kebenarannya adalah keduanya dapat diimplementasikan dengan aman atau tidak aman tergantung pada bagaimana Anda menggunakannya. Menurut Verizon DBIR 2024, kredensial yang dicuri menyumbang 24% dari semua pelanggaran data.
| Dimensi | JWT | Autentikasi Sesi |
|---|---|---|
| Model status | Stateless — diverifikasi lewat tanda tangan, tanpa pencarian database | Stateful — server mencari data sesi (biasanya di Redis) |
| Revokasi | Tidak dapat dibatalkan sebelum kedaluwarsa tanpa logika blacklist tambahan | Segera — cukup hapus catatan sesi |
| Overhead per permintaan | Tidak ada — hanya verifikasi tanda tangan, tanpa round trip jaringan | Kecil — pencarian Redis menambah sekitar 1-5ms |
| Skala horizontal | Mudah — tidak perlu penyimpanan bersama | Perlu penyimpanan sesi bersama (Redis) di semua server |
| Risiko implementasi | Lebih tinggi — kesalahan alg=none dan penyimpanan token yang salah sering terjadi | Lebih rendah — lebih sedikit cara untuk salah konfigurasi |
| Paling cocok untuk | Aplikasi API-first, microservices, autentikasi lintas layanan | Aplikasi server-rendered tradisional |
JWT (JSON Web Token) adalah token yang berisi sendiri yang membawa klaim pengguna (user_id, peran, izin) dan ditandatangani oleh server Anda. Klien mengirim token di setiap permintaan; server memverifikasi tanda tangan tanpa pencarian database. Sifat stateless ini adalah daya jual utama: skala horizontal tanpa penyimpanan sesi bersama, tidak ada round trip database di setiap permintaan. Biaya tersembunyi: JWT tidak dapat dibatalkan sebelum kedaluwarsa.
Persyaratan keamanan JWT yang tidak dapat ditawar: gunakan kedaluwarsa singkat untuk access token (15 menit adalah standar saya), gunakan refresh token yang berumur panjang yang terpisah yang disimpan dalam cookie httpOnly Secure SameSite=Strict (jangan pernah localStorage), tandatangani dengan RS256 untuk layanan-ke-layanan, HS256 untuk layanan tunggal dengan manajemen kunci yang tepat, validasi semua klaim termasuk exp, iat, iss, dan aud di setiap permintaan.
// NestJS JWT + Refresh Token auth implementation
import { Injectable } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { Response } from 'express'
import * as bcrypt from 'bcrypt'
import { createHash } from 'crypto'
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private usersService: UsersService,
private redisService: RedisService,
private prisma: PrismaService,
) {}
async login(email: string, password: string, res: Response) {
const user = await this.usersService.findByEmail(email)
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
throw new UnauthorizedException('Invalid credentials')
}
// Short-lived access token (15 min)
const accessToken = this.jwtService.sign(
{ sub: user.id, email: user.email, roles: user.roles, tenantId: user.tenantId },
{ expiresIn: '15m', algorithm: 'RS256' }
)
// Long-lived refresh token (7 days)
const refreshToken = crypto.randomBytes(64).toString('hex')
const tokenHash = createHash('sha256').update(refreshToken).digest('hex')
// Store hash in DB (never the raw token)
await this.prisma.refreshToken.create({
data: {
userId: user.id,
tokenHash,
deviceId: res.req.headers['x-device-id'] as string,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
}
})
// Set refresh token in httpOnly cookie
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
path: '/auth/refresh',
})
return { accessToken } // Return access token in body (stored in memory by client)
}
async refresh(refreshToken: string, res: Response) {
const tokenHash = createHash('sha256').update(refreshToken).digest('hex')
const record = await this.prisma.refreshToken.findUnique({
where: { tokenHash },
include: { user: true },
})
if (!record || record.expiresAt < new Date()) {
throw new UnauthorizedException('Invalid or expired refresh token')
}
// Rotation: delete old, issue new
await this.prisma.refreshToken.delete({ where: { id: record.id } })
return this.login(record.user.email, /* re-issue */)
}
}Dari pengalaman saya membangun autentikasi ERP: implementasikan strategi rotasi refresh token di mana setiap refresh menghasilkan refresh token baru dan membatalkan yang sebelumnya. Simpan hash refresh token yang saat ini valid di database. Ini memberi Anda kemampuan revokasi tanpa overhead penuh penyimpanan sesi. Jika Anda mendeteksi refresh token yang digunakan kembali setelah rotasi, ini mengindikasikan pencurian token — segera batalkan semua refresh token untuk pengguna tersebut.
Autentikasi sesi tradisional menyimpan ID sesi dalam cookie, dan server mencari data sesi di setiap permintaan (biasanya dari Redis). Keunggulan dibanding JWT: revokasi segera (hapus catatan sesi), dapat menyimpan data sesi tidak terbatas, lebih sederhana untuk diimplementasikan dengan benar. Kekurangan: memerlukan penyimpanan sesi bersama untuk skala horizontal (Redis menyelesaikan ini).
Kesalahan implementasi JWT paling berbahaya adalah menerima kerentanan JWT alg=none. Library JWT awal mengizinkan field algoritma di header token diatur ke 'none', yang berarti tidak diperlukan tanda tangan. Penyerang dapat memalsukan token dengan klaim apa pun dengan mengatur alg=none. Library modern telah memperbaiki ini, tetapi hanya jika dikonfigurasi dengan benar: secara eksplisit atur algoritma yang diizinkan dan jangan pernah menerima alg=none.
Menyimpan JWT access token di localStorage adalah antipattern umum yang membuka aplikasi Anda terhadap serangan XSS. JavaScript apa pun yang berjalan di halaman Anda (termasuk skrip yang disuntikkan dari XSS) dapat membaca localStorage dan mencuri token. Simpan access token dalam memori (variabel JavaScript) dan refresh token dalam cookie httpOnly. Ya, ini berarti access token hilang saat refresh halaman — selesaikan dengan memanggil endpoint refresh saat inisialisasi aplikasi.
Untuk sistem ERP API-first di Commsult Indonesia, saya menggunakan pendekatan hybrid: access token JWT (kedaluwarsa 15 menit, ditandatangani RS256, disimpan dalam memori), refresh token (kedaluwarsa 7 hari, disimpan sebagai cookie httpOnly Secure), rotasi refresh token dengan revokasi yang didukung database (tabel PostgreSQL), dan blacklist Redis untuk jendela 15 menit saat ini.
Sistem ERP sering membutuhkan beberapa sesi bersamaan (desktop + mobile) dan isolasi multi-tenant. Untuk beberapa perangkat: keluarkan refresh token terpisah per perangkat, simpan sidik jari perangkat bersama token, izinkan revokasi selektif sesi perangkat individual. Untuk multi-tenancy: sertakan tenant_id dalam payload JWT, validasi tenant_id cocok dengan sumber daya yang diminta di setiap panggilan API.
Daftar periksa implementasi saya untuk setiap sistem auth: kedaluwarsa access token ≤ 15 menit, refresh token dalam cookie httpOnly Secure SameSite=Strict, rotasi refresh token diaktifkan, whitelist algoritma eksplisit (tidak ada alg=none), mekanisme revokasi token (blacklist Redis atau catatan DB), rate limiting pada endpoint /login dan /refresh, lockout akun setelah N kegagalan, MFA tersedia (TOTP minimum), peristiwa auth dicatat dengan IP dan user agent.
Sumber & Bacaan Lanjutan