Logging out & Pre-filling the Email on Failure

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

Check this out: let's fail authentication with a bad password.

Ok: I noticed two things. First, we have an error:

Invalid credentials.

Great! But second, the form is not pre-filled with the email address I just used. Hmm.

Behind the scenes, the authenticator communicates to your SecurityController by storing things in the session. That's what the security.authentication_utils helps us with:

... lines 1 - 8
class SecurityController extends Controller
{
... lines 11 - 13
public function loginAction()
{
$authenticationUtils = $this->get('security.authentication_utils');
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
... lines 20 - 34
}
}

Hold command and open getLastAuthenticationError(). Ultimately, this reads a Security::AUTHENTICATION_ERROR string key from the session.

And the same is true for fetching the last username, or email in our case: it reads a key from the session.

Here's the deal: the login form is automatically setting the authentication error to the session for us. But, it is not setting the last username on the session... because it doesn't really know where to look for it.

No worries, fix this with $request->getSession()->set() and pass it the constant - Security::LAST_USERNAME - and $data['_username']:

... lines 1 - 9
use Symfony\Component\Security\Core\Security;
... lines 11 - 14
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 17 - 27
public function getCredentials(Request $request)
{
... lines 30 - 38
$data = $form->getData();
$request->getSession()->set(
Security::LAST_USERNAME,
$data['_username']
);
... lines 44 - 45
}
... lines 47 - 75
}

Now, try it again. Good-to-go!

Can I Logout?

Next challenge! Can we logout? Um... right now? Nope! But that seems important! So, let's do it.

Start like normal: In SecurityController, create a logoutAction, set its route to /logout and call the route security_logout:

... lines 1 - 8
class SecurityController extends Controller
{
... lines 11 - 36
/**
* @Route("/logout", name="security_logout")
*/
public function logoutAction()
{
... line 42
}
}

Now, here's the fun part. Don't put any code in the method. In fact, throw a new \Exception that says, "this should not be reached":

... lines 1 - 8
class SecurityController extends Controller
{
... lines 11 - 39
public function logoutAction()
{
throw new \Exception('this should not be reached!');
}
}

Adding the logout Key

Whaaaat? Yep, our controller will do nothing. Instead, Symfony will intercept any requests to /logout and take care of everything for us. To activate it, open security.yml and add a new key under your firewall: logout. Below that, add path: /logout:

... lines 1 - 2
security:
... lines 4 - 9
firewalls:
... lines 11 - 15
main:
... lines 17 - 21
logout:
path: /logout
... lines 24 - 31

Now, if the user goes to /logout, Symfony will automatically take care of logging them out. That's super magical, almost creepy - but it works pretty darn well.

So, why did I make you create a route and controller if Symfony wasn't going to use it? Am I trying to drive you crazy!

Come on, I'm looking out for you! It turns out, if you don't have a route that matches /logout, then the 404 page is triggered before Symfony has a chance to log the user out. That's why you need this.

It should work already, but let's add a friendly logout link. In base.html.twig, how can we figure out if the user is logged in? We're about to talk about that... but what the heck - let's get a preview. Use {% if is_granted('ROLE_USER') %}:

<!DOCTYPE html>
<html>
... lines 3 - 13
<body>
... lines 15 - 19
<header class="header">
... lines 21 - 22
<ul class="navi">
... line 24
{% if is_granted('ROLE_USER') %}
... lines 26 - 27
<li><a href="{{ path('security_login') }}">Login</a></li>
{% endif %}
</ul>
</header>
... lines 32 - 50
</body>
</html>

Remember this role? We returned it from getRoles() in User - so all authenticated users have this.

If they don't have this, show the login link. But if they do, show the logout link: path('security_logout'):

<!DOCTYPE html>
<html>
... lines 3 - 13
<body>
... lines 15 - 19
<header class="header">
... lines 21 - 22
<ul class="navi">
... line 24
{% if is_granted('ROLE_USER') %}
<li><a href="{{ path('security_logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ path('security_login') }}">Login</a></li>
{% endif %}
</ul>
</header>
... lines 32 - 50
</body>
</html>

Perfect!

Try the whole thing out: head to the homepage. We're anonymous right now.. so let's login! Cool! And there's the logout link. Click it! Ok, back to anonymous. If you need to control what happens after logging out, check the official docs on the logout stuff.

Alright. Now, as much as I like turtles, we should probably give our users a real password.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}