Differentiate between interactive and non-interactive authenticators

This commit is contained in:
Wouter de Jong 2020-03-13 15:21:38 +01:00
parent 6b9d78d5e0
commit ba3754a80f
6 changed files with 124 additions and 87 deletions

View File

@ -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));

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;