2
0
Web/auth/AuthMiddleware.php
2025-09-17 06:41:48 -05:00

201 lines
4.6 KiB
PHP

<?php
namespace Web;
/**
* AuthMiddleware provides authentication checks for routes
*/
class AuthMiddleware
{
private Auth $auth;
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
/**
* Require authentication
*/
public function requireAuth(): callable
{
return function(Context $context, callable $next) {
if ($this->auth->guest()) {
// Check if request expects JSON
if ($context->request->expectsJson()) {
$context->json(['error' => 'Unauthenticated'], 401);
return;
}
// Redirect to login page
$context->redirect('/login');
return;
}
// Add user to context
$context->set('user', $this->auth->user());
$next();
};
}
/**
* Require guest (not authenticated)
*/
public function requireGuest(): callable
{
return function(Context $context, callable $next) {
if ($this->auth->check()) {
// Check if request expects JSON
if ($context->request->expectsJson()) {
$context->json(['error' => 'Already authenticated'], 403);
return;
}
// Redirect to home or dashboard
$context->redirect('/');
return;
}
$next();
};
}
/**
* Optional authentication (sets user if authenticated)
*/
public function optional(): callable
{
return function(Context $context, callable $next) {
if ($this->auth->check()) $context->set('user', $this->auth->user());
$next();
};
}
/**
* Check if user has specific role
*/
public function requireRole(string|array $roles): callable
{
$roles = is_array($roles) ? $roles : [$roles];
return function(Context $context, callable $next) use ($roles) {
if ($this->auth->guest()) {
if ($context->request->expectsJson()) {
$context->json(['error' => 'Unauthenticated'], 401);
return;
}
$context->redirect('/login');
return;
}
$user = $this->auth->user();
$userRole = $user->role;
if (!in_array($userRole, $roles)) {
if ($context->request->expectsJson()) {
$context->json(['error' => 'Insufficient permissions'], 403);
return;
}
$context->error(403, 'Forbidden');
return;
}
$context->set('user', $user);
$next();
};
}
/**
* Rate limiting per user
*/
public function rateLimit(int $maxAttempts = 60, int $decayMinutes = 1): callable
{
return function(Context $context, callable $next) use ($maxAttempts, $decayMinutes) {
$identifier = $this->auth->guest() ? $context->request->ip() : 'user:' . $this->auth->id();
$key = 'rate_limit:' . $identifier . ':' . $context->request->path;
$attempts = $context->session->get($key, 0);
$resetTime = $context->session->get("$key:reset", 0);
// Reset counter if decay time has passed
if (time() > $resetTime) {
$attempts = 0;
$context->session->set("$key:reset", time() + $decayMinutes * 60);
}
if ($attempts >= $maxAttempts) {
$retryAfter = $resetTime - time();
$context->response->header('X-RateLimit-Limit', (string)$maxAttempts);
$context->response->header('X-RateLimit-Remaining', '0');
$context->response->header('Retry-After', (string)$retryAfter);
if ($context->request->expectsJson()) {
$context->json([
'error' => 'Too many requests',
'retry_after' => $retryAfter
], 429);
return;
}
$context->error(429, 'Too Many Requests');
return;
}
// Increment attempts
$context->session->set($key, $attempts + 1);
// Add rate limit headers
$context->response->header('X-RateLimit-Limit', (string)$maxAttempts);
$context->response->header('X-RateLimit-Remaining', (string)($maxAttempts - $attempts - 1));
$next();
};
}
/**
* CSRF protection
*/
public function verifyCsrf(): callable
{
return function(Context $context, callable $next) {
// Skip CSRF for safe methods
if (in_array($context->request->method, ['GET', 'HEAD', 'OPTIONS'])) {
$next();
return;
}
$token = $context->request->input('_token')
?? $context->request->header('X-CSRF-TOKEN')
?? $context->request->header('X-XSRF-TOKEN');
if (!$context->session->validateCsrf($token)) {
if ($context->request->expectsJson()) {
$context->json(['error' => 'CSRF token mismatch'], 419);
return;
}
$context->error(419, 'CSRF token mismatch');
return;
}
$next();
};
}
/**
* Remember user from cookie
*/
public function remember(): callable
{
return function(Context $context, callable $next) {
// Auth class already handles remember cookies in constructor
// This middleware can be used to refresh the remember token if needed
if ($this->auth->check()) {
$context->set('user', $this->auth->user());
}
$next();
};
}
}