Description
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)