Express.js 5
  • Bab 7 dari 9
  • 25 menit
  • 5 soal kuis

Autentikasi & Otorisasi

Di bab ini (8 bagian)
  1. Tujuan
  2. Memahami Perbedaan Autentikasi dan Otorisasi
  3. Mengamankan Kata Sandi dengan Bcrypt
  4. Mengenal JSON Web Token (JWT)
  5. Implementasi Register dan Login
  6. Memproteksi Rute dengan Middleware
  7. Praktik Terbaik Keamanan Tambahan
  8. Rangkuman

Tujuan bab ini

  • Membedakan proses autentikasi dan otorisasi.
  • Melakukan hashing password dengan bcrypt dan verifikasi JWT.
  • Memproteksi rute berdasarkan peran pengguna.

Memahami Perbedaan Autentikasi dan Otorisasi

Saat membangun antarmuka pemrograman aplikasi (API), keamanan adalah aspek yang paling utama. Langkah pertama untuk mengamankan data adalah memahami perbedaan antara autentikasi dan otorisasi. Walaupun terdengar mirip, keduanya memiliki peran yang sangat berbeda dalam sistem peladen.

Konsep Autentikasi dan Otorisasi

Autentikasi adalah proses untuk menjawab pertanyaan mengenai siapa kamu. Ini adalah tahap di mana sistem memverifikasi identitas pengguna. Contoh paling umum adalah saat pengguna memasukkan alamat email dan kata sandi di halaman login. Jika data yang dikirim cocok dengan yang ada di pangkalan data, sistem akan mengenali pengguna tersebut dan menerbitkan sebuah token atau sesi sebagai bukti identitas yang sah. Jika proses ini gagal, status HTTP yang paling tepat untuk dikembalikan adalah 401 Unauthorized.

Di sisi lain, otorisasi adalah proses untuk menjawab pertanyaan mengenai apa yang boleh kamu lakukan. Setelah sistem tahu siapa kamu, peladen harus memeriksa apakah kamu memiliki hak akses untuk melakukan suatu tindakan tertentu. Misalnya, pengguna biasa mungkin hanya boleh melihat data profilnya sendiri, sedangkan administrator memiliki hak istimewa untuk menambah atau menghapus data di seluruh sistem. Jika pengguna mencoba mengakses rute yang tidak diizinkan untuk perannya, status HTTP yang dikembalikan adalah 403 Forbidden.

Memisahkan kedua konsep ini akan sangat memudahkan kamu dalam menyusun logika keamanan aplikasi secara terstruktur.

Mengamankan Kata Sandi dengan Bcrypt

Salah satu aturan paling mendasar dalam keamanan basis data adalah tidak menyimpan kata sandi dalam bentuk teks biasa. Jika basis data berhasil dibobol, semua akun pengguna akan langsung terancam bahaya. Untuk mencegah musibah ini, kamu perlu mengubah kata sandi menjadi teks acak yang tidak bisa dikembalikan ke bentuk aslinya. Proses ini dikenal dengan istilah hashing.

Dalam ekosistem Node.js dan Express, pustaka yang paling sering digunakan untuk keperluan ini adalah bcrypt. Algoritma bcrypt dirancang secara khusus agar memakan waktu saat dihitung. Tujuannya adalah untuk mencegah peretas menggunakan metode coba-coba (brute force) dengan cepat. Saat kamu membuat hash baru dengan bcrypt, kamu akan diminta memasukkan faktor biaya (cost factor). Angka 12 adalah nilai standar yang disarankan pada saat ini karena memberikan keseimbangan yang paling baik antara tingkat keamanan dan beban kinerja peladen.

Setiap kali ada pengguna baru yang melakukan registrasi, kamu wajib mengubah kata sandinya menggunakan metode hash dari bcrypt. Kemudian, saat pengguna mencoba login di lain waktu, kamu akan menggunakan fungsi perbandingan (compare) dari bcrypt untuk mengecek apakah kata sandi yang diketikkan cocok dengan hash yang tersimpan secara permanen.

Mengenal JSON Web Token (JWT)

Setelah pengguna berhasil masuk, server perlu memberikan tanda pengenal agar pengguna tidak harus mengetikkan ulang email dan kata sandi pada setiap permintaan berikutnya. Di sinilah JSON Web Token atau yang disingkat JWT berperan.

Ada dua pendekatan utama dalam memelihara status sesi login. Pendekatan pertama adalah menggunakan sesi server yang menyimpan data login di dalam memori atau basis data Redis. Pendekatan ini sangat cocok untuk aplikasi web sisi peladen tradisional. Namun, untuk API modern yang dikonsumsi oleh banyak klien seperti aplikasi seluler atau layanan mikro, JWT jauh lebih disukai karena bersifat tanpa status (stateless).

Token JWT memuat klaim atau data identitas pengguna seperti ID pengguna dan perannya. Data ini ditandatangani secara kriptografi oleh peladen menggunakan sebuah kata sandi rahasia. Klien kemudian menyimpan token ini dan selalu menyertakannya kembali ke peladen pada setiap permintaan, biasanya diletakkan di dalam header Authorization.

Keuntungan utama memakai JWT adalah peladen hanya perlu memverifikasi tanda tangan pada token tersebut secara mandiri tanpa harus membebani pangkalan data. Hal ini membuat aplikasimu lebih hemat sumber daya dan mudah dikembangkan. Namun, tantangannya adalah peladen sedikit kesulitan mencabut akses pengguna di tengah jalan, sehingga kamu sangat dianjurkan untuk mengatur masa berlaku token dalam waktu yang singkat.

Implementasi Register dan Login

Mari kita lihat bagaimana cara menggabungkan bcrypt dan JWT di Express untuk membuat antarmuka registrasi dan login. Kode berikut mendemonstrasikan rute yang menangani kedua proses keamanan tersebut.

import express from 'express';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';

const app = express.Router();

// Simulasi model pangkalan data
const User = {
  create: async (data) => ({ id: 1, ...data }),
  findOne: async ({ email }) => {
    if (email === 'tes@app.id') {
      // Kata sandi "rahasia" yang sudah di-hash sebelumnya
      return { id: 1, email, role: 'user', password: '...' };
    }
    return null;
  }
};

app.post('/auth/register', async (req, res) => {
  const { email, name, password } = req.body;

  // Melakukan hashing dengan cost factor 12
  const hash = await bcrypt.hash(password, 12);

  const user = await User.create({
    email,
    name,
    password: hash,
  });

  res.status(201).json({
    id: user.id,
    email: user.email,
  });
});

app.post('/auth/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });

  // Verifikasi kata sandi dengan bcrypt
  const ok = user && await bcrypt.compare(password, user.password);

  if (!ok) {
    return res.status(401).json({ error: 'Email atau kata sandi salah' });
  }

  // Membuat token JWT
  const token = jwt.sign(
    { sub: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  );

  res.json({ token });
});

export default app;

Pada rute /auth/register, kita mulai dengan mengambil data input dari req.body. Kata sandi mentah itu diubah menggunakan bcrypt.hash. Hasil hash yang acak itulah yang dicatat di pangkalan data. Kita kemudian memberikan respons berstatus 201 Created dan dengan sengaja membuang informasi kata sandi dari balasan ke klien.

Berlanjut pada rute /auth/login, sistem mula-mula mencari dokumen pengguna menggunakan kriteria email. Jika berhasil ditemukan, fungsi bcrypt.compare akan mengambil alih pekerjaan mencocokkan kata sandi. Apabila ada satu saja syarat yang meleset, kita memberikan respons error bersandi 401. Menggunakan pesan kesalahan yang seragam adalah hal yang esensial agar peretas kesulitan menerka apakah tebakan email mereka tepat.

Jika semua syarat terpenuhi, peladen akan mencetak token lewat perintah jwt.sign. Di dalam badan token, kita menaruh properti khusus seperti sub yang merujuk pada identitas unik, serta properti peran. Pembuatan token membutuhkan variabel sandi JWT_SECRET dan kita menentukan kedaluwarsa token selama durasi satu jam.

Memproteksi Rute dengan Middleware

Kini peladenmu telah memiliki kemampuan untuk mencetak token identitas. Pekerjaan berikutnya adalah menyusun middleware untuk mengawal rute-rute yang sifatnya rahasia. Dengan cara ini, hanya pengguna dengan token resmi yang bisa mengakses fitur tertentu.

Kita akan menyusun dua lapis middleware. Lapisan pertama adalah authenticate untuk menjamin kesahihan token, dan lapisan kedua adalah authorize untuk menentukan kepantasan peran.

import jwt from 'jsonwebtoken';

export const authenticate = (req, res, next) => {
  const authHeader = req.get('Authorization') ?? '';
  const [type, token] = authHeader.split(' ');

  if (type !== 'Bearer' || !token) {
    return res.status(401).json({ error: 'Token tidak valid atau kosong' });
  }

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (error) {
    res.status(401).json({ error: 'Sesi berakhir, silakan login ulang' });
  }
};

export const authorize = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.sendStatus(403);
    }
    next();
  };
};

Middleware authenticate ditugaskan meneliti isi header Authorization. Standar yang lumrah disepakati di industri adalah mencantumkan kata pembuka Bearer lalu diikuti dengan token. Kita memotong string ini berdasar spasi pembatasnya. Jika rupa header tidak wajar, sistem tanpa ampun akan menyetopnya dengan kiriman status 401.

Perintah jwt.verify berfungsi menakar keaslian token memakai kunci sandi yang sama persis seperti saat pembuatan. Mengingat fungsi ini akan melempar kode error apabila menemukan token cacat atau basi, maka kita wajib menaruhnya di dalam pelindung try...catch. Jika berjalan mulus, identitas tersembunyi ditarik keluar lalu dititipkan ke properti req.user sebelum memanggil laju next().

Sementara itu, middleware authorize punya gaya sedikit berbeda sebab bentuknya menyerupai fungsi yang menelurkan middleware lain. Trik ini berguna agar kamu bebas menitipkan sembarang rupa daftar peran di argumen rute. Komponen ini menilik catatan dari req.user.role dan menyesuaikannya dengan deret peran berizin. Status 403 Forbidden dilempar bila perannya dilarang.

Praktik Terbaik Keamanan Tambahan

Biar begitu, perlindungan JWT dan bcrypt tak bisa berdiri seratus persen sempurna tanpa bantuan ekosistem lain. Ada serangkaian tindakan ekstra yang bisa kamu rakit demi pertahanan berlapis.

Hal yang paling rawan dari klien peramban adalah penyimpanan di ruang lokal. Tempat itu kerap jadi incaran serangan pemalsuan skrip (XSS). Oleh sebab itu, alih-alih melempar token wujud teks mentah ke badan respons, kamu bisa menyelipkannya di kantong cookie dengan penanda httpOnly. Dengan begitu, skrip jahat tak punya peluang untuk mencuri tokenmu.

Sebagai tambahan perisai tangkis peladen dari hantaman percobaan login tiada akhir, libatkan pustaka express-rate-limit khusus pada rute masuk. Pembatas laju ini cukup cerdas menangkal ulah usil bot otomatis. Hal mendasar lainnya, pastikan lajur komunikasimu senantiasa terbalut sertifikat HTTPS agar informasi peka tak terekspos di persimpangan jalan jaringan terbuka.

Rangkuman

  • Autentikasi fokus pada memvalidasi kepingan identitas dengan respons 401 bila melenceng, sedangkan otorisasi fokus menjaga batas akses otoritas dengan respons 403 bila melanggar.
  • Hindari menyimpan kata sandi polos dan manfaatkan rute hash di bcrypt dengan keseimbangan kecepatan optimal di putaran 12.
  • Token JWT membawa pesan terenkripsi nir-status yang sangat gampang beradaptasi di lingkungan tanpa sesi seperti antarmuka REST API.
  • Manfaatkan racikan fungsi Express berupa middleware guna merazia validitas identitas sebelum memproses logika bisnis hakiki.
  • Perkokoh pilar sistem keamananmu melingkupi praktik pembatas laju percobaan dan penggunaan cookie yang tak nampak oleh skrip luar.

Selesaikan bab ini

  1. 1. Baca materi
  2. 2. Kuis (5 soal)

Cek pemahaman

  1. 1Autentikasi dan otorisasi adalah dua istilah yang memiliki makna yang sama dan dapat ditukar penggunaannya di Express.

  2. 2Jika seorang pengguna gagal login karena menginputkan kata sandi yang keliru, kode status HTTP manakah yang paling pas dikirimkan?

  3. 3Perhatikan kode rute login berikut. Mengapa kode ini lebih memilih mengeluarkan tulisan kesahalan yang disamaratakan ketimbang mendetailkan apakah alamat emailnya yang tidak terdaftar?

    app.post('/auth/login', async (req, res) => {
      const user = await User.findOne({ email: req.body.email });
      const ok = user && await bcrypt.compare(req.body.password, user.password);
    
      if (!ok) {
        return res.status(401).json({ error: 'Email atau password salah' });
      }
      // ...
    });
  4. 4Metode penyimpanan token manakah yang disarankan agar kredensial klien terlindung dari risiko serangan pencurian skrip silang-situs (XSS)?

  5. 5Misalkan seorang klien memiliki nilai `req.user.role` dengan sebutan `'user'`. Apa yang bakal diputuskan oleh middleware di bawah ketika klien itu berupaya mengunjungi rute terlarang ini?

    export const authorize = (...roles) => (req, res, next) => {
      if (!roles.includes(req.user.role)) return res.sendStatus(403);
      next();
    };
    
    app.delete('/data', authorize('admin'), hapusData);