src/EventSubscriber/AuthMiddleware.php line 94

Open in your IDE?
  1. <?php
  2. namespace App\EventSubscriber;
  3. use App\Service\UserService;
  4. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  5. use Symfony\Component\HttpFoundation\JsonResponse;
  6. use Symfony\Component\HttpFoundation\Response;
  7. use Symfony\Component\HttpKernel\Event\RequestEvent;
  8. use Symfony\Component\HttpKernel\KernelEvents;
  9. /**
  10.  * JWT Authentication Middleware
  11.  *
  12.  * Validates Bearer tokens on protected routes and injects
  13.  * the authenticated AppUser into the request attributes.
  14.  *
  15.  * Public routes (no auth required):
  16.  * - /auth/login
  17.  * - /auth/setup
  18.  * - Pimcore admin routes (/admin/*)
  19.  *
  20.  * Backward-compatible routes (accept both JWT and legacy userid):
  21.  * - /dashboard
  22.  * - /picking_*
  23.  * - /stockAll
  24.  * - All other existing endpoints that use ?userid=
  25.  */
  26. class AuthMiddleware implements EventSubscriberInterface
  27. {
  28.     private UserService $userService;
  29.     /**
  30.      * Routes that never require authentication
  31.      */
  32.     private const PUBLIC_ROUTES = [
  33.         '/auth/login',
  34.         '/auth/setup',
  35.     ];
  36.     /**
  37.      * Route prefixes that are always public
  38.      */
  39.     private const PUBLIC_PREFIXES = [
  40.         '/admin',
  41.         '/_profiler',
  42.         '/_wdt',
  43.         '/public/',
  44.     ];
  45.     /**
  46.      * Routes that accept JWT but don't require it (backward compat)
  47.      * These still work with ?userid= parameter
  48.      */
  49.     private const OPTIONAL_AUTH_PREFIXES = [
  50.         '/dashboard',
  51.         '/picking_',
  52.         '/picklist',
  53.         '/stockAll',
  54.         '/stockdata',
  55.         '/stockdirectories',
  56.         '/orderData',
  57.         '/orders',
  58.         '/fullorders',
  59.         '/fullorder',
  60.         '/plc/',
  61.         '/paketa/',
  62.         '/printlabel',
  63.         '/document',
  64.         '/invoices',
  65.         '/invoice_categories',
  66.         '/import',
  67.         '/resetpicklist',
  68.         '/importpicklist',
  69.         '/upsertstock',
  70.         '/updatePickStatus',
  71.         '/deletePicklistOrder',
  72.         '/crosschannel',
  73.         '/api/',
  74.     ];
  75.     public function __construct(UserService $userService)
  76.     {
  77.         $this->userService $userService;
  78.     }
  79.     public static function getSubscribedEvents(): array
  80.     {
  81.         return [
  82.             KernelEvents::REQUEST => ['onKernelRequest'10],
  83.         ];
  84.     }
  85.     public function onKernelRequest(RequestEvent $event): void
  86.     {
  87.         if (!$event->isMainRequest()) {
  88.             return;
  89.         }
  90.         $request $event->getRequest();
  91.         $path $request->getPathInfo();
  92.         // Always allow public routes
  93.         if ($this->isPublicRoute($path)) {
  94.             return;
  95.         }
  96.         // Try to extract and validate JWT token
  97.         $authHeader $request->headers->get('Authorization''');
  98.         $token null;
  99.         if (strpos($authHeader'Bearer ') === 0) {
  100.             $token substr($authHeader7);
  101.         }
  102.         if ($token) {
  103.             $user $this->userService->validateToken($token);
  104.             if ($user) {
  105.                 // Inject authenticated user into request for controllers
  106.                 $request->attributes->set('_auth_user'$user);
  107.                 // Also set the userid for backward compat with existing code
  108.                 $request->query->set('userid'$user->getUsername());
  109.                 return;
  110.             }
  111.             // Token was provided but invalid — only reject on protected routes
  112.             if (!$this->isOptionalAuthRoute($path)) {
  113.                 $event->setResponse(new JsonResponse(
  114.                     ['error' => 'Invalid or expired token'],
  115.                     Response::HTTP_UNAUTHORIZED
  116.                 ));
  117.                 return;
  118.             }
  119.         }
  120.         // No token: if it's an optional-auth route, let it through (backward compat)
  121.         if ($this->isOptionalAuthRoute($path)) {
  122.             return;
  123.         }
  124.         // Protected route with no valid token
  125.         if ($this->isProtectedRoute($path)) {
  126.             $event->setResponse(new JsonResponse(
  127.                 ['error' => 'Authentication required'],
  128.                 Response::HTTP_UNAUTHORIZED
  129.             ));
  130.         }
  131.     }
  132.     private function isPublicRoute(string $path): bool
  133.     {
  134.         foreach (self::PUBLIC_ROUTES as $route) {
  135.             if ($path === $route) {
  136.                 return true;
  137.             }
  138.         }
  139.         foreach (self::PUBLIC_PREFIXES as $prefix) {
  140.             if (strpos($path$prefix) === 0) {
  141.                 return true;
  142.             }
  143.         }
  144.         return false;
  145.     }
  146.     private function isOptionalAuthRoute(string $path): bool
  147.     {
  148.         foreach (self::OPTIONAL_AUTH_PREFIXES as $prefix) {
  149.             if (strpos($path$prefix) === 0) {
  150.                 return true;
  151.             }
  152.         }
  153.         return false;
  154.     }
  155.     private function isProtectedRoute(string $path): bool
  156.     {
  157.         // Routes that strictly require auth
  158.         $protectedPrefixes = [
  159.             '/auth/me',
  160.             '/auth/users',
  161.             '/rounds',
  162.         ];
  163.         foreach ($protectedPrefixes as $prefix) {
  164.             if (strpos($path$prefix) === 0) {
  165.                 return true;
  166.             }
  167.         }
  168.         return false;
  169.     }
  170. }