Easier Authentication with Guard in Symfony 3

Share this article

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!

Frequently Asked Questions (FAQs) about Authentication with Guard in Symfony 3

How does Guard authentication differ from other Symfony authentication methods?

Guard authentication is a powerful and customizable authentication system provided by Symfony. Unlike other Symfony authentication methods, Guard gives you full control over the authentication process, allowing you to handle every single part of it. This includes the form submission process, checking the credentials, and handling the authentication success or failure. It’s a great choice if you need to implement complex authentication rules or want to have more control over your authentication process.

Can I use Guard authentication with Symfony versions other than 3?

Yes, you can use Guard authentication with other versions of Symfony. It was introduced in Symfony 2.8 and is available in all subsequent versions. However, the implementation might slightly differ depending on the Symfony version you’re using. Always refer to the official Symfony documentation for the version you’re using to ensure you’re implementing it correctly.

How can I customize the authentication failure response in Guard?

Guard allows you to customize the authentication failure response by overriding the onAuthenticationFailure method in your authenticator. You can return any response you want from this method, allowing you to fully control what happens when authentication fails. This could be a redirect to a specific page, a JSON response, or anything else that fits your application’s needs.

How can I handle remember me functionality with Guard?

Guard makes it easy to handle remember me functionality. You just need to implement the supportsRememberMe method in your authenticator and return true from it. Symfony will then automatically handle the remember me functionality for you, saving you from having to implement it manually.

Can I use Guard for API authentication?

Yes, Guard is a great choice for API authentication. It gives you full control over the authentication process, allowing you to handle things like token validation and generation. You can also customize the authentication success and failure responses to return JSON responses, which are typically used in APIs.

How can I customize the login form with Guard?

Guard allows you to fully control the login form. You can create your own form type and template, and then use them in your authenticator’s getLoginUrl method. This gives you full control over the form’s appearance and functionality, allowing you to create a login form that perfectly fits your application’s needs.

How can I handle roles with Guard?

Guard allows you to handle roles by implementing the getRoles method in your User class. This method should return an array of roles for the user. Symfony will then automatically handle role checking for you, allowing you to easily control access to different parts of your application based on the user’s roles.

Can I use Guard with a database for user authentication?

Yes, you can use Guard with a database for user authentication. You just need to implement the getUser method in your authenticator to fetch the user from the database based on the credentials provided. Symfony will then handle the rest of the authentication process for you.

How can I handle password encoding with Guard?

Guard allows you to handle password encoding by implementing the checkCredentials method in your authenticator. You can use Symfony’s password encoder service to encode the password provided and compare it with the encoded password stored in the database.

Can I use Guard for social authentication?

Yes, you can use Guard for social authentication. You can use the getUser method in your authenticator to fetch the user from the social provider and then handle the rest of the authentication process as usual. This makes Guard a flexible and powerful choice for any kind of authentication you need to implement.

Daniel SiposDaniel Sipos
View Author

Daniel Sipos is a Drupal developer who lives in Brussels, Belgium. He works professionally with Drupal but likes to use other PHP frameworks and technologies as well. He runs webomelette.com, a Drupal blog where he writes articles and tutorials about Drupal development, theming and site building.

authenticationauthorizationloginOOPHPPHPphp frameworkphp frameworkssymfony
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week