The start of something beautiful
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Firewall;
|
||||
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
|
||||
/**
|
||||
* A base class for listeners that can tell whether they should authenticate incoming requests.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
abstract class AbstractListener implements FirewallListenerInterface
|
||||
{
|
||||
final public function __invoke(RequestEvent $event): void
|
||||
{
|
||||
if (false !== $this->supports($event->getRequest())) {
|
||||
$this->authenticate($event);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getPriority(): int
|
||||
{
|
||||
return 0; // Default
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Firewall;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Http\AccessMapInterface;
|
||||
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
|
||||
|
||||
/**
|
||||
* AccessListener enforces access control rules.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class AccessListener extends AbstractListener
|
||||
{
|
||||
private TokenStorageInterface $tokenStorage;
|
||||
private AccessDecisionManagerInterface $accessDecisionManager;
|
||||
private AccessMapInterface $map;
|
||||
|
||||
public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionManagerInterface $accessDecisionManager, AccessMapInterface $map, bool $exceptionOnNoToken = false)
|
||||
{
|
||||
if (false !== $exceptionOnNoToken) {
|
||||
throw new \LogicException(sprintf('Argument $exceptionOnNoToken of "%s()" must be set to "false".', __METHOD__));
|
||||
}
|
||||
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->accessDecisionManager = $accessDecisionManager;
|
||||
$this->map = $map;
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
[$attributes] = $this->map->getPatterns($request);
|
||||
$request->attributes->set('_access_control_attributes', $attributes);
|
||||
|
||||
if ($attributes && [AuthenticatedVoter::PUBLIC_ACCESS] !== $attributes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles access authorization.
|
||||
*
|
||||
* @throws AccessDeniedException
|
||||
*/
|
||||
public function authenticate(RequestEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
$attributes = $request->attributes->get('_access_control_attributes');
|
||||
$request->attributes->remove('_access_control_attributes');
|
||||
|
||||
if (!$attributes || (
|
||||
[AuthenticatedVoter::PUBLIC_ACCESS] === $attributes && $event instanceof LazyResponseEvent
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$token = $this->tokenStorage->getToken() ?? new NullToken();
|
||||
|
||||
if (!$this->accessDecisionManager->decide($token, $attributes, $request, true)) {
|
||||
throw $this->createAccessDeniedException($request, $attributes);
|
||||
}
|
||||
}
|
||||
|
||||
private function createAccessDeniedException(Request $request, array $attributes): AccessDeniedException
|
||||
{
|
||||
$exception = new AccessDeniedException();
|
||||
$exception->setAttributes($attributes);
|
||||
$exception->setSubject($request);
|
||||
|
||||
return $exception;
|
||||
}
|
||||
|
||||
public static function getPriority(): int
|
||||
{
|
||||
return -255;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Firewall;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticatorManagerInterface;
|
||||
|
||||
/**
|
||||
* Firewall authentication listener that delegates to the authenticator system.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
class AuthenticatorManagerListener extends AbstractListener
|
||||
{
|
||||
private AuthenticatorManagerInterface $authenticatorManager;
|
||||
|
||||
public function __construct(AuthenticatorManagerInterface $authenticationManager)
|
||||
{
|
||||
$this->authenticatorManager = $authenticationManager;
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
return $this->authenticatorManager->supports($request);
|
||||
}
|
||||
|
||||
public function authenticate(RequestEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
$response = $this->authenticatorManager->authenticateRequest($request);
|
||||
if (null === $response) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Firewall;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\Security\Http\AccessMapInterface;
|
||||
|
||||
/**
|
||||
* ChannelListener switches the HTTP protocol based on the access control
|
||||
* configuration.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class ChannelListener extends AbstractListener
|
||||
{
|
||||
private AccessMapInterface $map;
|
||||
private ?LoggerInterface $logger;
|
||||
private int $httpPort;
|
||||
private int $httpsPort;
|
||||
|
||||
public function __construct(AccessMapInterface $map, ?LoggerInterface $logger = null, int $httpPort = 80, int $httpsPort = 443)
|
||||
{
|
||||
$this->map = $map;
|
||||
$this->logger = $logger;
|
||||
$this->httpPort = $httpPort;
|
||||
$this->httpsPort = $httpsPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles channel management.
|
||||
*/
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
[, $channel] = $this->map->getPatterns($request);
|
||||
|
||||
if ('https' === $channel && !$request->isSecure()) {
|
||||
if (null !== $this->logger) {
|
||||
if ('https' === $request->headers->get('X-Forwarded-Proto')) {
|
||||
$this->logger->info('Redirecting to HTTPS. ("X-Forwarded-Proto" header is set to "https" - did you set "trusted_proxies" correctly?)');
|
||||
} elseif (str_contains($request->headers->get('Forwarded', ''), 'proto=https')) {
|
||||
$this->logger->info('Redirecting to HTTPS. ("Forwarded" header is set to "proto=https" - did you set "trusted_proxies" correctly?)');
|
||||
} else {
|
||||
$this->logger->info('Redirecting to HTTPS.');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ('http' === $channel && $request->isSecure()) {
|
||||
$this->logger?->info('Redirecting to HTTP.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function authenticate(RequestEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
$event->setResponse($this->createRedirectResponse($request));
|
||||
}
|
||||
|
||||
private function createRedirectResponse(Request $request): RedirectResponse
|
||||
{
|
||||
$scheme = $request->isSecure() ? 'http' : 'https';
|
||||
if ('http' === $scheme && 80 != $this->httpPort) {
|
||||
$port = ':'.$this->httpPort;
|
||||
} elseif ('https' === $scheme && 443 != $this->httpsPort) {
|
||||
$port = ':'.$this->httpsPort;
|
||||
} else {
|
||||
$port = '';
|
||||
}
|
||||
|
||||
$qs = $request->getQueryString();
|
||||
if (null !== $qs) {
|
||||
$qs = '?'.$qs;
|
||||
}
|
||||
|
||||
$url = $scheme.'://'.$request->getHost().$port.$request->getBaseUrl().$request->getPathInfo().$qs;
|
||||
|
||||
return new RedirectResponse($url, 301);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Firewall;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
|
||||
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* ContextListener manages the SecurityContext persistence through a session.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class ContextListener extends AbstractListener
|
||||
{
|
||||
private TokenStorageInterface $tokenStorage;
|
||||
private string $sessionKey;
|
||||
private ?LoggerInterface $logger;
|
||||
private iterable $userProviders;
|
||||
private ?EventDispatcherInterface $dispatcher;
|
||||
private bool $registered = false;
|
||||
private AuthenticationTrustResolverInterface $trustResolver;
|
||||
private ?\Closure $sessionTrackerEnabler;
|
||||
|
||||
/**
|
||||
* @param iterable<mixed, UserProviderInterface> $userProviders
|
||||
*/
|
||||
public function __construct(TokenStorageInterface $tokenStorage, iterable $userProviders, string $contextKey, ?LoggerInterface $logger = null, ?EventDispatcherInterface $dispatcher = null, ?AuthenticationTrustResolverInterface $trustResolver = null, ?callable $sessionTrackerEnabler = null)
|
||||
{
|
||||
if (!$contextKey) {
|
||||
throw new \InvalidArgumentException('$contextKey must not be empty.');
|
||||
}
|
||||
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->userProviders = $userProviders;
|
||||
$this->sessionKey = '_security_'.$contextKey;
|
||||
$this->logger = $logger;
|
||||
$this->dispatcher = $dispatcher;
|
||||
|
||||
$this->trustResolver = $trustResolver ?? new AuthenticationTrustResolver();
|
||||
$this->sessionTrackerEnabler = null === $sessionTrackerEnabler ? null : $sessionTrackerEnabler(...);
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
return null; // always run authenticate() lazily with lazy firewalls
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the Security Token from the session.
|
||||
*/
|
||||
public function authenticate(RequestEvent $event): void
|
||||
{
|
||||
if (!$this->registered && null !== $this->dispatcher && $event->isMainRequest()) {
|
||||
$this->dispatcher->addListener(KernelEvents::RESPONSE, $this->onKernelResponse(...));
|
||||
$this->registered = true;
|
||||
}
|
||||
|
||||
$request = $event->getRequest();
|
||||
$session = $request->hasPreviousSession() ? $request->getSession() : null;
|
||||
|
||||
$request->attributes->set('_security_firewall_run', $this->sessionKey);
|
||||
|
||||
if (null !== $session) {
|
||||
$usageIndexValue = $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : 0;
|
||||
$usageIndexReference = \PHP_INT_MIN;
|
||||
$sessionId = $request->cookies->all()[$session->getName()] ?? null;
|
||||
$token = $session->get($this->sessionKey);
|
||||
|
||||
// sessionId = true is used in the tests
|
||||
if ($this->sessionTrackerEnabler && \in_array($sessionId, [true, $session->getId()], true)) {
|
||||
$usageIndexReference = $usageIndexValue;
|
||||
} else {
|
||||
$usageIndexReference = $usageIndexReference - \PHP_INT_MIN + $usageIndexValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $session || null === $token) {
|
||||
if ($this->sessionTrackerEnabler) {
|
||||
($this->sessionTrackerEnabler)();
|
||||
}
|
||||
|
||||
$this->tokenStorage->setToken(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$token = $this->safelyUnserialize($token);
|
||||
|
||||
$this->logger?->debug('Read existing security token from the session.', [
|
||||
'key' => $this->sessionKey,
|
||||
'token_class' => \is_object($token) ? $token::class : null,
|
||||
]);
|
||||
|
||||
if ($token instanceof TokenInterface) {
|
||||
$originalToken = $token;
|
||||
$token = $this->refreshUser($token);
|
||||
|
||||
if (!$token) {
|
||||
$this->logger?->debug('Token was deauthenticated after trying to refresh it.');
|
||||
|
||||
$this->dispatcher?->dispatch(new TokenDeauthenticatedEvent($originalToken, $request));
|
||||
}
|
||||
} elseif (null !== $token) {
|
||||
$this->logger?->warning('Expected a security token from the session, got something else.', ['key' => $this->sessionKey, 'received' => $token]);
|
||||
|
||||
$token = null;
|
||||
}
|
||||
|
||||
if ($this->sessionTrackerEnabler) {
|
||||
($this->sessionTrackerEnabler)();
|
||||
}
|
||||
|
||||
$this->tokenStorage->setToken($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the security token into the session.
|
||||
*/
|
||||
public function onKernelResponse(ResponseEvent $event): void
|
||||
{
|
||||
if (!$event->isMainRequest()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request = $event->getRequest();
|
||||
|
||||
if (!$request->hasSession() || $request->attributes->get('_security_firewall_run') !== $this->sessionKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dispatcher?->removeListener(KernelEvents::RESPONSE, $this->onKernelResponse(...));
|
||||
$this->registered = false;
|
||||
$session = $request->getSession();
|
||||
$sessionId = $session->getId();
|
||||
$usageIndexValue = $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : null;
|
||||
$token = $this->tokenStorage->getToken();
|
||||
|
||||
if (!$this->trustResolver->isAuthenticated($token)) {
|
||||
if ($request->hasPreviousSession()) {
|
||||
$session->remove($this->sessionKey);
|
||||
}
|
||||
} else {
|
||||
$session->set($this->sessionKey, serialize($token));
|
||||
|
||||
$this->logger?->debug('Stored the security token in the session.', ['key' => $this->sessionKey]);
|
||||
}
|
||||
|
||||
if ($this->sessionTrackerEnabler && $session->getId() === $sessionId) {
|
||||
$usageIndexReference = $usageIndexValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the user by reloading it from the user provider.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function refreshUser(TokenInterface $token): ?TokenInterface
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
$userNotFoundByProvider = false;
|
||||
$userDeauthenticated = false;
|
||||
$userClass = $user::class;
|
||||
|
||||
foreach ($this->userProviders as $provider) {
|
||||
if (!$provider instanceof UserProviderInterface) {
|
||||
throw new \InvalidArgumentException(sprintf('User provider "%s" must implement "%s".', get_debug_type($provider), UserProviderInterface::class));
|
||||
}
|
||||
|
||||
if (!$provider->supportsClass($userClass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$refreshedUser = $provider->refreshUser($user);
|
||||
$newToken = clone $token;
|
||||
$newToken->setUser($refreshedUser, false);
|
||||
|
||||
// tokens can be deauthenticated if the user has been changed.
|
||||
if ($token instanceof AbstractToken && $this->hasUserChanged($user, $newToken)) {
|
||||
$userDeauthenticated = true;
|
||||
|
||||
$this->logger?->debug('Cannot refresh token because user has changed.', ['username' => $refreshedUser->getUserIdentifier(), 'provider' => $provider::class]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$token->setUser($refreshedUser);
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$context = ['provider' => $provider::class, 'username' => $refreshedUser->getUserIdentifier()];
|
||||
|
||||
if ($token instanceof SwitchUserToken) {
|
||||
$originalToken = $token->getOriginalToken();
|
||||
$context['impersonator_username'] = $originalToken->getUserIdentifier();
|
||||
}
|
||||
|
||||
$this->logger->debug('User was reloaded from a user provider.', $context);
|
||||
}
|
||||
|
||||
return $token;
|
||||
} catch (UnsupportedUserException) {
|
||||
// let's try the next user provider
|
||||
} catch (UserNotFoundException $e) {
|
||||
$this->logger?->info('Username could not be found in the selected user provider.', ['username' => $e->getUserIdentifier(), 'provider' => $provider::class]);
|
||||
|
||||
$userNotFoundByProvider = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($userDeauthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($userNotFoundByProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new \RuntimeException(sprintf('There is no user provider for user "%s". Shouldn\'t the "supportsClass()" method of your user provider return true for this classname?', $userClass));
|
||||
}
|
||||
|
||||
private function safelyUnserialize(string $serializedToken): mixed
|
||||
{
|
||||
$token = null;
|
||||
$prevUnserializeHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
|
||||
$prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler) {
|
||||
if (__FILE__ === $file && !\in_array($type, [\E_DEPRECATED, \E_USER_DEPRECATED], true)) {
|
||||
throw new \ErrorException($msg, 0x37313BC, $type, $file, $line);
|
||||
}
|
||||
|
||||
return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false;
|
||||
});
|
||||
|
||||
try {
|
||||
$token = unserialize($serializedToken);
|
||||
} catch (\ErrorException $e) {
|
||||
if (0x37313BC !== $e->getCode()) {
|
||||
throw $e;
|
||||
}
|
||||
$this->logger?->warning('Failed to unserialize the security token from the session.', ['key' => $this->sessionKey, 'received' => $serializedToken, 'exception' => $e]);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
ini_set('unserialize_callback_func', $prevUnserializeHandler);
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
private static function hasUserChanged(UserInterface $originalUser, TokenInterface $refreshedToken): bool
|
||||
{
|
||||
$refreshedUser = $refreshedToken->getUser();
|
||||
|
||||
if ($originalUser instanceof EquatableInterface) {
|
||||
return !$originalUser->isEqualTo($refreshedUser);
|
||||
}
|
||||
|
||||
if ($originalUser instanceof PasswordAuthenticatedUserInterface || $refreshedUser instanceof PasswordAuthenticatedUserInterface) {
|
||||
if (!$originalUser instanceof PasswordAuthenticatedUserInterface || !$refreshedUser instanceof PasswordAuthenticatedUserInterface || $originalUser->getPassword() !== $refreshedUser->getPassword()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($originalUser instanceof LegacyPasswordAuthenticatedUserInterface xor $refreshedUser instanceof LegacyPasswordAuthenticatedUserInterface) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($originalUser instanceof LegacyPasswordAuthenticatedUserInterface && $refreshedUser instanceof LegacyPasswordAuthenticatedUserInterface && $originalUser->getSalt() !== $refreshedUser->getSalt()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$userRoles = array_map('strval', (array) $refreshedUser->getRoles());
|
||||
|
||||
if (
|
||||
\count($userRoles) !== \count($refreshedToken->getRoleNames())
|
||||
|| \count($userRoles) !== \count(array_intersect($userRoles, $refreshedToken->getRoleNames()))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($originalUser->getUserIdentifier() !== $refreshedUser->getUserIdentifier()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function handleUnserializeCallback(string $class): never
|
||||
{
|
||||
throw new \ErrorException('Class not found: '.$class, 0x37313BC);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Firewall;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Core\Exception\AccountStatusException;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\LazyResponseException;
|
||||
use Symfony\Component\Security\Core\Exception\LogoutException;
|
||||
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
|
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||
use Symfony\Component\Security\Http\EntryPoint\Exception\NotAnEntryPointException;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||
|
||||
/**
|
||||
* ExceptionListener catches authentication exception and converts them to
|
||||
* Response instances.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class ExceptionListener
|
||||
{
|
||||
use TargetPathTrait;
|
||||
|
||||
private TokenStorageInterface $tokenStorage;
|
||||
private string $firewallName;
|
||||
private ?AccessDeniedHandlerInterface $accessDeniedHandler;
|
||||
private ?AuthenticationEntryPointInterface $authenticationEntryPoint;
|
||||
private AuthenticationTrustResolverInterface $authenticationTrustResolver;
|
||||
private ?string $errorPage;
|
||||
private ?LoggerInterface $logger;
|
||||
private HttpUtils $httpUtils;
|
||||
private bool $stateless;
|
||||
|
||||
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationTrustResolverInterface $trustResolver, HttpUtils $httpUtils, string $firewallName, ?AuthenticationEntryPointInterface $authenticationEntryPoint = null, ?string $errorPage = null, ?AccessDeniedHandlerInterface $accessDeniedHandler = null, ?LoggerInterface $logger = null, bool $stateless = false)
|
||||
{
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->accessDeniedHandler = $accessDeniedHandler;
|
||||
$this->httpUtils = $httpUtils;
|
||||
$this->firewallName = $firewallName;
|
||||
$this->authenticationEntryPoint = $authenticationEntryPoint;
|
||||
$this->authenticationTrustResolver = $trustResolver;
|
||||
$this->errorPage = $errorPage;
|
||||
$this->logger = $logger;
|
||||
$this->stateless = $stateless;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a onKernelException listener to take care of security exceptions.
|
||||
*/
|
||||
public function register(EventDispatcherInterface $dispatcher): void
|
||||
{
|
||||
$dispatcher->addListener(KernelEvents::EXCEPTION, $this->onKernelException(...), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the dispatcher.
|
||||
*/
|
||||
public function unregister(EventDispatcherInterface $dispatcher): void
|
||||
{
|
||||
$dispatcher->removeListener(KernelEvents::EXCEPTION, $this->onKernelException(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles security related exceptions.
|
||||
*/
|
||||
public function onKernelException(ExceptionEvent $event): void
|
||||
{
|
||||
$exception = $event->getThrowable();
|
||||
do {
|
||||
if ($exception instanceof AuthenticationException) {
|
||||
$this->handleAuthenticationException($event, $exception);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($exception instanceof AccessDeniedException) {
|
||||
$this->handleAccessDeniedException($event, $exception);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($exception instanceof LazyResponseException) {
|
||||
$event->setResponse($exception->getResponse());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($exception instanceof LogoutException) {
|
||||
$this->handleLogoutException($event, $exception);
|
||||
|
||||
return;
|
||||
}
|
||||
} while (null !== $exception = $exception->getPrevious());
|
||||
}
|
||||
|
||||
private function handleAuthenticationException(ExceptionEvent $event, AuthenticationException $exception): void
|
||||
{
|
||||
$this->logger?->info('An AuthenticationException was thrown; redirecting to authentication entry point.', ['exception' => $exception]);
|
||||
|
||||
try {
|
||||
$event->setResponse($this->startAuthentication($event->getRequest(), $exception));
|
||||
$event->allowCustomResponseCode();
|
||||
} catch (\Exception $e) {
|
||||
$event->setThrowable($e);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleAccessDeniedException(ExceptionEvent $event, AccessDeniedException $exception): void
|
||||
{
|
||||
$event->setThrowable(new AccessDeniedHttpException($exception->getMessage(), $exception));
|
||||
|
||||
$token = $this->tokenStorage->getToken();
|
||||
if (!$this->authenticationTrustResolver->isFullFledged($token)) {
|
||||
$this->logger?->debug('Access denied, the user is not fully authenticated; redirecting to authentication entry point.', ['exception' => $exception]);
|
||||
|
||||
try {
|
||||
$insufficientAuthenticationException = new InsufficientAuthenticationException('Full authentication is required to access this resource.', 0, $exception);
|
||||
if (null !== $token) {
|
||||
$insufficientAuthenticationException->setToken($token);
|
||||
}
|
||||
|
||||
$event->setResponse($this->startAuthentication($event->getRequest(), $insufficientAuthenticationException));
|
||||
} catch (\Exception $e) {
|
||||
$event->setThrowable($e);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger?->debug('Access denied, the user is neither anonymous, nor remember-me.', ['exception' => $exception]);
|
||||
|
||||
try {
|
||||
if (null !== $this->accessDeniedHandler) {
|
||||
$response = $this->accessDeniedHandler->handle($event->getRequest(), $exception);
|
||||
|
||||
if ($response instanceof Response) {
|
||||
$event->setResponse($response);
|
||||
}
|
||||
} elseif (null !== $this->errorPage) {
|
||||
$subRequest = $this->httpUtils->createRequest($event->getRequest(), $this->errorPage);
|
||||
$subRequest->attributes->set(SecurityRequestAttributes::ACCESS_DENIED_ERROR, $exception);
|
||||
|
||||
$event->setResponse($event->getKernel()->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true));
|
||||
$event->allowCustomResponseCode();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger?->error('An exception was thrown when handling an AccessDeniedException.', ['exception' => $e]);
|
||||
|
||||
$event->setThrowable(new \RuntimeException('Exception thrown when handling an exception.', 0, $e));
|
||||
}
|
||||
}
|
||||
|
||||
private function handleLogoutException(ExceptionEvent $event, LogoutException $exception): void
|
||||
{
|
||||
$event->setThrowable(new AccessDeniedHttpException($exception->getMessage(), $exception));
|
||||
|
||||
$this->logger?->info('A LogoutException was thrown; wrapping with AccessDeniedHttpException', ['exception' => $exception]);
|
||||
}
|
||||
|
||||
private function startAuthentication(Request $request, AuthenticationException $authException): Response
|
||||
{
|
||||
if (null === $this->authenticationEntryPoint) {
|
||||
$this->throwUnauthorizedException($authException);
|
||||
}
|
||||
|
||||
$this->logger?->debug('Calling Authentication entry point.', ['entry_point' => $this->authenticationEntryPoint]);
|
||||
|
||||
if (!$this->stateless) {
|
||||
$this->setTargetPath($request);
|
||||
}
|
||||
|
||||
if ($authException instanceof AccountStatusException) {
|
||||
// remove the security token to prevent infinite redirect loops
|
||||
$this->tokenStorage->setToken(null);
|
||||
|
||||
$this->logger?->info('The security token was removed due to an AccountStatusException.', ['exception' => $authException]);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->authenticationEntryPoint->start($request, $authException);
|
||||
} catch (NotAnEntryPointException) {
|
||||
$this->throwUnauthorizedException($authException);
|
||||
}
|
||||
|
||||
if (!$response instanceof Response) {
|
||||
$given = get_debug_type($response);
|
||||
|
||||
throw new \LogicException(sprintf('The "%s::start()" method must return a Response object ("%s" returned).', get_debug_type($this->authenticationEntryPoint), $given));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function setTargetPath(Request $request): void
|
||||
{
|
||||
// session isn't required when using HTTP basic authentication mechanism for example
|
||||
if ($request->hasSession() && $request->isMethodSafe() && !$request->isXmlHttpRequest()) {
|
||||
$this->saveTargetPath($request->getSession(), $this->firewallName, $request->getUri());
|
||||
}
|
||||
}
|
||||
|
||||
private function throwUnauthorizedException(AuthenticationException $authException): never
|
||||
{
|
||||
$this->logger?->notice(sprintf('No Authentication entry point configured, returning a %s HTTP response. Configure "entry_point" on the firewall "%s" if you want to modify the response.', Response::HTTP_UNAUTHORIZED, $this->firewallName));
|
||||
|
||||
throw new HttpException(Response::HTTP_UNAUTHORIZED, $authException->getMessage(), $authException, [], $authException->getCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Firewall;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
|
||||
/**
|
||||
* Can be implemented by firewall listeners.
|
||||
*
|
||||
* @author Christian Scheb <me@christianscheb.de>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
interface FirewallListenerInterface
|
||||
{
|
||||
/**
|
||||
* Tells whether the authenticate() method should be called or not depending on the incoming request.
|
||||
*
|
||||
* Returning null means authenticate() can be called lazily when accessing the token storage.
|
||||
*/
|
||||
public function supports(Request $request): ?bool;
|
||||
|
||||
/**
|
||||
* Does whatever is required to authenticate the request, typically calling $event->setResponse() internally.
|
||||
*/
|
||||
public function authenticate(RequestEvent $event): void;
|
||||
|
||||
/**
|
||||
* Defines the priority of the listener.
|
||||
* The higher the number, the earlier a listener is executed.
|
||||
*/
|
||||
public static function getPriority(): int;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Firewall;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Exception\LogoutException;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
use Symfony\Component\Security\Http\ParameterBagUtils;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* LogoutListener logout users.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class LogoutListener extends AbstractListener
|
||||
{
|
||||
private TokenStorageInterface $tokenStorage;
|
||||
private array $options;
|
||||
private HttpUtils $httpUtils;
|
||||
private ?CsrfTokenManagerInterface $csrfTokenManager;
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
/**
|
||||
* @param array $options An array of options to process a logout attempt
|
||||
*/
|
||||
public function __construct(TokenStorageInterface $tokenStorage, HttpUtils $httpUtils, EventDispatcherInterface $eventDispatcher, array $options = [], ?CsrfTokenManagerInterface $csrfTokenManager = null)
|
||||
{
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->httpUtils = $httpUtils;
|
||||
$this->options = array_merge([
|
||||
'csrf_parameter' => '_csrf_token',
|
||||
'csrf_token_id' => 'logout',
|
||||
'logout_path' => '/logout',
|
||||
], $options);
|
||||
$this->csrfTokenManager = $csrfTokenManager;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
return $this->requiresLogout($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the logout if requested.
|
||||
*
|
||||
* If a CsrfTokenManagerInterface instance is available, it will be used to
|
||||
* validate the request.
|
||||
*
|
||||
* @throws LogoutException if the CSRF token is invalid
|
||||
* @throws \RuntimeException if the LogoutEvent listener does not set a response
|
||||
*/
|
||||
public function authenticate(RequestEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
if (null !== $this->csrfTokenManager) {
|
||||
$csrfToken = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']);
|
||||
|
||||
if (!\is_string($csrfToken) || false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['csrf_token_id'], $csrfToken))) {
|
||||
throw new LogoutException('Invalid CSRF token.');
|
||||
}
|
||||
}
|
||||
|
||||
$logoutEvent = new LogoutEvent($request, $this->tokenStorage->getToken());
|
||||
$this->eventDispatcher->dispatch($logoutEvent);
|
||||
|
||||
if (!$response = $logoutEvent->getResponse()) {
|
||||
throw new \RuntimeException('No logout listener set the Response, make sure at least the DefaultLogoutListener is registered.');
|
||||
}
|
||||
|
||||
$this->tokenStorage->setToken(null);
|
||||
|
||||
$event->setResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this request is asking for logout.
|
||||
*
|
||||
* The default implementation only processed requests to a specific path,
|
||||
* but a subclass could change this to logout requests where
|
||||
* certain parameters is present.
|
||||
*/
|
||||
protected function requiresLogout(Request $request): bool
|
||||
{
|
||||
return isset($this->options['logout_path']) && $this->httpUtils->checkRequestPath($request, $this->options['logout_path']);
|
||||
}
|
||||
|
||||
public static function getPriority(): int
|
||||
{
|
||||
return -127;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Firewall;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
|
||||
use Symfony\Component\Security\Http\SecurityEvents;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* SwitchUserListener allows a user to impersonate another one temporarily
|
||||
* (like the Unix su command).
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class SwitchUserListener extends AbstractListener
|
||||
{
|
||||
public const EXIT_VALUE = '_exit';
|
||||
|
||||
private TokenStorageInterface $tokenStorage;
|
||||
private UserProviderInterface $provider;
|
||||
private UserCheckerInterface $userChecker;
|
||||
private string $firewallName;
|
||||
private AccessDecisionManagerInterface $accessDecisionManager;
|
||||
private string $usernameParameter;
|
||||
private string $role;
|
||||
private ?LoggerInterface $logger;
|
||||
private ?EventDispatcherInterface $dispatcher;
|
||||
private bool $stateless;
|
||||
private ?UrlGeneratorInterface $urlGenerator;
|
||||
private ?string $targetRoute;
|
||||
|
||||
public function __construct(TokenStorageInterface $tokenStorage, UserProviderInterface $provider, UserCheckerInterface $userChecker, string $firewallName, AccessDecisionManagerInterface $accessDecisionManager, ?LoggerInterface $logger = null, string $usernameParameter = '_switch_user', string $role = 'ROLE_ALLOWED_TO_SWITCH', ?EventDispatcherInterface $dispatcher = null, bool $stateless = false, ?UrlGeneratorInterface $urlGenerator = null, ?string $targetRoute = null)
|
||||
{
|
||||
if ('' === $firewallName) {
|
||||
throw new \InvalidArgumentException('$firewallName must not be empty.');
|
||||
}
|
||||
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->provider = $provider;
|
||||
$this->userChecker = $userChecker;
|
||||
$this->firewallName = $firewallName;
|
||||
$this->accessDecisionManager = $accessDecisionManager;
|
||||
$this->usernameParameter = $usernameParameter;
|
||||
$this->role = $role;
|
||||
$this->logger = $logger;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->stateless = $stateless;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->targetRoute = $targetRoute;
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
// usernames can be falsy
|
||||
$username = $request->get($this->usernameParameter);
|
||||
|
||||
if (null === $username || '' === $username) {
|
||||
$username = $request->headers->get($this->usernameParameter);
|
||||
}
|
||||
|
||||
// if it's still "empty", nothing to do.
|
||||
if (null === $username || '' === $username) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$request->attributes->set('_switch_user_username', $username);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the switch to another user.
|
||||
*
|
||||
* @throws \LogicException if switching to a user failed
|
||||
*/
|
||||
public function authenticate(RequestEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
$username = $request->attributes->get('_switch_user_username');
|
||||
$request->attributes->remove('_switch_user_username');
|
||||
|
||||
if (null === $this->tokenStorage->getToken()) {
|
||||
throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
|
||||
}
|
||||
|
||||
if (self::EXIT_VALUE === $username) {
|
||||
$this->tokenStorage->setToken($this->attemptExitUser($request));
|
||||
} else {
|
||||
try {
|
||||
$this->tokenStorage->setToken($this->attemptSwitchUser($request, $username));
|
||||
} catch (AuthenticationException $e) {
|
||||
// Generate 403 in any conditions to prevent user enumeration vulnerabilities
|
||||
throw new AccessDeniedException('Switch User failed: '.$e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->stateless) {
|
||||
$request->query->remove($this->usernameParameter);
|
||||
$request->server->set('QUERY_STRING', http_build_query($request->query->all(), '', '&'));
|
||||
$response = new RedirectResponse($this->urlGenerator && $this->targetRoute ? $this->urlGenerator->generate($this->targetRoute) : $request->getUri(), 302);
|
||||
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to switch to another user and returns the new token if successfully switched.
|
||||
*
|
||||
* @throws \LogicException
|
||||
* @throws AccessDeniedException
|
||||
*/
|
||||
private function attemptSwitchUser(Request $request, string $username): ?TokenInterface
|
||||
{
|
||||
$token = $this->tokenStorage->getToken();
|
||||
$originalToken = $this->getOriginalToken($token);
|
||||
|
||||
if (null !== $originalToken) {
|
||||
if ($token->getUserIdentifier() === $username) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
// User already switched, exit before seamlessly switching to another user
|
||||
$token = $this->attemptExitUser($request);
|
||||
}
|
||||
|
||||
$currentUsername = $token->getUserIdentifier();
|
||||
$nonExistentUsername = '_'.hash('xxh128', random_bytes(8).$username);
|
||||
|
||||
// To protect against user enumeration via timing measurements
|
||||
// we always load both successfully and unsuccessfully
|
||||
try {
|
||||
$user = $this->provider->loadUserByIdentifier($username);
|
||||
|
||||
try {
|
||||
$this->provider->loadUserByIdentifier($nonExistentUsername);
|
||||
} catch (\Exception) {
|
||||
}
|
||||
} catch (AuthenticationException $e) {
|
||||
$this->provider->loadUserByIdentifier($currentUsername);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if (false === $this->accessDecisionManager->decide($token, [$this->role], $user)) {
|
||||
$exception = new AccessDeniedException();
|
||||
$exception->setAttributes($this->role);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$this->logger?->info('Attempting to switch to user.', ['username' => $username]);
|
||||
|
||||
$this->userChecker->checkPostAuth($user);
|
||||
|
||||
$roles = $user->getRoles();
|
||||
$originatedFromUri = str_replace('/&', '/?', preg_replace('#[&?]'.$this->usernameParameter.'=[^&]*#', '', $request->getRequestUri()));
|
||||
$token = new SwitchUserToken($user, $this->firewallName, $roles, $token, $originatedFromUri);
|
||||
|
||||
if (null !== $this->dispatcher) {
|
||||
$switchEvent = new SwitchUserEvent($request, $token->getUser(), $token);
|
||||
$this->dispatcher->dispatch($switchEvent, SecurityEvents::SWITCH_USER);
|
||||
// use the token from the event in case any listeners have replaced it.
|
||||
$token = $switchEvent->getToken();
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to exit from an already switched user and returns the original token.
|
||||
*
|
||||
* @throws AuthenticationCredentialsNotFoundException
|
||||
*/
|
||||
private function attemptExitUser(Request $request): TokenInterface
|
||||
{
|
||||
if (null === ($currentToken = $this->tokenStorage->getToken()) || null === $original = $this->getOriginalToken($currentToken)) {
|
||||
throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
|
||||
}
|
||||
|
||||
if (null !== $this->dispatcher && $original->getUser() instanceof UserInterface) {
|
||||
$user = $this->provider->refreshUser($original->getUser());
|
||||
$original->setUser($user);
|
||||
$switchEvent = new SwitchUserEvent($request, $user, $original);
|
||||
$this->dispatcher->dispatch($switchEvent, SecurityEvents::SWITCH_USER);
|
||||
$original = $switchEvent->getToken();
|
||||
}
|
||||
|
||||
return $original;
|
||||
}
|
||||
|
||||
private function getOriginalToken(TokenInterface $token): ?TokenInterface
|
||||
{
|
||||
if ($token instanceof SwitchUserToken) {
|
||||
return $token->getOriginalToken();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user