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!

  • 2019-09-10 Victor Bocharsky

    Hey Mike,

    Thank you for the reply and your shared research! It might be helpful for others.

    IIRC this is a new feature in Symfony 4.2, so probably it's not a big deal to not use it for now, i.e. I think it's OK to comment it out temporarily. Though, default recommendations are to use this, and that's it's in the official recipe I think.

    About the 2nd: "handler_id: ~" and "handler_id: null" are the same actually, but Symfony uses "null" lately, I think mostly for making it more clear because "~" might be not clear enough, so no matter what you will keep: "null" or "~" - both are the same.

    So, yeah, it depends on what you expect. I agree, it sounds totally not good if some users can't login because of their browsers. And fairly speaking, even after it will be fixed in Safari 13 as you said, it won't mean that all your users upgrade to it. I bet some users might be still on old version :/ I'd recommend you to search for some issues inside Symfony repository, maybe it's a well known problem and you can find some good tips there as well.

    Cheers!

  • 2019-09-09 Mike

    Very interesting!
    Thanks to your questions, I found out that my app is working fine in Firefox & Chrome but the error "Invalid CSRF token" does only persist, if I try to login inside Safari 12.1.2 on the latest macOS (10.14.6).
    Safari seems to behave differently than the other two browsers.

    It always throws me this message in dev env when "cookie_samesite: lax" is activated.
    Login does work if I comment out this line.
    Ive tried it in Safari private mode as well, I always get this error.

    I checked your linked configuration file, the only difference I have is "handler_id: ~" instead of "handler_id: null".
    But even when I change this value, Login is still impossible inside Safari.

    Any hints/ideas how to track down the error further or how to fix it?

    UPDATE://
    It seems that iam not the only one with this problem:
    https://stackoverflow.com/q...
    But no fix found yet...

    UPDATE2://
    It seems that its a Safari bug.
    "Warning: At the time of writing, the network library on iOS and Mac incorrectly handles unknown SameSite values and will treat any unknown value (including None) as if it was SameSite=Strict, which affects Safari on Mac and browsers wrapping WebKit on iOS (Safari, Chrome, Firefox, and others). This should be fixed in an upcoming release and may be available in the Tech Preview now. You can track their progress in the WebKit Bugzilla #198181."
    Source:
    https://web.dev/samesite-co...
    Bug explained:
    https://bugs.webkit.org/sho...

    So it will be fixed in the new Safari 13... well but that's not acceptable for me, because some people of the user base will be unable to login.
    Thats why I have to comment "#cookie_samesite: lax" out.

    Am I correct that this is acceptable? (No real "security threat")
    And is it ok to leave "handler_id: ~" instead of "handler_id: null"? (That was the default when installing SF months ago)

  • 2019-08-16 Victor Bocharsky

    Hey Mike,

    It is the best practice to use "cookie_samesite: lax" since Symfony 4.2. Hm, where did you get this error? While run your tests? Or while using your website yourself in dev mode? Or in prod mode? What if you log out and log in again, or even better open the website in Chrome Incognito mode? Do you still see this problem? Does it persistent? Or sometime it works and sometimes it randomly fails?

    Also, could you look at this official recipe: https://github.com/symfony/... - is your "session" configuration the same as there?

    Cheers!

  • 2019-08-16 Mike

    After updating this App to SF 4.3.3, I got "Invalid CSRF token.".

    I found out that this new config value is the reason:

    framework.yaml:

    cookie_samesite: lax

    After removing this entry, everything works as expected.
    Is this the best practice?

    May helpful info:
    My dev url is subdomain.symfony.local.

  • 2019-07-16 Diego Aguiar

    Of course, file permissions!

  • 2019-07-16 Chris G

    This issue as been solved. PHP did not have write access to its session storage.

  • 2019-07-16 Chris G

    Thanks for the prompt reply!!

    I am using the same code as you have using in your guide.

    LoginFormAuthenticator.php

    /**
    * @param mixed $credentials
    * @param UserProviderInterface $userProvider
    *
    * @return User|object|UserInterface|null
    */
    public function getUser($credentials, UserProviderInterface $userProvider)
    {

    $token = new CsrfToken('authenticate', $credentials['csrf_token']);

    dump($token);
    dump($credentials);
    dump($this->csrfTokenManager->isTokenValid($token));


    if (!$this->csrfTokenManager->isTokenValid($token)) {
    throw new InvalidCsrfTokenException('Token Invalid');
    }

    $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);

    if (!$user) {
    // fail authentication with a custom error
    throw new CustomUserMessageAuthenticationException('Email could not be found.');
    }

    return $user;
    }

    login.html.twig

    {% block body %}
    <div class="text-center container">
    <form class="form-signin" method="post">
    {% if error %}
    <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}


    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <label for="inputEmail" class="sr-only">Email address</label>
    <input type="email" id="inputEmail" value="{{ last_username }}" name="email" class="form-control" placeholder="Email address" required="" autofocus="">
    <label for="inputPassword" class="sr-only">Password</label>
    <input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required="">

    <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">

    <div class="checkbox mb-3">
    <label>
    <input type="checkbox" id="remember_me" name="_remember_me" checked=""> Remember me
    </label>
    </div>

    <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>

    © 2017-2018


    </form>

    </div>
    {% endblock %}

    I have checked requirements using the tool provided by symfony.

    All checks passed successfully. Your system is ready to run Symfony applications.
  • 2019-07-16 Diego Aguiar

    Hey Chris G

    How are you validating your CSRF token? remember to use the same name you used on your template

    Cheers!

  • 2019-07-16 Chris G

    I seem to be having an issue and cannot seem to figure out what could be causing it if you could help that would be awesome!

    https://stackoverflow.com/q...

  • 2019-05-31 Diego Aguiar

    Hey Camille Seuvin

    That's a good question, and to be honest I'm not expert on the topic but I would recommend you to protect all of your forms that handles sensitive information like user's data. If you have a form that allows to modify a user's data, then it's likely that you want it to be secure.
    Here is a good post about protecting your site with CSRF tokens: https://stackoverflow.com/a...

    Cheers!

  • 2019-05-30 Camille Seuvin

    Hello, do we have to secure ALL the forms we create (with and without symfony componen) with a token?
    for exemple the stripe buy button need a Token ?
    Thank you

  • 2019-05-07 Diego Aguiar

    Hey Дмитрий Кулаков

    Did you add the CSRF input field into your login form? Remember that you need to submit it as well and then add that field into your credentials

    Cheers!

  • 2019-05-07 Дмитрий Кулаков

    Hi! I'm trying to make a login form with standard Symfony Forms component. All fine but when i try to login it says: "Invalid CSRF token." Any idea how to fix it?

  • 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...