Security Auth - v1.0.0
Password Security
PasswordSecurityService (bound to PasswordSecurityServiceInterface) is the orchestrator. Four validation rules wrap individual concerns:
PasswordComplexity— length, uppercase, lowercase, numbers, symbolsNotCompromised— HaveIBeenPwned breach checkPasswordHistoryRule— disallow reuse of previous N passwordsPasswordPolicy— composite of all three plus expiration check
Use the composite rule
The simplest path — all checks in one rule:
use ArtisanPackUI\SecurityAuth\Rules\PasswordPolicy;
$request->validate([
'password' => ['required', 'confirmed', new PasswordPolicy],
]);
Use individual rules for finer control
use ArtisanPackUI\SecurityAuth\Rules\PasswordComplexity;
use ArtisanPackUI\SecurityAuth\Rules\NotCompromised;
use ArtisanPackUI\SecurityAuth\Rules\PasswordHistoryRule;
$request->validate([
'password' => [
'required',
'confirmed',
new PasswordComplexity,
new NotCompromised,
new PasswordHistoryRule( $user ),
],
]);
Recording a new password
After successful update, record it in history so future changes can't reuse it:
use ArtisanPackUI\SecurityAuth\Contracts\PasswordSecurityServiceInterface;
$user->password = Hash::make( $request->input('password') );
$user->save();
app( PasswordSecurityServiceInterface::class )
->recordPassword( $user->password, $user );
The service writes a PasswordHistory row. The PasswordHistoryRule checks against the configured history_count previous entries.
Checking against HaveIBeenPwned without writing
HaveIBeenPwnedService uses k-anonymity — only the first 5 chars of the SHA-1 hash leave your server.
use ArtisanPackUI\SecurityAuth\Services\HaveIBeenPwnedService;
$service = app( HaveIBeenPwnedService::class );
$compromisedCount = $service->check( $password ); // 0 = not seen, >0 = times seen in breaches
if ( $service->isCompromised( $password ) ) {
// refuse
}
Expiration
PasswordSecurityService::isExpired($user) and daysUntilExpiration($user) check against the configured expire_after_days. Combine with the password.policy middleware to force a reset when expired:
Route::middleware(['auth', 'password.policy'])->group(function (): void {
// Routes here redirect to a "change your password" flow when
// the user's password is expired or doesn't meet current policy.
});
Disabling individual checks
Configure thresholds to 0 / false to disable a specific check:
'password_security' => [
'breach_check' => false, // skip HIBP
'history_count' => 0, // allow reuse
'expire_after_days' => 0, // never expire
],
Even with checks disabled the rules don't throw — they simply pass.