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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,430 @@
# Auth Module Configuration
**Konfiguration und Setup** für das Auth Module des Custom PHP Frameworks.
## Dependency Injection Setup
### Container Bindings
```php
use App\Framework\Auth\PasswordHasher;
use App\Framework\Auth\AuthenticationService;
use App\Framework\Cryptography\KeyDerivationFunction;
// services.php oder Container Initialization
$container->singleton(PasswordHasher::class, function(Container $container) {
return new PasswordHasher(
kdf: $container->get(KeyDerivationFunction::class),
defaultAlgorithm: 'argon2id',
defaultSecurityLevel: PasswordHasher::LEVEL_STANDARD
);
});
$container->singleton(AuthenticationService::class, function(Container $container) {
return new AuthenticationService(
passwordHasher: $container->get(PasswordHasher::class),
sessionIdGenerator: $container->get(SessionIdGenerator::class),
repository: $container->get(AuthenticationRepository::class),
rateLimiter: $container->get(RateLimitService::class)
);
});
```
### Environment Configuration
```php
// .env Konfiguration
AUTH_SESSION_TIMEOUT=3600 # Session Timeout in Sekunden (1 Stunde)
AUTH_REMEMBER_TOKEN_EXPIRY=2592000 # Remember Token Expiry (30 Tage)
AUTH_MAX_LOGIN_ATTEMPTS=5 # Maximale Login-Versuche
AUTH_LOCKOUT_DURATION=900 # Account Lockout Duration (15 Minuten)
AUTH_RATE_LIMIT_WINDOW=300 # Rate Limit Window (5 Minuten)
# Password Hashing Configuration
AUTH_DEFAULT_ALGORITHM=argon2id # Standard Hash-Algorithmus
AUTH_DEFAULT_SECURITY_LEVEL=standard # low|standard|high
AUTH_PASSWORD_MIN_LENGTH=8 # Minimale Passwort-Länge
AUTH_PASSWORD_MAX_LENGTH=4096 # Maximale Passwort-Länge
# Session Security
AUTH_SESSION_REGENERATE_ON_LOGIN=true # Session ID bei Login regenerieren
AUTH_CHECK_IP_CONSISTENCY=false # IP-Konsistenz-Prüfung (optional)
AUTH_REMEMBER_TOKEN_LENGTH=32 # Remember Token Länge (Bytes)
```
### Typed Configuration Class
```php
final readonly class AuthConfig
{
public function __construct(
// Session Configuration
public int $sessionTimeout = 3600,
public int $rememberTokenExpiry = 2592000,
public bool $sessionRegenerateOnLogin = true,
public bool $checkIpConsistency = false,
public int $rememberTokenLength = 32,
// Security Configuration
public int $maxLoginAttempts = 5,
public int $lockoutDuration = 900,
public int $rateLimitWindow = 300,
// Password Configuration
public string $defaultAlgorithm = 'argon2id',
public string $defaultSecurityLevel = 'standard',
public int $passwordMinLength = 8,
public int $passwordMaxLength = 4096,
// Validation Configuration
public bool $enforcePasswordComplexity = true,
public bool $checkCommonPasswords = true,
public bool $preventSequentialChars = true,
public int $minPasswordScore = 50
) {}
public static function fromEnvironment(Environment $env): self
{
return new self(
sessionTimeout: $env->getInt(EnvKey::AUTH_SESSION_TIMEOUT, 3600),
rememberTokenExpiry: $env->getInt(EnvKey::AUTH_REMEMBER_TOKEN_EXPIRY, 2592000),
sessionRegenerateOnLogin: $env->getBool(EnvKey::AUTH_SESSION_REGENERATE_ON_LOGIN, true),
checkIpConsistency: $env->getBool(EnvKey::AUTH_CHECK_IP_CONSISTENCY, false),
rememberTokenLength: $env->getInt(EnvKey::AUTH_REMEMBER_TOKEN_LENGTH, 32),
maxLoginAttempts: $env->getInt(EnvKey::AUTH_MAX_LOGIN_ATTEMPTS, 5),
lockoutDuration: $env->getInt(EnvKey::AUTH_LOCKOUT_DURATION, 900),
rateLimitWindow: $env->getInt(EnvKey::AUTH_RATE_LIMIT_WINDOW, 300),
defaultAlgorithm: $env->get(EnvKey::AUTH_DEFAULT_ALGORITHM, 'argon2id'),
defaultSecurityLevel: $env->get(EnvKey::AUTH_DEFAULT_SECURITY_LEVEL, 'standard'),
passwordMinLength: $env->getInt(EnvKey::AUTH_PASSWORD_MIN_LENGTH, 8),
passwordMaxLength: $env->getInt(EnvKey::AUTH_PASSWORD_MAX_LENGTH, 4096),
enforcePasswordComplexity: $env->getBool(EnvKey::AUTH_ENFORCE_PASSWORD_COMPLEXITY, true),
checkCommonPasswords: $env->getBool(EnvKey::AUTH_CHECK_COMMON_PASSWORDS, true),
preventSequentialChars: $env->getBool(EnvKey::AUTH_PREVENT_SEQUENTIAL_CHARS, true),
minPasswordScore: $env->getInt(EnvKey::AUTH_MIN_PASSWORD_SCORE, 50)
);
}
public function getSecurityLevelParameters(string $algorithm): array
{
return match ([$algorithm, $this->defaultSecurityLevel]) {
['argon2id', 'low'] => [
'memory_cost' => 32768, // 32 MB
'time_cost' => 2,
'threads' => 2
],
['argon2id', 'standard'] => [
'memory_cost' => 65536, // 64 MB
'time_cost' => 4,
'threads' => 3
],
['argon2id', 'high'] => [
'memory_cost' => 131072, // 128 MB
'time_cost' => 6,
'threads' => 4
],
['pbkdf2-sha256', 'low'] => ['iterations' => 50000],
['pbkdf2-sha256', 'standard'] => ['iterations' => 100000],
['pbkdf2-sha256', 'high'] => ['iterations' => 200000],
default => []
};
}
}
```
## Database Schema
### Required Tables
```sql
-- Benutzer-Tabelle (beispielhaft)
CREATE TABLE users (
id VARCHAR(255) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(100) UNIQUE,
password_hash TEXT NOT NULL,
password_algorithm VARCHAR(50) NOT NULL DEFAULT 'argon2id',
password_parameters JSON,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
last_login_at DATETIME NULL,
is_active BOOLEAN DEFAULT TRUE,
INDEX idx_email (email),
INDEX idx_username (username),
INDEX idx_active (is_active)
);
-- Session-Tabelle
CREATE TABLE auth_sessions (
id VARCHAR(255) PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
created_at DATETIME NOT NULL,
expires_at DATETIME NOT NULL,
last_activity DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_expires_at (expires_at),
INDEX idx_last_activity (last_activity)
);
-- Remember Token Tabelle
CREATE TABLE auth_remember_tokens (
token_hash VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL,
expires_at DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_expires_at (expires_at)
);
-- Failed Login Attempts Tabelle
CREATE TABLE auth_failed_attempts (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(255),
identifier VARCHAR(255),
ip_address VARCHAR(45),
attempted_at DATETIME NOT NULL,
reason VARCHAR(100),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_identifier (identifier),
INDEX idx_ip_address (ip_address),
INDEX idx_attempted_at (attempted_at)
);
-- Security Events Tabelle (optional für Logging)
CREATE TABLE auth_security_events (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
event_type VARCHAR(50) NOT NULL,
user_id VARCHAR(255),
session_id VARCHAR(255),
ip_address VARCHAR(45),
user_agent TEXT,
event_data JSON,
created_at DATETIME NOT NULL,
INDEX idx_event_type (event_type),
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at),
INDEX idx_ip_address (ip_address)
);
```
### Migration Commands
```bash
# Migration erstellen
php console.php make:migration CreateAuthTables Auth
# Migration ausführen
php console.php db:migrate
# Migration Status prüfen
php console.php db:status
```
## Security Level Configuration
### Password Hashing Levels
```php
// Niedrige Sicherheit (Development, Testing)
$lowSecurity = new PasswordHasher(
kdf: $kdf,
defaultAlgorithm: 'argon2id',
defaultSecurityLevel: PasswordHasher::LEVEL_LOW
);
// Standard Sicherheit (Production Default)
$standardSecurity = new PasswordHasher(
kdf: $kdf,
defaultAlgorithm: 'argon2id',
defaultSecurityLevel: PasswordHasher::LEVEL_STANDARD
);
// Hohe Sicherheit (Banking, Healthcare)
$highSecurity = new PasswordHasher(
kdf: $kdf,
defaultAlgorithm: 'argon2id',
defaultSecurityLevel: PasswordHasher::LEVEL_HIGH
);
```
### Algorithm Performance Comparison
| Algorithm | Level | Memory | Time | Iterations | Performance | Security |
|-----------|-------|---------|------|------------|-------------|----------|
| Argon2ID | Low | 32 MB | 2 | - | Fast | Good |
| Argon2ID | Standard | 64 MB | 4 | - | Medium | Excellent |
| Argon2ID | High | 128 MB | 6 | - | Slow | Maximum |
| PBKDF2-SHA256 | Low | - | - | 50,000 | Fast | Good |
| PBKDF2-SHA256 | Standard | - | - | 100,000 | Fast | Good |
| PBKDF2-SHA256 | High | - | - | 200,000 | Medium | Good |
| Scrypt | Standard | 16 MB | - | - | Medium | Good |
## Rate Limiting Configuration
### Redis-based Rate Limiting
```php
use App\Framework\Auth\RateLimit\RedisRateLimitService;
$rateLimiter = new RedisRateLimitService(
redis: $redis,
config: new RateLimitConfig(
maxAttempts: 5,
windowSeconds: 300,
lockoutDuration: 900
)
);
```
### File-based Rate Limiting
```php
use App\Framework\Auth\RateLimit\FileRateLimitService;
$rateLimiter = new FileRateLimitService(
cacheDir: '/tmp/auth_rate_limits',
config: new RateLimitConfig(
maxAttempts: 5,
windowSeconds: 300,
lockoutDuration: 900,
cleanupInterval: 3600 // Cleanup alte Einträge jede Stunde
)
);
```
## Monitoring & Logging
### Security Event Configuration
```php
// Security Event Handler
final readonly class SecurityEventHandler
{
public function __construct(
private Logger $logger,
private ?AlertingService $alerting = null
) {}
public function handle(SecurityEvent $event): void
{
// Log alle Security Events
$this->logger->warning('Security Event', [
'event_type' => $event->getType(),
'user_id' => $event->getUserId(),
'ip_address' => (string) $event->getIpAddress(),
'data' => $event->getData()
]);
// Kritische Events alarmieren
if ($event->isCritical()) {
$this->alerting?->sendAlert($event);
}
}
}
```
### Performance Monitoring
```php
// Performance Metrics für Password Hashing
$start = microtime(true);
$hashedPassword = $passwordHasher->hash($password);
$hashTime = microtime(true) - $start;
$this->metrics->histogram('auth.password_hash_duration', $hashTime, [
'algorithm' => $hashedPassword->getAlgorithm(),
'security_level' => $securityLevel
]);
```
## Production Deployment
### Environment-specific Configuration
```php
// Production
AUTH_DEFAULT_SECURITY_LEVEL=high
AUTH_SESSION_TIMEOUT=1800 # 30 Minuten
AUTH_CHECK_IP_CONSISTENCY=true # Striktere IP-Prüfung
AUTH_MAX_LOGIN_ATTEMPTS=3 # Weniger Versuche
AUTH_LOCKOUT_DURATION=3600 # 1 Stunde Lockout
// Development
AUTH_DEFAULT_SECURITY_LEVEL=low
AUTH_SESSION_TIMEOUT=86400 # 24 Stunden
AUTH_CHECK_IP_CONSISTENCY=false
AUTH_MAX_LOGIN_ATTEMPTS=10
AUTH_LOCKOUT_DURATION=300 # 5 Minuten
// Testing
AUTH_DEFAULT_SECURITY_LEVEL=low
AUTH_SESSION_TIMEOUT=3600
AUTH_MAX_LOGIN_ATTEMPTS=5
AUTH_LOCKOUT_DURATION=60 # 1 Minute
```
### Security Headers Configuration
```php
// Middleware für Auth-bezogene Security Headers
final readonly class AuthSecurityHeadersMiddleware
{
public function handle(HttpRequest $request, callable $next): HttpResponse
{
$response = $next($request);
if ($request->getUri()->getPath() === '/login') {
$response = $response->withHeader('X-Frame-Options', 'DENY');
$response = $response->withHeader('X-Content-Type-Options', 'nosniff');
$response = $response->withHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
}
return $response;
}
}
```
## Backup & Recovery
### Session Cleanup
```bash
# Cron Job für Session Cleanup (täglich)
0 2 * * * php /path/to/console.php auth:cleanup-expired-sessions
# Manual Cleanup
php console.php auth:cleanup-expired-sessions
php console.php auth:cleanup-expired-tokens
```
### Data Retention
```php
// Cleanup Commands
final readonly class CleanupExpiredSessionsCommand
{
public function execute(): void
{
$expiredCount = $this->repository->deleteExpiredSessions();
$this->output->writeln("Deleted {$expiredCount} expired sessions");
$tokenCount = $this->repository->deleteExpiredRememberTokens();
$this->output->writeln("Deleted {$tokenCount} expired remember tokens");
$attemptCount = $this->repository->cleanupOldFailedAttempts(days: 30);
$this->output->writeln("Cleaned up {$attemptCount} old failed attempts");
}
}

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

View File

@@ -0,0 +1,310 @@
# Auth Module
**Sichere Authentifizierungs- und Autorisierungskomponenten** für das Custom PHP Framework.
## Überblick
Das Auth Module bietet umfassende Sicherheitsfeatures für Benutzerauthentifizierung, Passwort-Management und Session-Verwaltung. Es integriert sich nahtlos mit dem Cryptography Module für maximale Sicherheit.
## Kernkomponenten
### PasswordHasher Service
Sichere Passwort-Hashing und -Verifizierung mit automatischem Rehashing.
```php
use App\Framework\Auth\PasswordHasher;
use App\Framework\Cryptography\KeyDerivationFunction;
$passwordHasher = new PasswordHasher(
kdf: $keyDerivationFunction,
defaultAlgorithm: 'argon2id',
defaultSecurityLevel: PasswordHasher::LEVEL_STANDARD
);
// Passwort hashen
$hashedPassword = $passwordHasher->hash('userPassword123');
// Passwort verifizieren
$isValid = $passwordHasher->verify('userPassword123', $hashedPassword);
// Passwort-Stärke validieren
$validation = $passwordHasher->validatePasswordStrength('userPassword123');
if (!$validation->isValid) {
echo implode(', ', $validation->errors);
}
```
### HashedPassword Value Object
Immutables Value Object für gehashte Passwörter mit Metadaten.
```php
use App\Framework\Auth\HashedPassword;
use App\Framework\Auth\PasswordStrength;
// Aus DerivedKey erstellen
$hashedPassword = HashedPassword::fromDerivedKey($derivedKey);
// Eigenschaften abrufen
$algorithm = $hashedPassword->getAlgorithm(); // 'argon2id'
$parameters = $hashedPassword->getParameters(); // ['memory_cost' => 65536, ...]
$strength = $hashedPassword->getStrength(); // PasswordStrength::STRONG
$createdAt = $hashedPassword->getCreatedAt(); // DateTimeImmutable
// Rehashing-Prüfung
$needsRehash = $hashedPassword->needsRehash('argon2id', [
'memory_cost' => 131072, // Höhere Sicherheitsanforderungen
'time_cost' => 6
]);
// Sicherheits-Bewertung
$assessment = $hashedPassword->assessSecurity();
echo $assessment->getSummary(); // "Strong security with Argon2ID (2024 standards)"
```
### AuthenticationService
Zentrale Authentifizierungslogik mit erweiterten Sicherheitsfeatures.
```php
use App\Framework\Auth\AuthenticationService;
use App\Framework\Http\IpAddress;
$authService = new AuthenticationService(
passwordHasher: $passwordHasher,
sessionIdGenerator: $sessionIdGenerator,
repository: $authRepository
);
// Benutzer authentifizieren
$result = $authService->authenticate(
identifier: 'user@example.com',
password: 'userPassword123',
ipAddress: IpAddress::from('192.168.1.1'),
remember: true
);
if ($result->isSuccess()) {
$user = $result->getUser();
$session = $result->getSession();
$rememberToken = $result->getRememberToken(); // nullable
}
// Mit Session authentifizieren
$result = $authService->authenticateWithSession(
$sessionId,
IpAddress::from('192.168.1.1')
);
// Mit Remember Token authentifizieren
$result = $authService->authenticateWithRememberToken(
$tokenValue,
IpAddress::from('192.168.1.1')
);
```
### PasswordValidationResult Value Object
Detaillierte Passwort-Validierungsergebnisse.
```php
use App\Framework\Auth\PasswordValidationResult;
$validation = $passwordHasher->validatePasswordStrength('weakpass');
// Validierungsstatus prüfen
$isValid = $validation->isValid; // false
$errors = $validation->errors; // ['Password must be at least 8 characters long']
$warnings = $validation->warnings; // ['Consider using more character types']
$score = $validation->strengthScore; // 45
$strength = $validation->strength; // PasswordStrength::WEAK
// Sicherheitslevel prüfen
$meetsMinimum = $validation->meetsMinimumRequirements(); // false
$isRecommended = $validation->isRecommended(); // false
// Zusammenfassung
echo $validation->getSummary();
// "Password does not meet requirements: Password must be at least 8 characters long"
// API-Response Format
$apiResponse = $validation->toArray();
```
## Sicherheitsfeatures
### Rate Limiting & Account Lockout
Schutz vor Brute-Force-Angriffen durch intelligente Rate-Limitierung.
```php
// Automatisches Rate Limiting in AuthenticationService
const MAX_LOGIN_ATTEMPTS = 5;
const LOCKOUT_DURATION = 900; // 15 Minuten
const RATE_LIMIT_WINDOW = 300; // 5 Minuten
// Rate Limiting wird automatisch angewendet
$result = $authService->authenticate($email, $password, $ipAddress);
if ($result->isRateLimited()) {
$retryAfter = $result->getRetryAfter();
echo "Rate limit exceeded. Retry after {$retryAfter} seconds.";
}
if ($result->isAccountLocked()) {
$expiresAt = $result->getLockoutExpiresAt();
echo "Account locked until {$expiresAt->format('Y-m-d H:i:s')}";
}
```
### Session-Sicherheit
Sichere Session-Verwaltung mit IP-Tracking und automatischer Rotation.
```php
// Sessions werden automatisch erstellt und verwaltet
const SESSION_TIMEOUT = 3600; // 1 Stunde
const REMEMBER_TOKEN_LENGTH = 32; // 256-bit Token
// Session-Features:
// - Automatische IP-Konsistenz-Prüfung
// - Session-Timeout-Management
// - Aktivitäts-Tracking
// - Sichere Session-IDs über SessionIdGenerator
```
### Token-Hashing mit Hash Value Object
Sichere Token-Behandlung mit typisierter Hash-Verwaltung.
```php
// Remember Tokens werden sicher gehasht
private function hashToken(string $token): Hash
{
return Hash::sha256($token);
}
// Vorteile:
// - Typsicherheit für Hash-Werte
// - Explizite Algorithmus-Angabe (SHA-256)
// - Timing-safe Vergleiche mit hash_equals()
// - Framework-konsistente Hash-Behandlung
// - Automatische Hash-Validierung
```
### Passwort-Sicherheit
Umfassende Passwort-Validierung und -Bewertung.
```php
// Automatische Passwort-Stärke-Bewertung
$validation = $passwordHasher->validatePasswordStrength($password);
// Validierungsregeln:
// - Minimale Länge (8+ Zeichen)
// - Komplexitätsanforderungen (2+ Zeichentypen)
// - Häufige Muster-Erkennung
// - Sequenzielle Zeichen-Prüfung
// - Exzessive Wiederholungen
// Sichere Passwort-Generierung
$securePassword = $passwordHasher->generateSecurePassword(
length: 16,
includeUppercase: true,
includeLowercase: true,
includeNumbers: true,
includeSpecialChars: true,
excludeChars: '0O1l' // Mehrdeutige Zeichen ausschließen
);
```
## Integration mit Framework
### Abhängigkeiten
Das Auth Module nutzt vorhandene Framework-Komponenten:
- **Cryptography Module**: Für sichere Key Derivation Functions
- **SessionId/SessionIdGenerator**: Für Session-Management
- **IpAddress Value Object**: Für IP-basierte Sicherheitsfeatures
- **Hash Value Object**: Für typisierte Hash-Operationen
### Repository Pattern
Flexible Datenpersistierung durch Repository-Abstraktion.
```php
interface AuthenticationRepository
{
public function findUserByIdentifier(string $identifier): ?User;
public function findUserById(string $userId): ?User;
public function updateUserPassword(string $userId, HashedPassword $password): bool;
public function storeSession(AuthenticationSession $session): void;
public function findSessionById(SessionId $sessionId): ?AuthenticationSession;
public function updateSessionActivity(SessionId $sessionId, ?IpAddress $ipAddress): void;
public function deleteSession(SessionId $sessionId): bool;
public function deleteAllUserSessions(string $userId): void;
public function storeRememberToken(RememberToken $token): void;
public function findRememberToken(Hash $tokenHash): ?RememberToken;
public function deleteRememberToken(Hash $tokenHash): bool;
public function deleteAllUserRememberTokens(string $userId): void;
public function getFailedLoginAttempts(string $userId): int;
public function incrementFailedLoginAttempts(string $userId): void;
public function clearFailedLoginAttempts(string $userId): void;
public function getLastFailedAttemptTime(string $userId): ?\DateTimeImmutable;
}
```
## Best Practices
### Sichere Implementation
```php
// ✅ Sensitive Parameter verwenden
public function authenticate(
string $identifier,
#[SensitiveParameter] string $password,
?IpAddress $ipAddress = null
): AuthenticationResult
// ✅ Automatisches Rehashing
if ($this->passwordHasher->needsRehash($user->getHashedPassword())) {
$newHash = $this->passwordHasher->hash($password);
$this->repository->updateUserPassword($user->getId(), $newHash);
}
// ✅ Timing-safe Token-Vergleiche
$tokenHash = $this->hashToken($tokenValue);
$rememberToken = $this->repository->findRememberToken($tokenHash);
```
### Error Handling
```php
// ✅ Generische Fehlermeldungen für Sicherheit
if (!$user || !$this->passwordHasher->verify($password, $user->getHashedPassword())) {
return AuthenticationResult::failed('Invalid credentials');
}
// ✅ Security Event Logging
$this->recordSecurityEvent('authentication_failed', [
'identifier' => $identifier,
'ip_address' => $ipAddress ? (string) $ipAddress : null,
'reason' => $reason
]);
```
### Performance Optimierung
```php
// ✅ Konfigurierbare Sicherheitslevel
$passwordHasher = new PasswordHasher(
kdf: $keyDerivationFunction,
defaultAlgorithm: 'argon2id',
defaultSecurityLevel: PasswordHasher::LEVEL_STANDARD // vs HIGH für höhere Sicherheit
);
// ✅ Bulk-Operationen für bessere Performance
$this->repository->deleteAllUserSessions($userId);
$this->repository->deleteAllUserRememberTokens($userId);
```
## Nächste Schritte
- [ ] AuthenticationRepository Interface implementieren
- [ ] Authentication Exceptions definieren
- [ ] Unit Tests für alle Komponenten schreiben
- [ ] Rate Limiting Service implementieren
- [ ] Integration Tests für Authentication Flow
- [ ] Performance Benchmarks für Passwort-Hashing

View File

@@ -0,0 +1,904 @@
# Auth Module Security Guidelines
**Sicherheitsrichtlinien und Best Practices** für das Auth Module des Custom PHP Frameworks.
## Security Architecture
### Defense in Depth Strategy
Das Auth Module implementiert mehrschichtige Sicherheitsmaßnahmen:
```
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
├─────────────────────────────────────────────────────────────┤
│ • Rate Limiting & Account Lockout │
│ • Session Security & IP Tracking │
│ • Multi-Factor Authentication │
│ • Password Strength Validation │
├─────────────────────────────────────────────────────────────┤
│ Framework Layer │
├─────────────────────────────────────────────────────────────┤
│ • Secure Password Hashing (Argon2ID) │
│ • Timing-Safe Comparisons │
│ • Cryptographically Secure Random Generation │
│ • Hash Value Object with Algorithm Validation │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
├─────────────────────────────────────────────────────────────┤
│ • HTTPS Enforcement │
│ • Secure Headers (HSTS, CSP) │
│ • Database Security & Prepared Statements │
│ • Logging & Monitoring │
└─────────────────────────────────────────────────────────────┘
```
## Password Security
### Secure Password Hashing
```php
// ✅ Recommended: Argon2ID with high memory cost
$passwordHasher = new PasswordHasher(
kdf: $keyDerivationFunction,
defaultAlgorithm: 'argon2id',
defaultSecurityLevel: PasswordHasher::LEVEL_HIGH // Production
);
// Security parameters for different levels:
// LEVEL_HIGH: 128 MB memory, 6 iterations, 4 threads
// LEVEL_STANDARD: 64 MB memory, 4 iterations, 3 threads
// LEVEL_LOW: 32 MB memory, 2 iterations, 2 threads
// ❌ Avoid: Weak algorithms or low security levels in production
$weakHasher = new PasswordHasher(
kdf: $kdf,
defaultAlgorithm: 'pbkdf2-sha256', // Less secure than Argon2ID
defaultSecurityLevel: PasswordHasher::LEVEL_LOW
);
```
### Password Policy Enforcement
```php
final readonly class PasswordPolicy
{
public const int MIN_LENGTH = 12; // Increased from 8
public const int MIN_COMPLEXITY_TYPES = 3; // Upper, lower, numbers, special
public const int MIN_STRENGTH_SCORE = 70; // High security threshold
public function validatePassword(string $password): PasswordPolicyResult
{
$violations = [];
// Length requirement
if (mb_strlen($password) < self::MIN_LENGTH) {
$violations[] = "Password must be at least " . self::MIN_LENGTH . " characters";
}
// Complexity requirements
$complexityScore = $this->calculateComplexity($password);
if ($complexityScore < self::MIN_COMPLEXITY_TYPES) {
$violations[] = "Password must include at least " . self::MIN_COMPLEXITY_TYPES . " character types";
}
// Entropy check
if ($this->calculateEntropy($password) < 50) {
$violations[] = "Password lacks sufficient entropy";
}
// Dictionary check
if ($this->isCommonPassword($password)) {
$violations[] = "Password is too common";
}
// Personal information check
if ($this->containsPersonalInfo($password)) {
$violations[] = "Password should not contain personal information";
}
return new PasswordPolicyResult(
isValid: empty($violations),
violations: $violations,
strengthScore: $this->calculateStrengthScore($password)
);
}
private function calculateEntropy(string $password): float
{
$length = strlen($password);
$charsetSize = 0;
if (preg_match('/[a-z]/', $password)) $charsetSize += 26;
if (preg_match('/[A-Z]/', $password)) $charsetSize += 26;
if (preg_match('/[0-9]/', $password)) $charsetSize += 10;
if (preg_match('/[^A-Za-z0-9]/', $password)) $charsetSize += 32;
return $length * log($charsetSize, 2);
}
private function isCommonPassword(string $password): bool
{
// Check against common password lists (e.g., Have I Been Pwned)
$commonPasswords = [
'password', '123456', 'password123', 'admin', 'qwerty',
'letmein', 'welcome', 'monkey', 'dragon', '123456789'
];
return in_array(strtolower($password), $commonPasswords, true);
}
}
```
### Automatic Password Rehashing
```php
// ✅ Implement automatic rehashing on authentication
public function authenticate(string $identifier, string $password): AuthenticationResult
{
$user = $this->repository->findByIdentifier($identifier);
if (!$user || !$this->passwordHasher->verify($password, $user->getHashedPassword())) {
return AuthenticationResult::failed('Invalid credentials');
}
// Critical: Check for rehashing need
if ($this->passwordHasher->needsRehash($user->getHashedPassword())) {
$newHash = $this->passwordHasher->hash($password);
$this->repository->updateUserPassword($user->getId(), $newHash);
$this->logger->info('Password automatically rehashed', [
'user_id' => $user->getId(),
'old_algorithm' => $user->getHashedPassword()->getAlgorithm(),
'new_algorithm' => $newHash->getAlgorithm()
]);
}
return AuthenticationResult::success($user);
}
```
## Session Security
### Secure Session Management
```php
final readonly class SecureSessionManager
{
public function __construct(
private AuthenticationService $authService,
private SessionSecurity $sessionSecurity
) {}
public function createSecureSession(
User $user,
IpAddress $ipAddress,
UserAgent $userAgent
): AuthenticationSession {
// Generate cryptographically secure session ID
$sessionId = $this->generateSecureSessionId();
// Create session with security attributes
$session = new AuthenticationSession(
id: $sessionId,
userId: $user->getId(),
ipAddress: $ipAddress,
userAgent: $userAgent,
createdAt: new \DateTimeImmutable(),
expiresAt: new \DateTimeImmutable('+1 hour'),
lastActivity: new \DateTimeImmutable(),
securityAttributes: new SessionSecurityAttributes(
isSecure: true,
isHttpOnly: true,
sameSite: 'Strict',
fingerprint: $this->generateFingerprint($ipAddress, $userAgent)
)
);
// Set secure session cookie
$this->setSecureSessionCookie($sessionId, $session->getExpiresAt());
return $session;
}
private function generateSecureSessionId(): SessionId
{
// Use framework's SessionIdGenerator for consistency
return $this->sessionIdGenerator->generate();
}
private function setSecureSessionCookie(SessionId $sessionId, \DateTimeImmutable $expiresAt): void
{
setcookie('session_id', $sessionId->toString(), [
'expires' => $expiresAt->getTimestamp(),
'path' => '/',
'domain' => '', // Let browser determine
'secure' => true, // HTTPS only
'httponly' => true, // No JavaScript access
'samesite' => 'Strict' // CSRF protection
]);
}
private function generateFingerprint(IpAddress $ipAddress, UserAgent $userAgent): string
{
return Hash::sha256(
$ipAddress->toString() . '|' .
$userAgent->toString() . '|' .
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
)->toString();
}
}
```
### Session Hijacking Protection
```php
final readonly class SessionHijackingProtection
{
public function validateSession(
SessionId $sessionId,
IpAddress $currentIp,
UserAgent $currentUserAgent
): SessionValidationResult {
$session = $this->repository->findSessionById($sessionId);
if (!$session) {
return SessionValidationResult::invalid('Session not found');
}
// Check session expiration
if ($session->isExpired()) {
$this->repository->deleteSession($sessionId);
return SessionValidationResult::expired();
}
// IP address consistency check (configurable)
if ($this->config->checkIpConsistency) {
if (!$session->getIpAddress()->equals($currentIp)) {
$this->logSecurityEvent('session_ip_mismatch', [
'session_id' => $sessionId->toString(),
'original_ip' => (string) $session->getIpAddress(),
'current_ip' => (string) $currentIp
]);
// Suspicious activity - invalidate session
$this->repository->deleteSession($sessionId);
return SessionValidationResult::suspicious('IP address mismatch');
}
}
// User Agent consistency (less strict)
if (!$this->isUserAgentConsistent($session->getUserAgent(), $currentUserAgent)) {
$this->logSecurityEvent('session_user_agent_change', [
'session_id' => $sessionId->toString(),
'original_ua' => (string) $session->getUserAgent(),
'current_ua' => (string) $currentUserAgent
]);
}
// Update session activity
$this->repository->updateSessionActivity($sessionId, $currentIp);
return SessionValidationResult::valid($session);
}
private function isUserAgentConsistent(UserAgent $original, UserAgent $current): bool
{
// Allow minor version changes but detect major browser changes
$originalBrowser = $this->extractBrowser($original);
$currentBrowser = $this->extractBrowser($current);
return $originalBrowser === $currentBrowser;
}
}
```
## Token Security
### Remember Token Security with Hash Value Object
```php
final readonly class RememberTokenSecurity
{
public function createRememberToken(string $userId): RememberToken
{
// Generate cryptographically secure token
$tokenValue = bin2hex(random_bytes(32)); // 256-bit token
// Hash token using Hash Value Object for type safety
$tokenHash = Hash::sha256($tokenValue);
$rememberToken = new RememberToken(
hash: $tokenHash,
userId: $userId,
createdAt: new \DateTimeImmutable(),
expiresAt: new \DateTimeImmutable('+30 days'),
lastUsed: null
);
$this->repository->storeRememberToken($rememberToken);
// Return token with plain text value (only shown once)
return $rememberToken->withPlainTextValue($tokenValue);
}
public function validateRememberToken(string $tokenValue): RememberTokenValidation
{
// Hash provided token for comparison
$tokenHash = Hash::sha256($tokenValue);
$rememberToken = $this->repository->findRememberToken($tokenHash);
if (!$rememberToken) {
$this->logSecurityEvent('invalid_remember_token', [
'token_hash' => $tokenHash->toShort(8) // Only log first 8 chars
]);
return RememberTokenValidation::invalid();
}
// Check expiration
if ($rememberToken->isExpired()) {
$this->repository->deleteRememberToken($tokenHash);
return RememberTokenValidation::expired();
}
// Update last used timestamp
$this->repository->updateRememberTokenUsage($tokenHash);
return RememberTokenValidation::valid($rememberToken);
}
public function rotateRememberToken(Hash $oldTokenHash): RememberToken
{
$oldToken = $this->repository->findRememberToken($oldTokenHash);
if (!$oldToken) {
throw new SecurityException('Cannot rotate non-existent token');
}
// Create new token
$newToken = $this->createRememberToken($oldToken->getUserId());
// Delete old token
$this->repository->deleteRememberToken($oldTokenHash);
$this->logSecurityEvent('remember_token_rotated', [
'user_id' => $oldToken->getUserId(),
'old_token_created' => $oldToken->getCreatedAt()->format('Y-m-d H:i:s')
]);
return $newToken;
}
}
// Benefits of using Hash Value Object:
// ✅ Type safety - tokens can't be confused with other strings
// ✅ Algorithm specification - explicit SHA-256 usage
// ✅ Timing-safe comparison - uses hash_equals() internally
// ✅ Validation - ensures proper hash format and length
// ✅ Framework consistency - follows value object patterns
```
## Rate Limiting & Brute Force Protection
### Advanced Rate Limiting
```php
final readonly class AdvancedRateLimiter implements RateLimitService
{
public function __construct(
private Cache $cache,
private Logger $logger,
private RateLimitConfig $config
) {}
public function checkRateLimit(
IpAddress $ipAddress,
string $action,
string $identifier = null
): RateLimitResult {
// Multiple rate limiting strategies
$checks = [
$this->checkIpBasedLimit($ipAddress, $action),
$this->checkIdentifierBasedLimit($identifier, $action),
$this->checkGlobalLimit($action),
$this->checkAdaptiveLimit($ipAddress, $action)
];
foreach ($checks as $check) {
if ($check->isLimited()) {
$this->logRateLimitViolation($ipAddress, $action, $check);
return $check;
}
}
// Record successful attempt
$this->recordAttempt($ipAddress, $action, $identifier);
return RateLimitResult::allowed();
}
private function checkAdaptiveLimit(IpAddress $ipAddress, string $action): RateLimitResult
{
$suspiciousScore = $this->calculateSuspiciousScore($ipAddress);
// Adaptive limits based on suspicious activity
$maxAttempts = match (true) {
$suspiciousScore > 80 => 1, // Highly suspicious
$suspiciousScore > 60 => 2, // Suspicious
$suspiciousScore > 40 => 3, // Moderately suspicious
default => 5 // Normal
};
$key = "adaptive_limit:{$action}:" . $ipAddress->toString();
$attempts = $this->cache->get($key, 0);
if ($attempts >= $maxAttempts) {
return RateLimitResult::limited(
reason: 'Adaptive rate limit exceeded',
retryAfter: 900,
metadata: ['suspicious_score' => $suspiciousScore]
);
}
return RateLimitResult::allowed();
}
private function calculateSuspiciousScore(IpAddress $ipAddress): int
{
$score = 0;
// Check for various suspicious indicators
if ($this->isFromTorNetwork($ipAddress)) $score += 30;
if ($this->isFromVpnProvider($ipAddress)) $score += 15;
if ($this->hasRecentFailures($ipAddress)) $score += 20;
if ($this->isFromUncommonGeolocation($ipAddress)) $score += 10;
if ($this->hasRapidRequests($ipAddress)) $score += 25;
return min(100, $score);
}
private function logRateLimitViolation(
IpAddress $ipAddress,
string $action,
RateLimitResult $result
): void {
$this->logger->warning('Rate limit violation', [
'ip_address' => (string) $ipAddress,
'action' => $action,
'reason' => $result->getReason(),
'retry_after' => $result->getRetryAfter(),
'metadata' => $result->getMetadata()
]);
}
}
```
### Account Lockout Protection
```php
final readonly class AccountLockoutService
{
public function __construct(
private UserRepository $repository,
private Logger $logger,
private NotificationService $notifications
) {}
public function handleFailedLogin(string $identifier, IpAddress $ipAddress): void
{
$user = $this->repository->findByIdentifier($identifier);
if (!$user) {
return; // Don't reveal if user exists
}
$attempts = $this->repository->incrementFailedAttempts($user->getId());
// Progressive lockout strategy
$lockoutDuration = $this->calculateLockoutDuration($attempts);
if ($attempts >= 3) {
$this->repository->lockAccount($user->getId(), $lockoutDuration);
$this->logger->warning('Account locked due to failed attempts', [
'user_id' => $user->getId(),
'attempts' => $attempts,
'lockout_duration' => $lockoutDuration,
'ip_address' => (string) $ipAddress
]);
// Notify user of lockout (rate limited to prevent abuse)
$this->sendLockoutNotification($user, $attempts, $lockoutDuration);
}
}
private function calculateLockoutDuration(int $attempts): int
{
// Exponential backoff with jitter
return match (true) {
$attempts >= 10 => 3600 + random_int(0, 1800), // 1-2.5 hours
$attempts >= 7 => 1800 + random_int(0, 900), // 30-45 minutes
$attempts >= 5 => 900 + random_int(0, 300), // 15-20 minutes
$attempts >= 3 => 300 + random_int(0, 120), // 5-7 minutes
default => 0
};
}
private function sendLockoutNotification(
User $user,
int $attempts,
int $lockoutDuration
): void {
// Rate limit notifications to prevent spam
$notificationKey = "lockout_notification:" . $user->getId();
if ($this->cache->has($notificationKey)) {
return;
}
$this->cache->put($notificationKey, true, 300); // 5 minutes
$this->notifications->sendSecurityAlert($user->getEmail(), [
'type' => 'account_lockout',
'attempts' => $attempts,
'lockout_duration_minutes' => ceil($lockoutDuration / 60),
'timestamp' => new \DateTimeImmutable()
]);
}
}
```
## Multi-Factor Authentication Security
### TOTP Implementation with Security Features
```php
final readonly class SecureTotpService
{
private const int SECRET_LENGTH = 32; // 160-bit secret
private const int WINDOW_SIZE = 1; // Allow 1 time step tolerance
private const int TIME_STEP = 30; // 30-second time steps
public function generateSecret(): string
{
return Base32::encode(random_bytes(self::SECRET_LENGTH));
}
public function verify(string $code, string $secret, ?int $timestamp = null): bool
{
$timestamp = $timestamp ?? time();
$timeStep = intval($timestamp / self::TIME_STEP);
// Check current and adjacent time windows to account for clock drift
for ($i = -self::WINDOW_SIZE; $i <= self::WINDOW_SIZE; $i++) {
$calculatedCode = $this->calculateTotp($secret, $timeStep + $i);
if (hash_equals($code, $calculatedCode)) {
// Prevent replay attacks by storing used codes
$this->markCodeAsUsed($secret, $timeStep + $i);
return true;
}
}
return false;
}
private function calculateTotp(string $secret, int $timeStep): string
{
$binarySecret = Base32::decode($secret);
$timeBytes = pack('N*', 0) . pack('N*', $timeStep);
$hash = hash_hmac('sha1', $timeBytes, $binarySecret, true);
$offset = ord($hash[19]) & 0xf;
$code = (
((ord($hash[$offset]) & 0x7f) << 24) |
((ord($hash[$offset + 1]) & 0xff) << 16) |
((ord($hash[$offset + 2]) & 0xff) << 8) |
(ord($hash[$offset + 3]) & 0xff)
) % 1000000;
return str_pad((string) $code, 6, '0', STR_PAD_LEFT);
}
private function markCodeAsUsed(string $secret, int $timeStep): void
{
// Prevent replay attacks
$key = 'totp_used:' . Hash::sha256($secret . ':' . $timeStep)->toShort(16);
$this->cache->put($key, true, self::TIME_STEP * 2);
}
}
```
### Backup Code Security
```php
final readonly class BackupCodeService
{
private const int CODE_LENGTH = 8;
private const int CODE_COUNT = 8;
public function generateBackupCodes(string $userId): array
{
$codes = [];
for ($i = 0; $i < self::CODE_COUNT; $i++) {
$code = $this->generateHumanReadableCode();
$codes[] = $code;
// Store hashed version
$backupCode = new MfaBackupCode(
userId: $userId,
codeHash: Hash::sha256($code),
createdAt: new \DateTimeImmutable(),
usedAt: null
);
$this->repository->storeBackupCode($backupCode);
}
return $codes;
}
public function verifyBackupCode(string $userId, string $code): bool
{
$codeHash = Hash::sha256($code);
$backupCode = $this->repository->findBackupCode($userId, $codeHash);
if (!$backupCode || $backupCode->isUsed()) {
return false;
}
// Mark as used (one-time use only)
$this->repository->markBackupCodeAsUsed($backupCode, new \DateTimeImmutable());
return true;
}
private function generateHumanReadableCode(): string
{
// Generate readable codes avoiding confusing characters
$chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; // No 0, 1, I, O
$code = '';
for ($i = 0; $i < self::CODE_LENGTH; $i++) {
$code .= $chars[random_int(0, strlen($chars) - 1)];
}
// Format as XXXX-XXXX for readability
return substr($code, 0, 4) . '-' . substr($code, 4, 4);
}
}
```
## Monitoring & Incident Response
### Security Event Monitoring
```php
final readonly class SecurityEventMonitor
{
public function __construct(
private Logger $securityLogger,
private AlertingService $alerting,
private MetricsCollector $metrics
) {}
public function recordSecurityEvent(SecurityEvent $event): void
{
// Log all security events
$this->securityLogger->warning('Security event recorded', [
'event_type' => $event->getType(),
'severity' => $event->getSeverity(),
'user_id' => $event->getUserId(),
'ip_address' => (string) $event->getIpAddress(),
'user_agent' => (string) $event->getUserAgent(),
'metadata' => $event->getMetadata(),
'timestamp' => $event->getTimestamp()->format('Y-m-d H:i:s')
]);
// Update security metrics
$this->metrics->increment('auth.security_events', [
'type' => $event->getType(),
'severity' => $event->getSeverity()
]);
// Send alerts for critical events
if ($event->isCritical()) {
$this->alerting->sendSecurityAlert($event);
}
// Check for attack patterns
$this->analyzeForAttackPatterns($event);
}
private function analyzeForAttackPatterns(SecurityEvent $event): void
{
$ipAddress = $event->getIpAddress();
$recentEvents = $this->getRecentSecurityEvents($ipAddress, minutes: 10);
// Detect brute force attacks
$failedLogins = array_filter($recentEvents,
fn($e) => $e->getType() === 'authentication_failed'
);
if (count($failedLogins) >= 5) {
$this->alerting->sendAlert(new BruteForceDetectionAlert(
ipAddress: $ipAddress,
attemptCount: count($failedLogins),
timeWindow: 10
));
}
// Detect credential stuffing
$uniqueIdentifiers = array_unique(array_map(
fn($e) => $e->getMetadata()['identifier'] ?? null,
$failedLogins
));
if (count($uniqueIdentifiers) >= 10) {
$this->alerting->sendAlert(new CredentialStuffingAlert(
ipAddress: $ipAddress,
uniqueIdentifiers: count($uniqueIdentifiers)
));
}
}
}
```
### Automated Response System
```php
final readonly class AutomatedSecurityResponse
{
public function handleSecurityEvent(SecurityEvent $event): void
{
match ($event->getType()) {
'brute_force_detected' => $this->handleBruteForce($event),
'credential_stuffing_detected' => $this->handleCredentialStuffing($event),
'session_hijacking_suspected' => $this->handleSessionHijacking($event),
'suspicious_login_pattern' => $this->handleSuspiciousLogin($event),
default => null
};
}
private function handleBruteForce(SecurityEvent $event): void
{
$ipAddress = $event->getIpAddress();
// Immediately block IP for 1 hour
$this->firewall->blockIpAddress($ipAddress, duration: 3600);
// Increase monitoring for this IP
$this->monitoring->increaseWatchLevel($ipAddress);
// Notify security team
$this->alerting->sendUrgentAlert(new BruteForceBlockAlert($ipAddress));
}
private function handleSessionHijacking(SecurityEvent $event): void
{
$userId = $event->getUserId();
$sessionId = $event->getMetadata()['session_id'];
// Immediately terminate all user sessions
$this->authService->logoutAll($userId);
// Require password reset
$this->userService->requirePasswordReset($userId);
// Send security notification to user
$user = $this->userRepository->findById($userId);
$this->notifications->sendSecurityBreach($user->getEmail(), [
'incident_type' => 'session_hijacking',
'affected_session' => $sessionId,
'timestamp' => $event->getTimestamp()
]);
}
}
```
## Compliance & Audit
### GDPR Compliance
```php
final readonly class GdprCompliantAuthService
{
public function processPersonalData(User $user, string $purpose): void
{
// Log data processing for GDPR audit trail
$this->auditLogger->info('Personal data processed', [
'user_id' => $user->getId(),
'data_types' => ['email', 'login_timestamps', 'ip_addresses'],
'processing_purpose' => $purpose,
'legal_basis' => 'legitimate_interest', // or 'consent'
'retention_period' => '2_years'
]);
}
public function exportUserData(string $userId): UserDataExport
{
$user = $this->repository->findById($userId);
if (!$user) {
throw new UserNotFoundException($userId);
}
return new UserDataExport([
'personal_data' => [
'user_id' => $user->getId(),
'email' => (string) $user->getEmail(),
'created_at' => $user->getCreatedAt()->format('c'),
'last_login' => $user->getLastLoginAt()?->format('c')
],
'authentication_data' => [
'password_last_changed' => $user->getPasswordChangedAt()?->format('c'),
'mfa_enabled' => $user->hasMfaEnabled(),
'failed_login_attempts' => $this->repository->getFailedLoginAttempts($userId)
],
'session_data' => $this->getSessionHistory($userId),
'security_events' => $this->getSecurityEventHistory($userId)
]);
}
public function deleteUserData(string $userId): void
{
// GDPR right to erasure (right to be forgotten)
$this->repository->anonymizeUser($userId);
$this->repository->deleteAllUserSessions($userId);
$this->repository->deleteAllUserRememberTokens($userId);
$this->repository->deleteUserSecurityEvents($userId);
$this->auditLogger->info('User data deleted per GDPR request', [
'user_id' => $userId,
'deletion_timestamp' => (new \DateTimeImmutable())->format('c')
]);
}
}
```
### Security Audit Logging
```php
final readonly class SecurityAuditLogger
{
public function logAuthenticationEvent(string $eventType, array $context): void
{
$auditRecord = [
'event_type' => $eventType,
'timestamp' => (new \DateTimeImmutable())->format('c'),
'user_id' => $context['user_id'] ?? null,
'session_id' => $context['session_id'] ?? null,
'ip_address' => $context['ip_address'] ?? null,
'user_agent' => $context['user_agent'] ?? null,
'success' => $context['success'] ?? false,
'failure_reason' => $context['failure_reason'] ?? null,
'risk_score' => $this->calculateRiskScore($context)
];
// Store in secure audit log (append-only, tamper-evident)
$this->auditStorage->append($auditRecord);
// Forward to SIEM if configured
if ($this->siemForwarder) {
$this->siemForwarder->forward($auditRecord);
}
}
private function calculateRiskScore(array $context): int
{
$score = 0;
// Location-based risk
if (isset($context['ip_address'])) {
$location = $this->geolocator->locate($context['ip_address']);
if ($location->isHighRiskCountry()) $score += 25;
if ($location->isFromTor()) $score += 50;
}
// Time-based risk
$hour = (int) date('H');
if ($hour < 6 || $hour > 22) $score += 10; // Outside business hours
// Failure patterns
if (!($context['success'] ?? true)) $score += 20;
return min(100, $score);
}
}