gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
On a basic level, authenticating a user when we submit the login form is... pretty simple. We need to read the submitted email
, query the database for that User
object... and eventually check the user's password.
The weird thing about Symfony's security system is that... we're not going to write this logic in the controller. Nope. When we POST to /login
, our authenticator is going to intercept that request and do all the work itself. Yup, when we submit the login form, our controller will actually never be executed.
Now that our authenticator is activated, at the start of each request, Symfony will call the supports()
method on our class. Our job is to return true
if this request "contains authentication info that we know how to process". If not, we return false
. If we return false
, we don't fail authentication: it just means that our authenticator doesn't know how to authenticate this request... and the request continues processing like normal... executing whatever controller it matches.
So let's think: when do we want our authenticator to "do its work"? Which requests will "contains authentication info that we know how to process"? The answer to that is: whenever the user submits the login form.
Inside of supports()
return true if $request->getPathInfo()
- that's a fancy method to get the current URL - equals /login
and if $request->isMethod('POST')
:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
public function supports(Request $request): ?bool | |
{ | |
return ($request->getPathInfo() === '/login' && $request->isMethod('POST')); | |
} | |
... lines 18 - 43 | |
} |
So if the current request is a POST to /login
, we want to try to authenticate the user. If not, we want to allow the request to continue like normal.
To see what happens next, down in authenticate()
, dd('authenticate')
:
Tip
PassportInterface
was deprecated since Symfony 5.4: use Passport
as a return type instead.
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 14 - 18 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
dd('authenticate!'); | |
} | |
... lines 23 - 43 | |
} |
Testing time! Go refresh the homepage. Yup! The supports()
method returned false
... and the page kept loading like normal. In the web debug toolbar, we have a new security icon that says "Authenticated: no". But now go to the login form. This page still loads like normal. Enter abraca_admin@example.com
- that's the email of a real user in the database - and any password - I'll use foobar
. Submit and... got it! It hit our dd('authenticate')
!
So if supports()
returns true, Symfony then calls authenticate()
. This is the heart of our authenticator... and its job is to communicate two important things. First, who the user is that's trying to log in - specifically, which User
object they are - and second, some proof that they are this user. In the case of a login form, that would be a password. Since our users don't actually have passwords yet... we'll fake it temporarily.
We communicate these two things by returning a Passport
object: return new Passport()
:
... lines 1 - 12 | |
use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | |
... lines 14 - 15 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 18 - 22 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 25 - 27 | |
return new Passport( | |
... lines 29 - 32 | |
); | |
} | |
... lines 35 - 55 | |
} |
This simple object is basically just a container for things called "badges"... where a badge is a little piece of information that goes into the passport. The two most important badges are UserBadge
and some sort of "credentials badge" that helps prove that this user is who they say they are.
Start by grabbing the POSTed email and password: $email = $request->request->get('email')
. If you haven't seen it before, $request->request->get()
is how you read POST
data in Symfony. In the login template, the name of the field is email
... so we read the email
POST field. Copy and paste this line to create a $password
variable that reads the password
field from the form:
... lines 1 - 15 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 18 - 22 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
$email = $request->request->get('email'); | |
$password = $request->request->get('password'); | |
return new Passport( | |
... lines 29 - 32 | |
); | |
} | |
... lines 35 - 55 | |
} |
Next, inside of the Passport
, the first argument is always the UserBadge
. Say new UserBadge()
and pass this our "user identifier". For us, that's the $email
:
... lines 1 - 10 | |
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |
... lines 12 - 15 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 18 - 22 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
$email = $request->request->get('email'); | |
$password = $request->request->get('password'); | |
return new Passport( | |
new UserBadge($email), | |
... lines 30 - 32 | |
); | |
} | |
... lines 35 - 55 | |
} |
We'll talk very soon about how this is used.
The second argument to Passport
is some sort of "credentials". Eventually we're going to pass it a PasswordCredentials()
.... but since our users don't have passwords yet, use a new CustomCredentials()
. Pass this a callback with a $credentials
arguments and a $user
argument type-hinted with our User
class:
... lines 1 - 11 | |
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; | |
... lines 13 - 15 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 18 - 22 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 25 - 27 | |
return new Passport( | |
new UserBadge($email), | |
new CustomCredentials(function($credentials, User $user) { | |
... lines 31 - 32 | |
); | |
} | |
... lines 35 - 55 | |
} |
Symfony will execute our callback and allow us to manually "check the credentials" for this user... whatever that means in our app. To start, dd($credentials, $user)
. Oh, and CustomCredentials
needs a second argument - which is whatever our "credentials" are. For us, that's $password
:
... lines 1 - 15 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 18 - 22 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 25 - 27 | |
return new Passport( | |
new UserBadge($email), | |
new CustomCredentials(function($credentials, User $user) { | |
dd($credentials, $user); | |
}, $password) | |
); | |
} | |
... lines 35 - 55 | |
} |
If this CustomCredentials
thing is a little fuzzy, don't worry: we really need to see this in action.
But on a high level... it's kind of cool. We return a Passport
object, which says who the user is - identified by their email
- and some sort of a "credentials process" that will prove that the user is who they say they are.
Ok: with just this, let's try it. Go back to the login form and re-submit. Remember: we filled in the form using an email address that does exist in our database.
And... awesome! foobar
is what I submitted for my password and it's also dumping the correct User
entity object from the database! So... woh! Somehow it knew to query for the User
object using that email. How does that work?
The answer is the user provider! Let's dive into that next, learn how we can make a custom query for our user and finish the authentication process.
Wooow, I solved! This happened because I didn't type an email that exist on DB when login! That's crazy! I let the question here perhaps helps someone!
Hey zahariastefan462
That's so awesome that you solved it by yourself! Keep learning and stay in touch!
Cheers!
Hello,
in the callback function I type-hinted the argument $user to Symfony\Component\Security\Core\User\User
instead of using the entity : App\Entity\User
as mentioned in the tutorial, so I had this error Argument 2 passed to App\Security\LoginFormAuthenticator::App\Security\{closure}() must be an instance of Symfony\Component\Security\Core\User\User, instance of App\Entity\User given, called in
In case someone is having same error, just add use App\Entity\User;
instead of the one from Symfony\Component\Security\Core\User
Hey Henane,
Probably you mean Symfony\Component\Security\Core\User\UserInterface that your User entity should implement? Otherwise, make sure your user class extends that "Symfony\Component\Security\Core\User\User" core class if you want to typehint with it :)
Cheers!
Hello,
My User Entity already implements UserInterface, I am talking about the $user argument that we pass to callable of CustomCredentials, with autocompletion I had Symfony\Component\Security\Core\User\User
instead of App\Entity\User
which result to error mentioned in my previous comment.
Hey Hanane,
Yes, I understand! :) Just try to re-read my last comment one more time - you either should change your type hint to that "UserInterface" instead of that "Symfony\Component\Security\Core\User\User" class, or you should start extending that "Symfony\Component\Security\Core\User\User" in your User entity - that's how PHP works :) You just can't typehint method arguments with a class or interface and then pass an object that does not extends that class or implements that interface - that won't work, even if your class will have common method names with those classes/interfaces.
Or, it might be so that in the parent method there's already a tyehint with "App\Entity\User" and so you can't downgrade it to a more base class like "Symfony\Component\Security\Core\User\User". But that's only the case if your App\Entity\User class really extends that "Symfony\Component\Security\Core\User\User".
Cheers!
By the time you upgrade to a newer system, it's already deprecated again :(( I see PassportInterface is deprecated in 5.4 lol
Edit: it seems the only change is that authenticate() method should return Passport instead of PassportInterface
Hey The_nuts!
Whoops, that evolves very quickly :) Good catch btw! Yes, you're right, you should use Password instead of PasswordInterface, here's the link to the deprecation message for the reference: https://github.com/symfony/...
We will add a note in this tutorial. Thanks!
Cheers!
Hey wLcDesigns,
Do you have any specific use case? The main job of the authenticator is to say either the current user is authenticated or no, sanitizing might be an overkill, because if the data is corrupted in some way - authenticator will fail anyway.
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
}
}
Hello, after last step,
return new Passport(
new UserBadge($email),
new CustomCredentials(function($credentials, User $user) {
dd($credentials, $user);
}, $password)
);
I got this error when type Log In
Return value of App\Security\LoginFormAuthenticator::onAuthenticationFailure() must be an instance of Symfony\Component\HttpFoundation\Response or null, none returned
Seems that doesn't enter in new CustomCredentials function and I don't know why, is going directly to onAuthenticationFailure().