Chapters
-
Course Code
Subscribe to download the code!
Subscribe to download the code!
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeCustom 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:
security: | |
Show Lines
|
// ... lines 2 - 16 |
firewalls: | |
Show Lines
|
// ... lines 18 - 20 |
main: | |
Show Lines
|
// ... lines 22 - 23 |
#entry_point: App\Security\LoginFormAuthenticator | |
Show Lines
|
// ... lines 25 - 27 |
custom_authenticator: | |
# - App\Security\LoginFormAuthenticator | |
Show Lines
|
// ... 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:
security: | |
Show Lines
|
// ... lines 2 - 16 |
firewalls: | |
Show Lines
|
// ... lines 18 - 20 |
main: | |
Show Lines
|
// ... lines 22 - 24 |
form_login: | |
login_path: app_login | |
check_path: app_login | |
Show Lines
|
// ... 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
, orform_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
:
security: | |
Show Lines
|
// ... lines 2 - 16 |
firewalls: | |
Show Lines
|
// ... lines 18 - 20 |
main: | |
Show Lines
|
// ... lines 22 - 23 |
entry_point: form_login | |
Show Lines
|
// ... 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
:
Show Lines
|
// ... 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"> | |
Show Lines
|
// ... lines 10 - 15 |
<div class="col-12"> | |
Show Lines
|
// ... line 17 |
<input type="email" name="email" id="inputEmail" class="form-control" required autofocus> | |
</div> | |
<div class="col-12"> | |
Show Lines
|
// ... line 21 |
<input type="password" name="password" id="inputPassword" class="form-control" required> | |
</div> | |
Show Lines
|
// ... lines 24 - 34 |
</div> | |
</div> | |
</div> | |
{% 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
:
security: | |
Show Lines
|
// ... lines 2 - 16 |
firewalls: | |
Show Lines
|
// ... lines 18 - 20 |
main: | |
Show Lines
|
// ... lines 22 - 24 |
form_login: | |
Show Lines
|
// ... lines 26 - 27 |
username_parameter: email | |
password_parameter: password | |
Show Lines
|
// ... 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.
24 Comments
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!
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!
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!
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!
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!
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!
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
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!
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
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
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?
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)
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
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?
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
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!
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...
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...
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
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
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!
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
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!
"Houston: no signs of life"
Start the conversation!
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.21.6
"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
}
}
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