Differentiate between interactive and non-interactive authenticators
This commit is contained in:
parent
6b9d78d5e0
commit
ba3754a80f
@ -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));
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
<?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\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 <wouter@wouterj.nl>
|
||||
*/
|
||||
interface InteractiveAuthenticatorInterface extends AuthenticatorInterface
|
||||
{
|
||||
/**
|
||||
* Should return true to make this authenticator perform
|
||||
* an interactive login.
|
||||
*/
|
||||
public function isInteractive(): bool;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user