Easier Authentication with Guard in Symfony 3
The Symfony2 security system is a complex part of the framework, one that is difficult to understand and work with for many people. It is very powerful and flexible, however not the most straightforward. For an example of custom authentication, check out my previous article that integrates Symfony with the UserApp service.
With the release of version 2.8 (and the much awaited version 3), a new component was accepted into the Symfony framework: Guard. The purpose of this component is to integrate with the security system and provide a very easy way for creating custom authentications. It exposes a single interface, whose methods take you from the beginning to the end of the authentication chain: logical and all grouped together.
In this article, we are going to create a simple form authentication that requires a user to be logged in and have the ROLE_ADMIN
role for each page. The original way of building a form authentication can still be used, but we will use Guard to illustrate its simplicity. You can then apply the same concept to any kind of authentication (token, social media, etc).
If you want to follow along in your own IDE, you can clone this repository which contains our Symfony application with Guard for authentication. So, let’s begin.
The security configuration
Any security configuration will require a User class (to represent user data) and UserProvider (to fetch user data). To keep things simple, we will go with the InMemory
user provider which, in turn, uses the default Symfony User
class. So our security.yml
file can start off like this:
security:
providers:
in_memory:
memory:
users:
admin:
password: admin
roles: 'ROLE_ADMIN'
For more information about the Symfony security system and what this file can contain, I strongly recommend you read the book entry on the Symfony website.
Our InMemory
provider now has one single hardcoded test user which has the ROLE_ADMIN
.
Under the firewalls
key, we can define our firewall:
secured_area
anonymous: ~
logout:
path: /logout
target: /
guard:
authenticators:
- form_authenticator
This basically says that anonymous users can access the firewall and that the path to log a user out is /logout
. The new part is the guard
key that indicates which authenticator is used for the Guard configuration of this firewall: form_authenticator
. This needs to be the service name and we’ll see in a minute how and where it’s defined.
Lastly in the security configuration, we can specify some access rules:
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_ADMIN }
In this example we specify that users who are not logged in can access only the /login
path. For all the rest, the ROLE_ADMIN
role is needed.
The Login Controller
Before moving on to the actual authenticator, let’s see what we have in place for the actual login form and controller. Inside of our DefaultController
we have this action:
/**
* @Route("/login", name="login")
*/
public function loginAction(Request $request)
{
$user = $this->getUser();
if ($user instanceof UserInterface) {
return $this->redirectToRoute('homepage');
}
/** @var AuthenticationException $exception */
$exception = $this->get('security.authentication_utils')
->getLastAuthenticationError();
return $this->render('default/login.html.twig', [
'error' => $exception ? $exception->getMessage() : NULL,
]);
}
Defining the /login
route, this action is responsible for showing a rudimentary login form to users that are not logged in. The Twig template for this form looks something like this:
{{ error }}
<form action="{{ path('login') }}" method="POST">
<label for="username">Username</label>
<input type="text" name="username" class="form-control" id="username" placeholder="Username">
<label for="password">Password</label>
<input type="password" name="password" class="form-control"
id="password" placeholder="Password">
<button type="submit">Login</button>
</form>
So far nothing special. Just a simple form markup directly in HTML for quick scaffolding that posts back to the same /login
path.
The Guard Authenticator Service
We referenced in the security configuration the service for our Guard authenticator. Let’s make sure that we define this service in the services.yml
file:
services:
form_authenticator:
class: AppBundle\Security\FormAuthenticator
arguments: ["@router"]
This service references our FormAuthenticator
class:
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class FormAuthenticator extends AbstractGuardAuthenticator
{
/**
* @var \Symfony\Component\Routing\RouterInterface
*/
private $router;
/**
* Default message for authentication failure.
*
* @var string
*/
private $failMessage = 'Invalid credentials';
/**
* Creates a new instance of FormAuthenticator
*/
public function __construct(RouterInterface $router) {
$this->router = $router;
}
/**
* {@inheritdoc}
*/
public function getCredentials(Request $request)
{
if ($request->getPathInfo() != '/login' || !$request->isMethod('POST')) {
return;
}
return array(
'username' => $request->request->get('username'),
'password' => $request->request->get('password'),
);
}
/**
* {@inheritdoc}
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
if (!$userProvider instanceof InMemoryUserProvider) {
return;
}
try {
return $userProvider->loadUserByUsername($credentials['username']);
}
catch (UsernameNotFoundException $e) {
throw new CustomUserMessageAuthenticationException($this->failMessage);
}
}
/**
* {@inheritdoc}
*/
public function checkCredentials($credentials, UserInterface $user)
{
if ($user->getPassword() === $credentials['password']) {
return true;
}
throw new CustomUserMessageAuthenticationException($this->failMessage);
}
/**
* {@inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$url = $this->router->generate('homepage');
return new RedirectResponse($url);
}
/**
* {@inheritdoc}
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
/**
* {@inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
/**
* {@inheritdoc}
*/
public function supportsRememberMe()
{
return false;
}
}
Although this seems like a lot, it really isn’t. Let’s go step by step and understand what is happening here.
First, we are placing this in the Security
folder of our bundle. This is just a personal choice, we are not obligated to it. Then, we are extending from the AbstractGuardAuthenticator
because that already takes care of implementing a required method from the GuardAuthenticatorInterface
interface. If we needed a specific token class to represent our authentication, we could just implement the interface and its createAuthenticatedToken()
method as well. For now, we don’t have to.
The implementation of the interface methods is the crux of the matter and together these form the pipeline of authentication from the moment a user tries to access the site to the one in which they are granted or denied access.
We start with the getCredentials()
method which gets called on every request. The purpose of this method is to either return credentials data from the request or NULL (either denies access, allows another authenticator to then provide credentials, or calls the start()
method). Since only POST requests to our /login
path are containers of credentials, we return an array with the submitted username and password only if this is the case.
The very next method that gets called if getCredentials()
does not return NULL is getUser()
. The latter is responsible for loading up a user based on the credentials we receive from the former method. Using the default user provider (in our case the InMemory
provider), we load and return the user based on their username. Although we can return NULL here as well to trigger authentication failure, we can choose to throw a CustomUserMessageAuthenticationException
to specify our own custom failure message.
If, however, a user is retrieved and returned, the checkCredentials()
method kicks in. Quite expectedly, the purpose of this method is to verify that the passed credentials match those of the user that was found. And the same rules apply, if we return NULL or throw an exception, we fail the authentication.
At this point, the user gets logged in if the credentials match. In this case, the onAuthenticationSuccess()
method is called and here we can again do what we want. In our case, redirecting to the homepage seems like a good enough example. On the contrary, if the authentication fails, the onAuthenticationFailure()
method is called. What we do is redirect back to the /login
page but not before setting the last authentication exception in the session so that we can display the error message above the form. This method is called at any of the authentication failure points of the pipeline.
The start()
method is the entry point into the Guard system (and application). This method is called whenever a user is trying to access a page that requires authentication but no credentials are returned by getCredentials()
. In our case this means that if somebody tries to access the homepage, there are no credentials in the request so getCredentials()
returns NULL. What we want to happen then is to redirect to the /login
page so that the user can log in to access the homepage.
Let’s imagine another example: token based authentication. In such a case, each request needs to contain a token that authenticates the user. This means that getCredentials()
will always have to return the credentials. If it doesn’t, the start()
method would return a response indicating that access is denied (or whatever you may think of).
The final method is responsible for marking the RememberMe
functionality. In our case we don’t use it so we return false. For more information about Remember Me in Symfony check out the cookbook entry.
Conclusion
We now have a fully functioning login system using the Guard component. Having mentioned above the example of the token authentication, we could implement one and have it work in tandem with the one we wrote in this article. Multiple authenticators can exist like so:
guard:
authenticators:
- form_authenticator
- token_authenticator
entry_point: form_authenticator
If we configure multiple authenticators, we also need to specify which one should be the entry point (whose start()
method will be called when a user tries to access a resource without providing credentials).
Note that Guard does not replace anything that exists in Symfony but adds to it. So existing security set-ups are bound to continue working. For instance, the form_login
or simple_form
as we used it previously will continue to work.
Have you tried Guard out yet? How do you feel about it? Let us know!