Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Authenticator & The Passport

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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.

Symfony's Security Doesn't Happen in a Controller

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.

The supports() Method

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')!

The authenticate() Method

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.

The Passport Object: UserBadge & Credentials

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.

Leave a comment!

11
Login or Register to join the conversation

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().

Reply

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!

Reply

Hey Stefan Zaharia

That's so awesome that you solved it by yourself! Keep learning and stay in touch!

Cheers!

Reply
Hanane K. Avatar
Hanane K. Avatar Hanane K. | posted 7 months ago

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

Reply

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!

Reply
Hanane K. Avatar

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.

Reply

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!

1 Reply
Trafficmanagertech Avatar
Trafficmanagertech Avatar Trafficmanagertech | posted 7 months ago

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

Reply

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!

Reply
Wlc Avatar

What about sanitizing request data?

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "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
    }
}