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

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

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.4.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
    }
}