From e2af9ce344400e5319610ff8441360aad1fb2dab Mon Sep 17 00:00:00 2001 From: Martin Kirilov Date: Sun, 6 Dec 2020 05:19:29 +0200 Subject: [PATCH 1/2] add FirewallUserAuthenticator - authenticate users in any firewall --- .../DependencyInjection/SecurityExtension.php | 6 ++ .../config/security_authenticator.php | 8 ++ .../Security/FirewallUserAuthenticator.php | 77 +++++++++++++++++++ .../PreAuthenticatedAuthenticator.php | 59 ++++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 src/Symfony/Bundle/SecurityBundle/Security/FirewallUserAuthenticator.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Security/PreAuthenticatedAuthenticator.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 3c84bf34072d0..95d895f18880d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -266,6 +266,9 @@ private function createFirewalls(array $config, ContainerBuilder $container) // load firewall map $mapDef = $container->getDefinition('security.firewall.map'); + if ($this->authenticatorManagerEnabled = $config['enable_authenticator_manager']) { + $firewallUserAuthenticatorDef = $container->getDefinition('security.firewall_user_authenticator'); + } $map = $authenticationProviders = $contextRefs = []; foreach ($firewalls as $name => $firewall) { if (isset($firewall['user_checker']) && 'security.user_checker' !== $firewall['user_checker']) { @@ -292,6 +295,9 @@ private function createFirewalls(array $config, ContainerBuilder $container) } $mapDef->replaceArgument(0, ServiceLocatorTagPass::register($container, $contextRefs)); $mapDef->replaceArgument(1, new IteratorArgument($map)); + if ($this->authenticatorManagerEnabled = $config['enable_authenticator_manager']) { + $firewallUserAuthenticatorDef->replaceArgument(0, ServiceLocatorTagPass::register($container, $contextRefs)); + } if (!$this->authenticatorManagerEnabled) { // add authentication providers to authentication manager diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php index 3d0c6ddcb4f9e..8ef2f73c40706 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Bundle\SecurityBundle\Security\FirewallUserAuthenticator; use Symfony\Bundle\SecurityBundle\Security\UserAuthenticator; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; @@ -60,6 +61,13 @@ ]) ->alias(UserAuthenticatorInterface::class, 'security.user_authenticator') + ->set('security.firewall_user_authenticator', FirewallUserAuthenticator::class) + ->args([ + abstract_arg('Firewall context locator'), + service('event_dispatcher'), + ]) + ->alias(FirewallUserAuthenticator::class, 'security.firewall_user_authenticator') + ->set('security.authentication.manager', NoopAuthenticationManager::class) ->alias(AuthenticationManagerInterface::class, 'security.authentication.manager') diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallUserAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallUserAuthenticator.php new file mode 100644 index 0000000000000..da76b3aac6097 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallUserAuthenticator.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Security; + +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +/** + * @author Martin Kirilov + * + * @final + * @experimental in 5.2 + */ +class FirewallUserAuthenticator +{ + private $firewallLocator; + private $eventDispatcher; + + public function __construct(ContainerInterface $firewallLocator, EventDispatcherInterface $eventDispatcher) + { + $this->firewallLocator = $firewallLocator; + $this->eventDispatcher = $eventDispatcher; + } + + /** + * @param BadgeInterface[] $badges Optionally, pass some Passport badges to use for the manual login + */ + public function authenticateUser(UserInterface $user, Request $request, string $firewallName, array $badges = []): void + { + // TODO Tests + // TODO Throw if not master request? + if (!$request->hasSession()) { + return; + } + + if (!$this->firewallLocator->has('security.firewall.map.context.'.$firewallName)) { + throw new \LogicException(sprintf('Firewall "%s" not found. Did you register your firewall?', $firewallName)); + } + + /** @var FirewallContext $firewallContext */ + $firewallContext = $this->firewallLocator->get('security.firewall.map.context.'.$firewallName); + // todo can firewall config ever be null? + $firewallConfig = $firewallContext->getConfig(); + + // Note: We're only using PreAuthenticatedAuthenticator because of Symfony\Component\Security\Http\EventListener\RememberMeListener + $authenticator = new PreAuthenticatedAuthenticator(); + // create PreAuthenticatedToken token for the User + $token = $authenticator->createAuthenticatedToken($passport = new SelfValidatingPassport(new UserBadge($user->getUsername(), function () use ($user) { return $user; }), $badges), $firewallName); + + // announce the authenticated token + $token = $this->eventDispatcher->dispatch(new AuthenticationTokenCreatedEvent($token))->getAuthenticatedToken(); + + $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $passport, $token, $request, null, $firewallName)); + + $sessionKey = '_security_'.$firewallConfig->getContext(); + $session = $request->getSession(); + // increments the internal session usage index + $session->getMetadataBag(); + $session->set($sessionKey, serialize($token)); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Security/PreAuthenticatedAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Security/PreAuthenticatedAuthenticator.php new file mode 100644 index 0000000000000..a908c4d5e3b96 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Security/PreAuthenticatedAuthenticator.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Security; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; + +/** + * This class is only used in FirewallUserAuthenticator. + * Its sole reason if existence is LoginSuccessEvent requiring an authenticator. + * + * @author Martin Kirilov + * + * @internal + * + * @experimental in 5.2 + */ +class PreAuthenticatedAuthenticator implements AuthenticatorInterface +{ + public function supports(Request $request): ?bool + { + return true; + } + + public function authenticate(Request $request): PassportInterface + { + throw new \LogicException(sprintf('"%s" does not support %s::authenticate() calls', static::class, AuthenticatorInterface::class)); + } + + public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface + { + // TODO Tests + return new PreAuthenticatedToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles()); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + return null; + } +} From 787031acd31cc4346f9461bb42133a410e6cffd3 Mon Sep 17 00:00:00 2001 From: Martin Kirilov Date: Sun, 6 Dec 2020 05:29:21 +0200 Subject: [PATCH 2/2] improve exception message --- .../SecurityBundle/Security/PreAuthenticatedAuthenticator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Security/PreAuthenticatedAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Security/PreAuthenticatedAuthenticator.php index a908c4d5e3b96..1c49fcdbe1e79 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/PreAuthenticatedAuthenticator.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/PreAuthenticatedAuthenticator.php @@ -38,7 +38,7 @@ public function supports(Request $request): ?bool public function authenticate(Request $request): PassportInterface { - throw new \LogicException(sprintf('"%s" does not support %s::authenticate() calls', static::class, AuthenticatorInterface::class)); + throw new \LogicException(sprintf('"%s" does not support "%s::authenticate()" calls.', static::class, AuthenticatorInterface::class)); } public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface