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