Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Resources/config/authorization.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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'])
;
};
29 changes: 28 additions & 1 deletion src/Security/Authentication/JwtAuthentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,27 @@

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;
use Lcobucci\JWT\Token\RegisteredClaims;
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,
Expand All @@ -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
Expand All @@ -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()) {
Expand Down
75 changes: 75 additions & 0 deletions src/Security/Authentication/Passport/Badge/ImpersonationBadge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace AnzuSystems\AuthBundle\Security\Authentication\Passport\Badge;

use AnzuSystems\AuthBundle\Security\Http\EventListener\UserProviderListener;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;

final class ImpersonationBadge implements BadgeInterface
{
/**
* @var callable|null
*/
private $userLoader;
private ?UserInterface $originalUser = null;

public function __construct(
private string $userIdentifier,
?callable $userLoader = null,
) {
$this->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;
}
}
33 changes: 33 additions & 0 deletions src/Security/Http/EventListener/UserProviderListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace AnzuSystems\AuthBundle\Security\Http\EventListener;

use AnzuSystems\AuthBundle\Security\Authentication\Passport\Badge\ImpersonationBadge;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;

final class UserProviderListener
{
public function __construct(
private UserProviderInterface $userProvider,
) {
}

public function checkPassport(CheckPassportEvent $event): void
{
$passport = $event->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(...));
}
}