Under The Hood Of Symfony Security

It's easy to get a basic security setup with Symfony just by tweaking your security configuration. But what exactly is a set of instructions such as the one below doing? Quite a bit would be the short answer. Symfony's SecurityBundle does a great job of abstracting away the complexities from the developer - but just as with bicycles, i like to rip stuff apart and look at it from the inside out. This post merely holds some scribbles I took while trying to find my way around the Symfony security system.

The Configuration

Here's the setup we're trying to reproduce manually:

security:
    firewalls:
        my_secured_area:
            pattern: ^/backend
            anonymous: ~
            form_login:
                login_path: /login
                check_path: /login-check
            logout:
                path: /logout
                target: /   

    access_control:
        - { path: ^/backend, roles: ROLE_ADMIN }    

    encoders:
        Symfony\Component\Security\Core\User\User: pbkdf2   

    providers:
        in_memory:
            memory:
                users:
                    root: { password: YK2oAKcvWwRygi9BoqtOd7AbgsxWMOAX3EuTCw5vcFjn6jzRSUfWyw==, roles: [ 'ROLE_ADMIN' ] }

Authentication Setup

use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder;
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
use Symfony\Component\Security\Core\User\UserChecker;
use Symfony\Component\Security\Core\Encoder\EncoderFactory;
use Symfony\Component\Security\Core\Authentication\Provider\AnonymousAuthenticationProvider;
use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;

$anonymousKey = uniqid();
$passwordEncoder = new Pbkdf2PasswordEncoder();
$inMemoryUserProvider = new InMemoryUserProvider();
$userChecker = new UserChecker();   

$encoderFactory = new EncoderFactory(array(
    'Symfony\Component\Security\Core\User\User' => $passwordEncoder
)); 

$authenticationProviders = array(
    // validates AnonymousToken instances
    new AnonymousAuthenticationProvider($anonymousKey),
    // retrieve the user for a UsernamePasswordToken
    new DaoAuthenticationProvider($inMemoryUserProvider, $userChecker, 'my_secured_area', $encoderFactory)
);

$authenticationManager = new AuthenticationProviderManager($authenticationProviders);

This gets us a basic authentication infrastructure. There's a password encoder, a user provider and a set of authentication providers. It is the job of authentication providers to process specific token instances. A token basically holds the user's credentials. How to obtain a token from the session or a login form submission, will be demonstrated below.

Authorization Setup

use Symfony\Component\Security\Core\Authorization\Voter\Rolevoter;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;

$voters = array(
    // votes if any attribute starts with a given prefix
    new Rolevoter('ROLE_')
);

$accessDecisionManager = new AccessDecisionManager($voters);

The AccessDecisionManager decides whether or not the current user is entitled to access a given resource.

Entry Point

Here we're creating the main entry point of the Security component. It is called the security context and it gives access to the token representing the current user authentication.

use Symfony\Component\Security\Core\SecurityContext;

$securityContext = new SecurityContext($authenticationManager, $accessDecisionManager);

Adding Users

Let's add one user by the name of root with password rootpass:

use Symfony\Component\Security\Core\User\User;

$inMemoryUserProvider->createUser(new User(
    'root', // $username,
    $passwordEncoder->encodePassword('rootpass', ''), // password
    array('ROLE_ADMIN'), // $roles,
    true, // $enabled,
    true, // $userNonExpired
    true, // $credentialsNonExpired
    true // userNonLocked
));

Usecase: Handling A Loginform

Simplified example of what UsernamePasswordFormAuthenticationListener does to authenticate a user who sends his username and password by submitting a login form (see Firewall section):

use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\ProviderNotFoundException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;  

try {
    $username = $_POST['username']; // root
    $password = $_POST['password']; // rootpass
    $usernamePasswordToken = new UsernamePasswordToken($username, $password, 'my_secured_area');
    $authenticationManager->authenticate($usernamePasswordToken);
    $securityContext->setToken($usernamePasswordToken); 

    if ($securityContext->isGranted('ROLE_ADMIN')) {
        die('Access granted');
    } else {
        die('Access denied');
    }
} catch (BadCredentialsException $e) {
    die('Invalid username or password');
} catch (ProviderNotFoundException $e) {
    die('Provider could not be found');
}

Usecase: Authenticating A User From The Session

Here's a simplified example of what ContextListener does to authenticate a user from the session (see Firewall section):

$session = $request->getSession();
$token = $session->get('_security_' . 'my_secured_area');

if (null === $session || null === $token) {
    $this->context->setToken(null);
} else {
    $token = unserialize($token);
    $user = $token->getUser();
    $token->setUser($inMemoryUserProvider->refreshUser($user));
    $securityContext->setToken($token);
}

Setting Up The Firewall

Authenticating a user is done by the firewall. In our case we have one secured area: /backend for which we'll configure a request matcher and a collection of listeners. The request matcher gives the firewall the ability to find out if the current request points to a secured area. The listeners are then asked if the current request can be used to authenticate the user:

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\FirewallMap;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\ChannelListener;
use Symfony\Component\Security\Http\Firewall\ContextListener;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
use Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener;
use Symfony\Component\Security\Http\Firewall\AnonymousAuthenticationListener;
use Symfony\Component\Security\Http\Firewall\AccessListener;
use Symfony\Component\Security\Http\Firewall;
use Symfony\Component\Security\Http\AccessMap;
use Symfony\Component\Security\Http\EntryPoint\FormAuthenticationEntryPoint;
use Symfony\Component\Security\Http\Logout\DefaultLogoutSuccessHandler;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler;
use Symfony\Component\HttpFoundation\RequestMatcher;    

$dispatcher = new EventDispatcher();
$kernel = new HttpKernel($dispatcher, new ControllerResolver());
$httpUtils = new HttpUtils();   

// catches authentication exception and converts them to Response instances.
$exceptionListener = new ExceptionListener(
    $securityContext,
    // default implementation of the authentication trust resolver
    new AuthenticationTrustResolver('', ''), // $anonymousClass, $rememberMeClass
    // encapsulates the logic needed to create sub-requests, redirect the user, and match URLs.
    $httpUtils,
    ''
);  

// compares a pre-defined set of checks against a Request instance.
$requestMatcher = new RequestMatcher('^/backend');  

// allows configuration of different access control rules for specific parts of the website.
$accessMap = new AccessMap($requestMatcher, array('ROLE_ADMIN'));   

// instances of Symfony\Component\Security\Http\Firewall\ListenerInterface
$listeners = array(
    // switches the HTTP protocol based on the access control configuration.
    new ChannelListener(
        $accessMap,
        new FormAuthenticationEntryPoint($kernel, $httpUtils, '/login')
    ),
    // manages the SecurityContext persistence through a session
    new ContextListener(
        $securityContext,
        array($inMemoryUserProvider),
        'my_secured_area'
    ),
    // logout users
    new LogoutListener(
        $securityContext,
        $httpUtils,
        new DefaultLogoutSuccessHandler($httpUtils)
    ),
    // authentication via a simple form composed of a username and a password
    new UsernamePasswordFormAuthenticationListener(
        $securityContext,
        $authenticationManager,
        new SessionAuthenticationStrategy('migrate'),
        $httpUtils,
        'my_secured_area',
        new DefaultAuthenticationSuccessHandler($httpUtils, array()),
        new DefaultAuthenticationFailureHandler($kernel, $httpUtils, array())
    ),
    // enforces access control rules
    new AccessListener(
        $securityContext,
        $accessDecisionManager,
        $accessMap,
        $authenticationManager
    ),
    // automatically adds a Token if none is already present.
    new AnonymousAuthenticationListener($securityContext, '') // $key
);  

// allows configuration of different firewalls for specific parts of the website.
$firewallMap = new FirewallMap();
$firewallMap->add($requestMatcher, $listeners, $exceptionListener);

// uses a FirewallMap to register security listeners for the given request
// Firewall is triggered by subscribing itself to "kernel.request" event
$firewall = new Firewall($firewallMap, $dispatcher);

SecurityBundle

That's a lot of code for one "simple" setup. But thanks to the SecurityBundle we don't have to do any of that - many usecases can be implemented simply by configuration. SecurityBundle is also a great place to learn more about extending the dependency injection container, configuration validation and compiler passes.

References