Understanding Symfony Security by Using it Standalone
Setting up big Symfony components in a blank PHP project helps a lot to understand it. You’ll grasp the main architecture of the component much easier this way. Let’s try to understand Symfony Security by doing exactly this!
Security 101
On almost all platforms, security consists of two phases: Authentication and Authorization. Let’s quickly discuss them before digging deep into Symfony security:
- Who are you? is the main question of Authentication. Every HTTP request again, we have to find out who the requester is. At first visit to a site, this is usually a login form. A second request often uses a session stored after logging in to find out who you are. An API call often uses API tokens
- Are you allowed to do this? is the question answered during Authorization. Every request involves an action. For instance, you are reading this blog article, I’m editing it at the moment, over 10 years I’ll probably delete it, etc. During authorization, we find out if the person identified in (1) is allowed to do the action he wants to do
Set-up our Blank Project
Setting up a blank project is nothing more than creating a new directory. In the directory, we’ll use Composer to install the Security Core component:
1
2
3
$ mkdir security-101
$ cd security-101
$ composer require symfony/security-core
Security contains a couple sub components: Core, Http, Guard, Csrf. In this post, we’ll focus on the core only. In a funny plot twist, you’ll directly discover what HTTP is doing if you’re reading the side notes!
Authentication Input: The token
The question “who are you?” can only be answered if we have input from the user world (there must be a “you”). This input can be anything. As already explained, the values of the login form upon login is one example of this input. In all requests after the user logged in, a browser session represents the input from user world.
As our goal is just to write one working PHP file, we’ll use static strings as “input from the user”:
1
2
3
4
5
6
7
<?php // secure.php
require_once 'vendor/autoload.php';
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
$token = new UsernamePasswordToken('wouter', 'pa$$word', 'default');
This type of token is most often used in traditional applications, it holds a
username and a password (ignore that third argument for the moment). Other
tokens are for instance a RememberMeToken
, which uses a browser cookie, or
you can create an BearerToken
that contains a header used for API requests.
Let’s Authenticate the Token: Meet the AuthenticationManager
Now we have the token representing the user input, it’s time to authenticate
it. The Security component provides an AuthenticationManagerInterface
for this.
By default, only one implementation of this interface is provided: The
AuthenticationProviderManager
. Its name is quite confusing, it’s not managing
authentication providers, but it is the authentication manager based on
authentication providers.
AuthenticationProviderInterface
classes do the hard work for this manager:
They transform an unauthenticated token into an authenticated token. In other
words, they transform user input into a security identity.
The most commonly used provider is the DaoAuthenticationProvider
(Data
Access Object). It uses a user provider to find a user matching the
input from somewhere and then matches the password (using a password encoder).
Set-up some User Resources: The UserProvider
To get things working, we first define a class able to load users from “some
resource”. In this case, we use the InMemoryUserProvider
that fetches users
from a PHP array.
1
2
3
4
5
6
7
8
9
10
11
12
13
// secure.php
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
// ...
$userProvider = new InMemoryUserProvider([
'wouter' => [
'password' => 'pa$$word',
'roles' => ['TITLE_SUPERVISOR']
],
]);
// As an example of this class, let's find the user:
$wouterUser = $userProvider->loadUserByUsername('wouter');
Safety first: Encrypted User Passwords
Saving plain texts passwords is a no-go for web applications. They are often encoded using for instance bcrypt or sha256. This is abstracted in password encoders. They are able to encode a plain text password and check whether the password entered by the user was valid:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// secure.php
use Symfony\Component\Security\Core\Encoder\EncoderFactory;
use Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder;
use Symfony\Component\Security\Core\User\User;
// ...
$encoderFactory = new EncoderFactory([
// we take the easiest road: plain text passwords
User::class => new PlaintextPasswordEncoder(),
]);
// First, get the encoder associated with our user (other users
// can use other encoders).
$encoderFactory->getEncoder(User::class)
// Then, check whether some input matches our user's password:
->isPasswordValid($wouterUser->getPassword(), 'pa$$word', '');
Instantiating the Authentication Manager
Yay, it took quite some effort, but we’re ready to create the
AuthenticationProviderManager
!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// secure.php
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
use Symfony\Component\Security\Core\User\UserChecker;
// ...
$authenticationManager = new AuthenticationProviderManager([
new DaoAuthenticationProvider(
$userProvider,
new UserChecker(),
'default',
$encoderFactory
),
]);
As you see, there is one more class: The UserChecker
. It checks some “user
flags” after it is fetched from the user provider. This includes for instance
whether the user is activated, banned, etc.
Authenticating the Token
Finally – we’re more than halfway the post now – we are able to authenticate the token we created:
1
2
3
4
5
6
7
// secure.php
// ...
$authenticatedToken = $authenticationManager->authenticate($inputToken);
echo 'Hello '.$authenticatedToken->getUsername().'!';
Authorize Actions: The AccessDecisionManager
Now we’ve authenticated the user input and the Security system validated our
input, it’s time to answer the Are you allowe to do this? question. Let me
introduce you to yet another “manager”: AccessDecisionManagerInterface
.
The default implementation uses “Security voters” to decide whether the user is allowed to execute an action. These voters are provided with an attribute (representing the action) and optionally some context (i.e. the subject of the action).
1
2
3
4
5
6
7
// secure.php
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
// ...
$accessDecisionManager = new AccessDecisionManager([
// ... voters
]);
The default voters of Symfony are a bit strange, they don’t validate an action,
but validate the user’s identity. One of them is the RoleVoter
. It checks
whether User#getRoles()
contains the provided attribute:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// secure.php
use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter;
// ...
$accessDecisionManager = new AccessDecisionManager([
// TITLE_ is the prefix an attribute must have in order to be
// managed by this voter
new RoleVoter('TITLE_'),
]);
// now we can use the access decision manager to see if the
// authenticated token has the "supervisor" role:
$isSupervisor = $accessDecisionManager->decide(
$authenticatedToken,
['TITLE_SUPERVISOR']
);
// If we add a custom voter, we would be able to i.e. test if we are
// able to view a user's profile:
$canViewProfile = $accessDecisionManager->decide(
$authenticatedToken,
['VIEW'],
$authenticatedToken->getUser() // some context for the "view" action
);
Relation with the Symfony Framework
Now, if we take a look at the configuration for the SecurityBundle, we’ll find many relations with the architecture used above:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
security:
# Providers: UserProvider classes
providers:
in_memory:
# indicating the InMemoryUserProvider
memory: ~
# The UserPasswordEncoders
encoders:
Symfony\Component\Security\Core\User\UserInterface: plaintext
# Firewalls are listeners of the HTTP component that
# execute authentication based on paths
firewalls:
# ...
# Each firewall configures a list of authentication providers
# used for this AuthenticationProviderManager. I.e. all
# firewalls are one AuthenticationProviderManager (and thus,
# one security system).
main:
# AnonymousAuthenticationProvider
anonymous: true
# Use UsernamePasswordToken and DaoAuthenticationProvider
http_basic: true
form_login: true
# Relate to AccessDecisionManager#isGranted() calls for
# specific paths
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/profile, roles: ROLE_USER }
Take Home’s
I hope this posts has provided you some insights in how the Security component works.
- Security HTTP is only a small layer converting HTTP requests into input for the Security system
- There are many similarities between the config in the Symfony framework and the workings of the Security Core component
- A complete working implementation of the Security component can be achieved in 20 lines of PHP
In this posts, you might have also observed some weird things. I’ll go into further detail on them in later posts.
- There is no build-in authorization for API tokens
- The token representing user input is the same as the authenticated token
- Authorization by default is on the identity rather than the action