Nicolai Fröhlich
Nicolai Fröhlich

Reputation: 52503

How to get current firewall's check_path?

Question: How to get the form_login.check_path by given firewall name?

We subscribe to Symfony\Component\Security\Http\SecurityEvent::INTERACTIVE_LOGIN in order to log successful logins inside an Application that has multiple firewalls.

One firewall uses JWT tokens via Guard authentication which has the negative effect that this event is triggered for every request with a valid token.

We have currently solved this by manually checking whether the current route matches the firewall's check-path and stopping the event-propagation together with an early return otherwise.

As we're adding more firewalls (with different tokens) I'd like to solve this more generally. Therefore I want to check whether the current route matches the current firewalls check-path without hardcoding any route or firewall-name.

There is a class to generate Logout URLs for the current firewall used by Twig logout_path() method which gets the logout route/path from the firewall listeners somehow. (Symfony\Component\Security\Http\Logout\LogoutUrlGenerator)

Before I hop into a long debugging session I thought maybe someone has solved this case before ;)

Any ideas?

Example code:

class UserEventSubscriber implements EventSubscriberInterface
{

    /** @var LoggerInterface */
    protected $logger;

    /** @var FirewallMapInterface|FirewallMap */
    protected $firewallMap;

    public function __construct(LoggerInterface $logger, FirewallMapInterface $firewallMap)
    {
        $this->logger = $logger;
        $this->firewallMap = $firewallMap;
    }

    public function onInteractiveLogin(InteractiveLoginEvent $event)
    {
        $request = $event->getRequest();
        $firewallName = $this->firewallMap->getFirewallConfig($request)->getName();
        $routeName = $request->get('_route');

        if (('firewall_jwt' === $firewallName) && ('firewall_jwt_login_check' !== $routeName)) {
            $event->stopPropagation();
            return;
        }

        $this->logger->info(
            'A User has logged in interactively.',
            array(
                'event' => SecurityEvents::INTERACTIVE_LOGIN,
                'user' => $event->getAuthenticationToken()->getUser()->getUuid(),
        ));

Upvotes: 1

Views: 1717

Answers (2)

Mohammad Trabelsi
Mohammad Trabelsi

Reputation: 3660

for PHP8

In __construct :

public function __construct(
    private RequestStack    $requestStack,
    private FirewallMapInterface $firewallMap
)
{
}

use this :

    $firewallName = $this->firewallMap->getFirewallConfig($this->requestStack->getCurrentRequest())->getName();

Upvotes: 0

yceruto
yceruto

Reputation: 9585

The check_path option is only available from authentication factory/listener, so you could pass this configuration manually to the subscriber class while the container is building.

This solution take account that check_path could be a route name or path, that's why HttpUtils service is injected too:

namespace AppBundle\Subscriber;

use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\FirewallMapInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\SecurityEvents;

class UserEventSubscriber implements EventSubscriberInterface
{
    private $logger;
    private $httpUtils;
    private $firewallMap;
    private $checkPathsPerFirewall;

    public function __construct(LoggerInterface $logger, HttpUtils $httpUtils, FirewallMapInterface $firewallMap, array $checkPathsPerFirewall)
    {
        $this->logger = $logger;
        $this->httpUtils = $httpUtils;
        $this->firewallMap = $firewallMap;
        $this->checkPathsPerFirewall = $checkPathsPerFirewall;
    }

    public function onInteractiveLogin(InteractiveLoginEvent $event)
    {
        $request = $event->getRequest();
        $firewallName = $this->firewallMap->getFirewallConfig($request)->getName();
        $checkPath = $this->checkPathsPerFirewall[$firewallName];

        if (!$this->httpUtils->checkRequestPath($request, $checkPath)) {
            $event->stopPropagation();

            return;
        }

        $this->logger->info('A User has logged in interactively.', array(
            'event' => SecurityEvents::INTERACTIVE_LOGIN,
            'user' => $event->getAuthenticationToken()->getUser()->getUsername(),
        ));
    }

    public static function getSubscribedEvents()
    {
        return [SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin'];
    }
}

After regiter this subscriber as service (AppBundle\Subscriber\UserEventSubscriber) we need implement PrependExtensionInterface in your DI extension to be able to access the security configuration and complete the subscriber definition with the check paths per firewall:

namespace AppBundle\DependencyInjection;

use AppBundle\Subscriber\UserEventSubscriber;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;

class AppExtension extends Extension implements PrependExtensionInterface
{
    // ...

    public function prepend(ContainerBuilder $container)
    {
        $checkPathsPerFirewall = [];

        $securityConfig = $container->getExtensionConfig('security');
        foreach ($securityConfig[0]['firewalls'] as $name => $config) {
            if (isset($config['security']) && false === $config['security']) {
                continue; // skip firewalls without security
            }

            $checkPathsPerFirewall[$name] = isset($config['form_login']['check_path'])
                ? $config['form_login']['check_path']
                : '/login_check'; // default one in Symfony
        }

        $subscriber = $container->getDefinition(UserEventSubscriber::class);
        $subscriber->setArgument(3, $checkPathsPerFirewall);
    }
}

I hope it fits your need.

Upvotes: 1

Related Questions