Symfony 6: Check DB Entity when LDAP login fails or succeeds - symfony

I'm banging my head on the keyboard for a while now trying to solve something that should be simple but for some reason it has not been.
Here is the scenario. A user will use a form on my website to login (username/pwd). And the following workflow should work:
Try to authenticate the user on the LDAP server:
1.1. If it passes:
1.1.1. Check if there is an user with the same username on our database;
1.1.1.1. If it has, load some data and log in the user;
1.1.1.2. If it has not, throws a failed login.
1.2. If it fails:
1.2.1. Check if there is an user with the same username on our database;
1.2.1.1. If it exists
1.2.1.1.1 Check if the pwd match the save one, if it matches, log the user
1.2.1.2. If it has not, throws a failed login.
So basically, I'm trying to authenticate the user on the LDAP server, and if that fails, I want to try to authenticate locally.
I tried configuring multiple User Providers, using a chain provider, but it does not work. If the first UserProvider fails, it never hits the second one.
# config/security.yaml
security:
providers:
# base local provider
app_user_provider:
entity:
class: App\Entity\User\User
property: username
# LDAP provider
my_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: '%env(resolve:LDAP_BASEDN)%'
search_dn: '%env(resolve:LDAP_SEARCHDN)%'
search_password: '%env(resolve:LDAP_SEARCHPWD)%'
default_roles: ROLE_USER
extra_fields: '%env(resolve:LDAP_EXTRAFIELDS)%'
uid_key: uid
filter: null
# chain
all_users:
chain:
providers: ['my_ldap', 'app_user_provider']
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: all_users
form_login_ldap:
enable_csrf: true
default_target_path: app_homepage
login_path: app_login
check_path: app_login
service: Symfony\Component\Ldap\Ldap
username_parameter: username
password_parameter: password
dn_string: '%env(resolve:LDAP_BASEDN)%'
query_string: '%env(resolve:LDAP_QUERYSTR)%'
search_dn: '%env(resolve:LDAP_SEARCHDN)%'
search_password: '%env(resolve:LDAP_SEARCHPWD)%'
What am I missing? I found a couple of answers here (like Symfony 5: ldap authentication with custom user entity) but it uses the old Authenticator Guard that does not exists.

Related

Symfony 4 security login allow checking for entity property being false as well as username

Just stumbled across an issue, the system i've developed allows multiple user accounts with the same email address, only the user entity has a soft delete flag. Now if there are 2 users with the same email but 1 of them is flagged as deleted the login is picking up the first row in the database which is the deleted one, rather than being able to check if deleted = false during login
I have this in the security yaml:
security:
providers:
chain_provider:
chain:
providers: [app_user_username,app_user_email]
app_user_username:
entity:
class: App\Entity\User
property: username
app_user_email:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
provider: chain_provider
user_checker: App\Security\UserChecker
stateless: true
json_login:
failure_handler: App\Security\LoginFailureHandler
check_path: /user/login
guard:
authenticators:
- App\Security\TokenAuthenticator
encoders:
App\Entity\User:
algorithm: bcrypt
cost: 15
So it allows login using json for email or username, but symfony does the user selection itself before it gets to the final controller, what i need it to do is select the user in this sort of fashion:
SELECT * FROM user WHERE (email = 'email#email.com' OR username = 'username') AND deleted = 0
NOTE: There will never be more than 1 user with the same email/username that is not deleted but there will likely be multiple deleted ones with a single active one.

How to restrict json_login route with only POST method?

I use lexik JWT to secure my api and i can login with it.
But the login route works with get and post request when i test with postman.
I want to restrict with POST only.
To do so i tried to add - { path: ^/auth/login_check, roles: PUBLIC_ACCESS, methods:['POST'] } in the access control but it does not do the trick.
I have no error but i still can do get request and have my token back.
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'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
# used to reload user from session & other features (e.g. switch_user)
# used to reload user from session & other features (e.g. switch_user)
# used to reload user from session & other features (e.g. switch_user)
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api_login:
pattern: ^/auth/login
provider: app_user_provider
stateless: true
json_login:
username_path: email
check_path: /auth/login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
jwt: ~
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/auth/login, roles: PUBLIC_ACCESS }
- { path: ^/auth/login_check, roles: PUBLIC_ACCESS, methods:['POST'] }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
So i found the answer myself. The methods option in access_control is a matching option and it does not restrict.
To restrict a route to a specific option, it has to be set in the route like this
#[Route('/my_route', name: 'my_route',methods: ['POST'])]

Symfony 5.2 Validate json_login Request

I am developing a RESTFUL API in Symfony 5.2 that allows users to authenticate with username and password in order to access it.
My firewall config for this looks like:
login:
pattern: '^/v1/login'
methods: [POST]
anonymous: true
stateless: true
provider: user_provider
user_checker: App\Security\UserChecker
json_login:
check_path: '/v1/login'
success_handler: App\Security\Handler\AuthenticationSuccessHandler
failure_handler: lexik_jwt_authentication.handler.authentication_failure
I have created a validator and a matching route so that I can ensure users submit valid data, like not submitting empty passwords:
v1_login:
path: /v1/login
methods: POST
controller: App\Http\Token\Action\LoginAction
defaults:
_format: json
All my other routes are setup the same way with validators, but this one is not being called. I think it's Symfony is ignoring this and using the user_provider, user_checker and success handler to manage it all. But it means I can't validate the request first.
Is there something else I need to do?

The contents of the service container differ for no obvious reason

I work with Symfony 5.1.2 and my tests worked until I introduced a chain of user providers.
I derive all of my test classes from a class I created in order to put common methods and properties.
Among these methods there is a method that I use to connect a user.
public function connectUser(string $username)
{
$userProvider = static::$container->get(UserProviderInterface::class);
$user = $userProvider->loadUserByUsername($username);
$this->assertNotNull($user);
$this->kernelBrowser->loginUser($user);
}
With the following settings there were no problems
providers:
backend_users:
memory:
users:
admin#localhost.dev: { password: '$argon2id$v=19$m=65536,t=4,p=1$c0F2RmVYa21RclE4ZXJkTA$mvXd/skXaV9w1rqmWb6B5MTtgkP86inWSkj0E8hjtTA', roles: ['ROLE_ADMIN'] }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
logout:
path: security.authentication.logout
provider: backend_users
guard:
authenticators:
- App\Security\LoginFormAuthenticator
I then introduced a new user source ; a database.
With the following settings there is a problem
providers:
# used to reload user from session & other features (e.g. switch_user)
backend_users:
memory:
users:
admin#localhost.dev: { password: '$argon2id$v=19$m=65536,t=4,p=1$c0F2RmVYa21RclE4ZXJkTA$mvXd/skXaV9w1rqmWb6B5MTtgkP86inWSkj0E8hjtTA', roles: ['ROLE_ADMIN'] }
frontend_users:
entity:
class: App\Entity\User
property: email
all_users:
chain:
providers: ['backend_users','frontend_users']
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
logout:
path: security.authentication.logout
guard:
authenticators:
- App\Security\LoginFormAuthenticator
provider: all_users
With this configuration, the service container no longer seems to contain the UserProviderInterface class. Because I receive this message :
You have requested a non-existent service "Symfony\Component\Security\Core\User\UserProviderInterface".
I figured I might need to implement a custom user provider, but after doing some research in the source code, I realized that there is a class that seems specific to user provider chains that is :Symfony\Component\Security\Core\User\ChainUserProvider
As the name suggests, the UserProviderInterface class is an interface, so I'm not supposed to worry about the internal implementation.
Why is this interface no longer in the service container ? How to reintroduce it properly ?
Thank you !
Suppose you have multiple providers all implementing UserProviderInterface. When you type hint against the interface, which service do you want injected and how would the container know? The container does not know anything about your firewalls so it can't guess that you want the chain provider. So things worked when you only had one provider but will fail when you have multiple providers.
The same question arises anytime you have multiple implementations of the same interface. You either need to typehint against a specific implementation or inject the desired service manually or create an alias which will tie the interface to one specific implementation.
In your case:
bin/console debug:container | grep UserProvider
doctrine.orm.security.user.provider Symfony\Bridge\Doctrine\Security\User\EntityUserProvider
security.user.provider.chain Symfony\Component\Security\Core\User\ChainUserProvider
security.user.provider.concrete.all_users Symfony\Component\Security\Core\User\ChainUserProvider
security.user.provider.concrete.backend_users Symfony\Component\Security\Core\User\InMemoryUserProvider
security.user.provider.concrete.frontend_users Symfony\Bridge\Doctrine\Security\User\EntityUserProvider
security.user.provider.in_memory Symfony\Component\Security\Core\User\InMemoryUserProvider
security.user.provider.ldap Symfony\Component\Ldap\Security\LdapUserProvider
security.user.provider.missing Symfony\Component\Security\Core\User\MissingUserProvider
security.user_providers Symfony\Component\Security\Core\User\ChainUserProvider
If you always want the all_users chain provider to be injected then add an alias:
config/services.yaml
Symfony\Component\Security\Core\User\UserProviderInterface : '#security.user.provider.concrete.all_users'
And you should be good to go.

Symfony4 Two authentification methods in security

I want two authentications methods in my application.
One for the entity User, and other (admin) with a plaintext.
Very simple.
Thus, when I configure security.yaml, I specify the providers:
security:
providers:
user:
entity:
class: App\Entity\User
property: username
in_memory:
memory:
users:
admin:
password: admin
roles: 'ROLE_ADMIN'
encoders:
App\Entity\User: bcrypt
Symfony\Component\Security\Core\User\User: plaintext
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
admin:
provider: in_memory
pattern: ^/admin/
guard:
provider: in_memory
form_login:
login_path: admin_login
check_path: admin_login
logout:
path: /admin/logout
target: /
default:
provider: user
anonymous: ~
guard:
provider: user
form_login:
login_path: login
check_path: login
default_target_path: login_redirect
use_referer: true
logout:
path: /logout
target: /
access_control:
- { path: ^/admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/dashboard, roles: ROLE_USER }
And return the error:
In GuardAuthenticationFactory.php line 121:
Because you have multiple guard configurators, you need to set the "guard.e
ntry_point" key to one of your configurators ()
Then, if I have to set the guard.entry_point, I need do something like this:
admin:
entry_point: app.form_admin_authenticator
main:
entry_point: app.form_user_authenticator
And therefore, if I undestard, I need to configure a Authentication Listener like this: https://symfony.com/doc/current/components/security/authentication.html
(btw, this particular help page is very ambiguous and incomplete)
Is it necessary? It seems too complex for my purpose
I ran into this particular error. My situation might be a little different, but I had a similar need to authenticate using different authentication strategies depending on the entry point to the application.
One thing your config doesn't include is a reference to any Guard Authenticator objects. See this documentation for an intro to what role those objects play, and how to define them. Symfony's Security package is pretty complicated, and I found that using Guard Authenticators made the process a lot simpler for my use case.
Here is an example of a security.yaml config referencing two different authenticators. The entry_point configuration tells Symfony which one to try first, because in my particular case, Symfony otherwise wouldn't know which authentication strategy to apply first to an incoming request.
security:
providers:
user:
id: App\My\UserProviderClass
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
logout:
path: app_logout
guard:
entry_point: App\My\MainAuthenticator
authenticators:
- App\My\MainAuthenticator
- App\My\OtherAuthenticator
Custom Guard Authenticators contain a method called supports. This method takes the incoming request as its only argument, and returns true or false based on whether the given authenticator should be applied to the incoming request. A common practice might be to check the request's Symfony route name (as defined by the controller) or perhaps something like the full URI for the request. For example:
/**
* Does the authenticator support the given Request?
*
* If this returns false, the authenticator will be skipped.
*
* #param Request $request
*
* #return bool
*/
public function supports(Request $request): bool
{
$matchesMyRoute = 'some_route_name' ===
$request->attributes->get('_route');
$matchesMyUri = '/path/to/secured/resource' ===
$request->getUri();
return $matchesMyRoute || $matchesMyUri;
}
You can imagine that if multiple Guard Authenticators exist in the same application, it's likely the case that one would only want them to apply to a request of a certain type, whether the differentiation is based on the kind of auth applied (eg. a header with an API key vs. a stateful session cookie), whether the difference is more about the specific route being hit, or perhaps a combination of factors.
In this case, telling Symfony which Guard Authenticator to try first may be important for your security strategy, or may have performance ramifications. For example, if you had two authenticators, and one had to hit the database to verify a stateful session, but another could verify the request's authentication statelessly, eg. by verifying a JWT's signature, you'd probably want to make the JWT authenticator run first, because it might not need to make a round trip to the database to authenticate the request.
See this article for a deeper explanation: https://symfonycasts.com/screencast/symfony-security/entry-point

Resources