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:
430
docs/components/auth/configuration.md
Normal file
430
docs/components/auth/configuration.md
Normal 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");
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
310
docs/components/auth/index.md
Normal file
310
docs/components/auth/index.md
Normal 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
|
||||
904
docs/components/auth/security.md
Normal file
904
docs/components/auth/security.md
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user