Skip to main content

Symfony2 SecurityBundle and FOSUserBundle integration: How does it work?

Overview

A couple of days ago, I realized I needed to add some new functionality to the login process. Specifically, I needed to track all previous login attempts. Not knowing anything about the new Symfony2 SecurityBundle, I had to go through the underlying code to understand what was going on. In the process, I think got a basic idea about how the new SecurityBundle interacts with FOSUserBundle.

Configuration

I have a basic security configuration as illustrated below.

app/config/security.yml

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
        
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN:  ROLE_ADMIN
        
    providers:
        fos_userbundle:
            id: fos_user.user_manager

    firewalls:
        main:
            pattern: .*
            form_login:
                provider:   fos_userbundle
                check_path: /user/login_check
                login_path: /user/login
            logout:
                path: /user/logout
            anonymous:    true
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        login:
            pattern: ^/user/login$
            security: false

Full security configuration reference can be found at here

app/config/routing.yml

fos_user_security:
    resource: "@FOSUserBundle/Resources/config/routing/security.xml"
    prefix: /user

As you can see above, we are importing all FOSUserBundle security routing rules with the /user prefix. Now, let's have a look at the security routing rules under FOSUserBundle.

vendor/bundles/FOS/UserBundle/Resources/config/routing/security.xml

<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="fos_user_security_login" pattern="/login">
        <default key="_controller">FOSUserBundle:Security:login</default>
    </route>

    <route id="fos_user_security_check" pattern="/login_check">
        <default key="_controller">FOSUserBundle:Security:check</default>
    </route>

    <route id="fos_user_security_logout" pattern="/logout">
        <default key="_controller">FOSUserBundle:Security:logout</default>
    </route>

</routes>

It should be pretty self explanatory up to this point. We have a default security routing config file that we are importing into our application with the /user prefix and we have all the matching paths (login_path, check_path, etc.) defined in our app/config/config.yml for a working FOSUserBundle implementation. Now, let's have a look at FOSUserBundle SecurityController.

vendor/bundles/FOS/UserBundle/Controller/SecurityController.php

namespace FOS\UserBundle\Controller;

use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

class SecurityController extends ContainerAware
{
    public function loginAction()
    {
        $request = $this->container->get('request');
        /* @var $request \Symfony\Component\HttpFoundation\Request */
        $session = $request->getSession();
        /* @var $session \Symfony\Component\HttpFoundation\Session */

        // get the error if any (works with forward and redirect -- see below)
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        } elseif (null !== $session && $session->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
            $session->remove(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $error = '';
        }

        if ($error) {
            // TODO: this is a potential security risk (see http://trac.symfony-project.org/ticket/9523)
            $error = $error->getMessage();
        }
        // last username entered by the user
        $lastUsername = (null === $session) ? '' : $session->get(SecurityContext::LAST_USERNAME);

        return $this->container->get('templating')->renderResponse('FOSUserBundle:Security:login.html.'.$this->container->getParameter('fos_user.template.engine'), array(
            'last_username' => $lastUsername,
            'error'         => $error,
        ));
    }

    public function checkAction()
    {
        throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
    }

    public function logoutAction()
    {
        throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
    }
}

In the security controller, we have both check and logout action methods included but they are not defined. Yet, the security process works. How is this possible?

How does it work?

Well, a single word: listeners.

  • First, an appropriate listener factory class is identified based on the authentication type.
  • Second, this listener factory class registers a user provider identified by the provider key under the firewall definition in security.yml. In our case, our provider is called fos_userbundle. This process basically links SecurityBundle to FOSUserBundle for authenticating submitted from values against a database.
  • Next, the listener factory class registers an authentication listener identified by "security.authentication.listener.form". This code listens to login attempts at the /user/login path.
  • Finally, FOSUserBundle registers a login listener for security.interactive_login event. This event indicates a successful login and the listener is used to execute post login code.

Let's go back to the beginning; everything starts when SecurityBundle is initiated and our security factory definitions are loaded from security_factories.xml.

Symfony/Bundle/SecurityBundle/Resources/config/security_factories.xml

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="security.authentication.factory.form" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory">
            <tag name="security.listener.factory" />
        </service>

        <service id="security.authentication.factory.x509" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\X509Factory">
            <tag name="security.listener.factory" />
        </service>

        <service id="security.authentication.factory.basic" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicFactory">
            <tag name="security.listener.factory" />
        </service>

        <service id="security.authentication.factory.digest" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpDigestFactory">
            <tag name="security.listener.factory" />
        </service>

        <service id="security.authentication.factory.remember_me" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory">
            <tag name="security.listener.factory" />
        </service>
    </services>
</container>

Basically, the call to load the SecurityBundle (see SecurityExtension::load()) simply calls the SecurityExtension::createFirewalls() method which in turn makes a call to the SecurityExtension::createListenerFactories() method. This method then loads all services tagged with "security.listener.factory" in the security_factories.xml file which is included above.

Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

private function createFirewalls($config, ContainerBuilder $container)
{
    // ...

    // create security listener factories
    $factories = $this->createListenerFactories($container, $config);

    // ...
}

Listener factories are classes that are responsible for initiating listeners based on the authentication type. Symfony SecurityBundle provides factories for multiple authentication types. These authentication types include HTTP Basic, HTTP Digest, X509, RememberMe, and FormLogin. In our case, we are interested in the FormLogin type.

Once all listener factories are loaded, the code then loops over the firewall definitions in our app/config/security.yml file and calls SecurityExtension::createFirewall() method for each one found. In our app/config/config.yml file, we have three firewalls defined: main, dev, and login.

Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

private function createFirewalls($config, ContainerBuilder $container)
{
    // ...

    foreach ($firewalls as $name => $firewall) {
        list($matcher, $listeners, $exceptionListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $factories);

        $contextId = 'security.firewall.map.context.'.$name;
        $context = $container->setDefinition($contextId, new DefinitionDecorator('security.firewall.context'));
        $context
            ->replaceArgument(0, $listeners)
            ->replaceArgument(1, $exceptionListener)
        ;
        $map[$contextId] = $matcher;
    }

    // ...
}

Subsequently, the SecurityExtension::createFirewall() method initiates authentication listeners for each particular firewall by calling the SecurityExtension::createAuthenticationListeners() method.

Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

private function createFirewall(ContainerBuilder $container, $id, $firewall, &$authenticationProviders, $providerIds, array $factories)
{
    // ...

    // Authentication listeners
    list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $factories);

    // ...
}

The SecurityExtension::createAuthenticationListeners() method identifies the correct factory classes for a firewall by simply looping over all factories loaded into memory previously in a specific order (see SecurityBundle::$listenerPositions property and getPosition() methods inside factory classes) and then matching the keys found under the firewall definition (in our case, we have pattern, form_login, logout, and anonymous keys under the main firewall) to a factory instance (see getKey() methods inside factory classes). At the end, there is only one factory class defined with the form_login key and it is located at Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php.

Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

private function createAuthenticationListeners($container, $id, $firewall, &$authenticationProviders, $defaultProvider, array $factories)
{
    // ...

    foreach ($this->listenerPositions as $position) {
        foreach ($factories[$position] as $factory) {
            $key = str_replace('-', '_', $factory->getKey());

            if (isset($firewall[$key])) {
                $userProvider = isset($firewall[$key]['provider']) ? $this->getUserProviderId($firewall[$key]['provider']) : $defaultProvider;

                list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint);

                $listeners[] = new Reference($listenerId);
                $authenticationProviders[] = $provider;
                $hasListeners = true;
            }
        }
    }

    // ...
}

Note: SecurityExtension::createAuthenticationListeners() mehod also creates a listener for anonymous authentication. Code is excluded for the sake of brevity.

Once an appropriate factory class is identified, the FormLoginFactory::create() method is called. This registers a "security.authentication.listener.form" authentication listener that intercepts login attempts.

The FormLoginFactory::create() method also calls createAuthProvider() which is responsible for registering our custom user provider (fos_userbundle). This user provider class is used to verify login credentials against a database.

The FormLoginFactory class also declares security.authentication.listener.form as our listener id. (See FormLoginFactory::getListenerId())

Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>

        <parameter key="security.authentication.listener.form.class">Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener</parameter>

    </parameters>

    <services>

        <service id="security.authentication.listener.form"
                 class="%security.authentication.listener.form.class%"
                 parent="security.authentication.listener.abstract"
                 abstract="true">
        </service>

    </services>
</container>

As you can see in the security_listeners.xml, our listener class is Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener. Things get easier after this point. Any call to our UsernamePasswordFormAuthenticationListener::handle() method triggers attemptAuthentication() method. This method is where the interaction between FOSUserBundle and SecurityBundle begins.

First, the authenticate() method is called on our authentication provider, which then makes a call to our user provider. This is the UserManager class included in the FOSUserBundle package.

Symfony/Component/Security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php

protected function attemptAuthentication(Request $request)
{
    // ...

    return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
}

Then, based on return value of the authenticate() call, onSuccess() or onFailure() method is called. If user login is a success, the AbstractAuthenticationListener::onSuccess() method dispatches an event identfied by the SecurityEvents::INTERACTIVE_LOGIN constant.

Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php

private function onSuccess(GetResponseEvent $event, Request $request, TokenInterface $token)
{
    // ...

    if (null !== $this->dispatcher) {
        $loginEvent = new InteractiveLoginEvent($request, $token);
        $this->dispatcher->dispatch(SecurityEvents::INTERACTIVE_LOGIN, $loginEvent);
    }

    // ...
}

SecurityEvents::INTERACTIVE_LOGIN constant has a value of security.interactive_login so we can easily locate where the login listener registration happens:

vendor/bundles/FOS/UserBundle/Resources/config/security.xml

<?xml version="1.0" encoding="UTF-8"?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="fos_user.encoder_factory.class">FOS\UserBundle\Security\Encoder\EncoderFactory</parameter>
        <parameter key="fos_user.security.interactive_login_listener.class">FOS\UserBundle\Security\InteractiveLoginListener</parameter>
    </parameters>

    <services>
        <service id="fos_user.encoder_factory" class="%fos_user.encoder_factory.class%" public="false">
            <argument>%security.encoder.digest.class%</argument>
            <argument>%fos_user.encoder.encode_as_base64%</argument>
            <argument>%fos_user.encoder.iterations%</argument>
            <argument type="service" id="fos_user.encoder_factory.parent" />
        </service>

        <service id="fos_user.security.interactive_login_listener" class="%fos_user.security.interactive_login_listener.class%">
            <argument type="service" id="fos_user.user_manager" />
            <tag name="kernel.event_listener" event="security.interactive_login" method="onSecurityInteractiveLogin" />
        </service>
    </services>

</container>

And here is the custom listener code attached to the security.interactive_login event. It is purpose is to update the last login date for our users after a successful login.

FOS/UserBundle/Security/InteractiveLoginListener.php

namespace FOS\UserBundle\Security;

use FOS\UserBundle\Model\UserManagerInterface;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use DateTime;

class InteractiveLoginListener
{
    protected $userManager;

    public function __construct(UserManagerInterface $userManager)
    {
        $this->userManager = $userManager;
    }

    public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
    {
        $user = $event->getAuthenticationToken()->getUser();

        if ($user instanceof UserInterface) {
            $user->setLastLogin(new DateTime());
            $this->userManager->updateUser($user);
        }
    }
}

In my next post, I will illustrate how we can extend this behavior to implement additional functionality after a successful login.

Comments

  1. Big thanks Burak. I hope that you will flush more nice article like this.

    ReplyDelete
  2. great article...i'm waiting for your next article talks more on overriding

    ReplyDelete
    Replies
    1. Dizzo, please see http://blog.logicexception.com/2011/11/adding-post-login-logic-to.html

      Delete
  3. Sweet post! I'm currently working for this Software company. The use of

    Bundles is now so common that we literally have people who can't work

    with raw codes. I recently wrote this article after they worked on a

    project in which they used Symfony2 FOSUserBundle.

    Check it out: http://webmuch.com/override-fosuserbundle/

    ReplyDelete

Post a Comment

Popular posts from this blog

Securing Symfony2 REST services with FOSOAuthServerBundle

Overview In my previous article, I wrote about setting up a Symfony2 REST service using FOSRestBundle. However, this REST service was behind a firewall protected by a generic form_login provider. Not really ideal if you wish to open your REST API to other applications. So in this article, I will try to explain how to set up FOSOAuthServerBundle to protect your REST API methods using OAuth2. Before we start getting into the gritty details, it is a good idea to have a look at the official OAuth2 documentation . Let's begin... FOSOAuthServerBundle Installation You have to install v1.1.0 of FOSOAuthServerBundle if you are using Symfony 2.0.x. If not, see the docs . First, add the following entries to your deps file: [FOSOAuthServerBundle] git=git://github.com/FriendsOfSymfony/FOSOAuthServerBundle.git target=bundles/FOS/OAuthServerBundle version=origin/1.1.x [oauth2-php] git=git://github.com/FriendsOfSymfony/oauth2-php.git Run the vendors script to install these...

Unexpected token "name" of value "if" ("end of statement block" expected) in "WebProfilerBundle:Collector:logger.html.twig"

Encountered this WebProfilerBundle error message when I ran the bin/vendors script to update my Symfony2 bundles. Make sure your deps file is up to date; you need to pay special attention to your version values. In this case, update your twig version to v1.2.0 as illustrated below: [twig] git=http://github.com/fabpot/Twig.git version=v1.2.0 Run the vendors script to update your bundle and the error message should disappear. You can get the most up to date deps file from the symfony-standard repository located at: https://github.com/symfony/symfony-standard/blob/master/deps

A Parcelable Tutorial for Android

Parcelable Interface Overview In one of my earlier posts, I mentioned writing an article about FOSOAuthBundle integration with an Android client. To keep that article to the point, I need to explain some concepts beforehand. One of the important concepts is the Android Parcelable interface that allows data to be transferred between different processes/threads. Certain network operations with Android such as authentication with OAuth2 and then fetching data from a REST endpoint should be performed in the background in order not to block the UI thread. This requires data to be fetched by a service (I have opted for Intent Services in my implementation) in the background and then passed back to the calling activity/fragment with a result callback. This is where the Parcelable interface comes into play. Basically, the Parcelable interface allows your classes to be flattened inside a message container called a Parcel to facilitate high performance inter process communication. The rece...