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.