8000 Differentiate between interactive and non-interactive authenticators · symfony/symfony@ba3754a · GitHub
[go: up one dir, main page]

Skip to content

Commit ba3754a

Browse files
committed
Differentiate between interactive and non-interactive authenticators
1 parent 6b9d78d commit ba3754a

File tree

6 files changed

+122
-85
lines changed

6 files changed

+122
-85
lines changed

src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php

Lines changed: 25 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
2424
use Symfony\Component\Security\Core\User\UserInterface;
2525
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
26+
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
2627
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
2728
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
2829
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
@@ -63,10 +64,8 @@ public function authenticateUser(UserInterface $user, AuthenticatorInterface $au
6364
{
6465
// create an authenticated token for the User
6566
$token = $authenticator->createAuthenticatedToken($user, $this->providerKey);
66-
// authenticate this in the system
67-
$this->saveAuthenticatedToken($token, $request);
6867

69-
// return the success metric
68+
// authenticate this in the system
7069
return $this->handleAuthenticationSuccess($token, $request, $authenticator);
7170
}
7271

@@ -161,49 +160,33 @@ private function executeAuthenticator(string $uniqueAuthenticatorKey, Authentica
161160
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)));
162161
}
163162

164-
if (null !== $this->logger) {
165-
$this->logger->debug('Passing token information to the AuthenticatorManager', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($authenticator)]);
166-
}
167-
168163
// authenticate the credentials (e.g. check password)
169164
$token = $this->authenticateViaAuthenticator($authenticator, $credentials);
170165

171166
if (null !== $this->logger) {
172167
$this->logger->info('Authenticator successful!', ['token' => $token, 'authenticator' => \get_class($authenticator)]);
173168
}
174169

175-
// sets the token on the token storage, etc
176-
$this->saveAuthenticatedToken($token, $request);
177-
} catch (AuthenticationException $e) {
178-
// oh no! Authentication failed!
170+
// success! (sets the token on the token storage, etc)
171+
$response = $this->handleAuthenticationSuccess($token, $request, $authenticator);
172+
if ($response instanceof Response) {
173+
return $response;
174+
}
179175

180176
if (null !== $this->logger) {
181-
$this->logger->info('Authenticator failed.', ['exception' => $e, 'authenticator' => \get_class($authenticator)]);
177+
$this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]);
182178
}
183179

180+
return null;
181+
} catch (AuthenticationException $e) {
182+
// oh no! Authentication failed!
184183
$response = $this->handleAuthenticationFailure($e, $request, $authenticator);
185184
if ($response instanceof Response) {
186185
return $response;
187186
}
188187

189188
return null;
190189
}
191-
192-
// success!
193-
$response = $this->handleAuthenticationSuccess($token, $request, $authenticator);
194-
if ($response instanceof Response) {
195-
if (null !== $this->logger) {
196-
$this->logger->debug('Authenticator set success response.', ['response' => $response, 'authenticator' => \get_class($authenticator)]);
197-
}
198-
199-
return $response;
200-
}
201-
202-
if (null !== $this->logger) {
203-
$this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]);
204-
}
205-
206-
return null;
207190
}
208191

209192
private function authenticateViaAuthenticator(AuthenticatorInterface $authenticator, $credentials): TokenInterface
@@ -234,19 +217,17 @@ private function authenticateViaAuthenticator(AuthenticatorInterface $authentica
234217
return $authenticatedToken;
235218
}
236219

237-
private function saveAuthenticatedToken(TokenInterface $authenticatedToken, Request $request)
220+
private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, Request $request, AuthenticatorInterface $authenticator): ?Response
238221
{
239222
$this->tokenStorage->setToken($authenticatedToken);
240223

241-
$loginEvent = new InteractiveLoginEvent($request, $authenticatedToken);
242-
$this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
243-
}
244-
245-
private function handleAuthenticationSuccess(TokenInterface $token, Request $request, AuthenticatorInterface $authenticator): ?Response
246-
{
247-
$response = $authenticator->onAuthenticationSuccess($request, $token, $this->providerKey);
224+
$response = $authenticator->onAuthenticationSuccess($request, $authenticatedToken, $this->providerKey);
225+
if ($authenticator instanceof InteractiveAuthenticatorInterface && $authenticator->isInteractive()) {
226+
$loginEvent = new InteractiveLoginEvent($request, $authenticatedToken);
227+
$this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
228+
}
248229

249-
$this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $token, $request, $response, $this->providerKey));
230+
$this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $authenticatedToken, $request, $response, $this->providerKey));
250231

251232
return $loginSuccessEvent->getResponse();
252233
}
@@ -256,7 +237,14 @@ private function handleAuthenticationSuccess(TokenInterface $token, Request $req
256237
*/
257238
private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response
258239
{
240+
if (null !== $this->logger) {
241+
$this->logger->info('Authenticator failed.', ['exception' => $authenticationException, 'authenticator' => \get_class($authenticator)]);
242+
}
243+
259244
$response = $authenticator->onAuthenticationFailure($request, $authenticationException);
245+
if (null !== $response && null !== $this->logger) {
246+
$this->logger->debug('The "{authenticator}" authenticator set the failure response.', ['authenticator' => \get_class($authenticator)]);
247+
}
260248

261249
$this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->providerKey));
262250

src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
*
2626
* @experimental in 5.1
2727
*/
28-
abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, RememberMeAuthenticatorInterface
28+
abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, RememberMeAuthenticatorInterface, InteractiveAuthenticatorInterface
2929
{
3030
/**
3131
* Return the URL to the login page.
@@ -61,4 +61,9 @@ public function supportsRememberMe(): bool
6161
{
6262
return true;
6363
}
64+
65+
public function isInteractive(): bool
66+
{
67+
return true;
68+
}
6469
}

src/Symfony/Component/Security/Http/Authenticator/AnonymousAuthenticator.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,12 @@ public function createAuthenticatedToken(UserInterface $user, string $providerKe
6666
return new AnonymousToken($this->secret, 'anon.', []);
6767
}
6868

69-
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
69+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
7070
{
71-
return null;
71+
return null; // let the original request continue
7272
}
7373

74-
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
74+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
7575
{
7676
return null;
7777
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\Authenticator;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
17+
18+
/**
19+
* This is an extension of the authenticator interface that must
20+
* be used by interactive authenticators.
21+
*
22+
* Interactive login requires explicit user action (e.g. a login
23+
* form or HTTP basic authentication). Implementing this interface
24+
* will dispatcher the InteractiveLoginEvent upon successful login.
25+
*
26+
* @author Wouter de Jong <wouter@wouterj.nl>
27+
*/
28+
interface InteractiveAuthenticatorInterface extends AuthenticatorInterface
29+
{
30+
/**
31+
* Should return true to make this authenticator perform
32+
* an interactive login.
33+
*/
34+
public function isInteractive(): bool;
35+
}

src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
*
3434
* @final
3535
*/
36-
class RememberMeAuthenticator implements AuthenticatorInterface, CustomAuthenticatedInterface
36+
class RememberMeAuthenticator implements InteractiveAuthenticatorInterface, CustomAuthenticatedInterface
3737
{
3838
private $rememberMeServices;
3939
private $secret;
@@ -97,15 +97,20 @@ public function createAuthenticatedToken(UserInterface $user, string $providerKe
9797
return new RememberMeToken($user, $providerKey, $this->secret);
9898
}
9999

100+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
101+
{
102+
return null; // let the original request continue
103+
}
104+
100105
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
101106
{
102107
$this->rememberMeServices->loginFail($request, $exception);
103108

104109
return null;
105110
}
106111

107-
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
112+
public function isInteractive(): bool
108113
{
109-
return null;
114+
return true;
110115
}
111116
}

src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,17 @@
1212
namespace Symfony\Component\Security\Http\Tests\Authentication;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\EventDispatcher\EventDispatcher;
1516
use Symfony\Component\HttpFoundation\Request;
1617
use Symfony\Component\HttpFoundation\Response;
1718
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1819
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
19-
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
2020
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
2121
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
2222
use Symfony\Component\Security\Core\User\UserInterface;
2323
use Symfony\Component\Security\Http\Authentication\AuthenticatorManager;
24-
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
25-
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
26-
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
24+
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
2725
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
28-
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
2926

3027
class AuthenticatorManagerTest extends TestCase
3128
{
@@ -39,7 +36,7 @@ class AuthenticatorManagerTest extends TestCase
3936
protected function setUp(): void
4037
{
4138
$this->tokenStorage = $this->createMock(TokenStorageInterface::class);
42-
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
39+
$this->eventDispatcher = new EventDispatcher();
4340
$this->request = new Request();
4441
$this->user = $this->createMock(UserInterface::class);
4542
$this->token = $this->createMock(TokenInterface::class);
@@ -95,35 +92,22 @@ public function testAuthenticateRequest($matchingAuthenticatorIndex)
9592

9693
$matchingAuthenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']);
9794
$matchingAuthenticator->expects($this->any())->method('getUser')->willReturn($this->user);
98-
$this->eventDispatcher->expects($this->exactly(4))
99-
->method('dispatch')
100-
->with($this->callback(function ($event) use ($matchingAuthenticator) {
101-
if ($event instanceof VerifyAuthenticatorCredentialsEvent) {
102-
return $event->getAuthenticator() === $matchingAuthenticator
103-
&& $event->getCredentials() === ['password' => 'pa$$']
104-
&& $event->getUser() === $this->user;
105-
}
106-
107-
return $event instanceof InteractiveLoginEvent || $event instanceof LoginSuccessEvent || $event instanceof AuthenticationSuccessEvent;
108-
}))
109-
->will($this->returnCallback(function ($event) {
110-
if ($event instanceof VerifyAuthenticatorCredentialsEvent) {
111-
$event->setCredentialsValid(true);
112-
}
113-
114-
return $event;
115-
}));
95+
96+
$listenerCalled = false;
97+
$this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) use (&$listenerCalled, $matchingAuthenticator) {
98+
if ($event->getAuthenticator() === $matchingAuthenticator && $event->getCredentials() === ['password' => 'pa$$'] && $event->getUser() === $this->user) {
99+
$listenerCalled = true;
100+
101+
$event->setCredentialsValid(true);
102+
}
103+
});
116104
$matchingAuthenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
117105

118106
$this->tokenStorage->expects($this->once())->method('setToken')->with($this->token);
119107

120-
$matchingAuthenticator->expects($this->any())
121-
->method('onAuthenticationSuccess')
122-
->with($this->anything(), $this->token, 'main')
123-
->willReturn($this->response);
124-
125108
$manager = $this->createManager($authenticators);
126-
$this->assertSame($this->response, $manager->authenticateRequest($this->request));
109+
$this->assertNull($manager->authenticateRequest($this->request));
110+
$this->assertTrue($listenerCalled, 'The VerifyAuthenticatorCredentialsEvent listener is not called');
127111
}
128112

129113
public function provideMatchingAuthenticatorIndex()
@@ -174,15 +158,9 @@ public function testEraseCredentials($eraseCredentials)
174158

175159
$authenticator->expects($this->any())->method('getCredentials')->willReturn(['username' => 'john']);
176160
$authenticator->expects($this->any())->method('getUser')->willReturn($this->user);
177-
$this->eventDispatcher->expects($this->any())
178-
->method('dispatch')
179-
->will($this->returnCallback(function ($event) {
180-
if ($event instanceof VerifyAuthenticatorCredentialsEvent) {
181-
$event->setCredentialsValid(true);
182-
}
183-
184-
return $event;
185-
}));
161+
$this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) {
162+
$event->setCredentialsValid(true);
163+
});
186164

187165
$authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
188166

@@ -207,12 +185,38 @@ public function testAuthenticateUser()
207185
$this->tokenStorage->expects($this->once())->method('setToken')->with($this->token);
208186

209187
$manager = $this->createManager([$authenticator]);
210-
$this->assertSame($this->response, $manager->authenticateUser($this->user, $authenticator, $this->request));
188+
$manager->authenticateUser($this->user, $authenticator, $this->request);
189+
}
190+
191+
public function testInteractiveAuthenticator()
192+
{
193+
$authenticator = $this->createMock(InteractiveAuthenticatorInterface::class);
194+
$authenticator->expects($this->any())->method('isInteractive')->willReturn(true);
195+
$this->request->attributes->set('_guard_authenticators', [$authenticator]);
196+
197+
$authenticator->expects($this->any())->method('getCredentials')->willReturn(['password' => 'pa$$']);
198+
$authenticator->expects($this->any())->method('getUser')->willReturn($this->user);
199+
200+
$this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) {
201+
$event->setCredentialsValid(true);
202+
});
203+
$authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
204+
205+
$this->tokenStorage->expects($this->once())->method('setToken')->with($this->token);
206+
207+
$authenticator->expects($this->any())
208+
->method('onAuthenticationSuccess')
209+
->with($this->anything(), $this->token, 'main')
210+
->willReturn($this->response);
211+
212+
$manager = $this->createManager([$authenticator]);
213+
$response = $manager->authenticateRequest($this->request);
214+
$this->assertSame($this->response, $response);
211215
}
212216

213217
private function createAuthenticator($supports = true)
214218
{
215-
$authenticator = $this->createMock(AuthenticatorInterface::class);
219+
$authenticator = $this->createMock(InteractiveAuthenticatorInterface::class);
216220
$authenticator->expects($this->any())->method('supports')->willReturn($supports);
217221

218222
return $authenticator;

0 commit comments

Comments
 (0)
0