foundation/documentation/authentication.md
2025-06-13 18:29:55 +02:00

12 KiB

Authentication Guide

Installation

composer require firebase/php-jwt

Core Value Objects

Value Objects

// src/Core/Auth/ValueObjects/UserId.php
final class UserId
{
    public function __construct(public readonly int $value) {}
    public static function fromInt(int $id): self { return new self($id); }
}

// src/Core/Auth/ValueObjects/Email.php
final class Email
{
    public function __construct(public readonly string $value) {}
    public static function fromString(string $email): self { return new self($email); }
}

// src/Core/Auth/ValueObjects/HashedPassword.php
final class HashedPassword
{
    public function __construct(public readonly string $hash) {}
    
    public static function fromPlainText(string $password): self
    {
        return new self(password_hash($password, PASSWORD_ARGON2ID));
    }
    
    public function verify(string $password): bool
    {
        return password_verify($password, $this->hash);
    }
}

// src/Core/Application/ValueObjects/AuthConfig.php
final class AuthConfig
{
    public function __construct(
        public readonly string $jwtSecret,
        public readonly int $jwtAccessTokenTtl,
        public readonly int $sessionLifetime,
        public readonly int $maxLoginAttempts,
    ) {}
    
    public static function fromEnvironment(): self
    {
        return new self(
            $_ENV['JWT_SECRET'] ?? 'secret',
            (int) ($_ENV['JWT_ACCESS_TOKEN_TTL'] ?? 900),
            (int) ($_ENV['SESSION_LIFETIME'] ?? 7200),
            (int) ($_ENV['AUTH_MAX_LOGIN_ATTEMPTS'] ?? 5),
        );
    }
}

User Entity

// src/Core/Auth/Domain/User.php
final class User
{
    public function __construct(
        private readonly UserId $id,
        private readonly Email $email,
        private HashedPassword $password,
        private bool $isActive = true,
        private int $failedLoginAttempts = 0,
    ) {}
    
    public static function create(UserId $id, Email $email, HashedPassword $password): self
    {
        return new self($id, $email, $password);
    }
    
    public function getId(): UserId { return $this->id; }
    public function getEmail(): Email { return $this->email; }
    public function verifyPassword(string $password): bool { return $this->password->verify($password); }
    public function isActive(): bool { return $this->isActive; }
    public function recordFailedLogin(): void { $this->failedLoginAttempts++; }
    public function resetLoginAttempts(): void { $this->failedLoginAttempts = 0; }
}

// src/Core/Auth/Domain/UserRepositoryInterface.php
interface UserRepositoryInterface
{
    public function save(User $user): void;
    public function findById(UserId $id): ?User;
    public function findByEmail(Email $email): ?User;
    public function emailExists(Email $email): bool;
}

Authentication Services

// src/Core/Auth/AuthenticationManagerInterface.php
interface AuthenticationManagerInterface
{
    public function login(Email $email, string $password): AuthResult;
    public function register(Email $email, string $password): User;
    public function getCurrentUser(): ?User;
    public function isAuthenticated(): bool;
}

// src/Core/Auth/AuthResult.php
final class AuthResult
{
    public function __construct(
        public readonly bool $success,
        public readonly ?User $user = null,
        public readonly ?string $accessToken = null,
        public readonly ?string $error = null,
    ) {}
    
    public static function success(User $user, ?string $accessToken = null): self
    {
        return new self(true, $user, $accessToken);
    }
    
    public static function failure(string $error): self
    {
        return new self(false, null, null, $error);
    }
}

Session Authentication Manager

// src/Core/Auth/SessionAuthenticationManager.php
final class SessionAuthenticationManager implements AuthenticationManagerInterface
{
    public function __construct(
        private readonly UserRepositoryInterface $userRepository,
        private readonly SessionManagerInterface $sessionManager,
        private readonly AuthConfig $authConfig,
    ) {}

    public function login(Email $email, string $password): AuthResult
    {
        $user = $this->userRepository->findByEmail($email);
        
        if (!$user || !$user->verifyPassword($password)) {
            return AuthResult::failure('Invalid credentials');
        }

        $this->sessionManager->set('user_id', $user->getId()->value);
        return AuthResult::success($user);
    }

    public function register(Email $email, string $password): User
    {
        $user = User::create(
            UserId::fromInt(random_int(1, 999999)),
            $email,
            HashedPassword::fromPlainText($password)
        );
        
        $this->userRepository->save($user);
        return $user;
    }

    public function getCurrentUser(): ?User
    {
        $userId = $this->sessionManager->get('user_id');
        return $userId ? $this->userRepository->findById(UserId::fromInt($userId)) : null;
    }

    public function isAuthenticated(): bool
    {
        return $this->getCurrentUser() !== null;
    }
}

JWT Authentication Manager

// src/Core/Auth/JwtAuthenticationManager.php
final class JwtAuthenticationManager implements AuthenticationManagerInterface
{
    private ?User $currentUser = null;

    public function __construct(
        private readonly UserRepositoryInterface $userRepository,
        private readonly JwtTokenManagerInterface $tokenManager,
    ) {}

    public function login(Email $email, string $password): AuthResult
    {
        $user = $this->userRepository->findByEmail($email);
        
        if (!$user || !$user->verifyPassword($password)) {
            return AuthResult::failure('Invalid credentials');
        }

        $accessToken = $this->tokenManager->createAccessToken($user);
        return AuthResult::success($user, $accessToken);
    }

    public function register(Email $email, string $password): User
    {
        $user = User::create(
            UserId::fromInt(random_int(1, 999999)),
            $email,
            HashedPassword::fromPlainText($password)
        );
        
        $this->userRepository->save($user);
        return $user;
    }

    public function getCurrentUser(): ?User { return $this->currentUser; }
    public function setCurrentUser(?User $user): void { $this->currentUser = $user; }
    public function isAuthenticated(): bool { return $this->currentUser !== null; }
}

JWT Token Manager

// src/Core/Auth/JwtTokenManagerInterface.php
interface JwtTokenManagerInterface
{
    public function createAccessToken(User $user): string;
    public function validateAccessToken(string $token): ?array;
}

// src/Core/Auth/JwtTokenManager.php
final class JwtTokenManager implements JwtTokenManagerInterface
{
    public function __construct(private readonly AuthConfig $authConfig) {}

    public function createAccessToken(User $user): string
    {
        $payload = [
            'sub' => $user->getId()->value,
            'email' => $user->getEmail()->value,
            'exp' => time() + $this->authConfig->jwtAccessTokenTtl,
        ];

        return JWT::encode($payload, $this->authConfig->jwtSecret, 'HS256');
    }

    public function validateAccessToken(string $token): ?array
    {
        try {
            return (array) JWT::decode($token, new Key($this->authConfig->jwtSecret, 'HS256'));
        } catch (\Exception) {
            return null;
        }
    }
}

Authentication Middleware

// src/Core/Auth/Middleware/RequireAuthMiddleware.php
final class RequireAuthMiddleware implements MiddlewareInterface
{
    public function __construct(private readonly AuthenticationManagerInterface $authManager) {}

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if (!$this->authManager->isAuthenticated()) {
            $response = new Response();
            return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
        }

        return $handler->handle($request);
    }
}

Route Protection Attributes

// src/Core/Auth/Attributes/RequireAuth.php
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
final class RequireAuth {}

// src/Core/Auth/Attributes/AllowAnonymous.php
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
final class AllowAnonymous {}

Database Schema

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    is_active BOOLEAN DEFAULT TRUE,
    failed_login_attempts INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Repository Implementation

// src/Core/Auth/Infrastructure/Database/UserRepository.php
final class UserRepository implements UserRepositoryInterface
{
    public function __construct(private readonly PDO $pdo) {}

    public function save(User $user): void
    {
        $sql = "INSERT INTO users (id, email, password, is_active) VALUES (?, ?, ?, ?) 
                ON DUPLICATE KEY UPDATE email=VALUES(email), password=VALUES(password)";
        $this->pdo->prepare($sql)->execute([
            $user->getId()->value,
            $user->getEmail()->value, 
            $user->getPassword()->hash,
            $user->isActive()
        ]);
    }

    public function findById(UserId $id): ?User
    {
        $stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$id->value]);
        $data = $stmt->fetch(PDO::FETCH_ASSOC);
        return $data ? User::fromArray($data) : null;
    }

    public function findByEmail(Email $email): ?User
    {
        $stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?");
        $stmt->execute([$email->value]);
        $data = $stmt->fetch(PDO::FETCH_ASSOC);
        return $data ? User::fromArray($data) : null;
    }

    public function emailExists(Email $email): bool
    {
        $stmt = $this->pdo->prepare("SELECT COUNT(*) FROM users WHERE email = ?");
        $stmt->execute([$email->value]);
        return $stmt->fetchColumn() > 0;
    }
}

Bootstrapper Integration

// src/Core/Application/Bootstrapper/AuthenticationInitializer.php
class AuthenticationInitializer implements BootstrapperInterface
{
    public function bootstrap(InflectableContainer $container): void
    {
        $container->set(AuthConfig::class, AuthConfig::fromEnvironment());
        $container->bind(UserRepositoryInterface::class, UserRepository::class, [PDO::class]);
        
        if (($_ENV['AUTH_TYPE'] ?? 'session') === 'jwt') {
            $container->bind(JwtTokenManagerInterface::class, JwtTokenManager::class, [AuthConfig::class]);
            $container->bind(AuthenticationManagerInterface::class, JwtAuthenticationManager::class);
        } else {
            $container->bind(AuthenticationManagerInterface::class, SessionAuthenticationManager::class);
        }
    }
}

Usage Example

// Example controller
class AuthController implements ControllerInterface
{
    public function __construct(private readonly AuthenticationManagerInterface $authManager) {}

    #[Post('/login')]
    public function login(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $data = json_decode($request->getBody()->getContents(), true);
        $result = $this->authManager->login(
            Email::fromString($data['email']),
            $data['password']
        );

        $response->getBody()->write(json_encode($result->success ? 
            ['success' => true, 'token' => $result->accessToken] : 
            ['error' => $result->error]
        ));
        
        return $response->withHeader('Content-Type', 'application/json');
    }

    #[Post('/register')]
    public function register(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $data = json_decode($request->getBody()->getContents(), true);
        
        try {
            $user = $this->authManager->register(
                Email::fromString($data['email']),
                $data['password']
            );
            
            $response->getBody()->write(json_encode(['success' => true]));
        } catch (\Exception $e) {
            $response->getBody()->write(json_encode(['error' => $e->getMessage()]));
        }
        
        return $response->withHeader('Content-Type', 'application/json');
    }
}

Environment Configuration

AUTH_TYPE=session
JWT_SECRET=your-secret-key
JWT_ACCESS_TOKEN_TTL=900
SESSION_LIFETIME=7200
AUTH_MAX_LOGIN_ATTEMPTS=5