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

431 lines
No EOL
12 KiB
Markdown

# Authentication Guide
## Installation
```bash
composer require firebase/php-jwt
```
## Core Value Objects
### Value Objects
```php
// 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
```php
// 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
```php
// 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
```php
// 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
```php
// 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
```php
// 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
```php
// 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
```php
// 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
```sql
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
```php
// 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
```php
// 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
```php
// 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
```env
AUTH_TYPE=session
JWT_SECRET=your-secret-key
JWT_ACCESS_TOKEN_TTL=900
SESSION_LIFETIME=7200
AUTH_MAX_LOGIN_ATTEMPTS=5
```