Express.js dan JWT

Selamat datang di AnakInformatika! Di era digital ini, membangun aplikasi web modern seringkali melibatkan penggunaan REST API sebagai jembatan komunikasi antara frontend dan backend. Namun, fungsionalitas saja tidak cukup; keamanan adalah fondasi utama yang tidak bisa ditawar.

Bayangkan Anda membangun aplikasi dengan data pengguna yang sensitif. Tanpa pengamanan yang tepat, data tersebut bisa rentan terhadap akses tidak sah. Di tutorial ini, kami akan memandu Anda secara detail tentang Cara Membuat REST API yang Aman dengan Express.js dan JWT, sebuah kombinasi powerful untuk menjamin otentikasi dan otorisasi yang kokoh.

Kita akan membahas mulai dari menyiapkan proyek, membuat sistem registrasi dan login yang aman dengan hashing password menggunakan bcrypt, hingga mengimplementasikan JSON Web Tokens (JWT) untuk melindungi rute-rute API Anda. Siap membangun API yang tidak hanya fungsional tapi juga super aman? Mari kita mulai!

Prasyarat

Sebelum kita menyelam ke dalam kode, pastikan Anda memiliki beberapa prasyarat berikut:

  • Node.js dan npm (Node Package Manager): Pastikan sudah terinstal di sistem Anda. Anda bisa mengunduhnya dari situs resmi Node.js.
  • Pemahaman Dasar JavaScript dan Express.js: Anda diharapkan sudah familiar dengan sintaks dasar JavaScript dan konsep dasar Express.js (routing, middleware).
  • Editor Kode: Visual Studio Code sangat direkomendasikan.
  • Alat untuk Menguji API: Postman atau Insomnia akan sangat membantu untuk menguji endpoint API kita.
  • MongoDB: Kita akan menggunakan MongoDB sebagai database. Anda bisa menginstal MongoDB lokal atau menggunakan layanan MongoDB Atlas gratis.

Langkah 1: Inisialisasi Proyek dan Instalasi Dependensi

Pertama, mari kita siapkan folder proyek dan instal semua pustaka yang kita butuhkan.

1.1 Buat Folder Proyek Baru

Buka terminal atau command prompt Anda dan jalankan perintah berikut:


mkdir secure-api-jwt
cd secure-api-jwt

1.2 Inisialisasi Proyek Node.js

Di dalam folder proyek, inisialisasi proyek Node.js:


npm init -y

Ini akan membuat file package.json.

1.3 Instal Dependensi

Sekarang, instal semua dependensi yang diperlukan:


npm install express jsonwebtoken bcryptjs dotenv mongoose
  • express: Framework web untuk Node.js.
  • jsonwebtoken: Untuk membuat dan memverifikasi JSON Web Tokens (JWT).
  • bcryptjs: Untuk mengenkripsi (hash) password.
  • dotenv: Untuk memuat variabel lingkungan dari file .env.
  • mongoose: Pustaka ODM (Object Data Modeling) untuk MongoDB.

Langkah 2: Konfigurasi Variabel Lingkungan (.env)

Penting untuk menyimpan informasi sensitif seperti kunci rahasia JWT atau URI database di variabel lingkungan, bukan langsung di kode. Buat file bernama .env di root proyek Anda.


# .env
PORT=5000
JWT_SECRET=rahasia_super_kuat_jangan_dibocorkan_ya
MONGO_URI=mongodb://localhost:27017/secure_api_db

Penting: Ganti rahasia_super_kuat_jangan_dibocorkan_ya dengan string acak yang lebih kompleks. Untuk MONGO_URI, sesuaikan dengan konfigurasi MongoDB Anda (lokal atau Atlas).

Langkah 3: Koneksi Database MongoDB

Buat folder config dan file db.js di dalamnya untuk menangani koneksi database.


// config/db.js
const mongoose = require('mongoose');
require('dotenv').config();

const connectDB = async () => {
    try {
        await mongoose.connect(process.env.MONGO_URI, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
            // useCreateIndex: true, // Deprecated in Mongoose 6+
            // useFindAndModify: false // Deprecated in Mongoose 6+
        });
        console.log('MongoDB Connected...');
    } catch (err) {
        console.error(err.message);
        process.exit(1); // Exit process with failure
    }
};

module.exports = connectDB;

Penjelasan Kode:

  • require('dotenv').config(): Memuat variabel dari file .env.
  • mongoose.connect(): Menghubungkan ke MongoDB menggunakan URI dari variabel lingkungan.
  • Opsi useNewUrlParser dan useUnifiedTopology digunakan untuk menghindari peringatan deprecation.
  • Jika koneksi gagal, aplikasi akan keluar dengan error.

Langkah 4: Membuat Model User

Buat folder models dan file User.js di dalamnya. Model ini akan mendefinisikan struktur data pengguna di MongoDB dan menambahkan logika untuk hashing password.


// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const UserSchema = new mongoose.Schema({
    username: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: String,
        required: true
    }
});

// Hash password sebelum menyimpan user baru ke database
UserSchema.pre('save', async function(next) {
    if (!this.isModified('password')) {
        return next();
    }
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
    next();
});

// Metode untuk membandingkan password yang diinput dengan password yang di-hash di database
UserSchema.methods.comparePassword = async function(candidatePassword) {
    return await bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', UserSchema);

Penjelasan Kode:

  • UserSchema.pre('save', ...): Ini adalah middleware Mongoose yang akan berjalan sebelum dokumen User disimpan ke database. Kita menggunakannya untuk mengenkripsi password.
  • bcrypt.genSalt(10): Menghasilkan "salt" (string acak) dengan 10 putaran enkripsi. Salt ini penting untuk keamanan, memastikan dua password yang sama menghasilkan hash yang berbeda.
  • bcrypt.hash(this.password, salt): Mengenkripsi password pengguna menggunakan salt yang telah dibuat.
  • UserSchema.methods.comparePassword = ...: Menambahkan metode kustom ke skema User untuk memverifikasi password. Ini akan membandingkan password yang diinput saat login dengan hash password yang tersimpan.

Langkah 5: Membuat Middleware Autentikasi JWT

Middleware ini akan bertanggung jawab untuk memverifikasi JWT yang dikirimkan oleh klien di setiap permintaan ke rute yang dilindungi.

Buat folder middleware dan file auth.js di dalamnya.


// middleware/auth.js
const jwt = require('jsonwebtoken');
require('dotenv').config();

module.exports = function(req, res, next) {
    // Dapatkan token dari header
    const token = req.header('x-auth-token');

    // Cek jika tidak ada token
    if (!token) {
        return res.status(401).json({ msg: 'Tidak ada token, otorisasi ditolak' });
    }

    // Verifikasi token
    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded.user; // Menambahkan payload user ke objek request
        next();
    } catch (err) {
        res.status(401).json({ msg: 'Token tidak valid' });
    }
};

Penjelasan Kode:

  • req.header('x-auth-token'): Mengambil token dari header permintaan. Biasanya, token dikirimkan dengan nama header Authorization: Bearer , namun di sini kita menggunakan x-auth-token untuk kesederhanaan.
  • jwt.verify(token, process.env.JWT_SECRET): Memverifikasi token menggunakan kunci rahasia JWT yang kita definisikan di .env.
  • Jika token valid, payload (data pengguna) akan didekode dan disimpan di req.user, sehingga bisa diakses oleh rute berikutnya.
  • next(): Meneruskan kontrol ke middleware atau handler rute berikutnya.

Langkah 6: Membuat Rute Autentikasi (Register & Login)

Buat folder routes dan file auth.js di dalamnya. File ini akan berisi endpoint untuk pendaftaran dan login pengguna.


// routes/auth.js
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const User = require('../models/User');
require('dotenv').config();

// @route   POST api/auth/register
// @desc    Mendaftarkan user baru
// @access  Public
router.post('/register', async (req, res) => {
    const { username, password } = req.body;

    try {
        let user = await User.findOne({ username });
        if (user) {
            return res.status(400).json({ msg: 'User sudah terdaftar' });
        }

        user = new User({
            username,
            password
        });

        await user.save(); // Password akan di-hash secara otomatis oleh middleware pre('save') di model User

        // Buat payload untuk JWT
        const payload = {
            user: {
                id: user.id
            }
        };

        // Buat token
        jwt.sign(
            payload,
            process.env.JWT_SECRET,
            { expiresIn: '1h' }, // Token akan kadaluarsa dalam 1 jam
            (err, token) => {
                if (err) throw err;
                res.json({ token });
            }
        );

    } catch (err) {
        console.error(err.message);
        res.status(500).send('Server Error');
    }
});

// @route   POST api/auth/login
// @desc    Login user & mendapatkan token
// @access  Public
router.post('/login', async (req, res) => {
    const { username, password } = req.body;

    try {
        let user = await User.findOne({ username });
        if (!user) {
            return res.status(400).json({ msg: 'Kredensial tidak valid' });
        }

        const isMatch = await user.comparePassword(password);
        if (!isMatch) {
            return res.status(400).json({ msg: 'Kredensial tidak valid' });
        }

        // Buat payload untuk JWT
        const payload = {
            user: {
                id: user.id
            }
        };

        // Buat token
        jwt.sign(
            payload,
            process.env.JWT_SECRET,
            { expiresIn: '1h' },
            (err, token) => {
                if (err) throw err;
                res.json({ token });
            }
        );

    } catch (err) {
        console.error(err.message);
        res.status(500).send('Server Error');
    }
});

module.exports = router;

Penjelasan Kode:

  • Register:
    • Menerima username dan password dari body permintaan.
    • Mencari apakah username sudah ada.
    • Jika belum, membuat user baru. Password secara otomatis di-hash saat disimpan berkat pre('save') di model.
    • Membuat JWT dengan jwt.sign(). Payload hanya berisi user.id, bukan informasi sensitif lainnya.
    • Mengatur token kadaluarsa dalam 1 jam (expiresIn: '1h').
    • Mengirimkan token kembali ke klien.
  • Login:
    • Menerima username dan password.
    • Mencari user berdasarkan username.
    • Membandingkan password yang diinput dengan password yang di-hash di database menggunakan user.comparePassword().
    • Jika cocok, membuat dan mengirimkan JWT seperti pada registrasi.

Langkah 7: Membuat Rute Terlindungi

Sekarang, mari kita buat contoh rute yang hanya bisa diakses oleh pengguna yang sudah terotentikasi.

Buat file protected.js di folder routes.


// routes/protected.js
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth');
const User = require('../models/User');

// @route   GET api/protected/me
// @desc    Mendapatkan informasi user yang sedang login
// @access  Private
router.get('/me', auth, async (req, res) => {
    try {
        // req.user.id berasal dari middleware auth setelah token diverifikasi
        const user = await User.findById(req.user.id).select('-password'); // Jangan kirim password
        res.json(user);
    } catch (err) {
        console.error(err.message);
        res.status(500).send('Server Error');
    }
});

// @route   GET api/protected/data
// @desc    Contoh data yang hanya bisa diakses user terotentikasi
// @access  Private
router.get('/data', auth, (req, res) => {
    res.json({ msg: `Halo, ${req.user.id}! Anda berhasil mengakses data rahasia.` });
});

module.exports = router;

Penjelasan Kode:

  • router.get('/me', auth, ...): Perhatikan penggunaan auth sebagai middleware kedua. Ini berarti setiap permintaan ke rute ini akan melewati middleware auth terlebih dahulu. Jika token valid, permintaan akan dilanjutkan ke handler rute.
  • req.user.id: Kita bisa mengakses ID pengguna dari objek req karena middleware auth telah menambahkannya setelah memverifikasi token.
  • .select('-password'): Penting untuk tidak mengirimkan hash password ke klien, jadi kita mengecualikannya dari hasil query.

Langkah 8: Mengatur Server Utama (app.js)

Terakhir, mari kita satukan semua bagian di file server utama kita. Buat file app.js di root proyek.


// app.js
const express = require('express');
const connectDB = require('./config/db');
require('dotenv').config();

const app = express();

// Sambungkan ke Database
connectDB();

// Middleware: Body Parser (untuk membaca JSON dari request body)
app.use(express.json({ extended: false }));

// Rute Utama
app.get('/', (req, res) => res.send('API Berjalan...'));

// Definisi Rute API
app.use('/api/auth', require('./routes/auth'));
app.use('/api/protected', require('./routes/protected'));

const PORT = process.env.PORT || 5000;

app.listen(PORT, () => console.log(`Server berjalan di port ${PORT}`));

Penjelasan Kode:

  • connectDB(): Memanggil fungsi koneksi database yang telah kita buat.
  • app.use(express.json({ extended: false })): Middleware ini memungkinkan Express untuk mem-parsing JSON dari body permintaan (misalnya, saat register atau login).
  • app.use('/api/auth', require('./routes/auth')): Mengaitkan semua rute dari routes/auth.js di bawah path /api/auth.
  • app.use('/api/protected', require('./routes/protected')): Mengaitkan semua rute dari routes/protected.js di bawah path /api/protected.

Langkah 9: Menjalankan dan Menguji API

Sekarang saatnya menjalankan server Anda dan menguji endpoint API yang telah kita buat.

9.1 Jalankan Server

Di terminal Anda, jalankan server:


node app.js

Anda akan melihat output seperti ini:


MongoDB Connected...
Server berjalan di port 5000

9.2 Uji API Menggunakan Postman/Insomnia

  1. Registrasi Pengguna Baru

    • Metode: POST
    • URL: http://localhost:5000/api/auth/register
    • Headers: Content-Type: application/json
    • Body (Raw JSON):
      
      {
          "username": "tester",
          "password": "password123"
      }
                      
    • Response Sukses: Anda akan mendapatkan sebuah objek JSON berisi token. Simpan token ini!
  2. Login Pengguna

    • Metode: POST
    • URL: http://localhost:5000/api/auth/login
    • Headers: Content-Type: application/json
    • Body (Raw JSON):
      
      {
          "username": "tester",
          "password": "password123"
      }
                      
    • Response Sukses: Anda akan mendapatkan objek JSON berisi token yang baru. Simpan token ini, karena ini adalah token yang akan Anda gunakan untuk mengakses rute yang dilindungi.
  3. Akses Rute Terlindungi (Tanpa Token)

    • Metode: GET
    • URL: http://localhost:5000/api/protected/me
    • Response: Anda akan mendapatkan {"msg": "Tidak ada token, otorisasi ditolak"} dengan status 401. Ini menunjukkan middleware autentikasi kita bekerja!
  4. Akses Rute Terlindungi (Dengan Token)

    • Metode: GET
    • URL: http://localhost:5000/api/protected/me
    • Headers:
      • Content-Type: application/json
      • x-auth-token: (Ganti dengan token yang Anda simpan dari langkah login)
    • Response Sukses: Anda akan mendapatkan objek JSON berisi informasi pengguna Anda (username dan ID, tanpa password).
  5. Akses Rute Terlindungi Lainnya

    • Metode: GET
    • URL: http://localhost:5000/api/protected/data
    • Headers:
      • Content-Type: application/json
      • x-auth-token:
    • Response Sukses: Anda akan mendapatkan {"msg": "Halo, ! Anda berhasil mengakses data rahasia."}.

Selamat! Anda telah berhasil mengimplementasikan Cara Membuat REST API yang Aman dengan Express.js dan JWT.

Tips Praktis dan Best Practices untuk Keamanan API Anda

Meskipun kita sudah membuat dasar API yang aman, selalu ada ruang untuk peningkatan. Berikut adalah beberapa tips dan praktik terbaik untuk memperkuat keamanan API Anda lebih lanjut:

1. Keamanan JWT

  • Rahasia JWT yang Kuat: Gunakan string acak yang sangat panjang dan kompleks untuk JWT_SECRET Anda. Jangan pernah hardcode di kode; selalu gunakan variabel lingkungan. Anda bisa menghasilkan string acak dengan node -e "console.log(require('crypto').randomBytes(32).toString('hex'))".
  • Masa Kadaluarsa Token yang Wajar: Atur expiresIn agar token tidak berlaku terlalu lama. Untuk aplikasi web, beberapa menit hingga beberapa jam sudah cukup. Untuk aplikasi seluler, mungkin lebih lama tetapi pertimbangkan implementasi refresh token.
  • Refresh Token: Untuk UX yang lebih baik dan keamanan yang lebih tinggi, implementasikan sistem refresh token. Token akses berumur pendek, sedangkan token refresh berumur panjang dan digunakan untuk mendapatkan token akses baru tanpa harus login ulang.
  • Jangan Simpan Data Sensitif di Payload JWT: Payload JWT dapat di-decode oleh siapa pun. Simpan hanya informasi yang diperlukan untuk identifikasi pengguna (misalnya, user ID) dan otorisasi (misalnya, peran pengguna).
  • HTTPS/SSL: Selalu gunakan HTTPS di lingkungan produksi. Ini mengenkripsi komunikasi antara klien dan server, melindungi JWT agar tidak dicuri saat transit.

2. Keamanan Password

  • Salt Rounds yang Cukup: Nilai 10 untuk bcrypt.genSalt(10) adalah standar yang baik, tetapi Anda bisa meningkatkannya (misalnya 12) jika Anda memiliki sumber daya komputasi yang memadai. Semakin tinggi nilainya, semakin lama proses hashing dan semakin aman, tetapi juga semakin memakan waktu.
  • Validasi Input Password: Terapkan kebijakan password yang kuat (minimal panjang, kombinasi huruf besar/kecil, angka, simbol) di sisi klien dan server.
  • Jangan Simpan Plainteks Password: Ini adalah aturan emas. Selalu hash password sebelum menyimpannya.

3. Validasi Input

  • Validasi di Sisi Server: Selain validasi di sisi klien, selalu lakukan validasi ketat di sisi server untuk semua input pengguna (misalnya, untuk username, password, email). Pustaka seperti express-validator dapat sangat membantu.
  • Sanitasi Input: Bersihkan input pengguna untuk mencegah serangan seperti XSS (Cross-Site Scripting) atau SQL Injection (meskipun dengan ODM seperti Mongoose, risiko SQL Injection lebih rendah).

4. Penanganan Error

  • Pesan Error yang Informatif namun Tidak Terlalu Detail: Berikan pesan error yang membantu klien memahami apa yang salah, tetapi jangan sampai membocorkan detail implementasi internal (misalnya, nama tabel database, stack trace lengkap).
  • Global Error Handler: Implementasikan middleware penanganan error global untuk menangkap semua error yang tidak tertangani dan mengirimkan respons yang konsisten.

5. Rate Limiting

  • Mencegah Brute-Force Attacks: Gunakan middleware rate limiting (misalnya, express-rate-limit) untuk membatasi jumlah permintaan yang dapat dilakukan klien dalam jangka waktu tertentu, terutama pada endpoint login dan registrasi. Ini membantu mencegah serangan brute-force.

6. CORS (Cross-Origin Resource Sharing)

  • Konfigurasi yang Benar: Jika API Anda diakses dari domain yang berbeda (misalnya, aplikasi frontend di http://localhost:3000), pastikan Anda mengkonfigurasi CORS dengan benar. Gunakan pustaka cors dan hanya izinkan origin yang terpercaya.
    
    // app.js
    const cors = require('cors');
    // ...
    app.use(cors({
        origin: 'http://localhost:3000', // Ganti dengan domain frontend Anda
        methods: ['GET', 'POST', 'PUT', 'DELETE'],
        allowedHeaders: ['Content-Type', 'Authorization', 'x-auth-token']
    }));
            

Kesimpulan

Anda telah berhasil menyelesaikan tutorial komprehensif tentang Cara Membuat REST API yang Aman dengan Express.js dan JWT. Kita telah belajar bagaimana mengkonfigurasi proyek, mengamankan password dengan bcrypt, mengelola otentikasi menggunakan JWT, dan melindungi rute API Anda.

Memahami dan menerapkan praktik keamanan adalah kunci untuk membangun aplikasi yang andal dan terpercaya. Ingatlah bahwa keamanan adalah proses berkelanjutan; selalu perbarui pengetahuan Anda tentang ancaman dan solusi terbaru.

Teruslah bereksperimen, bangun proyek Anda sendiri, dan jangan ragu untuk kembali ke tutorial ini sebagai referensi. Selamat ngoding, AnakInformatika!