- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
797 lines
26 KiB
Markdown
797 lines
26 KiB
Markdown
# Auth Module Examples
|
|
|
|
**Praktische Implementierungsbeispiele** für das Auth Module des Custom PHP Frameworks.
|
|
|
|
## Basic Authentication Flow
|
|
|
|
### User Registration
|
|
|
|
```php
|
|
use App\Framework\Auth\PasswordHasher;
|
|
use App\Framework\Auth\PasswordValidationResult;
|
|
|
|
final readonly class UserRegistrationService
|
|
{
|
|
public function __construct(
|
|
private PasswordHasher $passwordHasher,
|
|
private UserRepository $userRepository
|
|
) {}
|
|
|
|
public function register(
|
|
string $email,
|
|
#[SensitiveParameter] string $password,
|
|
string $username = null
|
|
): RegistrationResult {
|
|
// Validate password strength
|
|
$validation = $this->passwordHasher->validatePasswordStrength($password);
|
|
if (!$validation->isValid) {
|
|
return RegistrationResult::passwordValidationFailed($validation);
|
|
}
|
|
|
|
// Check if user exists
|
|
if ($this->userRepository->findByEmail($email)) {
|
|
return RegistrationResult::failed('User with this email already exists');
|
|
}
|
|
|
|
// Hash password
|
|
$hashedPassword = $this->passwordHasher->hash($password);
|
|
|
|
// Create user
|
|
$user = new User(
|
|
id: Uuid::generate(),
|
|
email: new Email($email),
|
|
username: $username,
|
|
hashedPassword: $hashedPassword,
|
|
createdAt: new \DateTimeImmutable()
|
|
);
|
|
|
|
$this->userRepository->save($user);
|
|
|
|
return RegistrationResult::success($user);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Login Implementation
|
|
|
|
```php
|
|
use App\Framework\Auth\AuthenticationService;
|
|
use App\Framework\Http\IpAddress;
|
|
use App\Framework\Http\Session\SessionManager;
|
|
|
|
#[Route(path: '/login', method: Method::POST)]
|
|
final readonly class LoginController
|
|
{
|
|
public function __construct(
|
|
private AuthenticationService $authService,
|
|
private SessionManager $sessionManager
|
|
) {}
|
|
|
|
public function login(LoginRequest $request): JsonResult
|
|
{
|
|
$ipAddress = IpAddress::fromRequest();
|
|
|
|
$result = $this->authService->authenticate(
|
|
identifier: $request->email,
|
|
password: $request->password,
|
|
ipAddress: $ipAddress,
|
|
remember: $request->remember ?? false
|
|
);
|
|
|
|
if ($result->isSuccess()) {
|
|
$user = $result->getUser();
|
|
$session = $result->getSession();
|
|
|
|
// Store session in session manager
|
|
$this->sessionManager->start($session->getId());
|
|
$this->sessionManager->set('user_id', $user->getId());
|
|
$this->sessionManager->set('authenticated_at', time());
|
|
|
|
$responseData = [
|
|
'success' => true,
|
|
'user' => [
|
|
'id' => $user->getId(),
|
|
'email' => (string) $user->getEmail(),
|
|
'username' => $user->getUsername()
|
|
],
|
|
'session' => [
|
|
'id' => $session->getId()->toString(),
|
|
'expires_at' => $session->getExpiresAt()->format(\DateTimeInterface::ATOM)
|
|
]
|
|
];
|
|
|
|
// Add remember token to response if requested
|
|
if ($rememberToken = $result->getRememberToken()) {
|
|
$responseData['remember_token'] = $rememberToken->getPlainTextValue();
|
|
|
|
// Set secure HTTP-only cookie
|
|
setcookie(
|
|
'remember_token',
|
|
$rememberToken->getPlainTextValue(),
|
|
[
|
|
'expires' => $rememberToken->getExpiresAt()->getTimestamp(),
|
|
'path' => '/',
|
|
'secure' => true,
|
|
'httponly' => true,
|
|
'samesite' => 'Strict'
|
|
]
|
|
);
|
|
}
|
|
|
|
return new JsonResult($responseData);
|
|
}
|
|
|
|
// Handle different failure types
|
|
return match (true) {
|
|
$result->isRateLimited() => new JsonResult([
|
|
'success' => false,
|
|
'error' => 'Too many attempts',
|
|
'retry_after' => $result->getRetryAfter()
|
|
], 429),
|
|
|
|
$result->isAccountLocked() => new JsonResult([
|
|
'success' => false,
|
|
'error' => 'Account temporarily locked',
|
|
'locked_until' => $result->getLockoutExpiresAt()?->format(\DateTimeInterface::ATOM)
|
|
], 423),
|
|
|
|
default => new JsonResult([
|
|
'success' => false,
|
|
'error' => 'Invalid credentials'
|
|
], 401)
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### Auto-Login with Remember Token
|
|
|
|
```php
|
|
#[Route(path: '/auth/check', method: Method::GET)]
|
|
final readonly class AuthCheckController
|
|
{
|
|
public function __construct(
|
|
private AuthenticationService $authService,
|
|
private SessionManager $sessionManager
|
|
) {}
|
|
|
|
public function check(HttpRequest $request): JsonResult
|
|
{
|
|
$sessionId = $this->sessionManager->getCurrentSessionId();
|
|
|
|
// Try session authentication first
|
|
if ($sessionId) {
|
|
$result = $this->authService->authenticateWithSession(
|
|
$sessionId,
|
|
IpAddress::fromRequest()
|
|
);
|
|
|
|
if ($result->isSuccess()) {
|
|
return new JsonResult([
|
|
'authenticated' => true,
|
|
'user' => $this->formatUser($result->getUser()),
|
|
'session' => $this->formatSession($result->getSession())
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Try remember token authentication
|
|
$rememberToken = $request->getCookie('remember_token');
|
|
if ($rememberToken) {
|
|
$result = $this->authService->authenticateWithRememberToken(
|
|
$rememberToken,
|
|
IpAddress::fromRequest()
|
|
);
|
|
|
|
if ($result->isSuccess()) {
|
|
// Start new session
|
|
$session = $result->getSession();
|
|
$this->sessionManager->start($session->getId());
|
|
$this->sessionManager->set('user_id', $result->getUser()->getId());
|
|
|
|
// Update remember token cookie
|
|
$newRememberToken = $result->getRememberToken();
|
|
if ($newRememberToken) {
|
|
setcookie(
|
|
'remember_token',
|
|
$newRememberToken->getPlainTextValue(),
|
|
[
|
|
'expires' => $newRememberToken->getExpiresAt()->getTimestamp(),
|
|
'path' => '/',
|
|
'secure' => true,
|
|
'httponly' => true,
|
|
'samesite' => 'Strict'
|
|
]
|
|
);
|
|
}
|
|
|
|
return new JsonResult([
|
|
'authenticated' => true,
|
|
'user' => $this->formatUser($result->getUser()),
|
|
'session' => $this->formatSession($session)
|
|
]);
|
|
}
|
|
}
|
|
|
|
return new JsonResult(['authenticated' => false]);
|
|
}
|
|
|
|
private function formatUser(User $user): array
|
|
{
|
|
return [
|
|
'id' => $user->getId(),
|
|
'email' => (string) $user->getEmail(),
|
|
'username' => $user->getUsername(),
|
|
'last_login' => $user->getLastLoginAt()?->format(\DateTimeInterface::ATOM)
|
|
];
|
|
}
|
|
|
|
private function formatSession(AuthenticationSession $session): array
|
|
{
|
|
return [
|
|
'id' => $session->getId()->toString(),
|
|
'created_at' => $session->getCreatedAt()->format(\DateTimeInterface::ATOM),
|
|
'expires_at' => $session->getExpiresAt()->format(\DateTimeInterface::ATOM),
|
|
'last_activity' => $session->getLastActivity()->format(\DateTimeInterface::ATOM)
|
|
];
|
|
}
|
|
}
|
|
```
|
|
|
|
## Password Management
|
|
|
|
### Password Change
|
|
|
|
```php
|
|
#[Route(path: '/account/change-password', method: Method::POST)]
|
|
final readonly class ChangePasswordController
|
|
{
|
|
public function __construct(
|
|
private AuthenticationService $authService,
|
|
private SessionManager $sessionManager
|
|
) {}
|
|
|
|
public function changePassword(ChangePasswordRequest $request): JsonResult
|
|
{
|
|
$userId = $this->sessionManager->get('user_id');
|
|
if (!$userId) {
|
|
return new JsonResult(['error' => 'Not authenticated'], 401);
|
|
}
|
|
|
|
$result = $this->authService->changePassword(
|
|
userId: $userId,
|
|
currentPassword: $request->currentPassword,
|
|
newPassword: $request->newPassword
|
|
);
|
|
|
|
if ($result->isSuccess()) {
|
|
return new JsonResult([
|
|
'success' => true,
|
|
'message' => 'Password changed successfully'
|
|
]);
|
|
}
|
|
|
|
if ($result->hasValidationErrors()) {
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'validation_errors' => $result->getValidation()->toArray()
|
|
], 400);
|
|
}
|
|
|
|
return new JsonResult([
|
|
'success' => false,
|
|
'error' => $result->getErrorMessage()
|
|
], 400);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Password Reset Flow
|
|
|
|
```php
|
|
final readonly class PasswordResetService
|
|
{
|
|
public function __construct(
|
|
private PasswordHasher $passwordHasher,
|
|
private UserRepository $userRepository,
|
|
private TokenGenerator $tokenGenerator,
|
|
private EmailService $emailService
|
|
) {}
|
|
|
|
public function initiateReset(string $email): PasswordResetResult
|
|
{
|
|
$user = $this->userRepository->findByEmail($email);
|
|
if (!$user) {
|
|
// Return success even if user not found (security)
|
|
return PasswordResetResult::success();
|
|
}
|
|
|
|
// Generate secure reset token
|
|
$token = $this->tokenGenerator->generateSecureToken(32);
|
|
$tokenHash = Hash::sha256($token);
|
|
$expiresAt = new \DateTimeImmutable('+1 hour');
|
|
|
|
$resetToken = new PasswordResetToken(
|
|
hash: $tokenHash,
|
|
userId: $user->getId(),
|
|
createdAt: new \DateTimeImmutable(),
|
|
expiresAt: $expiresAt
|
|
);
|
|
|
|
$this->userRepository->storePasswordResetToken($resetToken);
|
|
|
|
// Send reset email
|
|
$this->emailService->sendPasswordResetEmail($user->getEmail(), $token);
|
|
|
|
return PasswordResetResult::success();
|
|
}
|
|
|
|
public function resetPassword(
|
|
string $token,
|
|
#[SensitiveParameter] string $newPassword
|
|
): PasswordResetResult {
|
|
$tokenHash = Hash::sha256($token);
|
|
$resetToken = $this->userRepository->findPasswordResetToken($tokenHash);
|
|
|
|
if (!$resetToken || $resetToken->isExpired()) {
|
|
return PasswordResetResult::failed('Invalid or expired reset token');
|
|
}
|
|
|
|
$user = $this->userRepository->findById($resetToken->getUserId());
|
|
if (!$user) {
|
|
return PasswordResetResult::failed('User not found');
|
|
}
|
|
|
|
// Validate new password
|
|
$validation = $this->passwordHasher->validatePasswordStrength($newPassword);
|
|
if (!$validation->isValid) {
|
|
return PasswordResetResult::validationFailed($validation);
|
|
}
|
|
|
|
// Hash new password
|
|
$hashedPassword = $this->passwordHasher->hash($newPassword);
|
|
|
|
// Update password and cleanup
|
|
$this->userRepository->updateUserPassword($user->getId(), $hashedPassword);
|
|
$this->userRepository->deletePasswordResetToken($tokenHash);
|
|
$this->userRepository->deleteAllUserSessions($user->getId());
|
|
|
|
return PasswordResetResult::success();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Advanced Security Features
|
|
|
|
### Multi-Factor Authentication Setup
|
|
|
|
```php
|
|
final readonly class MfaService
|
|
{
|
|
public function __construct(
|
|
private PasswordHasher $passwordHasher,
|
|
private TotpService $totpService,
|
|
private UserRepository $userRepository
|
|
) {}
|
|
|
|
public function setupTotp(string $userId): MfaSetupResult
|
|
{
|
|
$user = $this->userRepository->findById($userId);
|
|
if (!$user) {
|
|
return MfaSetupResult::failed('User not found');
|
|
}
|
|
|
|
// Generate TOTP secret
|
|
$secret = $this->totpService->generateSecret();
|
|
|
|
// Create QR code data
|
|
$qrCodeUri = $this->totpService->getQrCodeUri(
|
|
secret: $secret,
|
|
accountName: (string) $user->getEmail(),
|
|
issuer: 'Your App Name'
|
|
);
|
|
|
|
// Store temporary secret (not activated until verified)
|
|
$tempSecret = new TempTotpSecret(
|
|
userId: $userId,
|
|
secret: $secret,
|
|
createdAt: new \DateTimeImmutable(),
|
|
expiresAt: new \DateTimeImmutable('+10 minutes')
|
|
);
|
|
|
|
$this->userRepository->storeTempTotpSecret($tempSecret);
|
|
|
|
return MfaSetupResult::success($secret, $qrCodeUri);
|
|
}
|
|
|
|
public function verifyAndActivateTotp(
|
|
string $userId,
|
|
string $totpCode
|
|
): MfaActivationResult {
|
|
$tempSecret = $this->userRepository->findTempTotpSecret($userId);
|
|
if (!$tempSecret || $tempSecret->isExpired()) {
|
|
return MfaActivationResult::failed('Setup expired, please restart');
|
|
}
|
|
|
|
// Verify TOTP code
|
|
if (!$this->totpService->verify($totpCode, $tempSecret->getSecret())) {
|
|
return MfaActivationResult::failed('Invalid TOTP code');
|
|
}
|
|
|
|
// Activate TOTP for user
|
|
$this->userRepository->activateTotpForUser($userId, $tempSecret->getSecret());
|
|
$this->userRepository->deleteTempTotpSecret($userId);
|
|
|
|
// Generate backup codes
|
|
$backupCodes = $this->generateBackupCodes($userId);
|
|
|
|
return MfaActivationResult::success($backupCodes);
|
|
}
|
|
|
|
private function generateBackupCodes(string $userId): array
|
|
{
|
|
$codes = [];
|
|
for ($i = 0; $i < 8; $i++) {
|
|
$code = $this->generateReadableCode();
|
|
$codes[] = $code;
|
|
|
|
$backupCode = new MfaBackupCode(
|
|
userId: $userId,
|
|
code: Hash::sha256($code),
|
|
createdAt: new \DateTimeImmutable(),
|
|
usedAt: null
|
|
);
|
|
|
|
$this->userRepository->storeBackupCode($backupCode);
|
|
}
|
|
|
|
return $codes;
|
|
}
|
|
|
|
private function generateReadableCode(): string
|
|
{
|
|
// Generate 8-digit backup code
|
|
return sprintf('%04d-%04d', random_int(1000, 9999), random_int(1000, 9999));
|
|
}
|
|
}
|
|
```
|
|
|
|
### Session Management with Device Tracking
|
|
|
|
```php
|
|
final readonly class DeviceTrackingService
|
|
{
|
|
public function __construct(
|
|
private AuthenticationService $authService,
|
|
private SessionRepository $sessionRepository
|
|
) {}
|
|
|
|
public function authenticateWithDeviceTracking(
|
|
string $identifier,
|
|
#[SensitiveParameter] string $password,
|
|
HttpRequest $request
|
|
): AuthenticationResult {
|
|
$ipAddress = IpAddress::fromRequest();
|
|
$userAgent = $request->server->getUserAgent();
|
|
|
|
$result = $this->authService->authenticate($identifier, $password, $ipAddress);
|
|
|
|
if ($result->isSuccess()) {
|
|
$session = $result->getSession();
|
|
$user = $result->getUser();
|
|
|
|
// Create device fingerprint
|
|
$deviceInfo = new DeviceInfo(
|
|
userAgent: $userAgent,
|
|
ipAddress: $ipAddress,
|
|
screenResolution: $request->headers->get('X-Screen-Resolution'),
|
|
timezone: $request->headers->get('X-Timezone'),
|
|
language: $request->headers->get('Accept-Language')
|
|
);
|
|
|
|
// Check if this is a new device
|
|
$isNewDevice = !$this->sessionRepository->hasRecentSessionForDevice(
|
|
userId: $user->getId(),
|
|
deviceFingerprint: $deviceInfo->getFingerprint(),
|
|
withinDays: 30
|
|
);
|
|
|
|
if ($isNewDevice) {
|
|
// Send security notification
|
|
$this->sendNewDeviceNotification($user, $deviceInfo, $ipAddress);
|
|
}
|
|
|
|
// Update session with device info
|
|
$this->sessionRepository->updateSessionDeviceInfo($session->getId(), $deviceInfo);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function listActiveSessions(string $userId): array
|
|
{
|
|
$sessions = $this->sessionRepository->getActiveSessionsForUser($userId);
|
|
|
|
return array_map(function (AuthenticationSession $session) {
|
|
$deviceInfo = $this->sessionRepository->getSessionDeviceInfo($session->getId());
|
|
|
|
return [
|
|
'id' => $session->getId()->toString(),
|
|
'created_at' => $session->getCreatedAt()->format(\DateTimeInterface::ATOM),
|
|
'last_activity' => $session->getLastActivity()->format(\DateTimeInterface::ATOM),
|
|
'ip_address' => (string) $session->getIpAddress(),
|
|
'device_info' => $deviceInfo?->toArray(),
|
|
'is_current' => $this->isCurrentSession($session->getId())
|
|
];
|
|
}, $sessions);
|
|
}
|
|
|
|
public function revokeSession(string $userId, SessionId $sessionId): bool
|
|
{
|
|
// Verify session belongs to user
|
|
$session = $this->sessionRepository->findSessionById($sessionId);
|
|
if (!$session || $session->getUserId() !== $userId) {
|
|
return false;
|
|
}
|
|
|
|
return $this->authService->logout($sessionId);
|
|
}
|
|
|
|
private function sendNewDeviceNotification(
|
|
User $user,
|
|
DeviceInfo $deviceInfo,
|
|
IpAddress $ipAddress
|
|
): void {
|
|
// Implementation would send email/SMS notification
|
|
// about login from new device
|
|
}
|
|
|
|
private function isCurrentSession(SessionId $sessionId): bool
|
|
{
|
|
// Check if this is the current session
|
|
return session_id() === $sessionId->toString();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Rate Limiting & Security
|
|
|
|
### Custom Rate Limiting Implementation
|
|
|
|
```php
|
|
final readonly class CustomRateLimitService implements RateLimitService
|
|
{
|
|
public function __construct(
|
|
private Cache $cache,
|
|
private int $maxAttempts = 5,
|
|
private int $windowSeconds = 300,
|
|
private int $lockoutDuration = 900
|
|
) {}
|
|
|
|
public function isRateLimited(IpAddress $ipAddress, string $action): bool
|
|
{
|
|
$key = $this->getRateLimitKey($ipAddress, $action);
|
|
$attempts = $this->cache->get($key, 0);
|
|
|
|
return $attempts >= $this->maxAttempts;
|
|
}
|
|
|
|
public function recordAttempt(IpAddress $ipAddress, string $action): void
|
|
{
|
|
$key = $this->getRateLimitKey($ipAddress, $action);
|
|
$attempts = $this->cache->get($key, 0);
|
|
|
|
$this->cache->set($key, $attempts + 1, $this->windowSeconds);
|
|
|
|
if ($attempts + 1 >= $this->maxAttempts) {
|
|
$lockoutKey = $this->getLockoutKey($ipAddress, $action);
|
|
$this->cache->set($lockoutKey, true, $this->lockoutDuration);
|
|
}
|
|
}
|
|
|
|
public function clearAttempts(IpAddress $ipAddress, string $action): void
|
|
{
|
|
$key = $this->getRateLimitKey($ipAddress, $action);
|
|
$lockoutKey = $this->getLockoutKey($ipAddress, $action);
|
|
|
|
$this->cache->forget($key);
|
|
$this->cache->forget($lockoutKey);
|
|
}
|
|
|
|
public function getRetryAfter(IpAddress $ipAddress, string $action): int
|
|
{
|
|
$lockoutKey = $this->getLockoutKey($ipAddress, $action);
|
|
$lockoutExpiry = $this->cache->get($lockoutKey);
|
|
|
|
if (!$lockoutExpiry) {
|
|
return 0;
|
|
}
|
|
|
|
return max(0, $lockoutExpiry - time());
|
|
}
|
|
|
|
private function getRateLimitKey(IpAddress $ipAddress, string $action): string
|
|
{
|
|
return sprintf('rate_limit:%s:%s', $action, (string) $ipAddress);
|
|
}
|
|
|
|
private function getLockoutKey(IpAddress $ipAddress, string $action): string
|
|
{
|
|
return sprintf('lockout:%s:%s', $action, (string) $ipAddress);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing Examples
|
|
|
|
### Authentication Service Testing
|
|
|
|
```php
|
|
use PHPUnit\Framework\TestCase;
|
|
use App\Framework\Auth\AuthenticationService;
|
|
|
|
final class AuthenticationServiceTest extends TestCase
|
|
{
|
|
private AuthenticationService $authService;
|
|
private PasswordHasher $passwordHasher;
|
|
private MockAuthenticationRepository $repository;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->passwordHasher = new PasswordHasher(
|
|
kdf: new MockKeyDerivationFunction(),
|
|
defaultAlgorithm: 'argon2id',
|
|
defaultSecurityLevel: PasswordHasher::LEVEL_LOW // Fast for testing
|
|
);
|
|
|
|
$this->repository = new MockAuthenticationRepository();
|
|
|
|
$this->authService = new AuthenticationService(
|
|
passwordHasher: $this->passwordHasher,
|
|
sessionIdGenerator: new MockSessionIdGenerator(),
|
|
repository: $this->repository
|
|
);
|
|
}
|
|
|
|
public function test_successful_authentication(): void
|
|
{
|
|
$password = 'SecurePassword123!';
|
|
$hashedPassword = $this->passwordHasher->hash($password);
|
|
|
|
$user = new User(
|
|
id: 'user-1',
|
|
email: new Email('user@example.com'),
|
|
username: 'testuser',
|
|
hashedPassword: $hashedPassword
|
|
);
|
|
|
|
$this->repository->addUser($user);
|
|
|
|
$result = $this->authService->authenticate(
|
|
identifier: 'user@example.com',
|
|
password: $password,
|
|
ipAddress: IpAddress::localhost()
|
|
);
|
|
|
|
$this->assertTrue($result->isSuccess());
|
|
$this->assertEquals($user->getId(), $result->getUser()->getId());
|
|
$this->assertInstanceOf(AuthenticationSession::class, $result->getSession());
|
|
}
|
|
|
|
public function test_failed_authentication_with_invalid_password(): void
|
|
{
|
|
$hashedPassword = $this->passwordHasher->hash('CorrectPassword123!');
|
|
|
|
$user = new User(
|
|
id: 'user-1',
|
|
email: new Email('user@example.com'),
|
|
username: 'testuser',
|
|
hashedPassword: $hashedPassword
|
|
);
|
|
|
|
$this->repository->addUser($user);
|
|
|
|
$result = $this->authService->authenticate(
|
|
identifier: 'user@example.com',
|
|
password: 'WrongPassword',
|
|
ipAddress: IpAddress::localhost()
|
|
);
|
|
|
|
$this->assertFalse($result->isSuccess());
|
|
$this->assertEquals('Invalid credentials', $result->getErrorMessage());
|
|
}
|
|
|
|
public function test_account_lockout_after_max_attempts(): void
|
|
{
|
|
$hashedPassword = $this->passwordHasher->hash('CorrectPassword123!');
|
|
|
|
$user = new User(
|
|
id: 'user-1',
|
|
email: new Email('user@example.com'),
|
|
username: 'testuser',
|
|
hashedPassword: $hashedPassword
|
|
);
|
|
|
|
$this->repository->addUser($user);
|
|
|
|
// Simulate failed attempts
|
|
for ($i = 0; $i < AuthenticationService::MAX_LOGIN_ATTEMPTS; $i++) {
|
|
$this->authService->authenticate(
|
|
identifier: 'user@example.com',
|
|
password: 'WrongPassword',
|
|
ipAddress: IpAddress::localhost()
|
|
);
|
|
}
|
|
|
|
// Next attempt should be locked
|
|
$result = $this->authService->authenticate(
|
|
identifier: 'user@example.com',
|
|
password: 'CorrectPassword123!',
|
|
ipAddress: IpAddress::localhost()
|
|
);
|
|
|
|
$this->assertTrue($result->isAccountLocked());
|
|
$this->assertInstanceOf(\DateTimeImmutable::class, $result->getLockoutExpiresAt());
|
|
}
|
|
}
|
|
```
|
|
|
|
### Password Hashing Testing
|
|
|
|
```php
|
|
final class PasswordHasherTest extends TestCase
|
|
{
|
|
private PasswordHasher $passwordHasher;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->passwordHasher = new PasswordHasher(
|
|
kdf: new MockKeyDerivationFunction(),
|
|
defaultAlgorithm: 'argon2id',
|
|
defaultSecurityLevel: PasswordHasher::LEVEL_LOW
|
|
);
|
|
}
|
|
|
|
public function test_password_hashing_and_verification(): void
|
|
{
|
|
$password = 'TestPassword123!';
|
|
|
|
$hashedPassword = $this->passwordHasher->hash($password);
|
|
|
|
$this->assertInstanceOf(HashedPassword::class, $hashedPassword);
|
|
$this->assertEquals('argon2id', $hashedPassword->getAlgorithm());
|
|
$this->assertTrue($this->passwordHasher->verify($password, $hashedPassword));
|
|
$this->assertFalse($this->passwordHasher->verify('WrongPassword', $hashedPassword));
|
|
}
|
|
|
|
public function test_password_strength_validation(): void
|
|
{
|
|
$weakPassword = 'weak';
|
|
$strongPassword = 'StrongPassword123!@#';
|
|
|
|
$weakValidation = $this->passwordHasher->validatePasswordStrength($weakPassword);
|
|
$strongValidation = $this->passwordHasher->validatePasswordStrength($strongPassword);
|
|
|
|
$this->assertFalse($weakValidation->isValid);
|
|
$this->assertNotEmpty($weakValidation->errors);
|
|
$this->assertEquals(PasswordStrength::WEAK, $weakValidation->strength);
|
|
|
|
$this->assertTrue($strongValidation->isValid);
|
|
$this->assertEmpty($strongValidation->errors);
|
|
$this->assertContains($strongValidation->strength, [
|
|
PasswordStrength::STRONG,
|
|
PasswordStrength::VERY_STRONG
|
|
]);
|
|
}
|
|
|
|
public function test_secure_password_generation(): void
|
|
{
|
|
$password = $this->passwordHasher->generateSecurePassword(16);
|
|
|
|
$this->assertEquals(16, strlen($password));
|
|
|
|
$validation = $this->passwordHasher->validatePasswordStrength($password);
|
|
$this->assertTrue($validation->isValid);
|
|
$this->assertGreaterThanOrEqual(70, $validation->strengthScore);
|
|
}
|
|
} |