If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOur 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!
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.
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!
Hey there,
They say under the documentation that csrf is generated based on the
framework.secret value but I dont find that part of generate csrf on the
symfony code. Could you please to figure out that part?
Hey ahmedbhs!
Sorry for the super slow reply - this question was tough enough that it was left for me... and I've been buried in tutorial land :).
It is *fascinating* that you asked this question because, about 10 days ago, someone was asking me how CSRF worked internally in Symfony. I sent them some files/classes to look at from Symfony. While I did that, I noticed something surprising: I *also* could not find any place that framework.secret is used in its generation! I'm 99% sure I'm not missing it. This was an assumption (the use of framework.secret) that I've had for years, and apparently I was wrong. Btw, where did you see in the docs that the framework.secret is mentioned related to CSRF? I couldn't find it just now.
Anyways, I don't know the specifics about this, but my guess is that (to use some nerdy security words that are... above my pay grade) the CSRF token (which is randomly generate, just not using the secret) has sufficient "entropy" for its use :p. It may be the fact that there is an "id" that you choose when generating it that helps... or just the fact that these tokens are constantly regenerated per user (vs being some long-lived token). But I'm doing some guessing there.
My point is: you're right! And this surprised me also :).
Cheers!
Hi there !
I have a question. It is not really specific to Symfony, but about CSRF in general.
CSRF token is made to prevent this kind of attack https://stackoverflow.com/q...
It means that the victim must already be logged in to have enough privileges to alter his account.
But at the login step, the victim has no privileges at all.
Why CSRF token is required for the login form ?
Hey Stileex,
Hm, CSRF helps not only for authenticated users, it helps with forms in general. You don't have to be authenticated to use it. It generates a special token on the server side and renders it on the client side to send it back to the server and validate. It just prevents from sending not-authorised requests to any endpoint, and with login form I think it also makes sense to do. At least, it will complicate life of intruders who will try to brute force user credentials for example - they would need to know the correct CSRF token to pass the login form validation.
Cheers!
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.
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!
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)
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!
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!
Hey Chris G
How are you validating your CSRF token? remember to use the same name you used on your template
Cheers!
Thanks for the prompt reply!!
I am using the same code as you have using in your guide.
<b>LoginFormAuthenticator.php</b>
`
/**
* @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;
}
`
<b>login.html.twig</b>
`
{% 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>
<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
</form>
</div>
{% endblock %}
`
I have checked requirements using the tool provided by symfony.
<blockquote>All checks passed successfully. Your system is ready to run Symfony applications.</blockquote>
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
Hey ojtouch
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!
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?
Hey Dmitry-K
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!
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!
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.
Hey Markus L.!
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!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"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.17.6
"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
}
}
If someone is curious what exactly is CSRF, I really liked this explanation: https://stackoverflow.com/a...