From 7b68d3a767351e659a12c94c1fdf80e3a203e612 Mon Sep 17 00:00:00 2001 From: Andrej Hudec Date: Mon, 10 Nov 2025 17:04:06 +0100 Subject: [PATCH] Add support for impersonation users --- src/Resources/config/authorization.php | 10 +++ .../Authentication/JwtAuthentication.php | 29 ++++++- .../Passport/Badge/ImpersonationBadge.php | 75 +++++++++++++++++++ .../EventListener/UserProviderListener.php | 33 ++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/Security/Authentication/Passport/Badge/ImpersonationBadge.php create mode 100644 src/Security/Http/EventListener/UserProviderListener.php diff --git a/src/Resources/config/authorization.php b/src/Resources/config/authorization.php index 897b562..388a7fc 100644 --- a/src/Resources/config/authorization.php +++ b/src/Resources/config/authorization.php @@ -9,6 +9,8 @@ use AnzuSystems\AuthBundle\Event\Listener\LogoutListener; use AnzuSystems\AuthBundle\Security\AuthenticationFailureHandler; use AnzuSystems\AuthBundle\Security\AuthenticationSuccessHandler; +use AnzuSystems\AuthBundle\Security\Http\EventListener\UserProviderListener; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; return static function (ContainerConfigurator $configurator): void { $services = $configurator->services(); @@ -38,4 +40,12 @@ ->autoconfigure() ->autowire() ; + + $services + ->set(UserProviderListener::class) + ->args([ + service('security.user_providers'), + ]) + ->tag('kernel.event_listener', ['event' => CheckPassportEvent::class, 'priority' => 1024, 'method' => 'checkPassport']) + ; }; diff --git a/src/Security/Authentication/JwtAuthentication.php b/src/Security/Authentication/JwtAuthentication.php index 49338b4..57ff58e 100644 --- a/src/Security/Authentication/JwtAuthentication.php +++ b/src/Security/Authentication/JwtAuthentication.php @@ -6,6 +6,7 @@ use AnzuSystems\AuthBundle\Contracts\AnzuAuthUserInterface; use AnzuSystems\AuthBundle\Exception\NotFoundAccessTokenException; +use AnzuSystems\AuthBundle\Security\Authentication\Passport\Badge\ImpersonationBadge; use AnzuSystems\AuthBundle\Util\HttpUtil; use AnzuSystems\AuthBundle\Util\JwtUtil; use Lcobucci\JWT\Token; @@ -13,15 +14,19 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken; final class JwtAuthentication extends AbstractAuthenticator { + public const string CLAIM_IMPERSONATED_BY = 'impersonated_by'; + public function __construct( private readonly HttpUtil $httpUtil, private readonly JwtUtil $jwtUtil, @@ -42,10 +47,17 @@ public function authenticate(Request $request): Passport } /** @psalm-suppress ArgumentTypeCoercion */ - return new Passport( + $passport = new Passport( new UserBadge((string) $jwtToken->claims()->get(RegisteredClaims::SUBJECT)), new CustomCredentials($this->checkCredentials(...), $jwtToken), ); + + $impersonatedBy = (string) $jwtToken->claims()->get(self::CLAIM_IMPERSONATED_BY, 0); + if (false === empty($impersonatedBy)) { + $passport->addBadge(new ImpersonationBadge($impersonatedBy)); + } + + return $passport; } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?JsonResponse @@ -60,6 +72,21 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio ], Response::HTTP_UNAUTHORIZED); } + public function createToken(Passport $passport, string $firewallName): TokenInterface + { + if ($passport->hasBadge(ImpersonationBadge::class)) { + $impersonationBadge = $passport->getBadge(ImpersonationBadge::class); + /** @var ImpersonationBadge $impersonationBadge */ + $originalUser = $impersonationBadge->getOriginalUser(); + + $originalToken = new PostAuthenticationToken($originalUser, $firewallName, $originalUser->getRoles()); + + return new SwitchUserToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles(), $originalToken); + } + + return new PostAuthenticationToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles()); + } + private function checkCredentials(Token\Plain $token, AnzuAuthUserInterface $user): bool { if (false === $user->isEnabled()) { diff --git a/src/Security/Authentication/Passport/Badge/ImpersonationBadge.php b/src/Security/Authentication/Passport/Badge/ImpersonationBadge.php new file mode 100644 index 0000000..3d691b3 --- /dev/null +++ b/src/Security/Authentication/Passport/Badge/ImpersonationBadge.php @@ -0,0 +1,75 @@ +userLoader = $userLoader; + } + + /** + * @throws AuthenticationException when the user cannot be found + */ + public function getOriginalUser(): UserInterface + { + if ($this->originalUser instanceof UserInterface) { + return $this->originalUser; + } + + if (null === $this->userLoader) { + throw new \LogicException(\sprintf('No user loader is configured, did you forget to register the "%s" listener?', UserProviderListener::class)); + } + + $user = ($this->userLoader)($this->userIdentifier); + + // No user has been found via the $this->userLoader callback + if (null === $user) { + $exception = new UserNotFoundException(); + $exception->setUserIdentifier($this->userIdentifier); + + throw $exception; + } + + if (!$user instanceof UserInterface) { + throw new AuthenticationServiceException( + \sprintf('The user provider must return a UserInterface object, "%s" given.', get_debug_type($user)) + ); + } + + return $this->originalUser = $user; + } + + public function getUserLoader(): ?callable + { + return $this->userLoader; + } + + public function setUserLoader(callable $userLoader): void + { + $this->userLoader = $userLoader; + } + + public function isResolved(): bool + { + return true; + } +} diff --git a/src/Security/Http/EventListener/UserProviderListener.php b/src/Security/Http/EventListener/UserProviderListener.php new file mode 100644 index 0000000..1dd4aaf --- /dev/null +++ b/src/Security/Http/EventListener/UserProviderListener.php @@ -0,0 +1,33 @@ +getPassport(); + if (!$passport->hasBadge(ImpersonationBadge::class)) { + return; + } + + /** @var ImpersonationBadge $badge */ + $badge = $passport->getBadge(ImpersonationBadge::class); + if (null !== $badge->getUserLoader()) { + return; + } + + $badge->setUserLoader($this->userProvider->loadUserByIdentifier(...)); + } +}