<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Http\AccessMapInterface;
/**
* Public API routes must stay accessible even when clients send an expired Bearer JWT.
* The api firewall validates JWT before access_control runs; stripping Bearer on public
* routes prevents Lexik from rejecting the request. Basic Auth headers are preserved.
*/
final class PublicRouteBearerTokenSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly AccessMapInterface $accessMap,
) {
}
public static function getSubscribedEvents(): array
{
return [
// Run before Symfony FirewallListener (priority 8).
KernelEvents::REQUEST => ['onKernelRequest', 9],
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
if (!$this->isPublicApiRoute($request)) {
return;
}
$authHeader = $request->headers->get('Authorization');
if ($authHeader !== null && str_starts_with($authHeader, 'Bearer ')) {
$request->headers->remove('Authorization');
}
}
private function isPublicApiRoute(Request $request): bool
{
if (!str_starts_with($request->getPathInfo(), '/api/')) {
return false;
}
[$attributes] = $this->accessMap->getPatterns($request);
return \in_array('PUBLIC_ACCESS', $attributes, true);
}
}