Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Customizing Errors & 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

If we enter an email that doesn't exist, we get this

Username could not be found

error message. And, as we saw a moment ago, if we return false from checkCredentials(), the error is something about "Invalid credentials".

The point is, depending on where authentication fails, the user will see one of these two messages.

The question now is, what if we want to customize those? Because, username could not be found? Really? In an app that doesn't use usernames!? That's... confusing.

Customizing Error Messages

There are two ways to control these error messages. The first is by throwing a very special exception class from anywhere in your authenticator. It's called CustomUserMessageAuthenticationException. When you do this, you can create your own message. We'll do this later when we build an API authenticator.

The second way is to translate this message. No, this isn't a tutorial about translations. But, if you look at your login template, when we print this error.messageKey thing, we are already running it through Symfony's translation filter:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
... lines 16 - 29
</form>
{% endblock %}

Another way to look at this is on the web debug toolbar. See this little translation icon? Click that! Cool: you can see all the information about translations that are being processed on this page. Not surprisingly - since we're not trying to translate anything - there's only one: "Username could not be found."... which... is being translated into... um... "Username could not be found."

Internally, Symfony ships with translation files that will translate these authentication error messages into most other languages. For example, if we were using the es locale, we would see this message in Spanish.

Ok, so, why the heck do we care about all of this? Because, the errors are passed through the translator, we can translate the English into... different English!

Check this out: in your translations/ directory, create a security.en.yaml file:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
... lines 16 - 29
</form>
{% endblock %}

This file is called security because of this security key in the translator. This is called the translation "domain" - it's kind of a translation category - a way to organize things.

Anyways, inside the file, copy the message id, paste that inside quotes, and assign it to our newer, hipper message:

Oh no! It doesn't look like that email exists!

"Username could not be found.": "Oh no! It doesn't look like that email exists!"

That's it! If you go back to your browser and head over to the login page, in theory, if you try failing login now, this should work instantly. But... no! Same message. Today is not our lucky day.

This is thanks to a small, um, bug in Symfony. Yes, yes, they do happen sometimes, and this bug only affects our development... slightly. Here's the deal: whenever you create a new translation file, Symfony won't see that file until you manually clear the cache. In your terminal, run:

php bin/console cache:clear

When that finishes, go back and try it again: login with a bad email and... awesome!

Logging Out

Hey! Our login authentication system is... done! And... not that I want to rush our moment of victory - we did it! - but now that our friendly alien users can log in... they'll probably need a way to log out. They're just never satisfied...

Right now, I'm still logged in as spacebar1@example.com. Let's close a few files. Then, open SecurityController. Step 1 to creating a logout system is to create the route. Add public function logout():

... lines 1 - 8
class SecurityController extends AbstractController
{
... lines 11 - 30
public function logout()
{
... line 33
}
}

Above this, use the normal @Route("/logout") with the name app_logout:

... lines 1 - 5
use Symfony\Component\Routing\Annotation\Route;
... lines 7 - 8
class SecurityController extends AbstractController
{
... lines 11 - 27
/**
* @Route("/logout", name="app_logout")
*/
public function logout()
{
... line 33
}
}

And this is where things get interesting... We do need to create this route... but we don't need to write any logic to log out the user. In fact, I'm feeling so sure that I'm going to throw a new Exception():

will be intercepted before getting here

... lines 1 - 8
class SecurityController extends AbstractController
{
... lines 11 - 27
/**
* @Route("/logout", name="app_logout")
*/
public function logout()
{
throw new \Exception('Will be intercepted before getting here');
}
}

Remember how "authenticators" run automatically at the beginning of every request, before the controllers? The logout process works the same way. All we need to do is tell Symfony what URL we want to use for logging out.

In security.yaml, under your firewall, add a new key: logout and, below that, path set to our logout route. So, for us, it's app_logout:

security:
... lines 2 - 8
firewalls:
... lines 10 - 12
main:
... lines 14 - 19
logout:
path: app_logout
... lines 22 - 36

That's it! Now, whenever a user goes to the app_logout route, at the beginning of that request, Symfony will automatically log the user out and then redirect them... all before the controller is ever executed.

So... let's try it! Change the URL to /logout and... yes! The web debug toolbar reports that we are once again floating around the site anonymously.

By the way, there are a few other things that you can customize under the logout section, like where to redirect. You can find those options in the Symfony reference section.

But now, we need to talk about CSRF protection. We'll also add remember me functionality to our login form with almost no effort.

Leave a comment!

42
Login or Register to join the conversation
Angelika R. Avatar
Angelika R. Avatar Angelika R. | posted 2 years ago

In case of a brute-force attack giving the hacker the info that an e-mail is not present in the database is facilitating it for him 1000 times. I think usually it would be better to write something about wrong e-mail OR password.

2 Reply

Hey Angelika,

Yes, it's kind of controversial question... it's like a balance between good UX and more robust security. Giving a hint to your users about a misprint in their email may helps. But I see what you mean. But probably this kind of security problem should be fixed in a different way? :)

For example, you can use services like CloudFlare with custom rules and block users who is trying to sing in too often, like allowing only 5 failed attempts and the block his IP for some time, etc. Or you can easily write your own implementation to this kind of check. If have this security improvement in mind you still can consider a better UX. Anyway, this tutorial is not about protection from brute force, we just want to show our users what power they have with Symfony security.

Cheers!

Reply
Jeffrey C. Avatar
Jeffrey C. Avatar Jeffrey C. | posted 4 years ago

Hey!

for those who don't see changes after cache:clear try rm -rf var/cache/* in the directory you are working from. Apperently sometimes symfony doesn't delete the cache, so you have to manually remove it in order to make it work :)

Cheers!

2 Reply

Hey Jeffrey C.

Thanks for sharing it, that's a useful workaround when the "clear cache" command doesn't work properly

Cheers!

1 Reply

The AUTHENTICATION_ERROR will not be saved in the session, so I have to set it manually in onAuthenticationFailure method:


$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);

Any idea why?

1 Reply

Hey ahmadmayahi

Do you have sessions enabled in your project? if so, it should be done automatically

Cheers!

Reply
Cheshire Avatar
Cheshire Avatar Cheshire | posted 1 year ago

Minor question..

> Another way to look at this is on the web debug toolbar. See this little translation icon? Click that! Cool: you can see all the information about translations that are being processed on this page. Not surprisingly - since we're not trying to translate anything - there's only one: "Username could not be found."... which... is being translated into... um... "Username could not be found."

My web debug toolbar displays something different - I see the same message in the profiler, but it says missing translation instead. The video states that Symfony ships with some translations already, but I've been following along with the tutorial (using the code download) and my /translations directory had nothing inside it except a .gitignore file. Any clue why the discrepancy?

(Not a big deal - I was able to keep following along, made security.en.yaml and it used that translation)

Reply

Hey Cheshire Y.

In a fresh Symfony app that folder comes empty, but in this chapter, Ryan created the security.en.yaml file in order to slightly modify the internal security messages of Symfony

Cheers!

Reply
Cameron Avatar
Cameron Avatar Cameron | posted 2 years ago | edited

I tried the security.yaml configuration for logout (via a front-end Axios call) and I can see it does the 302 redirect - but the cookie remains (which isn't good).

If I try to build my own symfony controller with this:


        // clear the token, cancel session and redirect
        $this->tokenStorage->setToken(null);

        $session = $request->getSession();

        $session->clear();
        $session->invalidate();

        return $this->ra->assembleJsonResponse(null, FrontendUrl::HOME);

it still doesn't remove the cookie/s! (it even tells me I'm already logged in when I try to log in again - via the loginformAuthenticator)

Although if I use the profiler link to logout, it works fine! it's very strange.

Am I doing something wrong, like not configuring the response to delete the cookies? (am I supposed to do this? see below:)


$response = new Response();
    $response->headers->removeCookie('_inviter_id', '/', null);
    $response->send();

    return $this->render('security/register.html.twig');

Edit - solved:
it requires send "withCredentials" in the axios request

Reply

Hey Cameron,

I see your solved this problem yourself, good job! Thanks for sharing your solution with others!

Cheers!

Reply

Hey, I'm using Symfony <b>5.2</b>, and I've added a custom message in the <b>security.en.yaml</b> file like so "Invalid credentials.": "The password you entered was invalid!". However after clearing the cache it still shows me the default error message <i>Invalid credentials</i> instead of the custom one when I'm using error.messageKey|trans(error.messageData, 'security') in my Twig template. When I rename the file to <b>messages.en.yaml</b> and rename the second argument to <i>messages</i> it shows the correct message. What could possible cause this issue?

Reply

Hey Senet
That's a bit odd. Perhaps you had to clear the cache manually, e.g. rm var/cache/* or, the message's key in your translation file wasn't correct. If you rename your file back to security.en.yaml and clear the cache (manually), does it work?

Cheers!

1 Reply

Hey MolloKhan

Thanks, that seemed to do the trick! Still a bit weird that this works and cache:clear didnt.

Reply

Yea... That's a bug on Symfony, it only happens the first time you add a new translation file to your project. I don't know why after all these years it's not been fixed - perhaps it's too difficult and doesn't worth the effort :)

Reply
Andrei M. Avatar
Andrei M. Avatar Andrei M. | posted 2 years ago

when I try the /logout endpoint, I get the error: No route found for "GET /logout"

My logout method is inside LoginFormAuthenticator and looks like this:
/**
* @Route("/logout", name="app_logout")
*/
public function logout()
{
throw new \Exception('will be intercepeted');
}

My security.yaml config looks like this:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true

guard:
authenticators:
- App\Security\LoginFormAuthenticator

logout:
path: app_logout

I've tried clearing the cache but that didn't fix the problem either.

Reply

Hey Andrei M.

You created the logout route in a wrong spot. It should be inside a Controller or declared inside the config/routes.yaml file

Cheers!

Reply
Andrei M. Avatar

oops...my bad. Thanks! :D

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago

I did not follow these courses because they are archived:
Symfony 4 > Stellar Development with Symfony 4
Symfony 4 > Symfony 4 Fundamentals: Services, Config & Environments
Symfony 4 > Doctrine & the Database
Symfony 4 > Mastering Doctrine Relations! (This one is not archived, but there is Symfony 5 > Go Pro with Doctrine Queries)

Instead I followed:
Symfony 5 > Charming Development in Symfony
Symfony 5 > Symfony 5 Fundamentals: Services, Config & Environments
Symfony 5 > Doctrine, Symfony & the Database
Symfony 5 > Go Pro with Doctrine Queries

Therefore I am missing some things like: BaseFixtures.php and the whole translations directory (so far).
I am wondering what else am I missing by skipping the archived courses? And do you think the things I am missing are minor things? So I can continue with this course without major hiccups? Is there an updated version coming of this course?

Reply

Hey @Farry7!

Sorry for the confusion! You were correct to follow the Symfony 5 courses. However, since there isn't a Symfony 5 security course yet (I was waiting for Symfony 5.2, which has now been released - so there IS an updated version coming), yes, you need to follow the Symfony 4 version (there are not major security differences between 4 and 5). If you want to code along with this tutorial, you should use the starting code from this tutorial, instead of trying to use the code from the Symfony 5 series - as you saw, they're different projects, so you can't easily just continue the Symfony 5 project using the Symfony 4 tutorial.

> I am wondering what else am I missing by skipping the archived courses? And do you think the things I am missing are minor things?

It's not that you're missing big concepts by skipping the archived courses, just that they contain code (e.g. this BaseFixtures thing, which is ultimately not very important from a learning perspective) that you need in your project if you want to code along with this app.

> So I can continue with this course without major hiccups?

Yes, you definitely can :). And as you've been doing, if something seems confusing to you (like where did this BaseFixtures class come from), just ask and we're happy to help.

Cheers!

1 Reply
Oliver-W Avatar
Oliver-W Avatar Oliver-W | posted 2 years ago

Hi,

what if the cache:clear tells me, that there are "unexpected characters near ":"text"" at line 1" in my security.en.yaml? There is not a single special character in the whole file!? symfony/translation is 4.3.11.

Thx

Reply
Oliver-W Avatar

apparently the syntax has been changed:
Username could not be found.: On no! It doesnt look like that email exists!
is working.
But what if I need a : in my string? How do I escape it?

Reply

Hey Oliver W.

The : character should be inside the single (or double) quotes. Can I see how you are doing it?

Cheers!

Reply
Oliver-W Avatar
Oliver-W Avatar Oliver-W | MolloKhan | posted 2 years ago | edited

MolloKhan
I found my error. I did NOT put a blank after the :. Now it even works with double quotes.
Its always the same: fu.... details ;-)

1 Reply

LOL - Welcome to programming where anything can go wrong for many different reasons :D

Reply
Nicky S. Avatar
Nicky S. Avatar Nicky S. | posted 3 years ago

Hi,
I don't know if I am following the most recent judging by the age of the comments. But I was wanting to ask. I have a registration process that requires users to authenticate their email. This email is sent fine. It updates a status in the DB to be awaiting activation. Prior to following this tutorial status was checked and login denied even if credentials where correct. This process allows me to block users if accounts are at risk etc.

Is it possible to incorporate this process into the login authenticator so that if a user exists and they typed the credentials in correctly they can still be rejected based on the status of their account.

Thank you and thanks for all the work on these tutorials as well.

Reply

Hey Nicky,

Yes, sure it's possible! Symfony Guard system is very flex for such kind of things. I think checkCredentials() is the perfect place for doing this check. So, you can check that credentials are valid, and then add one more check to make sure that user account is activated. If not - you may want to throw a specific exception with a custom method - CustomUserMessageAuthenticationException is what you need, see https://symfony.com/doc/cur...

Cheers!

Reply
Default user avatar

Hi,

I was wondering what method would you recommend for add a flash message to the user logging out, such as "You logged out!".

Making a class that implements LogoutSuccessHandlerInterface (success_handler key in security.yaml/logout:), adds the flash and redirect into onLogoutSuccess() method seems to me to be clean but the problem is that each time you disconnect the session is completely empty.

Result, flash is deleted even before you can display it.

I found some solutions but old or bad practices. I think there is a better method.

Thank you for your work !

Reply

Hey John,

Tricky question :) Hm, probably the easiest way is to redirect user to a specific page after he's logged out, e.g. when user logout - redirect him to the "/logged-out". Then, create a controller for this URL, add a flash message there, and do one more redirect :) So it is like this workflow:

1. User click Logout

2. System sends him to "/logout" URL that is handled by framework and do actually log out the user, clearing the session.

3. In your configuration, system sees that it should redirect the logged out user to a specific page, i.e. "/logged-out" in our case

4. User is redirected to the "/logged-out" page, and user has already a new *cleared* session.

5. You set a new flash message to the new user session and redirect him one more time to whatever page you want

6. And finally the user see your flash message

IMO it's the best way in case you really want the real flash message to be displayed. Otherwise, you can just try to write/read some flags from/to the cookies and display some kind of flash message. Or just simply redirect users to a specific page that has that "You're logged out" message hardcoded in the HTML. To avoid hitting this URL accidentally by users - you can do some extra checks on HTTP referer of the request, and if the request was not came from your website, or from the specific page - just redirect to the homepage and do not show this special page with the message to users.

I hope this helps!

Cheers!

Reply

Hey Nicolas,

Hm, check your indentation probably. Anyway, it should be "security.firewalls.main.logout", not just "security.firewalls.logout". So, maybe you missed the level or confuse your indentation. Both good to check

Cheers!

Reply

the video has an error and it is not finished seeing

Reply

Hey YunierHdz,

We're sorry about that. What exactly error do you mean? I just was able to watch this video up to the end and it works for me. Please, could you refresh the page and try again? Do you still see this issue?

Cheers!

Reply
Dominik Avatar
Dominik Avatar Dominik | posted 4 years ago

Is it possible to change locale language automatically depends of user language detected in cookies?

Reply

Hey Usuri,

Yes, it's possible. You just need to register a listener. Actually, you can take a look at Symfony Demo: https://github.com/symfony/... - it has something similar to what you're looking for.

Cheers!

1 Reply
Alex F. Avatar
Alex F. Avatar Alex F. | posted 4 years ago

I may have fudged a little bit when setting up the initial dependencies, but I had to require the translation component, "composer require symfony/translation", in order to see the translation tab in the Web Profiler and use the translation service...just FYI in case someone else doesn't see the translation tab when you get to that step.

Reply

Hello Ryan,
How to disable translate in input value

Reply

Hey Ahmed,

If you do not want to translate field labels, you can set translation_domain to false, see: https://symfony.com/doc/cur...

Cheers!

Reply

Hello Victor Bocharsky,
My problem is when i set locale: 'ar'
and go to my product edit form i found the value of the input like this ١٢٠ instead of 120 and the value doesn't appear and when submit it's not validated

Reply

Hey Ahmed,

Oh, wait, but Symfony Form Component does not translate the input unless you do it by yourself, i.e. manually user |trans Twig filter for the input value. It translates only form field labels. Or do you use Google Translator to translate the whole page? Could you show me this field HTML layout?

Cheers!

Reply

Hey Ryan,
I try to change the default_locale in framework.yaml config file for translate message in french but it is not working. I don't understand why. I have php-intl extension installed.

Reply

Hey Stephane,

Did you enable translator in your project? If translator is enabled, then @niumis advice below may help, try to change locale value in "config/services.yaml"

Cheers!

Reply

Hello, Stephane,
Change in services.yaml 'locale' parameter ;)

Reply

Thank for your advice. It's working perfectly now.

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}
userVoice