FriendsOfSymfony/FOSUserBundle

Upgraded to Symfony 6 from 5.4, can no longer log in (Error: The presented password is invalid.)

kcaporaso opened this issue · 3 comments

Symfony FOSUserBundle versions: Symfony: v6.3.12, FOSUserBundle: v3.4.0

Description of the problem including expected versus actual behavior:
After upgrading Symfony 5.4 to 6.3.12 and FOSUserBundle from 3.2.1 to 3.4.0 I am unable to log into the symfony application.
I am not sure if FOSUserBundle no longer works with 6.0+ or not, perhaps I need to implement my own Authenticator?
It might be a simple oversight with the yaml file or perhaps I need to implement something?

I would expect to be able to login as usual, however I know there are quite a few changes to the Authentication system when moving from 5.4 to 6.0 so please elaborate if I missed something.

Thank you.

Provide logs (if relevant):

[2024-10-17 07:49:21] security.DEBUG: Checking for authenticator support. {"firewall_name":"main","authenticators":2} []
[2024-10-17 07:49:21] security.DEBUG: Checking support on authenticator. {"firewall_name":"main","authenticator":"Symfony\\Component\\Security\\Http\\Authenticator\\FormLoginAuthenticator"} []
[2024-10-17 07:49:21] security.DEBUG: Checking support on authenticator. {"firewall_name":"main","authenticator":"Symfony\\Component\\Security\\Http\\Authenticator\\RememberMeAuthenticator"} []
[2024-10-17 07:49:21] security.DEBUG: Authenticator does not support the request. {"firewall_name":"main","authenticator":"Symfony\\Component\\Security\\Http\\Authenticator\\RememberMeAuthenticator"} []

...
[2024-10-17 07:49:21] app.DEBUG: Notified event "Symfony\Component\Security\Http\Event\CheckPassportEvent" to listener "Symfony\Component\Security\Http\EventListener\UserProviderListener::checkPassport". {"event":"Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent","listener":"Symfony\\Component\\Security\\Http\\EventListener\\UserProviderListener::checkPassport"} []
[2024-10-17 07:49:21] app.DEBUG: Notified event "Symfony\Component\Security\Http\Event\CheckPassportEvent" to listener "Symfony\Component\Security\Http\EventListener\CsrfProtectionListener::checkPassport". {"event":"Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent","listener":"Symfony\\Component\\Security\\Http\\EventListener\\CsrfProtectionListener::checkPassport"} []
[2024-10-17 07:49:21] app.DEBUG: Notified event "Symfony\Component\Security\Http\Event\CheckPassportEvent" to listener "Symfony\Component\Security\Http\EventListener\UserCheckerListener::preCheckCredentials". {"event":"Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent","listener":"Symfony\\Component\\Security\\Http\\EventListener\\UserCheckerListener::preCheckCredentials"} []
[2024-10-17 07:49:21] app.DEBUG: Notified event "Symfony\Component\Security\Http\Event\CheckPassportEvent" to listener "Symfony\Component\Security\Http\EventListener\CheckCredentialsListener::checkPassport". {"event":"Symfony\\Component\\Security\\Http\\Event\\CheckPassportEvent","listener":"Symfony\\Component\\Security\\Http\\EventListener\\CheckCredentialsListener::checkPassport"} []
[2024-10-17 07:49:21] security.INFO: Authenticator failed. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException(code: 0): The presented password is invalid. at /private/var/www/dms-portal/digiflo/vendor/symfony/security-http/EventListener/CheckCredentialsListener.php:69)","authenticator":"Symfony\\Component\\Security\\Http\\Authenticator\\FormLoginAuthenticator"} []
[2024-10-17 07:49:21] security.DEBUG: Authentication failure, redirect triggered. {"failure_path":"/login"} []

security.yaml snippet BEFORE:

# you can read more about security in the related section of the documentation
# http://symfony.com/doc/current/book/security.html
security:
    enable_authenticator_manager: true
    # http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password
    #encoders:

    # http://symfony.com/doc/current/book/security.html#hierarchical-roles
    role_hierarchy:
        ROLE_USER: [ROLE_ADMIN]

    # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
    #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'
        FOS\UserBundle\Model\UserInterface: sha512
    # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

        #in_memory:
        #    memory:
        #        users:
        #            user:  { password: userpass, roles: [ 'ROLE_USER' ] }
        #            admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }

    # the main part of the security, where you can set up firewalls
    # for specific sections of your app
    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern:  ^/(_(profiler|wdt|fragment|error)|css|images|js)/
            security: false
        main:
            lazy: true
            #provider: users_in_memory -- ./bin/console barks about this setting.

            pattern: ^/
            user_checker: fos_user.user_checker
            entry_point: app.handler.entry_point
            remember_me:
                secret: "%env(resolve:APP_SECRET)%"
                path: /iframe
            form_login:
                provider: fos_userbundle
                enable_csrf: true
                #csrf_token_generator: security.csrf.token_manager # pre2.8 form.csrf_provider
                success_handler: app.handler.login_success_handler
                use_referer: true
            logout:
                path: fos_user_security_logout

    # with these settings you can restrict or allow access for different parts
    # of your application based on roles, ip, host or methods
    # http://symfony.com/doc/current/cookbook/security/access_control.html
    access_control:
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/registration, role: IS_AUTHENTICATED_ANONYMOUSLY }


security.yaml snippet AFTER:

security:
    # http://symfony.com/doc/current/book/security.html#hierarchical-roles
    role_hierarchy:
        ROLE_USER: [ROLE_ADMIN]

    # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
   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'
        FOS\UserBundle\Model\UserInterface: sha512

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

    # the main part of the security, where you can set up firewalls
    # for specific sections of your app
    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern:  ^/(_(profiler|wdt|fragment|error)|css|images|js)/
            security: false
        main:
            lazy: true
            #provider: users_in_memory -- ./bin/console barks about this setting.

            pattern: ^/
            user_checker: fos_user.user_checker
            entry_point: app.handler.entry_point
            remember_me:
                secret: "%env(resolve:APP_SECRET)%"
                path: /iframe
            form_login:
                provider: fos_userbundle
                enable_csrf: true
                #csrf_token_generator: security.csrf.token_manager # pre2.8 form.csrf_provider
                success_handler: app.handler.login_success_handler
                use_referer: true
            logout:
                path: fos_user_security_logout

    access_control:
        - { path: ^/login$, role: PUBLIC_ACCESS }
        - { path: ^/register, role: PUBLIC_ACCESS }
        - { path: ^/resetting, role: PUBLIC_ACCESS }
        - { path: ^/registration, role: PUBLIC_ACCESS }
stof commented

Please show your SecurityBundle configuration both before and after the migration.

The authentication is not a feature provided by FOSUserBundle but by the SecurityBundle of Symfony.
The logs say BadCredentialsException(code: 0): The presented password is invalid., which indicates a failure when the core authenticator of Symfony validates credentials.

@stof thank you, I've added the BEFORE and AFTER now, I think you're referring to the security.yaml file. I think I mainly changed to now using PUBLIC_ACCESS vs the older IS_AUTHENTICATED_ANONYMOUSLY. Let me know if I misunderstood your request.

Got it resolved.

CheckCredentialsListener.php and MessageDigestPasswordHasher.php is what tipped me off on the solution. Got a breakpoint down in there and saw the salt was passing as null. I needed to update my ./src/Entity/User.php to implement: LegacyPasswordAuthenticatedUserInterface so that it would take the salt from the DB user.

if (!$this->hasherFactory->getPasswordHasher($user)->verify($user->getPassword(), $presentedPassword, 
    $user instanceof LegacyPasswordAuthenticatedUserInterface ? $user->getSalt() : null)) {
                throw new BadCredentialsException('The presented password is invalid.');
     
}

// hash_equals was failing with null salt because i'm a legacy system.
public function verify(string $hashedPassword, #[\SensitiveParameter] string $plainPassword, ?string $salt = null): bool
{
        if (\strlen($hashedPassword) !== $this->hashLength || str_contains($hashedPassword, '$')) {
            return false;
        }

        return !$this->isPasswordTooLong($plainPassword) && hash_equals($hashedPassword, $this->hash($plainPassword, $salt));
}

I realize that the newer user security bundle and internal symfony authenticators handle the salting, but this is a system I've migrated and maintained for about 10 years now. Started in symfony 2.x and am one step away from 7.x now.

Thanks for always being here, @stof , have admired yours and all the symfony greats for many years now.