Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Cortafuegos y autenticadores

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.

Construimos este formulario de inicio de sesión haciendo una ruta, un controlador y renderizando una plantilla:

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

Muy sencillo. Cuando enviamos el formulario, se hace un POST de vuelta a /login. Así que, para autenticar al usuario, es de esperar que pongamos algo de lógica aquí: como si se tratara de una petición POST, leer el correo electrónico y la contraseña POSTados, consultar el objeto User... y, finalmente, comprobar la contraseña. ¡Eso tiene mucho sentido! Y eso no es en absoluto lo que vamos a hacer.

Hola cortafuegos

El sistema de autenticación de Symfony funciona de una manera... un poco mágica, que supongo que es adecuada para nuestro sitio. Al inicio de cada petición, antes de que Symfony llame al controlador, el sistema de seguridad ejecuta un conjunto de "autenticadores". El trabajo de cada autentificador es mirar la petición, ver si hay alguna información de autentificación que entienda -como un correo electrónico y una contraseña enviados, o una clave de la API que esté almacenada en una cabecera- y, si la hay, utilizarla para consultar al usuario y comprobar la contraseña. Si todo eso ocurre con éxito, entonces... ¡boom! Autenticación completa.

Nuestro trabajo es escribir y activar estos autentificadores. Abreconfig/packages/security.yaml. Recuerda las dos partes de la seguridad: la autenticación (quién eres) y la autorización (qué puedes hacer).

La parte más importante de este archivo es 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

Un cortafuegos tiene que ver con la autenticación: su trabajo es averiguar quién eres. Y, por lo general, tiene sentido tener sólo un cortafuegos en tu aplicación... incluso si hay varias formas diferentes de autenticarse, como un formulario de inicio de sesión y una clave de API y OAuth.

El cortafuegos "dev"

Pero... woh woh woh. Si casi siempre queremos un solo cortafuegos... ¿por qué hay ya dos? Así es como funciona: al inicio de cada petición, Symfony recorre la lista de cortafuegos, lee la clave pattern -que es una expresión regular- y encuentra el primer cortafuegos cuyo patrón coincida con la URL actual. Así que sólo hay un cortafuegos activo por petición.

Si te fijas bien, ¡este primer cortafuegos es falso! Básicamente, busca si la URL empieza por /_profiler o /_wdt... y luego establece la seguridad en false:

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

En otras palabras, básicamente se está asegurando de que no se crea un sistema de seguridad tan épico que... bloquea la barra de herramientas de depuración web y el perfilador.

Así que... en realidad, sólo tenemos un cortafuegos real llamado main. No tiene la clave pattern, lo que significa que coincidirá con todas las peticiones que no coincidan con el cortafuegos dev. Ah, ¿y los nombres de estos cortafuegos - main y dev? No tienen ningún sentido.

Activar los autentificadores

La mayor parte de la configuración que vamos a poner debajo del cortafuegos está relacionada con la activación de los autentificadores: esas cosas que se ejecutan al principio de cada petición y que intentan autentificar al usuario. Pronto añadiremos parte de esa configuración. Pero estas dos claves superiores hacen algo diferente. lazy permite que el sistema de autenticación no autentique al usuario hasta que lo necesite y provider vincula este cortafuegos al proveedor de usuarios del que hemos hablado antes. Deberías tener estas dos líneas... pero ninguna es terriblemente importante:

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

Crear una clase de autenticador personalizada

De todos modos, cada vez que queramos autentificar al usuario -como cuando enviamos un formulario de acceso- necesitamos un autentificador. Hay algunas clases de autentificadores principales que podemos utilizar, incluida una para los formularios de inicio de sesión.... y te mostraré algunas de ellas más adelante. Pero para empezar, vamos a construir nuestra propia clase de autentificador desde cero.

Para ello, ve al terminal y ejecuta:

symfony console make:auth

Como puedes ver, puedes seleccionar "Autenticador de formularios de inicio de sesión" para engañar y generar un montón de código para un formulario de inicio de sesión. Pero como estamos construyendo cosas desde cero, selecciona "Autentificador vacío" y llámalo LoginFormAuthenticator.

Espectacular. Esto hizo dos cosas: creó una nueva clase de autentificador y también actualizósecurity.yaml. Abre primero la clase: 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
}

La única regla sobre un autentificador es que necesita implementarAuthenticatorInterface... aunque normalmente extenderás AbstractAuthenticator... que implementa AuthenticatorInterface por ti:

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

Hablaremos de lo que hacen estos métodos uno por uno. En cualquier caso, AbstractAuthenticator es agradable porque implementa un método súper aburrido para ti.

Una vez que activemos esta nueva clase en el sistema de seguridad, al principio de cada petición, Symfony llamará a este método supports() y básicamente preguntará

¿Ves información de autenticación en esta petición que entiendas?

Para demostrar que Symfony llamará a esto, vamos a dd('supports'):

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

Activar los autenticadores con custom_authenticators

Bien, entonces, ¿cómo activamos este autentificador? ¿Cómo le decimos a nuestro cortafuegos que debe utilizar nuestra nueva clase? En security.yaml, ¡ya tenemos el código que lo hace! Esta línea custom_authenticator fue añadida por el comando make:auth:

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

Así que si tienes una clase de autentificador personalizada, así es como la activas. Más adelante, veremos que puedes tener varios autentificadores personalizados si quieres.

En cualquier caso, ¡esto significa que nuestro autentificador ya está activo! Así que vamos a probarlo. Actualiza la página de inicio de sesión. ¡Accede al método supports()! De hecho, si vas a cualquier URL, se encontrará con nuestro dd(). En cada petición, antes del controlador, Symfony pregunta ahora a nuestro autentificador si soporta la autentificación en esta petición.

A continuación, vamos a rellenar la lógica del autentificador y conseguir que nuestro usuario inicie la sesión

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 hace 9 meses | 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 hace 9 meses | 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 hace 9 meses | 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 hace 9 meses

Sorry, no change
no exclude
after rm - no change

Reply
Stefan_JustMe Avatar
Stefan_JustMe Avatar Stefan_JustMe | Stefan_JustMe | posted hace 9 meses | 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 hace 1 año

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 hace 1 año

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!

¡Este tutorial también funciona muy bien para 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