Skip to content

Commit 31c194f

Browse files
committed
feature #37359 [Security] Add event to inspect authenticated token before it becomes effective (scheb)
This PR was squashed before being merged into the 5.2-dev branch. Discussion ---------- [Security] Add event to inspect authenticated token before it becomes effective | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | License | MIT | Doc PR | n/a Hello there, I'm the author of `scheb/two-factor-bundle`, which extends Symfony's security layer with two-factor authentication. I've been closely following the recent changes by @wouterj to rework the security layer with "authenticators" (great work!). While I managed to make my bundle work with authenticators, I see some limitations in the security layer that I'd like to address to make such extensions easier to implement. This PR adds a new event, which is disapatched right after the authenticated token has been created by the authenticator, to "announce" it to the application *before* it becomes effective to the security system. The event works similar to `ResponseEvent`, but for security token. It allows listeners to inspect the new token before it becomes effective and - most importantly - apply modifications to it. So components other than the authenticator will be able to influence how the security token looks like, that will be set to the security layer on successful authentication. Why would you want to do this? Of course I'm looking at this from the 2fa perspective. To make 2fa work, it's necessary to prevent a newly created authenticated token from becoming visible to the security system and therefore exposing its privileges/roles. This is done by replacing the authenticated token with a temporary "TwoFactorToken". Currently I'm doing this through dependency injection, getting all the registered authenticators and decorating them with my own token-exchange logic. This is not very clean and overly complicated, but it works. Adding this event as a hook-in point would allow for a much cleaner integration for any component that wants to have a saying in how the security token should look like. Commits ------- 2030964 [Security] Add event to inspect authenticated token before it becomes effective
2 parents 3cd1123 + 2030964 commit 31c194f

File tree

3 files changed

+92
-0
lines changed

3 files changed

+92
-0
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
2727
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
2828
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
29+
use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent;
2930
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
3031
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
3132
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
@@ -70,6 +71,9 @@ public function authenticateUser(UserInterface $user, AuthenticatorInterface $au
7071
// create an authenticated token for the User
7172
$token = $authenticator->createAuthenticatedToken($passport = new SelfValidatingPassport($user, $badges), $this->firewallName);
7273

74+
// announce the authenticated token
75+
$token = $this->eventDispatcher->dispatch(new AuthenticationTokenCreatedEvent($token))->getAuthenticatedToken();
76+
7377
// authenticate this in the system
7478
return $this->handleAuthenticationSuccess($token, $passport, $request, $authenticator);
7579
}
@@ -167,6 +171,10 @@ private function executeAuthenticator(AuthenticatorInterface $authenticator, Req
167171

168172
// create the authenticated token
169173
$authenticatedToken = $authenticator->createAuthenticatedToken($passport, $this->firewallName);
174+
175+
// announce the authenticated token
176+
$authenticatedToken = $this->eventDispatcher->dispatch(new AuthenticationTokenCreatedEvent($authenticatedToken))->getAuthenticatedToken();
177+
170178
if (true === $this->eraseCredentials) {
171179
$authenticatedToken->eraseCredentials();
172180
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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\Event;
13+
14+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
15+
use Symfony\Contracts\EventDispatcher\Event;
16+
17+
/**
18+
* When a newly authenticated security token was created, before it becomes effective in the security system.
19+
*
20+
* @author Christian Scheb <me@christianscheb.de>
21+
*/
22+
class AuthenticationTokenCreatedEvent extends Event
23+
{
24+
private $authenticatedToken;
25+
26+
public function __construct(TokenInterface $token)
27+
{
28+
$this->authenticatedToken = $token;
29+
}
30+
31+
public function getAuthenticatedToken(): TokenInterface
32+
{
33+
return $this->authenticatedToken;
34+
}
35+
36+
public function setAuthenticatedToken(TokenInterface $authenticatedToken): void
37+
{
38+
$this->authenticatedToken = $authenticatedToken;
39+
}
40+
}

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
2525
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
2626
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
27+
use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent;
2728
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
2829

2930
class AuthenticatorManagerTest extends TestCase
@@ -154,6 +155,29 @@ public function provideEraseCredentialsData()
154155
yield [false];
155156
}
156157

158+
public function testAuthenticateRequestCanModifyTokenFromEvent(): void
159+
{
160+
$authenticator = $this->createAuthenticator();
161+
$this->request->attributes->set('_security_authenticators', [$authenticator]);
162+
163+
$authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user));
164+
165+
$authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
166+
167+
$modifiedToken = $this->createMock(TokenInterface::class);
168+
$listenerCalled = false;
169+
$this->eventDispatcher->addListener(AuthenticationTokenCreatedEvent::class, function (AuthenticationTokenCreatedEvent $event) use (&$listenerCalled, $modifiedToken) {
170+
$event->setAuthenticatedToken($modifiedToken);
171+
$listenerCalled = true;
172+
});
173+
174+
$this->tokenStorage->expects($this->once())->method('setToken')->with($this->identicalTo($modifiedToken));
175+
176+
$manager = $this->createManager([$authenticator]);
177+
$this->assertNull($manager->authenticateRequest($this->request));
178+
$this->assertTrue($listenerCalled, 'The AuthenticationTokenCreatedEvent listener is not called');
179+
}
180+
157181
public function testAuthenticateUser()
158182
{
159183
$authenticator = $this->createAuthenticator();
@@ -166,6 +190,26 @@ public function testAuthenticateUser()
166190
$manager->authenticateUser($this->user, $authenticator, $this->request);
167191
}
168192

193+
public function testAuthenticateUserCanModifyTokenFromEvent(): void
194+
{
195+
$authenticator = $this->createAuthenticator();
196+
$authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
197+
$authenticator->expects($this->any())->method('onAuthenticationSuccess')->willReturn($this->response);
198+
199+
$modifiedToken = $this->createMock(TokenInterface::class);
200+
$listenerCalled = false;
201+
$this->eventDispatcher->addListener(AuthenticationTokenCreatedEvent::class, function (AuthenticationTokenCreatedEvent $event) use (&$listenerCalled, $modifiedToken) {
202+
$event->setAuthenticatedToken($modifiedToken);
203+
$listenerCalled = true;
204+
});
205+
206+
$this->tokenStorage->expects($this->once())->method('setToken')->with($this->identicalTo($modifiedToken));
207+
208+
$manager = $this->createManager([$authenticator]);
209+
$manager->authenticateUser($this->user, $authenticator, $this->request);
210+
$this->assertTrue($listenerCalled, 'The AuthenticationTokenCreatedEvent listener is not called');
211+
}
212+
169213
public function testInteractiveAuthenticator()
170214
{
171215
$authenticator = $this->createMock(InteractiveAuthenticatorInterface::class);

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy