Authentication adalah fondasi keamanan di hampir setiap aplikasi web modern. Di antara berbagai metode yang ada, JSON Web Token (JWT) telah menjadi standar emas bagi banyak developer, terutama untuk membangun API RESTful yang stateless dan skalabel. Tapi, bagaimana sebenarnya kita “membuat” JWT dari nol di sisi backend? Apa saja komponen yang terlibat, dan bagaimana kita memastikan keamanannya?
Sebagai seorang developer yang sering berkutat dengan backend dan API, saya menemukan bahwa memahami JWT secara mendalam bukan hanya soal menghafal sintaksis, tapi juga memahami filosofi di baliknya. Ini bukan sekadar mekanisme untuk “login”, tapi sebuah protokol yang memfasilitasi komunikasi aman dan terpercaya antara klien dan server. Artikel ini akan memandu Anda langkah demi langkah untuk membangun sistem autentikasi JWT dari nol, lengkap dengan praktik terbaik dan masalah umum yang sering saya temui di lapangan.
Memahami Dasar-dasar JSON Web Token (JWT)
Sebelum kita terjun ke kode, mari kita pahami dulu apa itu JWT. JWT adalah standar terbuka (RFC 7519) yang mendefinisikan cara aman untuk mentransmisikan informasi antar pihak sebagai objek JSON. Informasi ini dapat diverifikasi karena ditandatangani secara digital.
Struktur JWT: Tiga Bagian Penting
Sebuah JWT terdiri dari tiga bagian, dipisahkan oleh titik (.), dan masing-masing dienkode dalam Base64Url:
- Header: Berisi metadata tentang token, seperti tipe token (misalnya, “JWT”) dan algoritma hashing yang digunakan untuk tanda tangan (misalnya, HS256 atau RS256).
- Payload: Ini adalah inti dari token, berisi klaim (claims) tentang entitas (biasanya user) dan metadata tambahan. Klaim bisa berupa klaim standar (seperti
iss– issuer,exp– expiration time,sub– subject), klaim publik, atau klaim privat yang didefinisikan oleh developer. - Signature: Bagian ini digunakan untuk memverifikasi bahwa token belum diubah di tengah jalan dan berasal dari sumber yang valid. Signature dibuat dengan menggabungkan Header dan Payload yang telah dienkode, lalu di-hash menggunakan algoritma yang ditentukan di Header, bersama dengan kunci rahasia (secret key) yang hanya diketahui oleh server.
Ketika server menerima JWT, server akan memverifikasi signature menggunakan kunci rahasia yang sama. Jika signature tidak cocok, artinya token telah dirusak atau tidak dikeluarkan oleh server yang sah.
Kenapa Memilih JWT untuk Autentikasi?
Ada beberapa alasan mengapa JWT populer:
- Statelessness: Server tidak perlu menyimpan informasi sesi. Setiap request yang datang membawa token berisi semua informasi yang diperlukan untuk autentikasi dan otorisasi. Ini sangat menguntungkan untuk aplikasi skalabel, microservices, atau mobile apps.
- Ringan: JWT relatif kecil, sehingga efisien untuk dikirimkan melalui header HTTP.
- Keamanan: Dengan digital signature, integritas token terjamin.
- Cepat: Verifikasi token di server biasanya lebih cepat daripada mencari sesi di database.
- Cross-domain: JWT dapat digunakan di berbagai domain, cocok untuk arsitektur SSO (Single Sign-On).
Persiapan Lingkungan Pengembangan
Untuk panduan ini, kita akan menggunakan Node.js dan framework Express. Pastikan Anda sudah menginstal Node.js dan npm (Node Package Manager) di sistem Anda. Kita akan menggunakan beberapa library npm yang esensial:
express: Framework web untuk Node.js.jsonwebtoken: Library untuk mengimplementasikan JWT.dotenv: Untuk mengelola environment variables (kunci rahasia JWT).bcryptjs: Untuk mengenkripsi password user sebelum disimpan di database.
Mari kita mulai dengan membuat proyek baru:
- Buat folder proyek baru dan masuk ke dalamnya.
- Inisialisasi proyek Node.js.
- Instal semua dependency yang diperlukan.
- Buat file
.envdi root folder proyek untuk menyimpan kunci rahasia JWT. - Di dalam file
.env, tambahkan baris berikut. GantiYOUR_SECRET_KEYdengan string acak yang kuat.
mkdir jwt-auth-app
cd jwt-auth-app
npm init -y
npm install express jsonwebtoken dotenv bcryptjs
touch .env
JWT_SECRET=YOUR_SECRET_KEY_YANG_SANGAT_RAHASIA
REFRESH_TOKEN_SECRET=ANOTHER_SUPER_SECRET_KEY
Langkah 1: Membuat Server Sederhana dengan Express
Kita akan memulai dengan membuat server Express dasar yang akan menangani request HTTP.
Buat file app.js di root folder proyek Anda:
touch app.js
Di dalam app.js, kita akan menginisialisasi Express dan mengatur beberapa konfigurasi dasar.
Pertama, kita akan memuat library yang dibutuhkan: Express, dotenv untuk mengakses variabel lingkungan, dan jsonwebtoken serta bcryptjs yang akan kita gunakan nanti.
Kemudian, inisialisasi aplikasi Express dan atur agar dapat mem-parse JSON dari body request. Kita juga akan menginisialisasi sebuah array untuk “database” sederhana kita yang akan menyimpan data user dan refresh token. Dalam aplikasi nyata, ini akan diganti dengan database seperti PostgreSQL atau MongoDB.
Terakhir, kita akan membuat server Express mendengarkan pada port tertentu, misalnya port 3000.
Langkah 2: Proses Registrasi dan Login (Membuat Token)
Ini adalah bagian krusial di mana user akan mendaftar dan login, lalu server akan mengeluarkan JWT.
Endpoint Registrasi
Endpoint ini akan menerima username dan password dari user, mengenkripsi password, lalu menyimpannya. Dalam contoh ini, kita akan menyimpannya dalam array users. Di produksi, Anda akan berinteraksi dengan database.
Saat membuat endpoint registrasi, kita akan menunggu request POST ke /register. Dari body request, kita akan mengambil username dan password. Password yang diterima tidak boleh langsung disimpan. Kita harus mengenkripsinya terlebih dahulu menggunakan bcryptjs.hash(). Proses hashing ini bersifat asinkron, jadi kita akan menggunakan await. Setelah password di-hash, kita akan simpan user baru ke dalam array users kita dan memberikan respons sukses.
Endpoint Login
Endpoint ini akan menerima username dan password, memverifikasinya, dan jika berhasil, akan membuat JWT dan mengirimkannya kembali ke klien.
Untuk endpoint login, kita akan menunggu request POST ke /login. Kita akan mencari user berdasarkan username yang diberikan. Jika user tidak ditemukan, kita akan memberikan respons error. Jika ditemukan, kita akan membandingkan password yang diberikan dengan password hash yang tersimpan menggunakan bcryptjs.compare(). Jika password cocok, ini saatnya membuat token.
Kita akan membuat dua jenis token di sini: Access Token dan Refresh Token.
- Access Token: Token utama yang akan digunakan klien untuk mengakses resource yang dilindungi. Access token sebaiknya memiliki masa berlaku pendek (misalnya, 15 menit hingga 1 jam) untuk meminimalkan risiko jika bocor.
- Refresh Token: Token yang memiliki masa berlaku lebih panjang. Digunakan untuk mendapatkan access token baru setelah access token lama kedaluwarsa, tanpa perlu user login ulang. Refresh token ini harus disimpan di database server dan juga di sisi klien.
Untuk membuat token, kita akan menggunakan fungsi jsonwebtoken.sign(). Fungsi ini membutuhkan payload (misalnya, ID user), kunci rahasia (diambil dari process.env.JWT_SECRET untuk access token dan process.env.REFRESH_TOKEN_SECRET untuk refresh token), dan opsi masa berlaku (misalnya, { expiresIn: '1h' } untuk access token dan { expiresIn: '7d' } untuk refresh token). Refresh token yang dihasilkan juga perlu disimpan di sisi server (dalam array refreshTokens kita) agar bisa divalidasi nanti.
Setelah kedua token dibuat, kita akan mengirimkannya kembali ke klien dalam respons HTTP.
Langkah 3: Melindungi Endpoint dengan Middleware (Memverifikasi Token)
Sekarang kita punya token, bagaimana cara menggunakannya untuk melindungi resource? Kita akan membuat middleware yang akan mencegat setiap request ke endpoint yang dilindungi, memverifikasi JWT yang dikirim, dan jika valid, akan mengizinkan request untuk melanjutkan.
Membuat Middleware authenticateToken
Middleware ini akan melakukan hal berikut:
- Mengekstrak token dari header
Authorization(format biasanya “Bearer TOKEN”). - Memeriksa apakah token ada.
- Memverifikasi token menggunakan
jsonwebtoken.verify()dengan kunci rahasia yang sama saat token dibuat. - Jika verifikasi gagal (misalnya, token tidak valid atau kedaluwarsa), kirim respons error.
- Jika verifikasi berhasil, tambahkan informasi user (dari payload token) ke objek
reqdan panggilnext()untuk melanjutkan ke handler route yang sebenarnya.
Dalam middleware ini, kita akan mendapatkan header Authorization dari request. Jika header tidak ada, berarti tidak ada token, dan kita akan mengirimkan respons 401 Unauthorized. Jika ada, kita akan mengekstrak bagian token-nya (setelah “Bearer “). Kemudian, kita akan menggunakan jsonwebtoken.verify(). Fungsi ini membutuhkan token yang akan diverifikasi, kunci rahasia (process.env.JWT_SECRET), dan sebuah callback function. Callback ini akan dipanggil dengan error jika token tidak valid atau dengan payload yang didekode jika token valid. Jika ada error, kita kirim respons 403 Forbidden. Jika berhasil, kita menyimpan data user dari payload ke req.user dan memanggil next().
Melindungi Contoh Endpoint
Sekarang kita bisa menggunakan middleware authenticateToken untuk melindungi endpoint manapun.
Misalnya, kita punya endpoint /dashboard yang hanya boleh diakses oleh user yang sudah login.
Kita cukup menambahkan authenticateToken sebagai argumen kedua di route handler Express kita. Jadi, saat ada request ke /dashboard, middleware akan dieksekusi terlebih dahulu. Jika token valid, request akan dilanjutkan ke handler yang sebenarnya dan kita bisa mengakses req.user.id untuk mendapatkan ID user yang sedang login.
Langkah 4: Implementasi Refresh Token (Opsional tapi Penting)
Masa berlaku access token yang pendek adalah praktik keamanan yang baik, tetapi bisa merepotkan user karena harus sering login. Di sinilah refresh token berperan.
Kenapa Refresh Token Penting?
- User Experience: User tidak perlu login ulang setiap kali access token kedaluwarsa.
- Keamanan: Jika access token dicuri, masa berlakunya yang pendek membatasi jendela waktu penyerang. Refresh token yang lebih sensitif disimpan lebih aman (misalnya, di HTTP-only cookie atau di database).
Workflow Refresh Token
- Klien menggunakan refresh token yang valid untuk meminta access token baru ke server.
- Server memverifikasi refresh token.
- Jika refresh token valid dan ada di daftar refresh token yang tersimpan di server, server akan mengeluarkan access token baru (dan opsional refresh token baru).
- Refresh token yang lama sebaiknya di-invalidate setelah digunakan, atau diatur untuk sekali pakai (one-time use).
Endpoint /token/refresh
Kita akan membuat endpoint baru yang menerima refresh token dari klien, memverifikasinya, dan jika valid, akan mengembalikan access token baru.
Endpoint ini akan menerima request POST ke /token/refresh. Klien akan mengirimkan refresh token di body request. Kita akan memeriksa apakah refresh token yang dikirim ada di daftar refreshTokens yang tersimpan di server kita. Jika tidak ada, itu adalah refresh token yang tidak valid, dan kita akan mengirim respons 403 Forbidden.
Jika refresh token ditemukan, kita akan memverifikasinya menggunakan jsonwebtoken.verify() dengan kunci rahasia refresh token (process.env.REFRESH_TOKEN_SECRET). Sama seperti verifikasi access token, jika ada error, kita kirim respons 403. Jika berhasil, kita bisa membuat access token baru dengan payload yang sama (atau diperbarui) dan mengirimkannya kembali ke klien.
Penting untuk diingat bahwa refresh token juga harus dilindungi. Dalam aplikasi nyata, refresh token sering disimpan di HTTP-only cookie untuk mencegah akses dari JavaScript di sisi klien, atau disimpan di database yang aman.
Masalah yang Sering Terjadi dan Solusinya
Dalam implementasi JWT, beberapa masalah umum sering muncul:
1. Token Expired atau Invalid
- Gejala: Klien menerima status 403 Forbidden atau 401 Unauthorized, meskipun sebelumnya bisa mengakses. Pesan error biasanya menyebut “jwt expired” atau “invalid signature”.
- Penyebab: Access token memiliki masa berlaku yang pendek dan telah kedaluwarsa, atau token telah dimodifikasi, atau kunci rahasia di server berbeda dengan yang digunakan saat pembuatan token.
- Solusi:
- Pastikan kunci rahasia (
JWT_SECRET) di file.envsama persis di lingkungan produksi dan pengembangan. - Jika access token kedaluwarsa, klien harus secara otomatis menggunakan refresh token untuk mendapatkan access token baru melalui endpoint
/token/refresh. - Di sisi server, tangani error dari
jsonwebtoken.verify()dengan tepat untuk membedakan antara expired token dan invalid token.
- Pastikan kunci rahasia (
2. Kunci Rahasia (Secret Key) Tidak Aman
- Gejala: Potensi kompromi keamanan jika kunci rahasia bocor, memungkinkan penyerang membuat token palsu yang valid.
- Penyebab: Kunci rahasia di-hardcode di kode, disimpan di repositori publik, atau terlalu sederhana.
- Solusi:
- Selalu simpan kunci rahasia di variabel lingkungan (
.env) dan pastikan file.envtidak di-commit ke Git (gunakan.gitignore). - Gunakan kunci rahasia yang panjang, acak, dan kompleks. Anda bisa membuat string acak yang kuat.
- Rotasi kunci rahasia secara berkala jika ada kekhawatiran kompromi.
- Selalu simpan kunci rahasia di variabel lingkungan (
3. Penyimpanan Token di Frontend
- Gejala: Klien menyimpan JWT di
localStorageatausessionStorage, yang rentan terhadap serangan XSS (Cross-Site Scripting). - Penyebab: Developer menyimpan token di tempat yang mudah diakses oleh JavaScript berbahaya.
- Solusi:
- Untuk access token, jika disimpan di
localStorage, pastikan untuk mengimplementasikan Content Security Policy (CSP) yang kuat dan praktik pengodean yang aman untuk mencegah XSS. - Untuk refresh token, praktik terbaik adalah menyimpannya di HTTP-only cookie. Cookie ini tidak dapat diakses oleh JavaScript, sehingga lebih aman dari XSS. Pastikan juga cookie memiliki flag
Secure(hanya dikirim melalui HTTPS) danSameSite=Lax/Strictuntuk mitigasi CSRF.
- Untuk access token, jika disimpan di
4. Replay Attack dengan Refresh Token
- Gejala: Penyerang berhasil mencuri refresh token dan menggunakannya berulang kali untuk mendapatkan access token baru.
- Penyebab: Refresh token tidak di-invalidate setelah digunakan atau tidak memiliki masa berlaku.
- Solusi:
- Implementasikan refresh token rotation: Setiap kali refresh token digunakan untuk mendapatkan access token baru, buat refresh token baru dan segera invalidate yang lama.
- Berikan masa berlaku yang panjang pada refresh token, tetapi tetap batasi.
- Simpan refresh token di database server dengan status “digunakan” atau “kedaluwarsa” setelah dipakai.
Pengalaman dan Pertimbangan Praktis
Membangun autentikasi JWT dari nol memang memberikan pemahaman mendalam, tetapi ada beberapa pertimbangan praktis yang sering saya alami di project nyata:
Kapan Menggunakan JWT vs. Session-Based Authentication?
Dalam praktiknya, pilihan antara JWT dan session-based auth seringkali tergantung pada arsitektur aplikasi.
- JWT lebih cocok untuk: API RESTful, aplikasi mobile, microservices, dan arsitektur Single Page Application (SPA) yang membutuhkan statelessness dan skalabilitas horizontal yang mudah. Misalnya, pada project dengan backend terpisah dari frontend, JWT adalah pilihan ideal.
- Session-based authentication lebih cocok untuk: Aplikasi web tradisional (monolitik) yang menggunakan SSR (Server-Side Rendering) dan memiliki server state yang kuat. Untuk aplikasi WordPress atau CMS tradisional, session seringkali lebih mudah diimplementasikan.
Di project skala kecil atau yang hanya melibatkan satu server, session mungkin terasa lebih sederhana pada awalnya. Namun, begitu Anda butuh scaling atau memisahkan frontend/backend, JWT mulai menunjukkan keunggulannya.
Revoking Token
Salah satu “kelemahan” JWT yang sering dibahas adalah sulitnya melakukan revocation (pembatalan) token yang sudah diterbitkan sebelum masa berlakunya habis. Karena JWT bersifat stateless, server tidak menyimpan catatan token mana yang aktif. Jika user logout atau token dicuri, kita ingin segera membatalkannya.
Untuk mengatasi ini, ada beberapa pendekatan:
- Blacklist (Daftar Hitam): Server menyimpan daftar JWT yang telah dibatalkan (misalnya, di Redis). Setiap kali token diterima, server akan memeriksa apakah token ada di blacklist. Ini mengorbankan sedikit statelessness karena server harus menyimpan state. Saya sering menggunakan Redis untuk implementasi blacklist ini karena kecepatannya.
- Masa Berlaku Pendek: Batasi masa berlaku access token sangat pendek (misalnya 15-30 menit) sehingga jika token dicuri, jangkauan serangannya terbatas. Refresh token yang lebih panjang bisa di-revoke atau di-rotate.
- Change Secret Key: Jika keamanan sangat terancam dan banyak token berpotensi bocor, mengganti kunci rahasia akan secara instan meng-invalidate semua token yang pernah diterbitkan dengan kunci lama. Ini adalah langkah drastis yang membutuhkan semua user untuk login ulang.
Scalability dan Performance
Karena JWT bersifat stateless, verifikasi token dapat dilakukan oleh server manapun di belakang load balancer tanpa perlu berbagi informasi sesi. Ini membuat arsitektur JWT sangat skalabel. Setiap instance server dapat memverifikasi token secara independen, yang sangat penting di lingkungan cloud computing atau arsitektur microservices.
Penyimpanan Kunci Rahasia yang Aman
Ini adalah poin yang tidak bisa ditawar. Kunci rahasia JWT harus diperlakukan seperti password database utama Anda. Dalam lingkungan produksi, sebaiknya gunakan layanan manajemen rahasia (secret management service) seperti AWS Secrets Manager, Google Secret Manager, atau HashiCorp Vault, bukan hanya file .env biasa. Ini akan memastikan kunci tidak pernah terekspos dalam kode atau sistem file yang tidak aman.
Payload Data yang Bijak
Jangan pernah menyimpan informasi sensitif di dalam payload JWT (misalnya, password, informasi keuangan). Meskipun dienkode Base64, payload tidak terenkripsi dan bisa dengan mudah dibaca. Simpan hanya informasi yang diperlukan untuk identifikasi user dan otorisasi (misalnya, user ID, role, permission). Jika ada data sensitif yang perlu diakses, gunakan ID user di payload untuk mengambil data tersebut dari database.
Pada project yang saya kerjakan, saya biasanya hanya menyimpan user_id dan role di payload access token. Ini sudah cukup untuk sebagian besar kebutuhan otorisasi tanpa mengorbankan keamanan.
FAQ
JWT bersifat stateless, artinya server tidak perlu menyimpan informasi sesi. Setiap request membawa token yang berisi semua data autentikasi. Session cookies bersifat stateful; server menyimpan informasi sesi dan klien hanya menyimpan ID sesi di cookie untuk merujuk ke sesi tersebut.
Apakah JWT terenkripsi?
Tidak. JWT hanya signed (ditandatangani), bukan dienkripsi. Payload JWT dienkode Base64, yang artinya informasi di dalamnya mudah dibaca oleh siapapun yang memiliki token. Oleh karena itu, jangan menyimpan informasi sensitif di payload JWT.
Bagaimana cara mengamankan JWT dari serangan XSS dan CSRF?
Untuk XSS, jangan menyimpan JWT (terutama refresh token) di localStorage atau sessionStorage. Gunakan HTTP-only cookie untuk refresh token. Untuk CSRF, pastikan cookie memiliki atribut SameSite=Lax atau Strict, dan untuk form POST, gunakan token CSRF tambahan.
Bolehkah saya menyimpan semua data user di payload JWT?
Sebaiknya tidak. Simpan hanya data minimal yang dibutuhkan untuk autentikasi dan otorisasi (misalnya, user ID, role). Data sensitif atau data yang sering berubah sebaiknya diambil dari database menggunakan user ID yang ada di payload JWT.
Bagaimana cara memperbarui token yang sudah kedaluwarsa?
Klien harus menggunakan refresh token (yang memiliki masa berlaku lebih panjang dan disimpan lebih aman) untuk meminta access token baru dari server melalui endpoint khusus (misalnya, /token/refresh). Server akan memverifikasi refresh token dan jika valid, mengeluarkan access token baru.
Kesimpulan
Membangun sistem autentikasi JWT dari nol memang membutuhkan pemahaman yang komprehensif, mulai dari struktur token, proses pembuatan, hingga mekanisme verifikasi dan manajemen refresh token. Dengan implementasi yang tepat, JWT menawarkan solusi autentikasi yang skalabel, efisien, dan aman untuk aplikasi modern.
Pengalaman saya menunjukkan bahwa kunci keberhasilan terletak pada detail: manajemen kunci rahasia yang kuat, masa berlaku token yang bijak, strategi penyimpanan token di sisi klien yang aman, dan implementasi refresh token yang kokoh. Ini bukan hanya tentang membuat kode bekerja, tetapi tentang membangun sistem yang tangguh dan terpercaya. Semoga panduan ini memberikan fondasi yang kuat bagi Anda para developer untuk melangkah lebih jauh dalam membangun aplikasi yang aman dan modern.
Sebuah ilustrasi digital yang menunjukkan flow authentication JWT. Di satu sisi, ada ikon user (klien) dengan laptop atau smartphone, mengirimkan username dan password. Di sisi lain, ada ikon server yang lebih besar dengan database dan gembok (keamanan). Panah menunjukkan aliran: user mengirim kredensial ke server, server memverifikasi dan mengeluarkan dua token (access token berwarna hijau cerah dan refresh token berwarna biru gelap), yang kemudian dikirim kembali ke klien. Panah lain menunjukkan klien mengirim access token ke server untuk mengakses resource yang dilindungi. Latar belakang memiliki elemen jaringan digital abstrak atau garis-garis kode. Gaya modern, bersih, dan profesional.
TAGS: JWT, Authentication, Node.js, Express.js, Keamanan API, Backend Development, Web Development, Programming Tutorial


