# 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); } }