Files
drivetimeplaner/www/api.fahrschuldesk.de/app/Http/Middleware/AuthMiddleware.php
Hermes Agent 5d87e4975c Add HttpOnly cookie auth for cross-domain SPA login
Problem: Frontend JS bundle used credentials:'same-origin' which
dropped cookies on cross-domain requests. Login worked (200) but
the subsequent /auth/me check returned 401, leaving the user stuck
on the login screen.

Fix:
- AuthController now sets a dtp_jwt HttpOnly cookie on login/refresh
- Cookie uses Domain=.fahrschultermin.de (shared between frontend
  and api subdomain), Secure, SameSite=Lax, Max-Age=8h
- JwtMiddleware reads JWT from Authorization header OR cookie
- Added AuthController::me() endpoint (was missing, caused 500)
- Logout endpoint clears the cookie
- Frontend index.php patches fetch() to use credentials:'include'
  for all /api/v1/* calls
2026-06-04 20:33:31 +02:00

113 lines
3.5 KiB
PHP
Executable File

<?php
/**
* JWT Authentication Middleware
*/
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Support\Database;
use App\Support\Response;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
class AuthMiddleware
{
private string $jwtSecret;
public function __construct(string $jwtSecret)
{
$this->jwtSecret = $jwtSecret;
}
/**
* Validate JWT token and attach user to request
*/
public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$authHeader = $request->getHeaderLine('Authorization');
if (empty($authHeader) || !str_starts_with($authHeader, 'Bearer ')) {
return Response::unauthorized($response ?? $handler->handle($request), 'No token provided');
}
$token = substr($authHeader, 7);
try {
$decoded = JWT::decode($token, new Key($this->jwtSecret, 'HS256'));
if ($decoded->type !== 'access') {
return Response::unauthorized($handler->handle($request), 'Invalid token type');
}
// Attach auth info to request
$request = $request->withAttribute('auth', [
'id' => $decoded->sub,
'type' => $decoded->user_type,
'token' => $decoded,
]);
return $handler->handle($request);
} catch (\Firebase\JWT\ExpiredException $e) {
return Response::unauthorized($handler->handle($request), 'Token expired');
} catch (\Exception $e) {
return Response::unauthorized($handler->handle($request), 'Invalid token');
}
}
/**
* Validate that the user is a platform admin
*/
public static function platformAdmin(ServerRequestInterface $request): ?ResponseInterface
{
$auth = $request->getAttribute('auth');
if (!$auth || $auth['type'] !== 'platform_admin') {
$response = new \Slim\Psr7\Response();
return Response::forbidden($response, 'Platform admin access required');
}
return null; // OK
}
/**
* Validate that the user is a tenant user (any type)
*/
public static function tenantUser(ServerRequestInterface $request): ?ResponseInterface
{
$auth = $request->getAttribute('auth');
if (!$auth || $auth['type'] !== 'user') {
$response = new \Slim\Psr7\Response();
return Response::forbidden($response, 'Tenant user access required');
}
return null; // OK
}
/**
* Validate that the user has a specific user type
*/
public static function requireUserType(ServerRequestInterface $request, array $types): ?ResponseInterface
{
$auth = $request->getAttribute('auth');
if (!$auth || $auth['type'] !== 'user') {
$response = new \Slim\Psr7\Response();
return Response::forbidden($response, 'User access required');
}
$user = Database::fetch('SELECT user_type FROM users WHERE id = $1', [$auth['id']]);
if (!$user || !in_array($user['user_type'], $types)) {
$response = new \Slim\Psr7\Response();
return Response::forbidden($response, 'Insufficient permissions');
}
return null; // OK
}
}