diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index c309485293..381195d833 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -23,6 +23,7 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; @@ -63,10 +64,8 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent { // create an authenticated token for the User $token = $authenticator->createAuthenticatedToken($user, $this->providerKey); - // authenticate this in the system - $this->saveAuthenticatedToken($token, $request); - // return the success metric + // authenticate this in the system return $this->handleAuthenticationSuccess($token, $request, $authenticator); } @@ -161,10 +160,6 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent throw new \UnexpectedValueException(sprintf('The return value of "%1$s::getCredentials()" must not be null. Return false from "%1$s::supports()" instead.', \get_class($authenticator))); } - if (null !== $this->logger) { - $this->logger->debug('Passing token information to the AuthenticatorManager', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]); - } - // authenticate the credentials (e.g. check password) $token = $this->authenticateViaAuthenticator($authenticator, $credentials); @@ -172,15 +167,19 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent $this->logger->info('Authenticator successful!', ['token' => $token, 'authenticator' => \get_class($authenticator)]); } - // sets the token on the token storage, etc - $this->saveAuthenticatedToken($token, $request); - } catch (AuthenticationException $e) { - // oh no! Authentication failed! - - if (null !== $this->logger) { - $this->logger->info('Authenticator failed.', ['exception' => $e, 'authenticator' => \get_class($authenticator)]); + // success! (sets the token on the token storage, etc) + $response = $this->handleAuthenticationSuccess($token, $request, $authenticator); + if ($response instanceof Response) { + return $response; } + if (null !== $this->logger) { + $this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]); + } + + return null; + } catch (AuthenticationException $e) { + // oh no! Authentication failed! $response = $this->handleAuthenticationFailure($e, $request, $authenticator); if ($response instanceof Response) { return $response; @@ -188,22 +187,6 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent return null; } - - // success! - $response = $this->handleAuthenticationSuccess($token, $request, $authenticator); - if ($response instanceof Response) { - if (null !== $this->logger) { - $this->logger->debug('Authenticator set success response.', ['response' => $response, 'authenticator' => \get_class($authenticator)]); - } - - return $response; - } - - if (null !== $this->logger) { - $this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]); - } - - return null; } private function authenticateViaAuthenticator(AuthenticatorInterface $authenticator, $credentials): TokenInterface @@ -234,19 +217,17 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent return $authenticatedToken; } - private function saveAuthenticatedToken(TokenInterface $authenticatedToken, Request $request) + private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, Request $request, AuthenticatorInterface $authenticator): ?Response { $this->tokenStorage->setToken($authenticatedToken); - $loginEvent = new InteractiveLoginEvent($request, $authenticatedToken); - $this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN); - } + $response = $authenticator->onAuthenticationSuccess($request, $authenticatedToken, $this->providerKey); + if ($authenticator instanceof InteractiveAuthenticatorInterface && $authenticator->isInteractive()) { + $loginEvent = new InteractiveLoginEvent($request, $authenticatedToken); + $this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN); + } - private function handleAuthenticationSuccess(TokenInterface $token, Request $request, AuthenticatorInterface $authenticator): ?Response - { - $response = $authenticator->onAuthenticationSuccess($request, $token, $this->providerKey); - - $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $token, $request, $response, $this->providerKey)); + $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $authenticatedToken, $request, $response, $this->providerKey)); return $loginSuccessEvent->getResponse(); } @@ -256,7 +237,14 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent */ private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response { + if (null !== $this->logger) { + $this->logger->info('Authenticator failed.', ['exception' => $authenticationException, 'authenticator' => \get_class($authenticator)]); + } + $response = $authenticator->onAuthenticationFailure($request, $authenticationException); + if (null !== $response && null !== $this->logger) { + $this->logger->debug('The "{authenticator}" authenticator set the failure response.', ['authenticator' => \get_class($authenticator)]); + } $this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->providerKey)); diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index e702144787..5e298418cb 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -25,7 +25,7 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface * * @experimental in 5.1 */ -abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, RememberMeAuthenticatorInterface +abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, RememberMeAuthenticatorInterface, InteractiveAuthenticatorInterface { /** * Return the URL to the login page. @@ -61,4 +61,9 @@ abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator impl { return true; } + + public function isInteractive(): bool + { + return true; + } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php index 93d6931218..4b6214668c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php @@ -66,13 +66,13 @@ class AnonymousAuthenticator implements AuthenticatorInterface, CustomAuthentica return new AnonymousToken($this->secret, 'anon.', []); } + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + return null; // let the original request continue + } + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { return null; } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response - { - return null; - } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php b/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php new file mode 100644 index 0000000000..a2abf96e4a --- /dev/null +++ b/src/Symfony/Component/Security/Http/Authenticator/InteractiveAuthenticatorInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Authenticator; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * This is an extension of the authenticator interface that must + * be used by interactive authenticators. + * + * Interactive login requires explicit user action (e.g. a login + * form or HTTP basic authentication). Implementing this interface + * will dispatcher the InteractiveLoginEvent upon successful login. + * + * @author Wouter de Jong + */ +interface InteractiveAuthenticatorInterface extends AuthenticatorInterface +{ + /** + * Should return true to make this authenticator perform + * an interactive login. + */ + public function isInteractive(): bool; +} diff --git a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php index 1ffdd1b997..72c6ea5288 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php @@ -33,7 +33,7 @@ use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices; * * @final */ -class RememberMeAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface +class RememberMeAuthenticator implements InteractiveAuthenticatorInterface, CustomAuthenticatedInterface { private $rememberMeServices; private $secret; @@ -97,6 +97,11 @@ class RememberMeAuthenticator implements AuthenticatorInterface, CustomAuthentic return new RememberMeToken($user, $providerKey, $this->secret); } + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + { + return null; // let the original request continue + } + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { $this->rememberMeServices->loginFail($request, $exception); @@ -104,8 +109,8 @@ class RememberMeAuthenticator implements AuthenticatorInterface, CustomAuthentic return null; } - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response + public function isInteractive(): bool { - return null; + return true; } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php index 46dc09e2f8..7343d79788 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php @@ -12,20 +12,17 @@ namespace Symfony\Component\Security\Http\Tests\Authentication; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Event\LoginSuccessEvent; -use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class AuthenticatorManagerTest extends TestCase { @@ -39,7 +36,7 @@ class AuthenticatorManagerTest extends TestCase protected function setUp(): void { $this->tokenStorage = $this->createMock(TokenStorageInterface::class); - $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $this->eventDispatcher = new EventDispatcher(); $this->request = new Request(); $this->user = $this->createMock(UserInterface::class); $this->token = $this->createMock(TokenInterface::class); @@ -95,35 +92,22 @@ class AuthenticatorManagerTest extends TestCase $matchingAuthenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']); $matchingAuthenticator->expects($this->any())->method('getUser')->willReturn($this->user); - $this->eventDispatcher->expects($this->exactly(4)) - ->method('dispatch') - ->with($this->callback(function ($event) use ($matchingAuthenticator) { - if ($event instanceof VerifyAuthenticatorCredentialsEvent) { - return $event->getAuthenticator() === $matchingAuthenticator - && $event->getCredentials() === ['password' => 'pa$$'] - && $event->getUser() === $this->user; - } - return $event instanceof InteractiveLoginEvent || $event instanceof LoginSuccessEvent || $event instanceof AuthenticationSuccessEvent; - })) - ->will($this->returnCallback(function ($event) { - if ($event instanceof VerifyAuthenticatorCredentialsEvent) { - $event->setCredentialsValid(true); - } + $listenerCalled = false; + $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) use (&$listenerCalled, $matchingAuthenticator) { + if ($event->getAuthenticator() === $matchingAuthenticator && $event->getCredentials() === ['password' => 'pa$$'] && $event->getUser() === $this->user) { + $listenerCalled = true; - return $event; - })); + $event->setCredentialsValid(true); + } + }); $matchingAuthenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); - $matchingAuthenticator->expects($this->any()) - ->method('onAuthenticationSuccess') - ->with($this->anything(), $this->token, 'main') - ->willReturn($this->response); - $manager = $this->createManager($authenticators); - $this->assertSame($this->response, $manager->authenticateRequest($this->request)); + $this->assertNull($manager->authenticateRequest($this->request)); + $this->assertTrue($listenerCalled, 'The VerifyAuthenticatorCredentialsEvent listener is not called'); } public function provideMatchingAuthenticatorIndex() @@ -174,15 +158,9 @@ class AuthenticatorManagerTest extends TestCase $authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']); $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); - $this->eventDispatcher->expects($this->any()) - ->method('dispatch') - ->will($this->returnCallback(function ($event) { - if ($event instanceof VerifyAuthenticatorCredentialsEvent) { - $event->setCredentialsValid(true); - } - - return $event; - })); + $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) { + $event->setCredentialsValid(true); + }); $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); @@ -207,12 +185,38 @@ class AuthenticatorManagerTest extends TestCase $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); $manager = $this->createManager([$authenticator]); - $this->assertSame($this->response, $manager->authenticateUser($this->user, $authenticator, $this->request)); + $manager->authenticateUser($this->user, $authenticator, $this->request); + } + + public function testInteractiveAuthenticator() + { + $authenticator = $this->createMock(InteractiveAuthenticatorInterface::class); + $authenticator->expects($this->any())->method('isInteractive')->willReturn(true); + $this->request->attributes->set('_guard_authenticators', [$authenticator]); + + $authenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']); + $authenticator->expects($this->any())->method('getUser')->willReturn($this->user); + + $this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) { + $event->setCredentialsValid(true); + }); + $authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token); + + $this->tokenStorage->expects($this->once())->method('setToken')->with($this->token); + + $authenticator->expects($this->any()) + ->method('onAuthenticationSuccess') + ->with($this->anything(), $this->token, 'main') + ->willReturn($this->response); + + $manager = $this->createManager([$authenticator]); + $response = $manager->authenticateRequest($this->request); + $this->assertSame($this->response, $response); } private function createAuthenticator($supports = true) { - $authenticator = $this->createMock(AuthenticatorInterface::class); + $authenticator = $this->createMock(InteractiveAuthenticatorInterface::class); $authenticator->expects($this->any())->method('supports')->willReturn($supports); return $authenticator;