Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

form_login: The Built-in Authenticator

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Custom authenticator classes like this give us tons of control. Like, imagine that, in addition to email and password fields, you needed a third field - like a "company" dropdown menu... and you use that value - along with the email - to query for the User. Doing that in here would be... pretty darn simple! Grab the company POST field, use it in your custom query and celebrate with nachos.

But a login form is a pretty common thing. And so, Symfony comes with a built-in login form authenticator that we can... just use!

Checking out the Core FormLoginAuthenticator

Let's open it up and check it out. Hit Shift+Shift and look for FormLoginAuthenticator.

The first thing to notice is that this extends the same base class that we do. And if you look at the methods - it references a bunch of options - but ultimately... it does the same stuff that our class does: getLoginUrl() generates a URL to the login page... and authenticate() creates a Passport with UserBadge, PasswordCredentials, a RememberMeBadge and a CsrfTokenBadge.

Both onAuthenticationSuccess and onAuthenticationFailure offload their work to another object... but if you looked inside of those, you would see that they're basically doing the same thing that we are.

Using form_login

So let's use this instead of our custom authenticator... which I would do in a real project unless I need the flexibility of a custom authenticator.

In security.yaml, comment-out our customer authenticator... and also comment-out the entry_point config:

... lines 2 - 16
... lines 18 - 20
... lines 22 - 23
#entry_point: App\Security\LoginFormAuthenticator
... lines 25 - 27
# - App\Security\LoginFormAuthenticator
... lines 30 - 50

Replace it with a new key form_login. This activates that authenticator. Below, this has a ton of options - I'll show you them in a minute. But there are two important ones we need: login_path: set to the route to your login page... so for us that's app_login... and also the check_path, which is the route that the login form submits to... which for us is also app_login: we submit to the same URL:

... lines 2 - 16
... lines 18 - 20
... lines 22 - 24
login_path: app_login
check_path: app_login
... lines 28 - 50

Setting the entry_point to form_login

And... that's it to start! Let's go try it! Refresh any page and... error! An error that we've seen:

Because you have multiple authenticators on firewall "main", you need to set "entry_point" to one of them: either DummyAuthenticator, or form_login.

I mentioned earlier that some authenticators provide an entry point and some don't. The remember_me authenticator does not provide one... but our DummyAuthenticator does and so does form_login. Its entry point redirects to the login page.

So since we have multiple, we need to choose one. Set entry_point: to form_login:

... lines 2 - 16
... lines 18 - 20
... lines 22 - 23
entry_point: form_login
... lines 25 - 50

Customizing the Login Form Field Names

Now if we refresh... cool: no error. So let's try to log in. Actually... I'll log out first... that still works... then go log in with abraca_admin@example.com password tada. And... ah! Another error!

The key "_username" must be a string, NULL given.

And it's coming from FormLoginAuthenticator::getCredentials(). Ok, so when you use the built-in form_login, you need to make sure a few things are lined up. Open the login template: templates/security/login.html.twig. Our two fields are called email... and password:

... 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 - 15
<div class="col-12">
... line 17
<input type="email" name="email" id="inputEmail" class="form-control" required autofocus>
<div class="col-12">
... line 21
<input type="password" name="password" id="inputPassword" class="form-control" required>
... lines 24 - 34
{% endblock %}

Whelp, it turns out that Symfony expects these fields to be called _username and _password... that's why we get this error: it's looking for an _username POST parameter... but it's not there. Fortunately, this is the type of thing you can configure.

Find your favorite terminal and run:

symfony console debug:config security

to see all of our current security configuration. Scroll up... and look for form_login... here it is. There are a bunch of options that allow you to control the form_login behavior. Two of the most important ones are username_parameter and password_parameter. Let's configure these to match our field names.

So, in security.yaml add username_parameter: email and password_parameter: password:

... lines 2 - 16
... lines 18 - 20
... lines 22 - 24
... lines 26 - 27
username_parameter: email
password_parameter: password
... lines 30 - 53

This tells it to read the email POST parameter... and then it will pass that string to our user provider... which will handle querying the database.

Let's test it. Refresh to resubmit and... got it! We're logged in!

The moral of the story is this: using form_login lets you have a login form with less code. But while using a custom authenticator class is more work... it has infinite flexibility. So, it's your choice.

Next: let's see a few other things that we can configure on the login form and add a totally new-feature: pre-filling the email field when we fail login.

Leave a comment!

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", //
        "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