Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Customize Error Messages & Adding Logout

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When we fail login, we store the AuthenticationException in the session - which explains what went wrong - and then redirect to the login page:

... lines 1 - 20
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 23 - 65
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
return new RedirectResponse(
$this->router->generate('app_login')
);
}
... lines 74 - 84
}

On that page, we read that exception out of the session using this nice AuthenticationUtils service:

... lines 1 - 9
class SecurityController extends AbstractController
{
... lines 12 - 14
public function login(AuthenticationUtils $authenticationUtils): Response
{
return $this->render('security/login.html.twig', [
'error' => $authenticationUtils->getLastAuthenticationError(),
]);
}
}

And ultimately, in the template, call the getMessageKey() method to render a safe message that describes why authentication failed:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="login-form bg-light mt-4 p-4">
<form method="post" class="row g-3">
... lines 10 - 11
{% if error %}
<div class="alert alert-danger">{{ error.messageKey }}</div>
{% endif %}
... lines 15 - 29
</form>
</div>
</div>
</div>
{% endblock %}

For example, if we enter an email that doesn't exist, we see:

Username could not be found.

On a technical level, this means that the User object could not be found. Cool... but for us, this isn't a great message because we're logging in via an email. Also, if we enter a valid user - abraca_admin@example.com - with an invalid password, we see:

Invalid credentials.

That's a better message... but it's not super friendly.

Translating the Error Messages?

So how can we customize these? The answer is both simple and... maybe a bit surprising. We translate them. Check it out: over in the template, after messageKey, add |trans to translate it. Pass this two arguments. The first is error.messageData. This isn't too important... but in the translation world, sometimes your translations can have "wildcard" values in them... and you pass in the values for those wildcards here. The second argument is called a "translation domain"... which is almost like a translation category. Pass security:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="login-form bg-light mt-4 p-4">
<form method="post" class="row g-3">
... lines 10 - 11
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
... lines 15 - 29
</form>
</div>
</div>
</div>
{% endblock %}

If you do have a multi-lingual site, all of the core authentication messages have already been translated to other languages... and those translations are available in a domain called security. So by using the security domain here, if we switched the site to Spanish, we would instantly get Spanish authentication messages.

If we stopped now... absolutely nothing would change! But because we're going through the translator, we have the opportunity to "translate" these strings from English to... different English!

In the translations/ directory - which you should automatically have because the translation component is already installed - create a new file called security.en.yaml: security because we're using the security translation domain and en for English. You can also create .xlf translation files - YAML is just easier for what we need to do.

Now, copy the exact error message including the period, paste - I'll wrap it in quotes to be safe - and set it to something different like:

Invalid password entered!

"Invalid credentials.": "Invalid password entered!"

Cool! Let's try it again. Log in as abraca_admin@example.com with an invalid password and... much better! Let's try with a bad email.

Ok, repeat the process: copy the message, go over to the translation file, paste... and change it to something a bit more user-friendly like:

Email not found!

... line 1
"Username could not be found.": "Email not found!"

Let's try it again: same email, any password and... got it!

Email not found.

Okay! Our authenticator is done! We load the User from the email, check their password and handle both success and failure. Booya! We are going to add more stuff to this later - including checking real user passwords - but this is fully functional.

Logging Out

Let's add a way to log out. So... like if the user goes to /logout, they get... logged it! This starts exactly like you expect: we need a route & controller.

Inside of SecurityController, I'll copy the login() method, paste, change it to /logout, app_logout and call the method logout:

... lines 1 - 9
class SecurityController extends AbstractController
{
... lines 12 - 21
/**
* @Route("/logout")
*/
public function logout()
{
... line 27
}
}

To perform the logging out itself... we're going to put absolutely no code in this method. Actually, I'll throw a new \Exception() that says "logout() should never be reached":

... lines 1 - 9
class SecurityController extends AbstractController
{
... lines 12 - 21
/**
* @Route("/logout")
*/
public function logout()
{
throw new \Exception('logout() should never be reached');
}
}

Let me explain. Logging out works a bit like logging in. Instead of putting some logic in the controller, we're going activate something on our firewall that says:

Hey! If the user goes to /logout, please intercept that request, log out the user, and redirect them somewhere else.

To activate that magic, open up config/packages/security.yaml. Anywhere under our firewall, add logout: true:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 25
logout: true
... lines 27 - 39

Internally, this activates a "listener" that looks for any requests to /logout.

Configuring logout

And actually, instead of just saying logout: true, you can customize how this works. Find your terminal and run:

symfony console debug:config security

As a reminder, this command shows you all of your current configuration under the security key. So all of our config plus any default values.

If we run this... and find the main firewall... check out the logout section. All of these keys are the default values. Notice there's one called path: /logout. This is why it's listening to the URL /logout. If you wanted to log out via another URL, you would just tweak this key here.

But since we have /logout here... and that matches our /logout right here, this should work. By the way, you might be wondering why we needed to create a route and controller at all! Great question! We actually don't need a controller, it will never be called. But we do need a route. If we didn't have one, the routing system would trigger a 404 error before the logout system could work its magic. Plus, it's nice to have a route, so we can generate a URL to it.

Ok: let's test this thing! Log in first: abraca_admin@example.com and password tada. Awesome: we are authenticated. Manually go to /logout and... we are now logged out! The default behavior of the system is to log us out and redirect back to the homepage. If you need to customize that, there are a few options. First, under the logout key, you can change target to some other URL or route name.

But we can also hook into the logout process via an event listener, a topic that we'll talk about towards the end of the tutorial.

Next: let's give each user a real password. This will involve hashing passwords, so we can securely store them in the database and then checking those hashed passwords during authentication. Symfony makes both of these easy.

Leave a comment!

12
Login or Register to join the conversation
Naglaa-Fouz Avatar
Naglaa-Fouz Avatar Naglaa-Fouz | posted 1 month ago

hi, when i use lougout true in security.yaml and when i try make debug de security i have this error

Unable to parse at line 23 (near "logout:true").

Reply

Hey Naglaa-Fouz,

Hm, that sounds like you have a syntax error in that security.yaml file. Please, double-check the syntax, in particular, add a space between logout: and true. Also, make sure the indentation in that file is consistent.

If you still cannot solve this error - please, share your security.yaml's content with us in a comment and I'll take a look for you.

Cheers!

Reply
Tomas norre M. Avatar
Tomas norre M. Avatar Tomas norre M. | posted 10 months ago

I find it a little ironic that the course on Symfony Security is suggesting to tell potential hackers, if the username/email exists or not.
By knowing that emails exists but entered a wrong password, you're half way there to hacking the site, this eases the attack a lot.

Invalid credentials message is in my opinion always a better option.

Any takes on this?

Reply

Hey Tomas norre M.!

Fair point to bring up :).

> By knowing that emails exists but entered a wrong password, you're half way there to hacking the site, this eases the attack a lot.

If you know for a fact that an email exists on the site, you've still got quite a lot of work (passwords to try) in order to actually gain access. If you are using a list of "pwned" passwords, then if an account is using that same password on your site, it will help the hacker a bit to know that the account exists... but probably not that much. It would only help if an email had 5 pwned passwords... and then they could have the confidence to try the next 4 or skip the next 4 after trying the first.

> Invalid credentials message is in my opinion always a better option.

I'm not sure. It's easy to say "yes" to this, but I think what you're mostly protecting by always using "invalid credentials" is whether or not someone has an account on your site... if that fact is sensitive. As I mentioned in the video, knowing someone has an account on Symfonycasts probably won't cause any scandal. However, if you run a gambling site or (use your imagination) something much more sensitive, then being able to discover who has accounts could be disastrous.

And, of course, the "email not found" message is a better user experience. That shouldn't trump security, but I'm not convinced there's a real security problem here for most sites. And, a lot of big sites seem to agree. If you try to, for example, log into facebook with a correct email but incorrect password, it DOES tell you that the account exists (and that your password is wrong).

Cheers!

Reply
Tomas norre M. Avatar
Tomas norre M. Avatar Tomas norre M. | weaverryan | posted 10 months ago

Thanks for taking your time to answer. I follow you, no doubt about that.
And I really enjoy watching your courses. I'm 20+ years into PHP Development, and still learn from the videos.
Using them as a brush-up course.

Reply

Thank you for the kind words! ❤️❤️❤️

Reply
Oliver W. Avatar
Oliver W. Avatar Oliver W. | posted 10 months ago

I am very, very glad to see that you will handle the Scheb 2fa!!!!! Can't wait to see this one.

Keep on with your excellent work!

Reply

Hey Oliver,

Thank you for your feedback! Yeah, 2FA is something that was asked a lot lately! And I'm happy to see it's covered in this course :)

Cheers!

Reply
Zaharedine R. Avatar
Zaharedine R. Avatar Zaharedine R. | posted 1 year ago

hi when this course will be updated ? I want to continue this symfony track !

Reply

Hey @zarloon,

What do you mean updated? This course is in active release state, so it is releasing now and it should be 1 chapter per work day.

Cheers!

Reply
Zaharedine R. Avatar
Zaharedine R. Avatar Zaharedine R. | sadikoff | posted 1 year ago

Oh yes sorry for my english. I wasn't aware about that :D
Just have to wait for the next chapter then. Really good job i'm glad to have suscribe.

Reply

I'm happy to hear that! Thanks that you are staying with us!

Stay safe and keep learning!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}