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:
| Method | URL | Aksi | Sukses | Galat |
|---|---|---|---|---|
POST | /daftar | Daftar pengguna baru | 201 | 400, 422, 409 |
POST | /login | Masuk dan terbitkan token | 200 + token | 400, 401, 429 |
GET | /aku | Profil pemilik token | 200 | 401 |
GET | /admin | Daftar seluruh pengguna | 200 | 401, 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:
{
"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:
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 sungguhanPerilaku 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:
node src/demo.mjsOutput yang Dihasilkan:
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:
RAHASIA_TOKEN=your-secret-here PORT=3000 node src/cli.jscurl 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:
- Bagian 1 — Penyimpanan Pengguna: membangun
src/db.js— tabelpenggunadinode:sqlite, query parameterized, dan penolakan nama ganda lewat constraintUNIQUE. - Bagian 2 — Registrasi dan Login: mengisi
src/sandi.jsdan handler pertama — hash scrypt bersalt, validasi pendaftaran, verifikasi login, serta status code 201/422/409/401. - Bagian 3 — Token dan Peran: membangun
src/token.jsdan rute terlindungi — token HS256 dengan klaimsub,peran,exp, middlewarewajibMasuk, serta pembedaan 401 dan 403. - 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.
Bangun penyimpanan pengguna dengan password hashing scrypt.
Buka → # Bagian 2: Registrasi dan LoginImplementasikan registrasi, login, dan session token yang aman.
Buka → # Bagian 3: Token dan PeranTambahkan token bertanda tangan dan authorization berbasis peran.
Buka → # Bagian 4: Pengerasan KeamananTambahkan rate limiting, audit log, dan pengujian keamanan.
Buka →