Buy
Buy

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

Login Subscribe

Our login form is working perfectly. But... there's one tiny annoying detail that we need to talk about: the fact that every form on your site that performs an action - like saving something or logging you in - needs to be protected by a CSRF token. When you use Symfony's form system, CSRF protection is built in. But because we're not using it here, we need to add it manually. Fortunately, it's no big deal!

Adding the CSRF Input Field

Step one: we need to add an <input type="hidden"> field to our form. For the name... this could be anything, how about _csrf_token. For the value, use a special csrf_token() Twig function and pass it the string authenticate:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
... lines 13 - 22
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>
... lines 26 - 34
</form>
{% endblock %}

What's that? It's sort of a "name" that's used when creating this token, and it could be anything. We'll use that same name in a minute when we check to make sure the submitted token is valid.

Verifying the CSRF Token

In fact, what a great idea! Let's do that now! Step 2 happens inside of LoginFormAuthenticator. Start in getCredentials(): in addition to the email and password, let's also return a csrf_token key set to $request->request->get('_csrf_token'):

... lines 1 - 17
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 20 - 37
public function getCredentials(Request $request)
{
$credentials = [
... lines 41 - 42
'csrf_token' => $request->request->get('_csrf_token'),
];
... lines 45 - 51
}
... lines 53 - 78
}

Next, in getUser(), this is where we'll check the CSRF token. We could do it down in checkCredentials(), but I'd rather make sure it's valid before we query for the user.

So... how do we check if a CSRF token is valid? Well... like pretty much everything in Symfony, it's done with a service. Without even reading the documentation, we can probably find the service we need by running:

php bin/console debug:autowiring

And searching for CSRF. Yea! There are a few: a CSRF token manager, a token generator and some sort of token storage. The second two are a bit lower-level: the CsrfTokenManagerInterface is what we want.

To get this, go back to your constructor and add a third argument: CsrfTokenManagerInterface. I'll re-type the "e" and hit tab to auto-complete that so that PhpStorm politely adds the use statement on top of the file:

... lines 1 - 14
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
... lines 16 - 17
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 20 - 23
public function __construct(UserRepository $userRepository, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager)
{
... lines 26 - 28
}
... lines 30 - 78
}

Call the argument $csrfTokenManager and hit Alt+Enter to initialize that field:

... lines 1 - 14
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
... lines 16 - 17
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 20 - 21
private $csrfTokenManager;
public function __construct(UserRepository $userRepository, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager)
{
... lines 26 - 27
$this->csrfTokenManager = $csrfTokenManager;
}
... lines 30 - 78
}

Perfect! To see how this interface works, hold Command or Ctrl and click into it. Ok: we have getToken(), refreshToken(), removeToken() and... yes: isTokenValid()! Apparently we need to pass this a CsrfToken object, which itself needs two arguments: id and value. The id is referring to that string - authenticate - or whatever string you used when you originally generated the token:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
... lines 13 - 22
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>
... lines 26 - 34
</form>
{% endblock %}

The value is the CSRF token value that the user submitted.

Let's close all of this. Go back to LoginFormAuthenticator and find getUser(). First, add $token = new CsrfToken() and pass this authenticate and then the submitted token: $credentials['csrf_token']:

... lines 1 - 13
use Symfony\Component\Security\Csrf\CsrfToken;
... lines 15 - 17
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 20 - 53
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
... lines 57 - 61
}
... lines 63 - 78
}

Because that's the key we used in getCredentials():

... lines 1 - 17
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 20 - 37
public function getCredentials(Request $request)
{
$credentials = [
... lines 41 - 42
'csrf_token' => $request->request->get('_csrf_token'),
];
... lines 45 - 51
}
... lines 53 - 78
}

Then, if not $this->csrfTokenManager->isTokenValid($token), throw a special new InvalidCsrfTokenException():

... lines 1 - 9
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
... lines 11 - 17
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 20 - 53
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
... lines 60 - 61
}
... lines 63 - 78
}

That's it! Let's first try logging in successfully. Refresh the login form to get the new hidden input. Use spacebar1@example.com, any password and... success!

Now, go back. Let's be shifty and mess with stuff. Inspect element on the form, find the token field, change it and queue your evil laugh. Mwahahaha. Log in! Ha! Yes! Invalid CSRF token! We rock!

Next: let's add a really convenient feature for users: a remember me checkbox!

Leave a comment!

  • 2018-11-26 Diego Aguiar

    Hey @Matt

    That's a great question and actually, you made do some research. What I understood from this answer is that you prevent your site from phishing attacks and other vulnerabilities: https://stackoverflow.com/a...

    Cheers!

  • 2018-11-26 Matt

    I wonder what is the purpose of csrf protection in login form?

  • 2018-11-01 weaverryan

    Hey docLommy!

    Cool question :). The way this is done inside Symfony isn't so different. Here's how it works:

    1) A unique "token id" is generated for each form (based on the form class usually). But you never see this value directly.
    2) A random string (using random_bytes - the class that does this is called UriSafeTokenGenerator) is generated. This will become the CSRF token value.
    3) The token string is stored in the session using the "token id" from step (1). On submit, Symfony looks up the token string in the session via this token id to make sure it's valid.

    So, the token IS invalid outside of a session context. If I somehow "stole" your CSRF token, I would not be able to use it - as my session would not have that stored inside of it. Or, if I created a form on my site that submitted back to your Symfony app, I would not be able to "predict" your CSRF token, because it's randomly generated and stored in YOUR session.

    Let me know if that makes sense!

    Cheers!

  • 2018-10-31 docLommy

    It seems the default csrf tokens of symfony forms are constant. So how can they actually protect from csrf ?

    I'm asking because I came across a non-symfony barebone example that uses a hashed session id as token source. That makes more sense to me, because the token becomes invalid when the form is being sent outside the session context.

  • 2018-10-18 Victor Bocharsky

    Hey Serge,

    Thank you for sharing it with others! Agree, good example.

    Cheers!

  • 2018-10-18 Serge Boyko

    If someone is curious what exactly is CSRF, I really liked this explanation: https://stackoverflow.com/a...