Skip to content

[Security] User is loaded on every request #43648

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
stlrnz opened this issue Oct 22, 2021 · 6 comments · Fixed by #43992
Closed

[Security] User is loaded on every request #43648

stlrnz opened this issue Oct 22, 2021 · 6 comments · Fixed by #43992

Comments

@stlrnz
Copy link
Contributor

stlrnz commented Oct 22, 2021

Symfony version(s) affected: 5.3.9

Description
After upgrading my application to Symfony 5.3 I tried to enable the new security system. Everything seems to work well. However, I discovered that a fresh user object is loaded by the UserProvider on every request (although it is still beeing loaded from the session).
I'm using REMOTE_USER authentication and a custom User and UserProvider implementation.

How to reproduce
I created a small dummy project to reproduce the problem: https://github.com/stlrnz/test-new-symfony-security

As you can see, there is nothing special in my implementation/configuration:

<?php

declare(strict_types=1);

namespace App\Security;

use Symfony\Component\Security\Core\User\UserInterface;

class User implements UserInterface
{
    private $username;


    public function __construct(string $username)
    {
        $this->username = $username;
    }


    public function getRoles()
    {
        return ['ROLE_USER'];
    }


    public function getPassword()
    {
        return null;
    }


    public function getSalt()
    {
        return null;
    }


    public function eraseCredentials()
    {
    }


    public function getUsername()
    {
        return $this->username;
    }


    public function getUserIdentifier()
    {
        return $this->getUsername();
    }
}
<?php

declare(strict_types=1);

namespace App\Security;

use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

final class UserProvider implements UserProviderInterface
{
    private $logger;


    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }


    public function refreshUser(UserInterface $user)
    {
        $this->logger->debug('Refreshing user ' . $user->getUserIdentifier());

        return $user;
    }


    public function supportsClass(string $class)
    {
        return $class === User::class;
    }


    public function loadUserByUsername(string $username)
    {
        $this->logger->debug('Loading user ' . $username);

        return new User($username);
    }


    public function loadUserByIdentifier(string $username)
    {
        return $this->loadUserByUsername($username);
    }
}
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:
        custom_remote_user_provider:
            id: App\Security\UserProvider
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            pattern: ^/
            remote_user:
                provider: custom_remote_user_provider

            # 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: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }

Additional context
As you can see the UserProvider writes two log messages for debugging.
When using the new security system the user is loaded on the first request:

Loading user stlrnz

and refreshed and loaded on the following:

Refreshing user stlrnz
Loading user stlrnz

When using the old system by configuring

security:
    enable_authenticator_manager: false

the user is still loaded on the first request

Loading user stlrnz

and refreshed on the following (no loading as expected).

Refreshing user stlrnz

Im my real application, loading a user is a very complex operation (requires some webservice calls etc.). And therfore it should not be done on every request. Is there a way to achive this?

I tried to understand why this happens in the new system. It seems that the Authenticator always triggers the load of the user through the passport to create an Authenticated Token.

return new PreAuthenticatedToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());

@xabbuh
Copy link
Member

xabbuh commented Oct 22, 2021

Can you try to debug where the call to loadUserByUsername() is coming from?

@stlrnz
Copy link
Contributor Author

stlrnz commented Oct 22, 2021

Sure. It's starting from the AuthenticatorManager which tries to create an AuthenticatedToken

$authenticatedToken = $authenticator->createAuthenticatedToken($passport, $this->firewallName);

return new PreAuthenticatedToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());

$this->user = $this->getBadge(UserBadge::class)->getUser();

$this->user = ($this->userLoader)($this->userIdentifier);

Call Stack:

UserProvider.php:38, App\Security\UserProvider->loadUserByUsername()
UserProvider.php:46, App\Security\UserProvider->loadUserByIdentifier()
UserBadge.php:67, Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge->getUser()
Passport.php:56, Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport->getUser()
AbstractPreAuthenticatedAuthenticator.php:105, Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator->createAuthenticatedToken()
AuthenticatorManager.php:193, Symfony\Component\Security\Http\Authentication\AuthenticatorManager->executeAuthenticator()
AuthenticatorManager.php:152, Symfony\Component\Security\Http\Authentication\AuthenticatorManager->executeAuthenticators()
AuthenticatorManager.php:132, Symfony\Component\Security\Http\Authentication\AuthenticatorManager->authenticateRequest()
AuthenticatorManagerListener.php:40, Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener->authenticate()
WrappedLazyListener.php:49, Symfony\Bundle\SecurityBundle\Debug\WrappedLazyListener->authenticate()
AbstractListener.php:26, Symfony\Bundle\SecurityBundle\Debug\WrappedLazyListener->__invoke()
TraceableFirewallListener.php:62, Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener->callListeners()
Firewall.php:86, Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener->onKernelRequest()
WrappedListener.php:117, Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke()
EventDispatcher.php:230, Symfony\Component\EventDispatcher\EventDispatcher->callListeners()
EventDispatcher.php:59, Symfony\Component\EventDispatcher\EventDispatcher->dispatch()
TraceableEventDispatcher.php:151, Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher->dispatch()
HttpKernel.php:132, Symfony\Component\HttpKernel\HttpKernel->handleRaw()
HttpKernel.php:78, Symfony\Component\HttpKernel\HttpKernel->handle()
Kernel.php:199, App\Kernel->handle()
HttpKernelRunner.php:37, Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner->run()
autoload_runtime.php:35, require_once()
index.php:5, {main}()

@stlrnz
Copy link
Contributor Author

stlrnz commented Nov 2, 2021

Is there anything more I can do to help on this?

@a1812
Copy link
Contributor

a1812 commented Nov 6, 2021

@stlrnz
Copy link
Contributor Author

stlrnz commented Nov 8, 2021

Thank you for the response, but this does not help. Actually, it does the opposite from what I want: It tells the firewall to be stateless and load the user on every request.

I want the user to be loaded only on the first request and refreshed on the following. That's what the old firewall did.
With the new firewall the user is loaded on the first request and refreshed and loaded on every following.

That seems like a bug to me. It would be great if this could be fixed before releasing 5.4/6.0. I really would like to contribute a fix, but don't know enough about the internals of the new security system. Maybe @wouterj or someone other could look into this?

@stlrnz
Copy link
Contributor Author

stlrnz commented Nov 9, 2021

I created another scenario without using custom code to make sure there is no error in my implementation. I configured http_basic auth and a memory user provider.

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:
            algorithm: 'plaintext'
    # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
    providers:
        memory_users:
            memory:
                users:
                    demo: { password: '123', roles: ['ROLE_USER'] }
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            pattern: ^/
            http_basic:
                realm: Test
                provider: memory_users

            # 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: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/, roles: ROLE_USER }

When setting break points to Symfony\Component\Security\Core\User\InMemoryUserProviders loadUserByIdentifier() and refreshUser methods you can see that

  • loadUserByIdentifier() is called on the first request
  • refreshUser() and loadUserByIdentifier() is called on the following requests

After activating the old firewall

security:
    enable_authenticator_manager: false

the behavior changes, so that

  • loadUserByIdentifier() is called on the first request
  • only refreshUser() is called on the following requests

So, the Bug (?) seems not only to occour when using REMOTE_USER auth. Is it really intended that the user is fully loaded on every request using the new firewall?

Any feedback from the creators would be helpful.

nicolas-grekas added a commit that referenced this issue Nov 29, 2021
…TE_USER authentication (stlrnz)

This PR was squashed before being merged into the 5.3 branch.

Discussion
----------

[Security] Do not overwrite already stored tokens for REMOTE_USER authentication

| Q             | A
| ------------- | ---
| Branch?       | 5.3
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #43648
| License       | MIT

As described in #43648 the user is currently loaded on every request for REMOTE_USER authentication.
Thanks to `@wouterj` for confirming me on Slack that this seems weird. So, I looked deeper into this.

I found out that other Authenticators tell the AuthenticatorManager only under special conditions (like matching route etc.) that they support the current request. However, the `AbstractPreAuthenticatedAuthenticator` is not so picky. In consequence, the user is authenticated again on every request.

Inspired by `RememberMeAuthenticator`, this PR adds an addition check to `AbstractPreAuthenticatedAuthenticator` to solve this issue.
https://github.com/symfony/symfony/blob/07a891f6c57d9da513d75402f2aa2da73d897044/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php#L63

Commits
-------

ce1ee74 [Security] Do not overwrite already stored tokens for REMOTE_USER authentication
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants