CSRF Protection

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

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!

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
    }
}