# Tahap Penerapan Security by Design di Laravel pada Sistem SDM Puskesmas

Dokumen ini menjelaskan **empat tahap** penerapan Security by Design di Laravel yang diterapkan pada sistem ini.

---

## 1. Validasi Input Sejak Awal (Form Request)

**Prinsip:** Input divalidasi **sebelum** masuk ke logika bisnis. Hanya data yang lolos validasi yang diproses.

### Penerapan di Sistem

- **Form Request** dipakai untuk setiap form penting. Controller memakai **type-hint** Form Request sehingga request **tidak sampai** ke method controller sebelum validasi selesai.
- Jika validasi gagal, Laravel mengembalikan redirect dengan error; kode controller **tidak dijalankan**.

### Contoh

**Login** – `LoginRequest` (`app/Http/Requests/LoginRequest.php`):

```php
public function rules()
{
    return [
        'username' => ['required', 'string', 'max:50'],
        'password' => ['required', 'string', 'min:8', 'max:255'],
        'role' => ['required', 'in:pegawai,kepala_tata_usaha,pimpinan'],
    ];
}
```

**Tambah Pegawai** – `StorePegawaiRequest` (`app/Http/Requests/StorePegawaiRequest.php`):

- `nip`: required, unique di tabel pegawai, max 50 karakter  
- `nama_lengkap`, `email`, `tanggal_lahir`, `jenis_kelamin`, dll. dengan rule ketat (format, range, unique bila perlu).

**Controller** hanya memakai data yang sudah lolos validasi:

```php
// AuthController.php
public function login(LoginRequest $request)
{
    $validated = $request->validated();  // Hanya data yang lolos
    // ...
}
```

### Daftar Form Request

| Form Request           | Dipakai untuk   | Lokasi                          |
|------------------------|-----------------|----------------------------------|
| LoginRequest           | Login          | `app/Http/Requests/LoginRequest.php` |
| StorePegawaiRequest    | Tambah pegawai | `app/Http/Requests/StorePegawaiRequest.php` |
| UpdatePegawaiRequest    | Edit pegawai   | `app/Http/Requests/UpdatePegawaiRequest.php` |
| StoreAbsensiRequest    | Input absensi  | `app/Http/Requests/StoreAbsensiRequest.php` |
| StoreCutiRequest       | Pengajuan cuti | `app/Http/Requests/StoreCutiRequest.php` |
| StoreGajiRequest       | Input gaji     | `app/Http/Requests/StoreGajiRequest.php` |

**Ringkas:** Validasi input sejak awal = Form Request di setiap form; controller hanya memproses `$request->validated()`.

---

## 2. Hash Password Otomatis (Bukan Plain Text)

**Prinsip:** Password **tidak pernah** disimpan dalam bentuk plain text. Disimpan dalam bentuk hash (bcrypt) dan pengecekan memakai `Hash::check()`.

### Penerapan di Sistem

- **Penyimpanan:** Di model `User`, mutator `setPasswordAttribute()` otomatis meng-hash nilai yang di-set ke atribut password sebelum disimpan ke kolom `password_hash`.

**`app/Models/User.php`:**

```php
public function setPasswordAttribute($value)
{
    $this->attributes['password_hash'] = Hash::make($value);
}
```

- **Pengecekan:** Method `checkPassword()` memakai `Hash::check()`; password plain dari input **tidak pernah** dibandingkan langsung dengan isi database.

```php
public function checkPassword($password)
{
    $passwordHash = $this->attributes['password_hash'] ?? $this->password_hash;
    return Hash::check($password, $passwordHash);
}
```

- **Kompatibilitas Auth:** `getAuthPassword()` mengembalikan nilai `password_hash` dari database (sudah hash), sehingga Laravel Auth/Guard tetap aman.

### Alur

1. User mendaftar / reset password: nilai plain (misalnya `password123`) di-set ke `$user->password` → mutator memanggil `Hash::make()` → yang disimpan ke DB adalah hash.
2. Login: input password dicek dengan `Hash::check($passwordInput, $passwordHashDariDB)`; plain text tidak disimpan dan tidak dibandingkan langsung.

**Ringkas:** Hash password otomatis = mutator di `User` + `Hash::check()` saat login; tidak ada penyimpanan atau perbandingan plain text.

---

## 3. Authorization dari Desain (Policy & Gate)

**Prinsip:** Siapa boleh melakukan apa (view, create, update, delete) ditentukan **sejak desain** lewat Policy dan Gate, bukan hanya di satu dua controller.

### Penerapan di Sistem

**A. Policy (model-based)**

- Policy mendefinisikan aksi per model (view, create, update, delete) berdasarkan role dan kepemilikan data.
- Mapping model–policy didaftarkan di `AuthServiceProvider`.

**`app/Providers/AuthServiceProvider.php`:**

```php
protected $policies = [
    \App\Models\Pegawai::class => \App\Policies\PegawaiPolicy::class,
    \App\Models\Cuti::class => \App\Policies\CutiPolicy::class,
];
```

**Contoh Policy – `app/Policies/PegawaiPolicy.php`:**

- `viewAny`: semua role yang login boleh lihat daftar (dengan filter di controller).
- `view`: Kepala TU & Pimpinan boleh lihat semua pegawai; Pegawai hanya data sendiri (`$user->pegawai->id === $pegawai->id`).
- `create`, `update`, `delete`: hanya Kepala TU.

```php
public function view(User $user, Pegawai $pegawai): bool
{
    if ($user->isKepalaTataUsaha() || $user->isPimpinan()) {
        return true;
    }
    return $user->isPegawai() && $user->pegawai && $user->pegawai->id === $pegawai->id;
}

public function create(User $user): bool
{
    return $user->isKepalaTataUsaha();
}
```

**B. Gate (ability-based)**

- Gate mendefinisikan ability global (view_pegawai, create_pegawai, update_pegawai, delete_pegawai) berdasarkan role dan hak akses.

**`app/Providers/AuthServiceProvider.php` – `boot()`:**

```php
Gate::define('view_pegawai', function ($user) {
    return $user->canAccess('view_pegawai') || $user->isPimpinan();
});

Gate::define('create_pegawai', function ($user) {
    return ($user->isKepalaTataUsaha() || $user->isPimpinan()) && 
           ($user->canAccess('create_pegawai') || $user->isPimpinan());
});
// update_pegawai, delete_pegawai serupa
```

**C. Penggunaan di controller**

- Cek role sebelum aksi sensitif (create/store/update/delete) dan batasi data (pegawai hanya data sendiri).
- Bisa memakai `$this->authorize('update', $pegawai)` atau helper `authorizeOrAbort()` di base Controller untuk memakai Policy dan mengembalikan 403 + log jika tidak diizinkan.

**Contoh di `PegawaiController`:**

- `create()`: jika bukan Kepala TU → `abort(403, ...)`.
- `store()`: jika pegawai → `abort(403)`; lalu validasi dengan Form Request (yang di `authorize()` juga cek `isKepalaTataUsaha()`).
- `index()`: Pegawai hanya melihat data diri sendiri (`$query->where('id', $pegawai->id)`).

**Ringkas:** Authorization dari desain = Policy (Pegawai, Cuti) + Gate (view/create/update/delete pegawai) + pengecekan di controller dan Form Request.

---

## 4. Proteksi CSRF (Aktif Default)

**Prinsip:** Request state-changing (POST, PUT, PATCH, DELETE) dari browser **harus** menyertakan token CSRF yang valid. Token dihasilkan per session dan divalidasi oleh middleware; mencegah serangan Cross-Site Request Forgery.

### Penerapan di Sistem

**A. Middleware CSRF aktif di grup `web`**

**`app/Http/Kernel.php` – `$middlewareGroups['web']`:**

```php
'web' => [
    \App\Http\Middleware\EncryptCookies::class,
    \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
    \Illuminate\Session\Middleware\StartSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    \App\Http\Middleware\VerifyCsrfToken::class,   // ← CSRF aktif
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
    \App\Http\Middleware\SecurityHeaders::class,
],
```

Semua route yang memakai middleware group `web` (hampir semua halaman aplikasi) **otomatis** divalidasi CSRF untuk method yang mengubah state.

**B. Tidak ada pengecualian**

**`app/Http/Middleware/VerifyCsrfToken.php`:**

```php
protected $except = [
    //
];
```

Tidak ada URI yang di-exclude; semua form POST/PUT/DELETE wajib punya token.

**C. Form wajib menyertakan token**

Di view, setiap form yang mengirim POST/PUT/PATCH/DELETE memakai directive Blade `@csrf` agar token ikut terkirim:

```blade
<form action="{{ route('login') }}" method="POST">
    @csrf
    ...
</form>
```

Contoh lokasi: form login, form tambah/edit pegawai, absensi, cuti, gaji, profile, admin users, dll.

### Alur

1. Session dimulai → Laravel menyimpan token CSRF di session dan menyediakan ke view (misalnya lewat meta tag atau helper `csrf_token()`).
2. Form dengan `@csrf` mengirim field `_token` bersama data.
3. Setiap request POST/PUT/PATCH/DELETE yang lewat middleware `web` → `VerifyCsrfToken` membandingkan `_token` dengan nilai di session.
4. Jika tidak cocok atau tidak ada → respons 419 (atau 403); request tidak sampai ke controller.

**Ringkas:** Proteksi CSRF aktif default = middleware `VerifyCsrfToken` di grup `web`, tidak ada pengecualian, dan setiap form memakai `@csrf`.

---

## Ringkasan Empat Tahap

| Tahap | Penerapan di Laravel pada sistem ini |
|-------|--------------------------------------|
| **1. Validasi input sejak awal** | Form Request (LoginRequest, StorePegawaiRequest, StoreAbsensiRequest, StoreCutiRequest, StoreGajiRequest, UpdatePegawaiRequest); controller memakai `$request->validated()`. |
| **2. Hash password otomatis** | Mutator `User::setPasswordAttribute()` → `Hash::make()`; pengecekan `User::checkPassword()` → `Hash::check()`; tidak ada plain text di DB. |
| **3. Authorization dari desain** | Policy (PegawaiPolicy, CutiPolicy) + Gate (view/create/update/delete pegawai) di AuthServiceProvider; pengecekan di controller dan Form Request `authorize()`. |
| **4. Proteksi CSRF (aktif default)** | Middleware `VerifyCsrfToken` di grup `web`; `$except` kosong; semua form memakai `@csrf`. |

Dengan keempat tahap ini, Security by Design di Laravel diterapkan secara konsisten pada sistem SDM Puskesmas ini.

















Berikut tutorial praktis untuk mengecek penerapan Security by Design pada sistem Laravel Anda (berdasarkan 4 tahap yang Anda tulis). Saya buat langkahnya bisa langsung dipakai di lokal/hosting.
Ringkasan
Yang dicek: (1) validasi Form Request berjalan sebelum controller, (2) password tersimpan hash, bukan plain text, (3) otorisasi Policy/Gate benar-benar membatasi akses, (4) CSRF aktif dan menolak request tanpa token.

1. Cara cek validasi input sejak awal (Form Request)
A. Cek di kode (paling cepat)
1. Buka controller terkait (mis. AuthController, PegawaiController, AbsensiController, CutiController, GajiController).
2. Pastikan method menerima Form Request, contoh:
public function login(LoginRequest $request)
3. Pastikan controller mengambil data dari:
$validated = $request->validated();
Bukan dari $request->all() untuk data inti.
B. Cek via browser (uji gagal validasi)
Contoh: login
1. Buka halaman login.
2. Kosongkan username/password/role atau isi password kurang dari 8 karakter.
3. Klik �Masuk�.
Hasil yang benar:
* Sistem tetap di halaman login dan muncul pesan error validasi.
* Tidak masuk dashboard.
* Jika menggunakan session/redirect: biasanya status 302 redirect + error.
C. Cek via Postman/cURL (lebih tegas)
Kirim request login tanpa field wajib:
curl -i -X POST http://localhost:8000/login \
  -H "Accept: application/json" \
  -d "username=" -d "password=" -d "role="
Hasil yang benar:
* Response 422 Unprocessable Entity (jika Accept: application/json)
* Ada detail error field (required/min/max/in).
D. Cek via testing Laravel (paling rapi untuk skripsi)
Buat Feature Test yang sengaja invalid:
php artisan test
Jika belum ada test, pola umumnya:
* kirim data invalid ? assertStatus(422) atau assertSessionHasErrors()
Cek cepat validasi:
* Input salah ? ditolak sebelum proses bisnis berjalan.
* Pesan error muncul konsisten.

2. Cara cek hash password otomatis (bukan plain text)
A. Cek isi database
1. Buka database (phpMyAdmin / MySQL client).
2. Lihat tabel users, kolom password_hash.
Hasil yang benar:
* Nilai berbentuk hash, biasanya diawali $2y$ atau format hash bcrypt/argon.
* Tidak pernah sama dengan password asli.
Contoh cek via MySQL:
SELECT id, username, password_hash FROM users LIMIT 5;
B. Cek lewat Tinker
php artisan tinker
Lalu:
$user = \App\Models\User::first();
$user->password_hash; // harus hash
C. Uji Hash::check (validasi metode cek password)
use Illuminate\Support\Facades\Hash;

Hash::check('password_asli', $user->password_hash);
Hasil yang benar:
* true jika password sesuai, false jika tidak.
Cek cepat password:
* Kolom yang tersimpan adalah password_hash (hash), bukan password.
* Tidak ada penyimpanan plain text di DB.

3. Cara cek authorization dari desain (Policy & Gate)
A. Cek Policy terdaftar
Buka app/Providers/AuthServiceProvider.php, pastikan ada:
protected $policies = [
  \App\Models\Pegawai::class => \App\Policies\PegawaiPolicy::class,
  \App\Models\Cuti::class => \App\Policies\CutiPolicy::class,
];
B. Cek controller memanggil authorize
Contoh yang benar:
$this->authorize('update', $pegawai);
atau gate:
abort_unless(Gate::allows('create_pegawai'), 403);
C. Cek dengan login role berbeda (uji akses)
1. Login sebagai Pegawai.
2. Coba buka fitur yang seharusnya hanya untuk Kepala TU (misalnya tambah pegawai / delete pegawai).
Hasil yang benar:
* Ditolak 403 Forbidden (atau diarahkan + pesan akses ditolak).
3. Login sebagai Kepala TU ? coba akses fitur yang sama.
Hasil yang benar:
* Diizinkan.
D. Cek cepat lewat Tinker (langsung cek ability)
php artisan tinker
$user = \App\Models\User::where('role','pegawai')->first();
$pegawai = \App\Models\Pegawai::first();

Gate::forUser($user)->allows('create_pegawai'); // harus false untuk pegawai
$user->can('view', $pegawai);                   // tergantung aturan policy
Cek cepat authorization:
* Role tidak berwenang ? 403 (bukan hanya tombol disembunyikan).
* Pegawai tidak bisa lihat data pegawai lain (uji langsung URL detail pegawai lain).

4. Cara cek CSRF aktif (VerifyCsrfToken)
A. Cek middleware aktif
Buka app/Http/Kernel.php, group web harus memuat:
\App\Http\Middleware\VerifyCsrfToken::class,
Dan file VerifyCsrfToken.php bagian $except kosong.
B. Uji lewat browser (tanpa token)
Cara paling mudah: coba kirim POST tanpa token memakai Postman/cURL.
Contoh:
curl -i -X POST http://localhost:8000/login \
  -d "username=test" -d "password=12345678" -d "role=pegawai"
Hasil yang benar (untuk route web):
* Response 419 (Page Expired) / 403, tergantung konfigurasi.
C. Uji dengan token (harus lolos)
1. Buka halaman form (login) di browser.
2. Pastikan di HTML form ada:
<input type="hidden" name="_token" value="...">
(ini hasil @csrf)
3. Jika token ada, submit form normal harus diterima (asalkan kredensial benar).
Catatan untuk Postman:
* Jika pakai session cookie Laravel, Postman harus menyertakan cookie session + token _token atau header X-CSRF-TOKEN/X-XSRF-TOKEN (tergantung implementasi).
Cek cepat CSRF:
* POST tanpa token ? ditolak (419).
* POST dengan token valid ? diproses.

Cek Cepat (praktik lapangan, 2 menit)
1. Login kosong/invalid ? muncul error validasi (bukan error server).
2. Cek DB users.password_hash ? format hash (bukan plain).
3. Role pegawai coba akses tambah pegawai ? 403.
4. Kirim POST tanpa CSRF token ? 419.
Kalau Anda mau, kirim base URL route login dan contoh route tambah pegawai (web.php), nanti saya buatkan skrip cURL lengkap untuk 4 pengujian tersebut (biar bisa Anda lampirkan di bab pengujian).

