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 SubscribeAfter we return the Passport
object, we know that two things happen. First, the UserBadge
is used to get the User
object:
... lines 1 - 21 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 24 - 37 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 40 - 42 | |
return new Passport( | |
new UserBadge($email, function($userIdentifier) { | |
// optionally pass a callback to load the User manually | |
$user = $this->userRepository->findOneBy(['email' => $userIdentifier]); | |
if (!$user) { | |
throw new UserNotFoundException(); | |
} | |
return $user; | |
}), | |
... line 54 | |
); | |
} | |
... lines 57 - 83 | |
} |
In our case, because we passed this a second argument, it just calls our function, and we do the work. But if you only pass one argument, then the user provider does the work.
The second thing that happens is that the "credentials badge" is "resolved":
... lines 1 - 21 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 24 - 37 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 40 - 42 | |
return new Passport( | |
... lines 44 - 53 | |
new PasswordCredentials($password) | |
); | |
} | |
... lines 57 - 83 | |
} |
Originally it did this by executing our callback. Now it checks the user's password in the database.
All of this is powered by a really cool event system. After our authenticate()
method, the security system dispatches several events... and there are a set of listeners to these events that do different work. We're going to see a full list of these listeners later... and even add our own listeners to the system.
But let's look at a few of them. Hit Shift
+Shift
so we can load some core files from Symfony. The first is called UserProviderListener
. Make sure to "Include non-project items"... and open it up.
This is called after we return our Passport
. It first checks to make sure the Passport
has a UserBadge
- it always will in any normal situation - and then grabs that object. It then checks to see if the badge has a "user loader": that's the function that we're passing to the second argument of our UserBadge
. If the badge already has a user loader, like in our case, it does nothing. But if it does not, it sets the user loader to the loadUserByIdentifier()
method on our user provider.
It's... a little technical... but this is what causes our user provider in security.yaml
to be responsible for loading the user if we only pass one argument to UserBadge
.
Let's check one other class. Close this one and hit Shift
+Shift
to open CheckCredentialsListener
. As the name suggests, this is responsible for checking the user's "credentials". It first checks to see if the Passport
has a PasswordCredentials
badge. Even though its name doesn't sound like it, the "credentials" objects are just badges... like any other badge. So this checks to see if the Passport
has that badge and if it does, it grabs the badge, reads the plain-text password off of it, and, eventually way down here, uses the password hasher to verify that the password is correct. So this contains all of that password hashing logic. Below, this listener also handles the CustomCredentials
badge.
So your Passport
always has at least these two badges: the UserBadge
and also some sort of "credentials badge". One important property of badges is that each one must be "resolved". You can see this in CheckCredentialsListener
. After it finishes checking the password, it calls $badge->markResolved()
. If, for some reason, this CheckCredentialsListener
was never called due to some misconfiguration... the badge would remain "unresolved" and that would actually cause authentication to fail. Yup, after calling the listeners, Symfony checks to make sure that all badges have been resolved. This means that you can confidently return PasswordCredentials
and not have to wonder if something did actually verify that password.
And here's where things start to get more interesting. In addition to these two badges, we can also add more badges to our Passport
to activate more super powers. For example, one good thing to have on a login form is CSRF protection. Basically you add a hidden field to your form that contains a CSRF token... then, on submit, you validate that token.
Let's do this. Anywhere inside your form, add an input type="hidden"
, name="_csrf_token"
- this name could be anything, but this is a standard name - then value="{{ csrf_token() }}"
. Pass this the string authenticate
:
... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="login-form bg-light mt-4 p-4"> | |
<form method="post" class="row g-3"> | |
... lines 10 - 24 | |
<input type="hidden" name="_csrf_token" | |
value="{{ csrf_token('authenticate') }}" | |
> | |
... lines 28 - 33 | |
</form> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
That authenticate
could also be anything... it's like a unique name for this form.
Now that we have the field, copy its name and head over to LoginFormAuthenticator
. Here, we need to read that field from the POST data and then ask Symfony:
Is this CSRF token valid?
Well, in reality, that second part will happen automatically.
How? The Passport
object has a third argument: an array of any other badges that we want to add. Add one: a new CsrfTokenBadge()
:
... lines 1 - 15 | |
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; | |
... lines 17 - 22 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 25 - 38 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 41 - 43 | |
return new Passport( | |
... lines 45 - 55 | |
[ | |
new CsrfTokenBadge( | |
... lines 58 - 59 | |
) | |
] | |
); | |
} | |
... lines 64 - 90 | |
} |
This needs two things. The first is the CSRF token ID. Say authenticate
:
... lines 1 - 22 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 25 - 38 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 41 - 43 | |
return new Passport( | |
... lines 45 - 55 | |
[ | |
new CsrfTokenBadge( | |
'authenticate', | |
... line 59 | |
) | |
] | |
); | |
} | |
... lines 64 - 90 | |
} |
this just needs to match whatever we used in the form. The second argument is the submitted value, which is $request->request->get()
and the name of our field: _csrf_token
:
... lines 1 - 22 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 25 - 38 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 41 - 43 | |
return new Passport( | |
... lines 45 - 55 | |
[ | |
new CsrfTokenBadge( | |
'authenticate', | |
$request->request->get('_csrf_token') | |
) | |
] | |
); | |
} | |
... lines 64 - 90 | |
} |
And... we're done! Internally, a listener will notice this badge, validate the CSRF token and resolve the badge.
Let's try it! Go to /login
, inspect the form... and find the hidden field. There it is. Enter any email, any password... but mess with the CSRF token value. Hit "Sign in" and... yes! Invalid CSRF token! Now if we don't mess with the token... and use any email and password... beautiful! The CSRF token was valid... so it continued to the email error.
Next: let's leverage Symfony's "remember me" system to allow users to stay logged in for a long time. This feature also leverages the listener system and a badge.
Hi there!
Sorry for the slow reply! Do your frontend and API live on the same domain name? Or different domain names? Are you using session-based authentication or something different?
Cheers!
How CSRF protection work in Symfony? How app know what CSRF token was send to form and after that validate it ?
Hey!
Somehow we completely missed your comment! Bah - sorry!
The answer is that, when generating a CSRF token, Symfony stores that value in the session. Then, when the user submits the CSRF token, we check that it matches what was in the session. This isn't the only want to do CSRF tokens, but it's the most standard and the one Symfony uses by default.
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.3", // v3.3.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"doctrine/annotations": "^1.0", // 1.13.2
"doctrine/doctrine-bundle": "^2.1", // 2.6.3
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
"doctrine/orm": "^2.7", // 2.10.1
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"knplabs/knp-time-bundle": "^1.11", // v1.16.1
"pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
"pagerfanta/twig": "^3.3", // v3.3.0
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"scheb/2fa-bundle": "^5.12", // v5.12.1
"scheb/2fa-qr-code": "^5.12", // v5.12.1
"scheb/2fa-totp": "^5.12", // v5.12.1
"sensio/framework-extra-bundle": "^6.0", // v6.2.0
"stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
"symfony/asset": "5.3.*", // v5.3.4
"symfony/console": "5.3.*", // v5.3.7
"symfony/dotenv": "5.3.*", // v5.3.8
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/form": "5.3.*", // v5.3.8
"symfony/framework-bundle": "5.3.*", // v5.3.8
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/property-access": "5.3.*", // v5.3.8
"symfony/property-info": "5.3.*", // v5.3.8
"symfony/rate-limiter": "5.3.*", // v5.3.4
"symfony/runtime": "5.3.*", // v5.3.4
"symfony/security-bundle": "5.3.*", // v5.3.8
"symfony/serializer": "5.3.*", // v5.3.8
"symfony/stopwatch": "5.3.*", // v5.3.4
"symfony/twig-bundle": "5.3.*", // v5.3.4
"symfony/ux-chartjs": "^1.3", // v1.3.0
"symfony/validator": "5.3.*", // v5.3.8
"symfony/webpack-encore-bundle": "^1.7", // v1.12.0
"symfony/yaml": "5.3.*", // v5.3.6
"symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.3
"twig/string-extra": "^3.3", // v3.3.3
"twig/twig": "^2.12|^3.0" // v3.3.3
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
"symfony/debug-bundle": "5.3.*", // v5.3.4
"symfony/maker-bundle": "^1.15", // v1.34.0
"symfony/var-dumper": "5.3.*", // v5.3.8
"symfony/web-profiler-bundle": "5.3.*", // v5.3.8
"zenstruck/foundry": "^1.1" // v1.13.3
}
}
i am working in project reast api and i don't kow how to implement csrf beccause i have not the parte frondend so what i can do kow?thanks