Skip to content

Feat: history password#1096

Open
pandigresik wants to merge 4 commits into
rilis-devfrom
feat/history_password
Open

Feat: history password#1096
pandigresik wants to merge 4 commits into
rilis-devfrom
feat/history_password

Conversation

@pandigresik

Copy link
Copy Markdown
Contributor

Pull Request: Feature Password History (10 Riwayat Kata Sandi)

Description

Menerapkan mekanisme Password History yang menyimpan 10 riwayat kata sandi terakhir untuk setiap akun dan mencegah penggunaan kembali kata sandi yang pernah digunakan sebelumnya. Validasi diterapkan di seluruh mekanisme perubahan dan reset kata sandi melalui FormRequest dan penyimpanan riwayat dilakukan otomatis melalui UserObserver.

Changes made:

  1. New: app/Observers/UserObserver.php — Observer pada model User yang otomatis menyimpan hash password lama ke tabel password_histories dan melakukan pruning (hapus riwayat > 10) dalam transaksi database
  2. Modified: app/Rules/StrongPassword.php — Menambah parameter $user untuk pengecekan riwayat password pada konteks unauthenticated (reset password via email); default historySize berubah jadi 10; tracking $failedRule agar message() hanya mengembalikan 1 pesan relevan (bukan semua 6)
  3. Modified: app/Http/Requests/ChangePasswordRequest.php — Override failedValidation() untuk menampilkan flash error notification saat validasi gagal
  4. Modified: app/Http/Requests/ResetPasswordRequest.php — Mencari user by email dan meneruskan ke StrongPassword(user: $user) agar validasi riwayat tetap berjalan pada reset password via email; tambah flash error notification saat validasi gagal
  5. Modified: app/Http/Controllers/Auth/ResetPasswordController.php — Menghapus manual PasswordHistory::create(), diganti dengan $user->passwordHistoryReason yang diproses oleh observer
  6. Modified: app/Http/Controllers/ForcePasswordResetController.php — Sama, menghapus manual history creation, diganti dengan observer pattern
  7. Modified: app/Models/User.php — Menambah properti $passwordHistoryReason dan $oldPasswordHash; setPasswordWithHistory() didelegasikan ke observer
  8. Modified: app/Providers/AppServiceProvider.php — Mendaftarkan UserObserver pada model User
  9. Modified: config/password.phphistory_count dari 5 menjadi 10
  10. Fixed: tests/Feature/LoginControllerTest.php — Captcha test menggunakan user agent konsisten ('Symfony') dan Setting::updateOrCreate() untuk mengaktifkan captcha
  11. Fixed: tests/Feature/OtpLoginControllerTest.php — Sama, captcha test diperbaiki
  12. New: tests/Feature/PasswordHistoryTest.php — 8 test untuk UserObserver (simpan hash, custom reason, skip new user, pruning, dll)
  13. New: tests/Feature/PasswordResetFlowTest.php — 6 test untuk ForcePasswordResetController (form, redirect, sukses, history, reject reuse)
  14. Modified: tests/Feature/StrongPasswordRuleTest.php — 4 test baru untuk $user parameter, batas 10 entry, custom historySize; test error_messages diadaptasi ke single-message pattern

Reason for change:

  • Keamanan: Mencegah pengguna menggunakan kembali kata sandi lama yang mungkin telah terkompromi
  • Standar: Menyesuaikan dengan praktik keamanan aplikasi modern (NIST SP 800-63B merekomendasikan pengecekan riwayat password)
  • Sentralisasi: Memindahkan logika pencatatan riwayat dari tiap controller ke observer, menjamin konsistensi di seluruh flow perubahan password

Impact of change:

  • Keamanan: Riwayat 10 password terakhir dicek di semua mekanisme perubahan/reset password
  • Konsistensi: Observer menjamin setiap perubahan password terekam, tanpa ada celah controller yang terlewat
  • Maintainability: Logika pruning dan penyimpanan terpusat di observer, bukan tersebar di controller

Related Issue

#1095

Steps to Reproduce

Sebelum:

  1. User mengganti password berulang kali dengan password yang sama
  2. Password lama tidak dicek, user bisa menggunakan password yang sama terus-menerus
  3. Riwayat hanya menyimpan 5 password terakhir
  4. Validasi riwayat hanya berjalan saat user sedang login (tidak pada reset password via email)

Sesudah:

  1. User mengganti password → hash lama tersimpan otomatis di password_histories via observer
  2. User mengganti password dengan salah satu dari 10 terakhir → ditolak dengan pesan error
  3. Riwayat > 10 otomatis dipangkas oleh observer
  4. Validasi riwayat juga berjalan pada reset password via email (unauthenticated context)

Checklist

  • I have complied with script writing rules
  • I have followed pull request review process
  • I have created unit/integration test to verify the fix
  • Manual testing has been done in development environment
  • No console errors or warnings
  • Code has been reviewed by team

Technical Details

Arsitektur Observer

User (model)
  │
  ├─ $user->password = '...'
  ├─ $user->save()
  │
  └─ UserObserver::saving()
       └─ jika password dirty → simpan hash lama ke $user->oldPasswordHash
     UserObserver::saved()
       └─ jika oldPasswordHash isset →
            ├─ INSERT ke password_histories (hash lama + reason)
            ├─ DELETE history > 10 (dalam DB::transaction)
            └─ $user->oldPasswordHash = null

Validasi FormRequest

Setiap form request yang menerima input password baru menginstansiasi StrongPassword:

  • ChangePasswordRequest → user dari auth()->user()
  • ResetPasswordRequest → user ditemukan dari email, diteruskan via StrongPassword(user: $user)
  • ForcePasswordResetRequest → user dari auth()->user()

StrongPassword::isNotInHistory() menggunakan prioritas: $this->user > auth()->user() > skip.

Pesan Error Spesifik

StrongPassword::passes() mencatat jenis kegagalan via $this->failedRule:

  • 'length' → "Password harus memiliki minimal N karakter."
  • 'complexity' → "Password harus mengandung huruf kapital, huruf kecil, angka, dan karakter spesial."
  • 'history' → "Password ini telah digunakan sebelumnya."
  • 'hibp' → "Password ini telah bocor di HIBP."
  • 'weak_pattern' → "Password terlalu lemah atau mudah ditebak."
  • 'common' → "Password ini terlalu umum."

message() hanya mengembalikan 1 pesan sesuai jenis kegagalan terakhir, bukan seluruh array. failedValidation() di FormRequest me-flash pesan ini ke session()->flash('error', ...) sehingga flash_message.blade.php menampilkan notifikasi merah yang relevan.

Configuration changes

  • config/password.php'history_count' => 10

Dependencies added

Tidak ada dependensi baru

Testing

Manual Testing

  • Ganti password via profil dengan password yang sama → ditolak
  • Ganti password via force reset → history tercatat
  • Reset password via email → validasi riwayat tetap berjalan
  • Isi riwayat > 10 → otomatis dipangkas

Automated Testing

  • PasswordHistoryTest (8 tests) — Observer, pruning, user context
  • PasswordResetFlowTest (6 tests) — ForcePasswordResetController end-to-end
  • StrongPasswordRuleTest (17 tests) — 4 baru + 13 existing tetap pass
  • LoginControllerTest (15 of 16) — Captcha test diperbaiki, 1 skipped pre-existing
  • OtpLoginControllerTest (19 of 22) — Captcha test diperbaiki, 3 skipped pre-existing
  • TwoFactorControllerTest, BruteForceSimulationTest, IdorPreventionTest — Semua tetap pass
  • Total: 109 passed, 0 failures, 4 skipped

Screenshots / Video

simplescreenrecorder-2026-07-15_10.59.18.mp4

Tidak ada perubahan UI

Breaking Changes

Tidak ada. Semua method existing (setPasswordWithHistory, forcePasswordReset) tetap berfungsi. Perubahan hanya pada internal storage mechanism (dari manual ke observer).

Migration Guide

Tidak diperlukan. Migration 2026_03_11_000001_create_password_histories_table.php sudah ada di codebase.


@pandigresik
pandigresik requested a review from vickyrolanda July 15, 2026 04:03
@github-actions

Copy link
Copy Markdown

🔄 AI PR Review sedang antri di server...

Proses review akan segera dimulai di background — hasil akan muncul sebagai komentar setelah selesai.
Powered by CrewAI · PR #1096

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant