Skip to content

[IMPERSONATOR] user with prop-type user causes the impersonating to fail #50758

Closed
@ludescher

Description

@ludescher

Symfony version(s) affected

6.2.9

Description

I just came across a weird bug or behaviour.

The issue is caused by the entity User with a property of type User.
Let's assume you have multiple security roles, so user1 has role X and user2 has role Y.

You are logged in as user1 and you start to impersonate user2, everything still works, but if you stop the impersonation an exception will be thrown.

How to reproduce

A few instructions to reproduce the issue.

Or reproduce the bug with a sample project.

1. install symfony

$ symfony new --webapp my_project
$ cd my_project
$ composer require symfony/security-bundle

2. create a user via

Follow the instructions here.

$ php bin/console make:migration
$ php bin/console doctrine:migrations:migrate

3. create basic login page

Follow the instructions here

4. configure security.yaml to match

security:
    role_hierarchy:
        ROLE_SYSADMIN:        [ROLE_USER, ROLE_ALLOWED_TO_SWITCH]
        ROLE_SALE:             ROLE_USER

    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
            switch_user: true
    access_control:
        - { path: ^/login,                   roles: PUBLIC_ACCESS }
        - { path: ^/,                        roles: ROLE_USER }

5. create two Users

test1@symfony.com with ROLE ROLE_SYSADMIN

and

test2@symfony.com with ROLE ROLE_SALE

6. finally create a new Controller HomeController

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class HomeController extends AbstractController {

    #[Route('/', name: 'app_home')]
    public function home(): Response {
        return $this->render('home.html.twig');
    }
}
{# templates/home.html.twig #}
{% extends 'base.html.twig' %}

{# ... #}

{% block body %}
    <h1>
        {{ app.user.email }}
    </h1>

    <div style="margin: 25px">
        {% if is_granted('IS_IMPERSONATOR') %}
            <a href="{{ path('app_home', {'_switch_user': '_exit'}) }}">
                Stop impersonating
            </a>
        {% endif %}
    </div>

    <div style="margin: 25px">
        {% if app.user.email != 'test2@symfony.com' %}
            <a class="btn" href="{{ path('app_home', { '_switch_user': 'test2@symfony.com' }) }}">
                Start impersonating
            </a>
        {% endif %}
    </div>
{% endblock %}

7. testing

Now we have everything ready to start testing.

Login as test1@symfony.com and start impersonating.

Until now everything should work just fine. But if we modify the User entity and add a new property created_by with type User, this causes the impersonating to fail.

+    #[ORM\ManyToOne(targetEntity: self::class)]
+    private ?self $created_by = null;

Right after we stop the impersonating of a user we get an error.
Oddly, when we refresh the page, the problem goes away. Therefore, the problem definitely lies in the user transition.

App\Entity\User::getPassword(): Return value must be of type string, null returned

Possible Solution

When I looked at the callstack, I initially assumed that the error must come from the security package.
But I've already spoken to the Symfony security team and they assured me that it's not a security issue and it might be in the User class that returns an invalid value.

At this point I could not find any indication of where the problem really comes from.

Additional Context

callstack:

App/Entity/User.php&line=###

    /**
     * @see PasswordAuthenticatedUserInterface
     */
    public function getPassword(): string {
        return $this->password; // <<<
    }

in vendor/symfony/security-http/Firewall/ContextListener.php -> getPassword (line 290)

in vendor/symfony/security-http/Firewall/ContextListener.php :: hasUserChanged (line 212)

in vendor/symfony/security-http/Firewall/ContextListener.php -> refreshUser (line 127)

in vendor/symfony/security-bundle/Debug/WrappedLazyListener.php -> authenticate (line 46)

in vendor/symfony/security-bundle/Security/LazyFirewallContext.php -> authenticate (line 60)

in vendor/symfony/security-bundle/Debug/TraceableFirewallListener.php -> __invoke (line 70)

in vendor/symfony/security-http/Firewall.php -> callListeners (line 92)

in vendor/symfony/event-dispatcher/Debug/WrappedListener.php -> onKernelRequest (line 116)

in vendor/symfony/event-dispatcher/EventDispatcher.php -> __invoke (line 206)

in vendor/symfony/event-dispatcher/EventDispatcher.php -> callListeners (line 56)

in vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php -> dispatch (line 127)

in vendor/symfony/http-kernel/HttpKernel.php -> dispatch (line 139)

in vendor/symfony/http-kernel/HttpKernel.php -> handleRaw (line 74)

in vendor/symfony/http-kernel/Kernel.php -> handle (line 184)

in vendor/symfony/runtime/Runner/Symfony/HttpKernelRunner.php -> handle (line 35)

in vendor/autoload_runtime.php -> run (line 29)

in public/index.php (line 5)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions