Login Form Authenticator

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

Now that we've added our authenticator under the authenticators key:

security:
... lines 2 - 8
firewalls:
... lines 10 - 12
main:
... lines 14 - 15
guard:
authenticators:
- App\Security\LoginFormAuthenticator
... lines 19 - 33

Symfony calls its supports() method at the beginning of every request, which is why we see this little die statement:

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
public function supports(Request $request)
{
die('Our authenticator is alive!');
}
... lines 17 - 41
}

These authenticator classes are really cool because each method controls just one small part of the authentication process.

The supports() Method

The first method - supports() - is called on every request. Our job is simple: to return true if this request contains authentication info that this authenticator knows how to process. And if not, to return false.

In this case, when we submit the login form, it POSTs to /login. So, our authenticator should only try to authenticate the user in that exact situation. Return $request->attributes->get('_route') === 'app_login':

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
public function supports(Request $request)
{
// do your work when we're POSTing to the login page
return $request->attributes->get('_route') === 'app_login'
... line 17
}
... lines 19 - 43
}

Let me... explain this. If you look in SecurityController, the name of our login route is app_login:

... lines 1 - 8
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="app_login")
*/
public function login(AuthenticationUtils $authenticationUtils)
{
... lines 16 - 25
}
}

And, though you don't need to do it very often, if you want to find out the name of the currently-matched route, you can do that by reading this special _route key from the request attributes. In other words, this is checking to see if the URL is /login. We also only want our authenticator to try to login the user if this is a POST request. So, add && $request->isMethod('POST'):

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
public function supports(Request $request)
{
// do your work when we're POSTing to the login page
return $request->attributes->get('_route') === 'app_login'
&& $request->isMethod('POST');
}
... lines 19 - 43
}

Here's how this works: if we return false from supports(), nothing else happens. Symfony doesn't call any other methods on our authenticator, and the request continues on like normal to our controller, like nothing happened. It's not an authentication failure - it's just that nothing happens at all.

If we return true from supports(), well, that's when the fun starts. If we return true, Symfony will immediately call getCredentials():

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 19
public function getCredentials(Request $request)
{
... line 22
}
... lines 24 - 43
}

To see if things are working, let's just dump($request->request->all()), then die():

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 19
public function getCredentials(Request $request)
{
dump($request->request->all());die;
}
... lines 24 - 43
}

I know, that looks funny. Unrelated to security, if you want to read POST data off of the request, you use the $request->request property.

Anyways, let's try it! Go back to your browser and hit enter on the URL so that it makes a GET request to /login. Hello login page! Our supports() method just returned false. And so, the request continued anonymously, like normal.

Log in with one of our dummy users: spacebar1@example.com. The password doesn't matter. And... enter! Yes! This time, because this is a POST request to /login, supports() returns true! So, Symfony calls getCredentials() and our dump fires! As expected, we can see the email and password POST parameters, because the login form uses these names:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
... lines 13 - 18
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
... line 20
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
... lines 22 - 29
</form>
{% endblock %}

The Brand-New dd() Function

Oh, and I want to show you a quick new Easter egg in Symfony 4.1, unrelated to security. Instead of dump() and die, use dd() and then remove the die:

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 19
public function getCredentials(Request $request)
{
dd($request->request->all());
}
... lines 24 - 43
}

Refresh! Same result. This is just a nice, silly shortcut: dd() is dump() and die. We'll use it... because... why not?

The getCredentials() Method

Back to work! Our job in getCredentials() is simple: to read our authentication credentials off of the request and return them. In this case, we'll return the email and password. But, if this were an API token authenticator, we would return that token. We'll see that later.

Return an array with an email key set to $request->request->get('email') and password set to $request->request->get('password'):

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 19
public function getCredentials(Request $request)
{
return [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
];
}
... lines 27 - 46
}

I'm just inventing these email and password keys for the new array: we can really return whatever we want from this method. Because, after we return from getCredentials(), Symfony will immediately call getUser() and pass this array back to us as the first $credentials argument:

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 27
public function getUser($credentials, UserProviderInterface $userProvider)
{
... line 30
}
... lines 32 - 46
}

Let's see that in action: dd($credentials):

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 27
public function getUser($credentials, UserProviderInterface $userProvider)
{
dd($credentials);
}
... lines 32 - 46
}

Move back to your browser and, refresh! Coincidentally, it dumps the exact same thing as before. But, this time, it's coming from line 30 - our line in getUser().

The getUser() Method

Let's keep going! Our job in getUser() is to use these $credentials to return a User object, or null if the user isn't found. Because we're storing our users in the database, we need to query for the user via their email. And to do that, we need the UserRepository that was generated with our entity.

At the top of the class, add public function __construct() with a UserRepository $userRepository argument:

... lines 1 - 4
use App\Repository\UserRepository;
... lines 6 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 15
public function __construct(UserRepository $userRepository)
{
... line 18
}
... lines 20 - 54
}

I'll hit Alt+Enter and select "Initialize Fields" to add that property and set it:

... lines 1 - 4
use App\Repository\UserRepository;
... lines 6 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
... lines 20 - 54
}

Back down in getUser(), just return $this->userRepository->findOneBy() to query by email, set to $credentials['email']:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 35
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $this->userRepository->findOneBy(['email' => $credentials['email']]);
}
... lines 40 - 54
}

This will return our User object, or null. The cool thing is that if this returns null, the whole authentication process will stop, and the user will see an error. But if we return a User object, then Symfony immediately calls checkCredentials(), and passes it the same $credentials and the User object we just returned:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 40
public function checkCredentials($credentials, UserInterface $user)
{
... line 43
}
... lines 45 - 54
}

Inside, dd($user) so we can see if things are working:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 40
public function checkCredentials($credentials, UserInterface $user)
{
dd($user);
}
... lines 45 - 54
}

Refresh and... got it! That's our User object!

The checkCredentials() Method

Ok, final step: checkCredentials(). This is your opportunity to check to see if the user's password is correct, or any other last, security checks. Right now... well... we don't have a password, so, let's return true:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 40
public function checkCredentials($credentials, UserInterface $user)
{
// only needed if we need to check a password - we'll do that later!
return true;
}
... lines 46 - 55
}

And actually, in many systems, simply returning true is perfect! For example, if you have an API token system, there's no password.

If you did return false, authentication would fail and the user would see an "Invalid Credentials" message. We'll see that soon.

But, when you return true... authentication is successful! Woo! To figure out what to do, now that the user is authenticated, Symfony calls onAuthenticationSuccess():

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 46
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
... line 49
}
... lines 51 - 55
}

Put a dd() here that says "Success":

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 46
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
dd('success!');
}
... lines 51 - 55
}

Move over and... refresh the POST! Yes! We hit it! At this point, we have fully filled in all the authentication logic. We used supports() to tell Symfony whether or not our authenticator should be used in this request, fetched credentials off of the request, used those to find the user, and returned true in checkCredentials() because we don't have a password.

Next, let's fill in these last two methods and finally see - for real - that our user is logged in. We'll also learn a bit more about what happens when authentication fails and how the error message is rendered.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "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.2.7
        "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
    }
}