431 lines
No EOL
12 KiB
Markdown
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
|
|
``` |