src/EventSubscriber/AdminHtpasswdBasicAuthSubscriber.php line 33

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\EventSubscriber;
  4. use Symfony\Component\DependencyInjection\Attribute\Autowire;
  5. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\HttpKernel\Event\RequestEvent;
  9. use Symfony\Component\HttpKernel\KernelEvents;
  10. /**
  11.  * HTTP Basic gate for /admin using Apache-style htpasswd (bcrypt lines only: htpasswd -B).
  12.  * Runs before the security firewall so it works reliably on Laragon/Apache without mod_auth expr.
  13.  */
  14. class AdminHtpasswdBasicAuthSubscriber implements EventSubscriberInterface
  15. {
  16.     public function __construct(
  17.         #[Autowire('%kernel.project_dir%')]
  18.         private readonly string $projectDir,
  19.     ) {
  20.     }
  21.     public static function getSubscribedEvents(): array
  22.     {
  23.         return [
  24.             KernelEvents::REQUEST => ['onKernelRequest'300],
  25.         ];
  26.     }
  27.     public function onKernelRequest(RequestEvent $event): void
  28.     {
  29.         if (!$event->isMainRequest() || \PHP_SAPI === 'cli') {
  30.             return;
  31.         }
  32.         $request $event->getRequest();
  33.         if (!str_starts_with($request->getPathInfo(), '/admin')) {
  34.             return;
  35.         }
  36.         $htpasswdPath $this->projectDir '/public/.htpasswd';
  37.         if (!is_readable($htpasswdPath)) {
  38.             $event->setResponse(new Response(
  39.                 'Admin HTTP Basic is enabled but public/.htpasswd is missing or not readable.',
  40.                 503
  41.             ));
  42.             $event->stopPropagation();
  43.             return;
  44.         }
  45.         $authHeader $this->getAuthorizationHeader($request);
  46.         if (!str_starts_with($authHeader'Basic ')) {
  47.             $this->challenge($event);
  48.             return;
  49.         }
  50.         $decoded base64_decode(substr($authHeader6), true);
  51.         if (false === $decoded || !str_contains($decoded':')) {
  52.             $this->challenge($event);
  53.             return;
  54.         }
  55.         [$username$password] = explode(':'$decoded2);
  56.         foreach ($this->readHtpasswdLines($htpasswdPath) as $line) {
  57.             if (!str_contains($line':')) {
  58.                 continue;
  59.             }
  60.             [$fileUser$hash] = explode(':'$line2);
  61.             if (!hash_equals($fileUser$username)) {
  62.                 continue;
  63.             }
  64.             if (str_starts_with($hash'$2y$') || str_starts_with($hash'$2a$') || str_starts_with($hash'$2b$')) {
  65.                 if (password_verify($password$hash)) {
  66.                     return;
  67.                 }
  68.             }
  69.             $this->challenge($event);
  70.             return;
  71.         }
  72.         $this->challenge($event);
  73.     }
  74.     /**
  75.      * @return \Generator<string>
  76.      */
  77.     private function readHtpasswdLines(string $path): \Generator
  78.     {
  79.         $handle fopen($path'rb');
  80.         if (false === $handle) {
  81.             return;
  82.         }
  83.         try {
  84.             while (($line fgets($handle)) !== false) {
  85.                 $line trim($line);
  86.                 if ($line === '' || str_starts_with($line'#')) {
  87.                     continue;
  88.                 }
  89.                 yield $line;
  90.             }
  91.         } finally {
  92.             fclose($handle);
  93.         }
  94.     }
  95.     private function challenge(RequestEvent $event): void
  96.     {
  97.         $event->setResponse(new Response('Authentication required.'401, [
  98.             'WWW-Authenticate' => 'Basic realm="Pimcore Admin"',
  99.         ]));
  100.         $event->stopPropagation();
  101.     }
  102.     private function getAuthorizationHeader(Request $request): string
  103.     {
  104.         $fromHeader $request->headers->get('Authorization');
  105.         if (\is_string($fromHeader) && $fromHeader !== '') {
  106.             return $fromHeader;
  107.         }
  108.         foreach (['HTTP_AUTHORIZATION''REDIRECT_HTTP_AUTHORIZATION'] as $key) {
  109.             $v $request->server->get($key);
  110.             if (\is_string($v) && $v !== '') {
  111.                 return $v;
  112.             }
  113.         }
  114.         return '';
  115.     }
  116. }