Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
797
docs/components/auth/examples.md
Normal file
797
docs/components/auth/examples.md
Normal file
@@ -0,0 +1,797 @@
|
||||
# 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user