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.