Bonus - How To Show A Flash Message On Successful Login, or Failed Login Attempt


In this video we are going to tackle a question I received from site user Andrew regarding displaying flash messages if the login form submission is either successful, or unsuccessful. Here's the question:

Hello again! Thanks, you helped me fixing that problem :) One last question (from your course): how can I "$this->addFlash" for login? Because now its working only with registration. How can I make it work with login? Check if signing in was successful/not successful?

This is possible, though not entirely intuitive, and not particular clearly documented.

One way to achieve this is to use the concepts of success_handler, and failure_handler under your form_login security configuration. This sounds more complicated than it really is, so let's cover this with an example:

# app/config/security.yml

security:

    # other stuff removed for brevity

    firewalls:

        main:
            pattern: ^/
            provider: chain_provider
            form_login:
                login_path: registration
                failure_path: login
                check_path: login
                success_handler: crv.authentication_success_handler
                failure_handler: crv.authentication_failure_handler
            logout: true
            anonymous: ~

The important lines are success_handler, and failure_handler.

It's worth noting that there are other handlers available to customise such as the access_denied_handler, and that logout can have its own success_handler. You can also have different handlers for different login strategies. Be sure to read the security configuration reference to find all your available options.

If you're well accustomed to Symfony you can likely discern that in the configuration above, both success_handler and failure_handler are pointing to Symfony services.

These services are things we define. Let's cover the service definitions now:

# app/config/services.yml

services:

    crv.authentication_success_handler:
        class: AppBundle\Security\AuthenticationSuccessHandler

    crv.authentication_failure_handler:
        class: AppBundle\Security\AuthenticationFailedHandler

These won't work just yet.

Partly that's because neither of these files exist yet, and partly because both classes will need a set of arguments injecting in.

The success_handler is the easier of the two (fewer dependencies, anyway) so we will start with that.

Great Success

It might not be immediately obvious but there is a default success_handler configured for use with form_login.

When we define our own service we are overriding the default implementation with our own.

This is good, because we can take ownership of what the implementation is and does.

However, we must ensure our implementation behaves how Symfony expects a success_handler to behave.

As best I am aware, there are only two ways we can accomplish this:

  • Implementing everything ourselves, or;
  • Inheritance

By this I mean either we can take a copy / paste of everything the default implementation of the AuthenticationSuccessHandlerInterface does, or we can extends the default implementation and add on our own logic.

For reference, here is the implementation for DefaultAuthenticationSuccessHandler. This is accurate for Symfony 3.3.6, which is the latest version at the time of recording.

There's a bunch of things happening in here that I do not wish to take ownership of. Reimplementing all of this is not a good option for me.

All I want to do is show a flash message. Everything else should behave as though this were the DefaultAuthenticationSuccessHandler.

There are two important parts of the DefaultAuthenticationSuccessHandler that we must cover if our implementation is to work in the same way:

  • The constructor
  • The onAuthenticationSuccess method

The constructor is important as the DefaultAuthenticationSuccessHandler's __construct method takes two arguments:

    /**
     * Constructor.
     *
     * @param HttpUtils $httpUtils
     * @param array     $options   Options for processing a successful authentication attempt
     */
    public function __construct(HttpUtils $httpUtils, array $options = array())
    {
        $this->httpUtils = $httpUtils;
        $this->setOptions($options);
    }

If we are extends'ing this class then we too will need to pass in those two arguments.

The onAuthenticationSuccess method is important because this is the method anything implementing AuthenticationSuccessHandlerInterface must implement. In other words, this is the 'contract' that must be honoured. Symfony is going to expect this method to be defined, and that's what brings all of this process together.

We will need to override this method. Therefore, we will also need to call the same method on the parent. More on this shortly.

Let's start our implementation:

<?php

// src/AppBundle/Security/AuthenticationSuccessHandler.php

namespace AppBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;

class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    public function __construct()
    {
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        // add our success flash message here
    }
}

There's not a huge amount happening here.

We have a class definition. We implement the required interface. By implementing that interface we must also have the method from the interface (onAuthenticationSuccess) with the expected method signature.

At this point we could try to log in.

And if we do, we will see:

Authentication Success Handler did not return a Response. 500 Internal Server Error - RuntimeException

This is good, as it proves our own service is being used, rather than the default one provided by Symfony.

Of course it's bad because now we've broken everything :)

All we need to do to fix this is to return a Response.

However, the DefaultAuthenticationSuccessHandler implementation already takes care of all of this for us, so I'm going to "piggy-back" on to this by way of inheritance:

<?php

namespace AppBundle\Security;

use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;

class AuthenticationSuccessHandler
  extends DefaultAuthenticationSuccessHandler
  implements AuthenticationSuccessHandlerInterface
{

As soon as we do this we must update our constructor. We are now extending the functionality of DefaultAuthenticationSuccessHandler, but in order to do so we must ensure that our base class (DefaultAuthenticationSuccessHandler) continues to work in the way it expects.

What this means is we must call through to the base / parent constructor with the expected arguments:

// vendor/symfony/symfony/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php

    /**
     * Constructor.
     *
     * @param HttpUtils $httpUtils
     * @param array     $options   Options for processing a successful authentication attempt
     */
    public function __construct(HttpUtils $httpUtils, array $options = array())
    {

This means our own __construct method needs - at least - these two arguments, too.

Let's add this knowledge in:

<?php

// src/AppBundle/Security/AuthenticationSuccessHandler.php

namespace AppBundle\Security;

use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
use Symfony\Component\Security\Http\HttpUtils;

class AuthenticationSuccessHandler
  extends DefaultAuthenticationSuccessHandler
  implements AuthenticationSuccessHandlerInterface
{

    public function __construct(HttpUtils $httpUtils, array $options = array())
    {
        parent::__construct($httpUtils, $options);
    }

Note the inclusion of the use statement for HttpUtils.

A call to parent::__construct(...) simply means to call the method (__construct in this case) on the parent or base class. This is the class we extends from. In other words, call the __construct method on the DefaultAuthenticationSuccessHandler.

For this to work we must tell Symfony that we want to inject these two new arguments:

  • HttpUtils $httpUtils
  • array $options

This means we must update our service definition:

# app/config/services.yml

services:

    crv.authentication_success_handler:
        class: AppBundle\Security\AuthenticationSuccessHandler
        arguments:
            - "@security.http_utils"
            - []

    crv.authentication_failure_handler:
        class: AppBundle\Security\AuthenticationFailedHandler

The two new arguments ensure this service now behaves properly.

However, you may be wondering how I knew what arguments to add. Good question.

Finding the security.http_utils service isn't that tricky:

php bin/console debug:container http

 Select one of the following services to display its information:
  [0] http_kernel
  [1] form.type_extension.form.http_foundation
  [2] security.http_utils
  [3] twig.runtime.httpkernel
 > 2

Information for Service "security.http_utils"
=============================================

 ------------------ -------------------------------------------
  Option             Value
 ------------------ -------------------------------------------
  Service ID         security.http_utils
  Class              Symfony\Component\Security\Http\HttpUtils
  Tags               -
  Public             no
  Synthetic          no
  Lazy               no
  Shared             yes
  Abstract           no
  Autowired          no
  Autowiring Types   -
 ------------------ -------------------------------------------

More confusing is the second argument - [].

Firstly, why is it a []? Well, that's YAML syntax for an array. An empty array in this case.

Secondly, how did I know it is an empty array?

:)

Ok, so this one is a little bit trickier.

The way I knew it was an empty array was to find the service definition provided by Symfony. To do this I did a search for DefaultAuthenticationSuccessHandler inside the vendor directory, and then looked for an XML service definition. Here it is:

<!-- vendor/symfony/symfony/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml -->

<service id="security.authentication.success_handler" class="Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler" abstract="true" public="false">
    <argument type="service" id="security.http_utils" />
    <argument type="collection" /> <!-- Options -->
</service>

The first argument we have already covered.

The second argument is an empty collection. Translated to YAML, that means an empty array.

As it stands, our implementation still won't work.

However making it work at this point is easy. We just need to behave identically to the parent / base class when our success_handler is called. In other words, we just need to do whatever the parent::onAuthenticationSuccess method does currently:

<?php

// src/AppBundle/Security/AuthenticationSuccessHandler.php

namespace AppBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
use Symfony\Component\Security\Http\HttpUtils;

class AuthenticationSuccessHandler
  extends DefaultAuthenticationSuccessHandler
  implements AuthenticationSuccessHandlerInterface
{

    public function __construct(HttpUtils $httpUtils, array $options = array())
    {
        parent::__construct($httpUtils, $options);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        return parent::onAuthenticationSuccess($request, $token);
    }
}

Cool, we can now log in.

What sucks is that we have come all this way and yet all we have done is re-implement what we already had! Yikes.

Adding A Flash Message

It turns out we've done 90% of the hard work already.

All we need to do now is the tiny bit of extra work that we wanted all along:

Showing a flash message on login success.

If we want to show a flash message then we need access to the Flash Bag.

If we want access to something inside a Symfony service then we need to think: Injection!

Is there a pre-defined service for the Flash Bag? You bet there is:

php bin/console debug:container session.flash_bag

Information for Service "session.flash_bag"
===========================================

 ------------------ ---------------------------------------------------------
  Option             Value
 ------------------ ---------------------------------------------------------
  Service ID         session.flash_bag
  Class              Symfony\Component\HttpFoundation\Session\Flash\FlashBag
  Tags               -
  Public             no
  Synthetic          no
  Lazy               no
  Shared             yes
  Abstract           no
  Autowired          no
  Autowiring Types   -
 ------------------ ---------------------------------------------------------

Therefore all we need to do is inject session.flash_bag, and use it, and we are done:

# app/config/services.yml

services:

    crv.authentication_success_handler:
        class: AppBundle\Security\AuthenticationSuccessHandler
        arguments:
            - "@security.http_utils"
            - []
            - "@session.flash_bag"

    crv.authentication_failure_handler:
        class: AppBundle\Security\AuthenticationFailedHandler

That's the Flash Bag injected as our third constructor argument. Using this is very straightforward:

<?php

// src/AppBundle/Security/AuthenticationSuccessHandler.php

namespace AppBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
use Symfony\Component\Security\Http\HttpUtils;

class AuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    /**
     * @var FlashBagInterface
     */
    private $flashBag;

    public function __construct(HttpUtils $httpUtils, array $options = array(), FlashBagInterface $flashBag)
    {
        parent::__construct($httpUtils, $options);

        $this->flashBag = $flashBag;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        $this->flashBag->add(
            'success',
            'Welcome!'
        );

        return parent::onAuthenticationSuccess($request, $token);
    }
}

Note the extra use statement for the FlashBagInterface.

Note also that we do not need to pass the $flashBag into the call to parent::__construct. This is specific to our implementation.

And that's it. With this, when logging in you should now see a flash message indicating success.

Failure Is -Not- An Option

The success_handler unsurprisingly handles the outcomes whereby things went well. If we provided good details to the login form then we are logged in, and our new success_handler implementation ensures we see a flash message saying "Welcome!".

If we provided bad login details - an invalid username, or password, or both - then maybe we want to show a message like "Denied!" or some other such suitable bit of feedback.

The failure_handler allows us to do just this. And in a manner very similar to the success_handler, only, not identical.

We are still going to use inheritance. We will still need a service definition.

What changes is the interface we need to implement, and the services arguments:

Again, Symfony provides a default implementation - DefaultAuthenticationFailureHandler.

This implementation has a more involved constructor:

// vendor/symfony/symfony/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationFailureHandler.php

    /**
     * Constructor.
     *
     * @param HttpKernelInterface $httpKernel
     * @param HttpUtils           $httpUtils
     * @param array               $options    Options for processing a failed authentication attempt
     * @param LoggerInterface     $logger     Optional logger
     */
    public function __construct(
      HttpKernelInterface $httpKernel,
      HttpUtils $httpUtils,
      array $options = array(),
      LoggerInterface $logger = null
      )
    {
        $this->httpKernel = $httpKernel;
        $this->httpUtils = $httpUtils;
        $this->logger = $logger;
        $this->setOptions($options);
    }

With what we know from the success_handler implementation we could likely guess at the options. But instead, let's refer back to the XML service definition provided by Symfony and replicate this in our own services.yml file:

<!-- vendor/symfony/symfony/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml -->

<service id="security.authentication.failure_handler" class="Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler" abstract="true" public="false">
    <tag name="monolog.logger" channel="security" />
    <argument type="service" id="http_kernel" />
    <argument type="service" id="security.http_utils" />
    <argument type="collection" /> <!-- Options -->
    <argument type="service" id="logger" on-invalid="null" />
</service>

All the service ID's are there for us now. Adding this to YAML is pretty much copy / paste:

# app/config/services.yml

services:

    crv.authentication_failure_handler:
        class: AppBundle\Security\AuthenticationFailedHandler
        arguments:
            - "@http_kernel"
            - "@security.http_utils"
            - []
            - "@logger"

The only real oddity is the empty array, which we have already discussed.

Again, we'd probably like to inject the Flash Bag:

# app/config/services.yml

services:

    crv.authentication_failure_handler:
        class: AppBundle\Security\AuthenticationFailedHandler
        arguments:
            - "@http_kernel"
            - "@security.http_utils"
            - []
            - "@logger"
            - "@session.flash_bag"

Much like in the success handler implementation, we need to ensure we extends the Default Symfony implementation, and that our constructor function calls the parent / base constructor.

We also need to provide our own overridden implementation of onAuthenticationFailure, the contractual obligation of us implementing AuthenticationFailureHandlerInterface.

Here's everything we need to do:

<?php

// src/AppBundle/Security/AuthenticationFailedHandler.php

namespace AppBundle\Security;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler;
use Symfony\Component\Security\Http\HttpUtils;

class AuthenticationFailedHandler extends DefaultAuthenticationFailureHandler
{
    /**
     * @var FlashBagInterface
     */
    private $flashBag;

    public function __construct(
        HttpKernelInterface $httpKernel,
        HttpUtils $httpUtils,
        array $options = array(),
        LoggerInterface $logger,
        FlashBagInterface $flashBag
    )
    {
        parent::__construct($httpKernel, $httpUtils, $options, $logger);

        $this->flashBag = $flashBag;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
            $this->flashBag->add(
                'warning',
                'Denied!'
            );

        return parent::onAuthenticationFailure($request, $exception);
    }
}

And with that, we are done.

Code For This Course

Get the code for this course.

Episodes