PHP
Article

Easier Authentication with Guard in Symfony 3

By Daniel Sipos

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.

Lock image

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!

  • Martin Zajíc

    Hi, did u use session with guard? I have actually 3 providers, one is form like this, second for facebook login and third for api. In first two I want to create session, but token storage is empty $this->get(‘security.token_storage’)->getToken()->getUser() returns always “anon.” even if I setup always_remember_me

  • Bolla Sándor

    How about crsf token for the login form?

  • Felipy Amorim

    hello, i have problem with firefox and IE, don’t login, but in chrome works fine. any help ?

  • cj5

    This example is great but does NOT work at all. No matter how I code it, it always return invalid crednetials

  • Breid

    Works perfect! Thanks dude!

  • dav.big

    Great tutorial!

    How can i change the session lifetime to perform an auto logout?
    Just adding the “cookie_lifetime” value doesn’t help me.

    Maybe you can help me!
    Thank you!

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.