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.
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