Password Policy
This page documents the password policy and associated security mechanisms.
Overview
The password policy defines the rules for:
- Minimum password complexity
- Expiration and rotation
- Lockout after failures
- Password history
Complexity Rules
Minimum Requirements
| Criterion | Requirement |
|---|---|
| Minimum length | 8 characters |
| Uppercase | At least 1 |
| Lowercase | At least 1 |
| Numbers | At least 1 |
| Special characters | At least 1 (!@#$%^&*) |
Forbidden Passwords
- Same as username
- In the list of common passwords
- Same as the last 5 passwords
Configuration
Environment Variables
# Password policy
PASSWORD_MIN_LENGTH=8
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_LOWERCASE=true
PASSWORD_REQUIRE_NUMBER=true
PASSWORD_REQUIRE_SPECIAL=true
PASSWORD_HISTORY_COUNT=5
PASSWORD_EXPIRY_DAYS=90
# Account lockout
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_MINUTES=15
Symfony Configuration
# config/packages/security.yaml
security:
password_hashers:
App\Entity\User:
algorithm: bcrypt
cost: 13
Validation
Symfony Validator
use Symfony\Component\Validator\Constraints as Assert;
class ChangePasswordRequest
{
#[Assert\NotBlank]
#[Assert\Length(min: 8)]
#[Assert\Regex(
pattern: '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/',
message: 'Password must contain uppercase, lowercase, numbers and special characters'
)]
private string $newPassword;
}
Validation Service
class PasswordPolicyService
{
public function validate(string $password, User $user): array
{
$errors = [];
if (strlen($password) < 8) {
$errors[] = 'Minimum 8 characters required';
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = 'At least one uppercase letter required';
}
if ($password === $user->getEmail()) {
$errors[] = 'Password cannot be the same as email';
}
if ($this->isInHistory($password, $user)) {
$errors[] = 'This password has been used recently';
}
return $errors;
}
}
Account Lockout
Mechanism
Attempt 1: Failure → Counter = 1
Attempt 2: Failure → Counter = 2
...
Attempt 5: Failure → ACCOUNT LOCKED
User Fields
| Field | Type | Description |
|---|---|---|
failedLoginAttempts | int | Number of failed attempts |
lockedUntil | datetime | Unlock date |
lastFailedLogin | datetime | Last failed attempt |
Implementation
class LoginService
{
public function handleFailedLogin(User $user): void
{
$attempts = $user->getFailedLoginAttempts() + 1;
$user->setFailedLoginAttempts($attempts);
$user->setLastFailedLogin(new \DateTime());
if ($attempts >= 5) {
$user->setLockedUntil(new \DateTime('+15 minutes'));
}
$this->em->flush();
}
public function handleSuccessfulLogin(User $user): void
{
$user->resetFailedAttempts();
$user->setLockedUntil(null);
$this->em->flush();
}
public function isLocked(User $user): bool
{
$lockedUntil = $user->getLockedUntil();
return $lockedUntil && $lockedUntil > new \DateTime();
}
}
Password Expiration
Configuration
# config/services.yaml
parameters:
password_expiry_days: 90
Verification
public function isPasswordExpired(User $user): bool
{
$lastChange = $user->getPasswordChangedAt();
if (!$lastChange) {
return true;
}
$expiryDate = (clone $lastChange)->modify('+90 days');
return $expiryDate < new \DateTime();
}
Force Change
// Listener that redirects to password change
if ($this->passwordService->isPasswordExpired($user)) {
return new RedirectResponse('/password/change');
}
Password History
Storage
#[ORM\Entity]
class PasswordHistory
{
#[ORM\Id]
#[ORM\Column(type: 'uuid')]
private UuidInterface $id;
#[ORM\ManyToOne(targetEntity: User::class)]
private User $user;
#[ORM\Column]
private string $passwordHash;
#[ORM\Column]
private \DateTimeInterface $createdAt;
}
Verification
public function isInHistory(string $newPassword, User $user): bool
{
$history = $this->historyRepo->findRecent($user, 5);
foreach ($history as $old) {
if (password_verify($newPassword, $old->getPasswordHash())) {
return true;
}
}
return false;
}
API Endpoints
Change Password
POST /api/auth/change-password
{
"currentPassword": "OldPass123!",
"newPassword": "NewPass456@",
"confirmPassword": "NewPass456@"
}
Reset Password (Admin)
POST /api/admin/users/{id}/reset-password
{
"newPassword": "TempPass789#"
}
Console Commands
Reset a Password
php bin/console app:reset-user-password user@example.com NewPassword123!
Check Expired Accounts
php bin/console app:check-password-expiry
User Messages
| Situation | Message |
|---|---|
| Password too short | Password must contain at least 8 characters |
| Missing uppercase | Password must contain at least one uppercase letter |
| Account locked | Account locked. Try again in X minutes |
| Password expired | Your password has expired. Please change it |
| History | This password has been used recently |
Best Practices
:::tip Recommendations
- Strong hashing: Use bcrypt with cost >= 12
- No plain storage: Never store passwords in plain text
- Secure transport: HTTPS mandatory
- Logs without passwords: Never log passwords
- Rate limiting: Limit login attempts :::
Audit
Logged Events
| Event | Level | Data |
|---|---|---|
| Successful login | INFO | email, IP, timestamp |
| Failed login | WARNING | email, IP, attempt # |
| Account locked | WARNING | email, IP |
| Password changed | INFO | email, by_whom |
| Password reset | WARNING | email, by_whom |