Proyek ini menghimpun materi Section Authentication dan Authorization, Security, dan Database Integration menjadi satu layanan yang utuh: layanan autentikasi tempat client mendaftar, masuk, lalu membawa token untuk mengakses endpoint terlindungi. Kita membangunnya tanpa framework dan tanpa package autentikasi — cukup node:http, node:crypto, dan node:sqlite — karena tujuan proyek ini adalah melihat seluruh keputusan keamanan berada di baris kode mana. Sandi disimpan sebagai hash scrypt bersalt, token ditandatangani HMAC-SHA256 dan membawa peran, serta percobaan login dibatasi rate limiter jendela tetap. Untuk akses terlindungi kita memilih pola header Authorization: Bearer karena paling mudah diuji dari test; pola session dengan cookie HttpOnly dibahas sebagai alternatif pada Bagian 3.

Yang Kita Bangun #

Resource utama layanan ini adalah pengguna dengan tiga field: nama (3-30 karakter, unik), sandi (tidak pernah disimpan dalam bentuk aslinya), dan peran (pengguna atau admin). Berikut seluruh endpoint yang kita sediakan:

MethodURLAksiSuksesGalat
POST/daftarDaftar pengguna baru201400, 422, 409
POST/loginMasuk dan terbitkan token200 + token400, 401, 429
GET/akuProfil pemilik token200401
GET/adminDaftar seluruh pengguna200401, 403

Pemisahan status code mengikuti disiplin proyek REST API sebelumnya: 400 berarti body tidak bisa dibaca sebagai JSON, 422 berarti data tidak lolos validasi, 409 berarti nama sudah terdaftar, 401 berarti kredensial atau token tidak diterima, 403 berarti token sah tetapi peran kurang, dan 429 berarti percobaan login melewati batas dalam satu jendela waktu. Semua galat memakai satu bentuk response:

json Bentuk error seragam
{
  "error": {
    "kode": "LOGIN_GAGAL",
    "pesan": "Nama atau sandi salah"
  }
}

Field kode stabil untuk dibaca mesin; field pesan untuk manusia; field detail opsional dan berisi daftar galat validasi per field. Pesan galat login dan token sengaja dibuat generik — tidak membedakan nama yang tak dikenal dari sandi yang salah — supaya response tidak menjadi alat pemeriksaan nama terdaftar.

Struktur Direktori #

Kode proyek terbagi per tanggung jawab agar setiap berkas kecil dan mudah diuji:

text Struktur direktori proyek
secure-authentication-service/
├── package.json          # type module — semua berkas .js adalah ESM
├── data/
│   └── auth.db           # dibuat otomatis oleh src/cli.js (demo memakai memori)
├── src/
│   ├── json.js           # helper baca body dan kirim response JSON
│   ├── db.js             # tabel pengguna + query parameterized
│   ├── sandi.js          # hash scrypt dan verifikasi waktu konstan
│   ├── token.js          # token HS256: terbitkan dan verifikasi
│   ├── pembatas.js       # rate limiter login jendela tetap
│   ├── server.js         # server HTTP, validasi, middleware, handler
│   ├── cli.js            # titik masuk produksi (rahasia + port dari env)
│   └── demo.mjs          # sesi demo deterministik
└── tests/
    ├── penyimpanan.test.js  # unit test src/db.js
    ├── validasi.test.js     # unit test validasi pendaftaran
    ├── sandi.test.js        # unit test src/sandi.js
    ├── token.test.js        # unit test src/token.js
    ├── pembatas.test.js     # unit test src/pembatas.js
    └── integrasi.test.js    # integration test lewat HTTP sungguhan

Perilaku yang Diharapkan #

Cara termudah melihat perilaku layanan adalah menjalankan sesi demo yang sudah disertakan — satu proses yang menyalakan server pada port ephemeral dengan jam yang dibekukan, mengirim rangkaian permintaan sukses dan penolakan, lalu menutup server dengan rapi:

bash
node src/demo.mjs

Output yang Dihasilkan:

text
daftar Budi                -> 201 {"nama":"Budi","peran":"pengguna"}
daftar Siti (admin)        -> 201 {"nama":"Siti","peran":"admin"}
daftar Budi ulang          -> 409 {"error":{"kode":"NAMA_TERPAKAI","pesan":"Nama Budi sudah terdaftar"}}
daftar Dewi sandi pendek   -> 422 {"error":{"kode":"VALIDASI_GAGAL","pesan":"Data pendaftaran tidak lolos validasi","detail":[{"field":"sandi","pesan":"sandi harus 8-128 karakter"}]}}
login Budi sandi salah     -> 401 {"error":{"kode":"LOGIN_GAGAL","pesan":"Nama atau sandi salah"}}
login Budi                 -> 200 (token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX..., 148 karakter)
GET /aku (token Budi)      -> 200 {"nama":"Budi","peran":"pengguna"}
GET /aku tanpa token       -> 401 (WWW-Authenticate: Bearer realm="layanan-auth") {"error":{"kode":"WAJIB_MASUK","pesan":"Kirim token lewat header Authorization: Bearer"}}
GET /aku (token palsu)     -> 401 {"error":{"kode":"TOKEN_TIDAK_VALID","pesan":"Token tidak valid atau kedaluwarsa"}}
GET /admin (token Budi)    -> 403 {"error":{"kode":"PERAN_TIDAK_CUKUP","pesan":"Endpoint ini hanya untuk admin"}}
login Siti                 -> 200 (token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX..., 144 karakter)
GET /admin (token Siti)    -> 200 {"jumlah":2,"pengguna":[{"id":1,"nama":"Budi","peran":"pengguna"},{"id":2,"nama":"Siti","peran":"admin"}]}
login Dewi percobaan 1     -> 401 {"error":{"kode":"LOGIN_GAGAL","pesan":"Nama atau sandi salah"}}
login Dewi percobaan 2     -> 401 {"error":{"kode":"LOGIN_GAGAL","pesan":"Nama atau sandi salah"}}
login Dewi percobaan 3     -> 401 {"error":{"kode":"LOGIN_GAGAL","pesan":"Nama atau sandi salah"}}
login Dewi percobaan 4     -> 429 (Retry-After: 60) {"error":{"kode":"TERLALU_BANYAK_PERCOBAAN","pesan":"Terlalu banyak percobaan login; tunggu sebentar"}}
GET /aku (token +1 jam)    -> 401 {"error":{"kode":"TOKEN_TIDAK_VALID","pesan":"Token tidak valid atau kedaluwarsa"}}
login Dewi (jendela baru)  -> 401 {"error":{"kode":"LOGIN_GAGAL","pesan":"Nama atau sandi salah"}}

Bacaan singkat output di atas: pendaftaran menjawab 201 dan nama ganda ditolak 409, sandi pendek ditolak 422 beserta detail per field, login yang salah menjawab 401 dengan pesan yang sama untuk nama tak dikenal maupun sandi salah, token yang dipalsukan dan yang kedaluwarsa ditolak 401 tanpa membedakan alasannya, /admin membedakan 401 (tanpa token) dari 403 (peran kurang), dan percobaan login keempat untuk nama yang sama dalam satu menit dijawab 429 beserta header Retry-After.

Untuk pemakaian sungguhan, server dinyalakan lewat titik masuk produksi yang membaca rahasia token dari environment variable:

bash
RAHASIA_TOKEN=your-secret-here PORT=3000 node src/cli.js
bash
curl 127.0.0.1:3000/daftar -H 'Content-Type: application/json' -d '{"nama": "budi", "sandi": "sandi-budi-2026"}'
curl 127.0.0.1:3000/login -H 'Content-Type: application/json' -d '{"nama": "budi", "sandi": "sandi-budi-2026"}'
curl 127.0.0.1:3000/aku -H 'Authorization: Bearer <token>'

Server berjalan sampai dihentikan dengan Ctrl+C, dan data pengguna tersimpan di data/auth.db sehingga tetap ada pada pemakaian berikutnya. Seluruh perilaku layanan dikunci oleh 42 test yang dijalankan dengan node --test dari akar proyek.

Pembagian Empat Bagian #

Pembangunan proyek ini mengikuti empat tahap yang berdiri di atas tahap sebelumnya:

  1. Bagian 1 — Penyimpanan Pengguna: membangun src/db.js — tabel pengguna di node:sqlite, query parameterized, dan penolakan nama ganda lewat constraint UNIQUE.
  2. Bagian 2 — Registrasi dan Login: mengisi src/sandi.js dan handler pertama — hash scrypt bersalt, validasi pendaftaran, verifikasi login, serta status code 201/422/409/401.
  3. Bagian 3 — Token dan Peran: membangun src/token.js dan rute terlindungi — token HS256 dengan klaim sub, peran, exp, middleware wajibMasuk, serta pembedaan 401 dan 403.
  4. Bagian 4 — Pengerasan Keamanan: memasang pertahanan tambahan — rate limiter login dengan jam yang disuntik, pesan galat generik yang konsisten, kedaluwarsa token yang teruji, dan secret produksi dari environment variable.

Setiap bagian menutup satu keputusan keamanan: bagian 1 menempatkan kebenaran data di database, bagian 2 memastikan sandi tidak pernah disimpan asli, bagian 3 memindahkan kepercayaan ke tanda tangan token, dan bagian 4 menahan penyalahgunaan volume. Mari kita mulai dari fondasinya: tempat data pengguna berada.