Skip to main content
BlogDevOps

JWT vs Autentikasi Sesi di Produksi: Yang Benar-Benar Saya Gunakan

Perbandingan jujur JWT dan autentikasi berbasis sesi untuk API produksi — mencakup trade-off keamanan, revokasi, penyimpanan, dan apa yang saya implementasikan untuk sistem ERP.

Matthews Wong

Software Engineer

Juni 202510 menit baca
DevOps

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.

DimensiJWTAutentikasi Sesi
Model statusStateless — diverifikasi lewat tanda tangan, tanpa pencarian databaseStateful — server mencari data sesi (biasanya di Redis)
RevokasiTidak dapat dibatalkan sebelum kedaluwarsa tanpa logika blacklist tambahanSegera — cukup hapus catatan sesi
Overhead per permintaanTidak ada — hanya verifikasi tanda tangan, tanpa round trip jaringanKecil — pencarian Redis menambah sekitar 1-5ms
Skala horizontalMudah — tidak perlu penyimpanan bersamaPerlu penyimpanan sesi bersama (Redis) di semua server
Risiko implementasiLebih tinggi — kesalahan alg=none dan penyimpanan token yang salah sering terjadiLebih rendah — lebih sedikit cara untuk salah konfigurasi
Paling cocok untukAplikasi API-first, microservices, autentikasi lintas layananAplikasi server-rendered tradisional

JWT: Yang Baik, Yang Buruk, dan Yang Berbahaya

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.

Implementasi JWT yang Aman

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: Lebih Sederhana dan Lebih Dapat Direvokasi

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

Kerentanan algorithm=none

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.

Yang Saya Implementasikan untuk Sistem ERP

Untuk sistem ERP API-first, pendekatan hybrid bekerja dengan baik: 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.

Pertimbangan Multi-Perangkat dan Multi-Tenant

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 Auth yang Aman

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.

Artikel Terkait

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Mengapa JWT tidak bisa dibatalkan sebelum kedaluwarsa, dan bagaimana cara mengatasinya?

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.

Apa itu kerentanan alg=none pada JWT dan bagaimana cara mencegahnya?

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.

Mengapa JWT access token tidak boleh disimpan di localStorage?

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.

Pendekatan hybrid seperti apa yang bekerja dengan baik untuk autentikasi ERP?

Kombinasi yang baik adalah access token JWT dengan kedaluwarsa 15 menit (ditandatangani RS256, disimpan di memori), refresh token berkedaluwarsa 7 hari 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.

Kapan sebaiknya memilih autentikasi sesi dibanding JWT untuk sistem produksi?

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.