Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Firewalls & Authenticators

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.

We built this log in form by making a route, controller and rendering a template:

... lines 1 - 8
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="app_login")
*/
public function login(): Response
{
return $this->render('security/login.html.twig');
}
}

Dead simple. When we submit the form, it POSTs right back to /login. So, to authenticate the user, you might expect us to put some logic right here: like if this is a POST request, read the POSTed email & password, query for the User object... and eventually check the password. That makes perfect sense! And that is completely not what we're going to do.

Hello Firewalls

Symfony's authentication system works in a... bit of a magic way, which I guess is fitting for our site. At the start of every request, before Symfony calls the controller, the security system executes a set of "authenticators". The job of each authenticator is to look at the request, see if there is any authentication information that it understands - like a submitted email and password, or an API key that's stored on a header - and if there is, use that to query the user and check the password. If all that happens successfully then... boom! Authentication complete.

Our job is to write and activate these authenticators. Open up config/packages/security.yaml. Remember the two parts of security: authentication (who you are) and authorization (what you can do).

The most important part of this file is firewalls:

security:
... lines 2 - 13
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
... lines 27 - 33

A firewall is all about authentication: its job is to figure out who you are. And, it usually makes sense to have only one firewall in your app... even if there are multiple different ways to authenticate, like a login form and an API key and OAuth.

The "dev" Firewall

But... woh woh woh. If we almost always want only one firewall... why are there are already two? Here's how this works: at the start of each request, Symfony goes down the list of firewalls, reads the pattern key - which is a regular expression - and finds the first firewall whose pattern matches the current URL. So there's only ever one firewall active per request.

If you look closely, this first firewall is a fake! It basically matches if the URL starts with /_profiler or /_wdt... and then sets security to false:

security:
... lines 2 - 13
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
... lines 18 - 33

In other words, it's basically making sure that you don't create a security system that is so epically awesome that... you block the web debug toolbar and profiler.

So... in reality, we only have one real firewall called main. It has no pattern key, which means that it will match all requests that don't match the dev firewall. Oh, and the names of these firewalls - main and dev? They're totally meaningless.

Activating Authenticators

Most of the config that we're going to put beneath the firewall relates to activating authenticators: those things that execute early in each request and try to authenticate the user. We'll add some of that config soon. But these two top keys do something different. lazy allows the authentication system to not authenticate the user until it needs to and provider ties this firewall to the user provider we talked about earlier. You should have both of these lines... but neither are terribly important:

security:
... lines 2 - 13
firewalls:
... lines 15 - 17
main:
lazy: true
provider: app_user_provider
... lines 21 - 33

Creating a Custom Authenticator Class

Anyways, anytime that we want to authenticate the user - like when we submit a login form - we need an authenticator. There are some core authenticator classes that we can use, including one for login forms.... and I'll show you some of those later. But to start, let's build our own authenticator class from scratch.

To do that, go to terminal and run:

symfony console make:auth

As you can see, you can select "Login form authenticator" to cheat and generate a bunch of code for a login form. But since we're building things from scratch, select "Empty authenticator" and call it LoginFormAuthenticator.

Awesome. This did two things: it created a new authenticator class and also updated security.yaml. Open the class first: src/Security/LoginFormAuthenticator.php:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
// TODO: Implement supports() method.
}
public function authenticate(Request $request): PassportInterface
{
// TODO: Implement authenticate() method.
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// TODO: Implement onAuthenticationSuccess() method.
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
// TODO: Implement onAuthenticationFailure() method.
}
... lines 33 - 43
}

The only rule about an authenticator is that it needs to implement AuthenticatorInterface... though usually you'll extend AbstractAuthenticator... which implements AuthenticatorInterface for you:

... lines 1 - 8
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
... lines 10 - 11
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 14 - 43
}

We'll talk about what these methods do one-by-one. Anyways, AbstractAuthenticator is nice because it implements a super boring method for you.

Once we activate this new class in the security system, at the beginning of every request, Symfony will call this supports() method and basically ask:

Do you see authentication information on this request that you understand?

To prove that Symfony will call this, let's just dd('supports'):

... lines 1 - 11
class LoginFormAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
dd('supports!');
}
... lines 18 - 43
}

Activating Authenticators with custom_authenticators

Okay, so how do we activate this authenticator? How do we tell our firewall that it should use our new class? Back in security.yaml, we already have the code that does that! This custom_authenticator line was added by the make:auth command:

security:
... lines 2 - 13
firewalls:
... lines 15 - 17
main:
... lines 19 - 20
custom_authenticator: App\Security\LoginFormAuthenticator
... lines 22 - 34

So if you have a custom authenticator class, this is how you activate it. Later, we'll see that you can have multiple custom authenticators if you want.

Anyways, this means that our authenticator is already active! So let's try it. Refresh the login page. It hits the supports() method! In fact, if you go to any URL it will hit our dd(). On every request, before the controller, Symfony now asks our authenticator if it supports authentication on this request.

Next let's fill in the authenticator logic and get our user logged in!

Leave a comment!

19
Login or Register to join the conversation

Error does not added to use Passport

symfony console make:auth
with selection 0 generate Empty authenticator does not generate use Passport by default.


namespace App\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;

class LoginFormAuthenticator extends AbstractAuthenticator
{
    public function supports(Request $request): ?bool
    {
        // TODO: Implement supports() method.
    }

    public function authenticate(Request $request): Passport
    {
        // TODO: Implement authenticate() method.
    }
1 Reply

Hey Maxim!

Ah, you're right! It only affects empty authenticator, with login form authenticator it's already there. I just created a PR in maker-bundle to fix it: https://github.com/symfony/... - feel free to give a review ;)

Cheers!

Reply
Stefan_JustMe Avatar
Stefan_JustMe Avatar Stefan_JustMe | posted 9 months ago | edited

Hi,
after bin/console make:auth
I get following error:

The service "security.command.debug_firewall" has a dependency on a non-existent service "App\Security\LoginFormAuthenticator".

I'm using Symfony 5.4.11

console output:

php bin/console make:auth

 What style of authentication do you want? [Empty authenticator]:
  [0] Empty authenticator
  [1] Login form authenticator
 > 0
0

 The class name of the authenticator to create (e.g. AppCustomAuthenticator):
 > LoginFormAuthenticator

 Which firewall do you want to update? [pimcore_admin_webdav]:
  [0] pimcore_admin_webdav
  [1] pimcore_admin
  [2] wir_beten_fw
 > 2
2

 created: src/Security/LoginFormAuthenticator.php
 updated: config/packages/security.yaml
Reply

Hey Stefan,

I believe you don't have enabled the autoconfigure feature. Do you have these lines in your config/services.yaml file?

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
Reply
Stefan_JustMe Avatar
Stefan_JustMe Avatar Stefan_JustMe | MolloKhan | posted 9 months ago | edited

My config/services.yaml:

services:
    # default configuration for services in *this* file
    _defaults:
        # automatically injects dependencies in your services
        autowire: true
        # automatically registers your services as commands, event subscribers, etc.
        autoconfigure: true
        # this means you cannot fetch services directly from the container via $container->get()
        # if you need to do this, you can override this setting on individual services
        public: false

Yes I do. So it should work - but it does not?
What should I add to configure LoginFormAuthenticator ?

I also updated to Symfony 5.4.14 - same problem.

Reply

Ok, let's review a few things.
First: double-check that the file's name matches to the class name of your LoginFormAuthenticator
Second: Double-check its namespace, it should be App\Security
If everything is ok, could you show me your config/packages/security.yaml file? There might be a hint

Reply
Stefan_JustMe Avatar
Stefan_JustMe Avatar Stefan_JustMe | MolloKhan | posted 9 months ago | edited

Hi MolloKhan,
thanks for your time and help...

filenames - check
namespace - check
doublecheck - check

-security.yaml - sure - appended afterwards.
It's a pimcore application where I try to extend security for some pages
I want to configure firewall: 'wir_beten_fw'
path '/intern/' is working and redirecting to 'beter_login'

security:
    enable_authenticator_manager: true

    password_hashers:
        App\Model\DataObject\User: website_beten.security.password_hasher_factory

    providers:
        pimcore_admin:
            id: Pimcore\Bundle\AdminBundle\Security\User\UserProvider
        wir_beten_provider:
            id: website_beten.security.user_provider

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        # Pimcore WebDAV HTTP basic // DO NOT CHANGE!
        pimcore_admin_webdav:
            pattern: ^/admin/asset/webdav
            provider: pimcore_admin
            http_basic: ~

        # Pimcore admin form login // DO NOT CHANGE!
        pimcore_admin:
            pattern: ^/admin(/.*)?$
            # admin firewall is stateless as we open the admin
            # session on demand for non-blocking parallel requests
            stateless: true
            provider: pimcore_admin
            login_throttling:
                max_attempts: 3
                interval: '5 minutes'

            logout:
                path: pimcore_admin_logout
                target: pimcore_admin_login
            custom_authenticators:
                - Pimcore\Bundle\AdminBundle\Security\Authenticator\AdminLoginAuthenticator
                - Pimcore\Bundle\AdminBundle\Security\Authenticator\AdminTokenAuthenticator
                - Pimcore\Bundle\AdminBundle\Security\Authenticator\AdminSessionAuthenticator
            two_factor:
                auth_form_path: /admin/login/2fa                   # Path or route name of the two-factor form
                check_path: /admin/login/2fa-verify                # Path or route name of the two-factor code check
                default_target_path: /admin            # Where to redirect by default after successful authentication
                always_use_default_target_path: false  # If it should always redirect to default_target_path
                auth_code_parameter_name: _auth_code   # Name of the parameter for the two-factor authentication code
                trusted_parameter_name: _trusted       # Name of the parameter for the trusted device option
                multi_factor: false                    # If ALL active two-factor methods need to be fulfilled (multi-factor authentication)

        # demo_cms firewall is valid for the whole site
        wir_beten_fw:
            pattern: (/.*)?/intern(/.*)?$
            # the provider defined above
            provider: wir_beten_provider
            form_login:
                enable_csrf: true
                login_path: beter_login
                check_path: beter_login
                failure_path: beter_login
                default_target_path: login_ueberblick
                always_use_default_target_path: true
                password_parameter: _password
                username_parameter: _username
            custom_authenticator: App\Security\LoginFormAuthenticator
#            login_throttling:
#                max_attempts: 3
#                interval: '5 minutes'

    access_control:
        # Pimcore admin ACl  // DO NOT CHANGE!
        - { path: ^/admin/settings/display-custom-logo, roles: PUBLIC_ACCESS }
        - { path: ^/admin/login/2fa-verify, roles: IS_AUTHENTICATED_2FA_IN_PROGRESS}
        - { path: ^/admin/login/2fa, roles: IS_AUTHENTICATED_2FA_IN_PROGRESS}
        - { path: ^/admin/login$, roles: PUBLIC_ACCESS }
        - { path: ^/admin/login/(login|lostpassword|deeplink|csrf-token)$, roles: PUBLIC_ACCESS }
        - { path: ^/admin, roles: ROLE_PIMCORE_USER }

        # interner Bereich - nur mit login weiter
        - { path: /intern/, roles: [ROLE_PIMCORE_USER, ROLE_USER] }

    role_hierarchy:
        # Pimcore admin  // DO NOT CHANGE!
        ROLE_PIMCORE_ADMIN: [ROLE_PIMCORE_USER]

some stuff from config.yaml

    models:
        class_overrides:
            'Pimcore\Model\DataObject\User': 'App\Model\DataObject\User'

    security:
        factory_type: password_hasher
        password_hasher_factories:
            App\Model\DataObject\User: website_beten.security.password_hasher_factory

Reply

Your config looks good to me. For some reason your authenticator is not being auto-registered. Double-check in your config/services.yaml if you're not excluding it under the key:

services:
    App\:
        exclude: ...

Also try clearing the cache manually rm -rf var/cache

Reply
Stefan_JustMe Avatar
Stefan_JustMe Avatar Stefan_JustMe | MolloKhan | posted 9 months ago

Sorry, no change
no exclude
after rm - no change

Reply
Stefan_JustMe Avatar
Stefan_JustMe Avatar Stefan_JustMe | Stefan_JustMe | posted 9 months ago | edited

Don't know why - but this was missing in config/services.yaml:

    App\Security\:
        resource: '../src/Security/*'

so now - LoginFormAuthenticator - is found and executed
I can follow the tut

Thanks a lot

Reply

Ohh, so it was not excluded, but it was not been auto-registered. Makes sense :)

Reply
Tomasz-S Avatar

Hi
I have a question, how to secure form against changes value in hidden fields?

Reply

Hey Tomas,

Perhaps what you need here is a custom validation constraint. Here you can learn how to create own yourself https://symfony.com/doc/current/validation/custom_constraint.html

Cheers!

Reply
Tomasz-S Avatar

Hey
not, I'm looking for something what protect my form against "html" code manipulation
In cakephp is special component, maybe in symfony is something similarar
FormProtect

regards

Reply

It seems to me that that component does something similar to what you can achieve with custom validation constraints. The only difference is that you have to attach the validations to your entity fields.
Here you can learn how to create your own validators https://symfonycasts.com/screencast/symfony-forms/custom-validator
the tutorial is based on Symfony 4, but nothing meaningful has changed since then

Reply
Default user avatar
Default user avatar GianlucaF | posted 1 year ago

Hi, if symfony calls the authenticator on each request, which is the reason for lazy parameter?

Reply

Hey GianlucaF,

Hm, yes, authenticator is called on each request... but we only call that supports() method on it, which returns true *only* on login route when we're sending a POST request to it, right? But if supports() returns false - we won't continue.

Cheers!

Reply
Swicku M. Avatar
Swicku M. Avatar Swicku M. | posted 1 year ago

Great series! You guys are always having what I need atm! (that's the true magic)

Reply

Hey Maciej,

Thank you for your feedback! We're really happy to hear it was useful for you :)

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": ">=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
    }
}
userVoice