Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

form_login: El autentificador incorporado

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

Las clases de autentificadores personalizados como ésta nos dan mucho control. Por ejemplo, imagina que, además de los campos de correo electrónico y contraseña, necesitaras un tercer campo, como un menú desplegable de "empresa"... y utilizaras ese valor -junto con el email - para consultar el User. Hacerlo aquí sería... ¡bastante sencillo! Coge el campocompany POST, úsalo en tu consulta personalizada y celébralo con nachos.

Pero un formulario de acceso es algo bastante común. Y por eso, Symfony viene con un autentificador de formularios de inicio de sesión incorporado que podemos... ¡simplemente usar!

Comprobando el Core FormLoginAuthenticator

Vamos a abrirlo y comprobarlo. Pulsa Shift+Shift y buscaFormLoginAuthenticator.

Lo primero que hay que observar es que extiende la misma clase base que nosotros. Y si te fijas en los métodos -hace referencia a un montón de opciones- pero en última instancia... hace lo mismo que nuestra clase: getLoginUrl() genera una URL a la página de inicio de sesión... y authenticate() crea un Passport con UserBadge,PasswordCredentials, un RememberMeBadge y un CsrfTokenBadge.

Tanto onAuthenticationSuccess como onAuthenticationFailure descargan su trabajo a otro objeto... pero si miraras dentro de ellos, verías que básicamente hacen lo mismo que nosotros.

Usar form_login

Así que vamos a usar esto en lugar de nuestro autentificador personalizado... lo que yo haría en un proyecto real a menos que necesite la flexibilidad de un autentificador personalizado.

En security.yaml, comenta el autentificador de nuestro cliente... y también comenta la configuración de entry_point:

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

Sustitúyelo por una nueva clave form_login. Esto activa ese autentificador. Abajo, esto tiene un montón de opciones - te las mostraré en un minuto. Pero hay dos importantes que necesitamos: login_path: establecido a la ruta a tu página de inicio de sesión... así que para nosotros eso es app_login... y también el check_path, que es la ruta a la que se somete el formulario de inicio de sesión... que para nosotros también es app_login: nos sometemos a la misma URL:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 24
form_login:
login_path: app_login
check_path: app_login
... lines 28 - 50

Configurando el punto_de_entrada como form_login

Y... ¡eso es todo para empezar! ¡Vamos a probarlo! Actualiza cualquier página y... ¡error! Un error que hemos visto:

Como tienes varios autentificadores en el firewall "principal", necesitas establecer "punto_de_entrada" en uno de ellos: o bien DummyAuthenticator, o bien form_login.

Ya he mencionado que algunos autenticadores proporcionan un punto de entrada y otros no. El autentificador remember_me no proporciona uno... pero nuestroDummyAuthenticator sí lo hace y también form_login. Su punto de entrada redirige a la página de inicio de sesión.

Así que, como tenemos varios, tenemos que elegir uno. Establece entry_point: como form_login:

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

Personalizar los nombres de los campos del formulario de inicio de sesión

Ahora si refrescamos... genial: no hay error. Así que vamos a intentar iniciar la sesión. En realidad... Primero cerraré la sesión... eso sigue funcionando... luego entraré con abraca_admin@example.comcontraseña tada. Y... ¡ah! ¡Otro error!

La clave "_nombredeusuario" debe ser una cadena, dada NULL.

Y viene de FormLoginAuthenticator::getCredentials(). Vale, pues cuando utilices el built-in form_login, tienes que asegurarte de que algunas cosas están alineadas. Abre la plantilla de inicio de sesión: templates/security/login.html.twig. Nuestros dos campos se llaman email... y 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>
<div class="col-12">
... line 21
<input type="password" name="password" id="inputPassword" class="form-control" required>
</div>
... lines 24 - 34
</div>
</div>
</div>
{% endblock %}

Resulta que Symfony espera que estos campos se llamen _usernamey _password... por eso nos da este error: está buscando un parámetro POST _username... pero no está ahí. Afortunadamente, este es el tipo de cosas que puedes configurar.

Busca tu terminal favorito y ejecuta

symfony console debug:config security

para ver toda la configuración de seguridad actual. Desplázate hacia arriba... y buscaform_login... aquí está. Hay un montón de opciones que te permiten controlar el comportamiento de form_login. Dos de las más importantes son username_parametery password_parameter. Vamos a configurarlas para que coincidan con nuestros nombres de campo.

Así, en security.yaml añade username_parameter: email ypassword_parameter: password:

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

Esto le dice que lea el parámetro email POST... y luego pasará esa cadena a nuestro proveedor de usuarios... que se encargará de consultar la base de datos.

Vamos a probarlo. Actualiza para volver a enviar y... ¡ya está! ¡Ya hemos iniciado la sesión!

La moraleja de la historia es la siguiente: usar form_login te permite tener un formulario de inicio de sesión con menos código. Pero mientras que usar una clase de autentificador personalizada es más trabajo... tiene una flexibilidad infinita. Así que, es tu elección.

A continuación: vamos a ver algunas otras cosas que podemos configurar en el formulario de inicio de sesión y a añadir una característica totalmente nueva: rellenar previamente el campo del correo electrónico cuando falle el inicio de sesión.

Leave a comment!

24
Login or Register to join the conversation
Claudio-B Avatar
Claudio-B Avatar Claudio-B | posted hace 20 días

First of all, thank you for your great job.
I have a question: at the beginning of this chapter you say: "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 ...."
Do you think is also possible to use the "company" or similar field to prefix table names, including User tabel, as stated at https://www.doctrine-project.org/projects/doctrine-orm/en/2.16/cookbook/sql-table-prefixes.html ??
Thank you in advance for your help

Reply

Hey @Claudio-B ,

IMO the Doctrine DB prefixes feature is a completely standalone feature that is implemented via an event listener, so in theory yes, you can use that in this case if you need it. Unfortunately, I have never used that personally, so can't tell you for sure... but give it a try, I think it should work.

Cheers!

Reply
t5810 Avatar

Hi there!

Which option is better for login form which will:

  • authenticate the users from the database
  • have forgot password option
  • have remember me checkbox
  • allow login with google, github and twitter

The custom authenticator class, or the built in LoginFormAuthenticator?

Thanks!

Reply

Hey @t5810

The LoginFormAuthenticator will work great for the first 3 items, but for social login, you'll have to integrate one of these third party bundles (or do it by yourself): KnpUOAuth2ClientBundle HWIOAuthBundle

Cheers!

1 Reply
Bartlomeij Avatar
Bartlomeij Avatar Bartlomeij | posted hace 6 meses

Hello!
Awesome job with the Security course! I got a question - how can we add some password requirements using login_form? Eg. min and max len, requires at least one special character etc?
Thanks!

Reply

Hey Bartlomeij,

If you're using the Symfony Form component, you can create a custom form for your login page and add a field constraint to the password. Or, you can use the Validator component manually https://symfony.com/doc/current/components/validator.html

Cheers!

Reply
Georg H. Avatar
Georg H. Avatar Georg H. | posted hace 1 año | edited

Hi there,
thanks for this wonderful introduction to the security system.

Everything works as expected with symfony's test server. As soon as I move the code to the hosting provider all redirects (after login or logout) go to "http" instead of "https". The .htaccess-file contains this snippet to send requests to index.php:
`
RewriteCond %{REQUEST_FILENAME} !-f

RewriteRule ^ %{ENV:BASE}/public/index.php [L]

`

Then in framework.yaml I have to set
<br />assets:<br /> base_path: 'public'<br />

The following in .htaccess does not work
<br />RewriteCond %{HTTP:X-Forwarded-Proto} !https<br />RewriteRule ^.*$ https://%{SERVER_NAME}%{REQUEST_URI}<br />
It uses the full path as URL (including 'public/index.php') which looks ugly in the browser address-line, and the images are missing.
Thanks for a hint!

Reply
skocdopolet Avatar
skocdopolet Avatar skocdopolet | posted hace 1 año | edited

Hello,

could anybody help me please with using multiple authentifications providers? I need authenticate users first from local user database (App\Entity\User) and then from LDAP. When the user is not found in local database, then check from LDAP.

I have this security.yml
`security:

enable_authenticator_manager: true

# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
    Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    App\Entity\User:
        algorithm: auto

providers:
    app_user_provider:
        entity:
            class: App\Entity\User
            property: email

    ldap_user_provider:
        ldap:
            service: Symfony\Component\Ldap\Ldap
            base_dn: 'dc=ipp,dc=local'
            search_dn: 'cn=searchuser,cn=Users,dc=example,dc=com'
            search_password: 'password'
            default_roles: ROLE_USER    
            uid_key: userPrincipalName
            #extra_fields: ['mail', 'cn']

    chain_provider:
        chain:
            providers: ['app_user_provider', 'ldap_user_provider']

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

    main:
        lazy: true
        provider: chain_provider

        form_login:
            login_path: login
            check_path: login
            enable_csrf: true
        
        form_login_ldap:
            service: Symfony\Component\Ldap\Ldap
            login_path: login
            check_path: login
            dn_string: '{username}'

        logout:
            path: logout
            target: login

# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
    # allow unauthenticated users to access the login form
    - { path: ^/login, roles: PUBLIC_ACCESS }
    
    - { path: ^/admin, roles: ROLE_ADMIN }
    
    - { path: ^/app, roles: ROLE_USER }`

Actually, users from form_login are successfully authenticated but, from LDAP not. When I comment form_login from main firewall, users should authenticate from LDAP, but not from local database.

Cheers

Reply

Hey Tomáš S.

You need to create a custom authenticator where you'll check the database first for the user's credentials, and in case it was not found, you'll use your Ldap service to fetch the user. Here are the details of how to write a custom authenticator: https://symfony.com/doc/cur...

Cheers!

Reply
skocdopolet Avatar
skocdopolet Avatar skocdopolet | MolloKhan | posted hace 1 año | edited

Hello Diego,

thank you for your advice. Before when I start to creating a custom authenticator. Could you please explain me purpose of chain provider?

I have expected, when I set chain_provider like this:
`

    chain_provider:
        chain:
            providers: ['app_user_provider', 'ldap_user_provider']

</code >

then the application try to authenticate with app_user_provider first and when user is not found, then try second ldap_user_provider. Or I am wrong?

Thank you
Tomas

Reply

Yes, that's exactly the behavior of the ChainProvider, you can read more about it here: https://symfony.com/doc/cur...

Do you have multiple authenticators as well? If that's the case, then, I think you don't need a ChainProvider because each authenticator can implement the specific logic for finding users

Reply
skocdopolet Avatar
skocdopolet Avatar skocdopolet | MolloKhan | posted hace 1 año | edited

Thank you for confirming ChainProvider behavior.

Multiple authenticators? I thnink yes, I have...

`form_login:

login_path: login
check_path: login
enable_csrf: true

form_login_ldap:

service: Symfony\Component\Ldap\Ldap
login_path: login
check_path: login
dn_string: '{username}'`

So what did you thing I have wrong in security.yml?

Reply

Oh, I think I see the problem. You're using the same login/check route on both cases. You need to use different ones, otherwise, Symfony will always use the first service declared (in this case, form_login)

Reply
skocdopolet Avatar
skocdopolet Avatar skocdopolet | MolloKhan | posted hace 1 año | edited

Hey Diego,

Thank you for look at my problem. I try to change security.yml this way:`form_login:
login_path: login
check_path: login
enable_csrf: true

form_login_ldap:
service: Symfony\Component\Ldap\Ldap
login_path: login_ldap
check_path: login_ldap
dn_string: '{username}'`

but without success. The users are still validated only from local database, but not from LDAP.

There must be somewhere any different error...

Tomas

Reply

Hmmm, that's odd. Are you using different routes on each authenticator, right? I think I need to see your security.yaml file (preserving formatting). Is there a place where you can upload it?

Reply
skocdopolet Avatar

Hello Diego,

Here is my security.yml uploaded...
http://ftp.intevia.cz/secur...

I you need, I can prepare new symfony applcation with this issue only and give you access to repo...

Cheers
Tomas

Reply

Hey man, I think I found your problem! and it's quite sneaky. Under your "access_control" key, you have defined a few paths, but those actually work as a REGEX, and the first path that matches is the winner. In your case, you have ^/login at the top, then you have your Ldap route ^loginldap/ - Can you see the problem here? Your first login route will always win (because of the REGEX thing)

Cheers!

Reply
skocdopolet Avatar
skocdopolet Avatar skocdopolet | MolloKhan | posted hace 1 año | edited

Hello,
OK, I change paths to login

            form_login:
               login_path: login
               check_path: login
               enable_csrf: true

            form_login_ldap:
                service: Symfony\Component\Ldap\Ldap
                login_path: ldaplogin
                check_path: ldaplogin
                search_dn: 'cn=ldapuser,cn=Users,dc=example,dc=com'
                search_password: 'ldapuser_password'
                dn_string: '{username}'```


And under "access_control" like this:
    - { path: ^/login, roles: PUBLIC_ACCESS }
    - { path: ^/ldaplogin, roles: PUBLIC_ACCESS }```

But when I open the page /login, I cannot login from local database and from ldap too (any method does not work)
When I open the /ldaplogin page, I am able to log with local database user only, not from ldap.

Could I upload messages from profiler? I can make some printscreen...

Reply

That's interesting, I was not expecting that behavior. I believe your case fits into having multiple firewalls enabled, one for your login form, and another one for the Ldap form. Here's an example: https://symfony.com/doc/cur...

Reply
skocdopolet Avatar
skocdopolet Avatar skocdopolet | MolloKhan | posted hace 1 año | edited

Hello,

In this example are two firewalls...But I have define entry points in one firewall.


# config/packages/security.yaml
security:
    # ...
    firewalls:
        api:
            pattern: ^/api/
            custom_authenticators:
                - App\Security\ApiTokenAuthenticator
        main:
            lazy: true
            form_login: ~

    access_control:
        - { path: '^/login', roles: PUBLIC_ACCESS }
        - { path: '^/api', roles: ROLE_API_USER }
        - { path: '^/', roles: ROLE_USER }

My goal is to have ONE page for login and use chain_provider. So if the user is not found in local database, then try ldap.

I try to make example application.

Cheers
Tomas

Reply
skocdopolet Avatar

Hey Diego,

I have created a repository where is everything described. Here is https://github.com/skocdopo...

Now it should be everything clear for you.

Thank you

Reply

Hey Tomáš S.!

I noticed your message before Diego did today ;). Thanks for. the repository, that's very helpful. I understand what you're trying to do... and it's oddly complex. With the setup in the repo, what will happen is when you submit the login form, either form_login OR form_login_ldap will FIRST try to authenticate the request. Let's assume form_login is first. If you log in using a user in your database, I bet it will work.

But if you login with a user that's only in LDAP, it will fail, throw an authentication error and NEVER try form_login_ldap.

So... does having the "chain provider" help with this? MAYBE, and this is where my limited knowledge of LDAP comes into play. In theory, when form_login is processing, if it can't find the user in the database, it would then go find it in ldap thanks to the chain provider. Then, effectively, Symfony will call $user->getPassword() on that LdapUser and compare it to the submitted password. And so, in theory, if the password is being loaded from LDAP, this should work. However, I don't think this is the standard way of checking LDAP passwords (I believe this is the standard way: https://github.com/symfony/symfony/blob/f3ec7a0238c2503f1f653c23344660575160ebb6/src/Symfony/Component/Ldap/Security/CheckLdapCredentialsListener.php#L99 ).

So... it might work with a chain provider and form_login only? But I'm not sure... and it seems weird to me. Also, even if you got this working, it would mean that, if the user logs in and is in the database, then the logged in user is App\Entity\User. If the user is in Ldap, it is Symfony\Component\Ldap\Security\LdapUser... which gets weird, because you can't use any custom methods that you have inside of App\Entity\User reliably.

A more common flow that I've seen works like this:

A) You ALWAYS authenticate against LDAP... because that's where user passwords are stored.
B) After login, you load or create a User entity to correspond to this LDAP user.
C) Then, you. are finally logged in AS the Entity User.

I would probably do this via a custom authenticator using https://github.com/symfony/symfony/blob/f3ec7a0238c2503f1f653c23344660575160ebb6/src/Symfony/Component/Ldap/Security/CheckLdapCredentialsListener.php as inspiration for doing the LDAP logic.

But, i'm assuming a lot, so that's enough for now. Let me know if this is helpful :).

Cheers!

Reply
skocdopolet Avatar

Hi Ryan,

Thank you for your very comprehensive answer.

The LDAP authentication is performed using two connections. First connection is created (using the search_dn and search_password parameters from the configuration) to query whether the user exists in the LDAP database. If the given user is found, a second connection is established (using the given user and his password) for check a password. If connection is successfully established, it means that the password is correct. Then is the user successfully authenticated!

I know it's probably best to do the mechanism you're proposing, but in this case I don't need to store logged-in users in a local database...

But I have lot of questions about chain_provider. Why is it implemented at all? What is its purpose?

I mean, isn't this a symfony bug?

Sincerely, Thomas

Reply

Hey @Thomas!

I actually very much appreciate that description of LDAP. I get questions about it somewhat frequently, but have never needed to use it directly myself. Great explanation!

> But I have lot of questions about chain_provider. Why is it implemented at all? What is its purpose?

The user provider, in general, has 2 purposes:

A) It's used to "load the user" for some features like switch_user and remember_me (e.g. take the "username" from the cookie and find that User).

B) Most "authentication mechanisms" (though not a requirement) use this to "load the user" during authentication. For example, form_login and, I think, form_login_ldap use this. However, different authentication mechanisms then have totally different ways for checking the password. And THAT is the part that's causing you trouble.

> I mean, isn't this a symfony bug?

Things are working as designed. However, I DO think that what you're trying to accomplish should be less trouble.

> I know it's probably best to do the mechanism you're proposing, but in this case I don't need to store logged-in users in a local database...

Ah, that's no problem! I was guessing some things about your implementation before, but let me rephrase them. Create a custom authenticator that:

A) Pass the email/username to the UserPassport. That's all your need to do. If you're using the chain provider, then the User will be found in your normal user provider or ldap.
B) For credentials, pass a CustomCredentials. In the callback you pass this, the 2nd argument will be the User object that was found (first will be the submitted password). Based on whether the user was found in the database vs in LDAP, you will have one of two different classes (your entity or a core LdapUser). If normal User, check the password (inject the user password checker service). If Ldap, try to bind the password as you described.

You're still going to be building some things by hand and doing more work than you should need to, but this general plan should work. Let me know what you think - I'm still doing some guessing on your requirements. Also, it IS weird that some users will be logged in via one User class and others via a different LdapUser. That's allowed... but you won't be able to call any methods that both of these do not share.

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