src/EventSubscriber/AdminHtpasswdBasicAuthSubscriber.php line 31

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