Skip to content

Commit b3d37d2

Browse files
committed
initial work on more advanced authentication success sensitive event
Signed-off-by: Rob Frawley 2nd <rmf@src.run>
1 parent 21cf149 commit b3d37d2

File tree

4 files changed

+191
-14
lines changed

4 files changed

+191
-14
lines changed

src/Symfony/Component/Security/Core/Authentication/AuthenticationProviderManager.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Security\Core\Authentication;
1313

14+
use Symfony\Component\EventDispatcher\Event;
1415
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
1516
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
1617
use Symfony\Component\Security\Core\AuthenticationEvents;
@@ -67,6 +68,7 @@ public function authenticate(TokenInterface $token)
6768
{
6869
$lastException = null;
6970
$result = null;
71+
$providerClassName = null;
7072

7173
foreach ($this->providers as $provider) {
7274
if (!$provider instanceof AuthenticationProviderInterface) {
@@ -81,6 +83,7 @@ public function authenticate(TokenInterface $token)
8183
$result = $provider->authenticate($token);
8284

8385
if (null !== $result) {
86+
$providerClassName = get_class($provider);
8487
break;
8588
}
8689
} catch (AccountStatusException $e) {
@@ -94,7 +97,7 @@ public function authenticate(TokenInterface $token)
9497

9598
if (null !== $result) {
9699
if (null !== $this->eventDispatcher) {
97-
$this->eventDispatcher->dispatch(AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, new AuthenticationSensitiveEvent($result));
100+
$this->eventDispatcher->dispatch(AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, new AuthenticationSensitiveEvent($result, $token, $providerClassName));
98101
}
99102

100103
if (true === $this->eraseCredentials) {

src/Symfony/Component/Security/Core/Event/AuthenticationSensitiveEvent.php

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,67 @@
1111

1212
namespace Symfony\Component\Security\Core\Event;
1313

14+
use Symfony\Component\EventDispatcher\Event;
15+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
16+
1417
/**
1518
* This is an authentication event that includes sensitive data.
1619
*
1720
* @author Rob Frawley 2nd <rmf@src.run>
1821
*/
19-
class AuthenticationSensitiveEvent extends AuthenticationEvent
22+
class AuthenticationSensitiveEvent extends Event
2023
{
24+
private $authenticationToken;
25+
private $preAuthenticationToken;
26+
private $authenticationProviderClassName;
27+
28+
public function __construct(TokenInterface $authenticationToken, TokenInterface $preAuthenticationToken, string $authenticationProviderClassName = null)
29+
{
30+
$this->authenticationToken = $authenticationToken;
31+
$this->preAuthenticationToken = $preAuthenticationToken;
32+
$this->authenticationProviderClassName = $authenticationProviderClassName;
33+
}
34+
35+
public function getAuthenticationToken(): TokenInterface
36+
{
37+
return $this->authenticationToken;
38+
}
39+
40+
public function getPreAuthenticationToken(): TokenInterface
41+
{
42+
return $this->preAuthenticationToken;
43+
}
44+
45+
public function getAuthenticationProviderClassName(): ?string
46+
{
47+
return $this->authenticationProviderClassName;
48+
}
49+
50+
public function getAuthenticationTokenPassword(\Closure $extractor = null): ?string
51+
{
52+
return $this->extractAuthenticationTokenPassword($this->preAuthenticationToken, $extractor)
53+
?: $this->extractAuthenticationTokenPassword($this->authenticationToken, $extractor);
54+
}
55+
56+
private function extractAuthenticationTokenPassword(TokenInterface $token, \Closure $extractor = null): ?string
57+
{
58+
return ($extractor ?? function (TokenInterface $token): ?string {
59+
$c = $token->getCredentials();
60+
61+
return is_array($c) && (null !== $p = $this->resolveArrayAuthenticationTokenCredentialsPassword($c))
62+
? $p ?: null
63+
: $c ?: null;
64+
})($token, $this);
65+
}
66+
67+
private function resolveArrayAuthenticationTokenCredentialsPassword(array $credentials): ?string
68+
{
69+
foreach (array('password', 'secret', 'api_key') as $index) {
70+
if (isset($credentials[$index]) && !empty($credentials[$index])) {
71+
return $credentials[$index];
72+
}
73+
}
74+
75+
return null;
76+
}
2177
}

src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationProviderManagerTest.php

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -157,41 +157,50 @@ public function testAuthenticateDispatchesAuthenticationFailureEvent()
157157

158158
public function testAuthenticateDispatchesAuthenticationSuccessEvents()
159159
{
160-
$token = new UsernamePasswordToken('foo', 'bar', 'key');
160+
$finalToken = new UsernamePasswordToken('foo', 'bar', 'baz', array('role-01', 'role-02'));
161+
$priorToken = new UsernamePasswordToken('foo', 'bar', 'baz');
162+
163+
$provider = $this->getAuthenticationProvider(true, $finalToken);
164+
$providerCN = get_class($provider);
161165

162166
$dispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock();
163167
$dispatcher
164168
->expects($this->exactly(2))
165169
->method('dispatch')
166170
->withConsecutive(array(
167-
AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, $this->equalTo(new AuthenticationSensitiveEvent($token)),
171+
AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, $this->equalTo(new AuthenticationSensitiveEvent($finalToken, $priorToken, $providerCN)),
168172
), array(
169-
AuthenticationEvents::AUTHENTICATION_SUCCESS, $this->equalTo(new AuthenticationEvent($token)),
173+
AuthenticationEvents::AUTHENTICATION_SUCCESS, $this->equalTo(new AuthenticationEvent($finalToken)),
170174
));
171175

172-
$manager = new AuthenticationProviderManager(array(
173-
$this->getAuthenticationProvider(true, $token),
174-
));
176+
$manager = new AuthenticationProviderManager(array($provider));
175177
$manager->setEventDispatcher($dispatcher);
176178

177-
$this->assertSame($token, $manager->authenticate($token));
179+
$this->assertSame($finalToken, $manager->authenticate($priorToken));
178180
}
179181

180182
public function testAuthenticateDispatchesAuthenticationSuccessEventsWithCredentialsAvailableAndRemovedForSuccessiveDispatches()
181183
{
184+
$finalToken = new UsernamePasswordToken('foo', 'bar', 'baz', array('role-01', 'role-02'));
185+
$priorToken = new UsernamePasswordToken('foo', 'bar', 'baz');
186+
187+
$provider = $this->getAuthenticationProvider(true, $finalToken);
188+
$providerCN = get_class($provider);
189+
182190
$dispatcher = new EventDispatcher();
183-
$dispatcher->addListener(AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, function (AuthenticationSensitiveEvent $event) {
191+
$dispatcher->addListener(AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, function (AuthenticationSensitiveEvent $event) use ($providerCN) {
192+
$this->assertSame($providerCN, $event->getAuthenticationProviderClassName());
193+
$this->assertSame('bar', $event->getAuthenticationTokenPassword());
194+
$this->assertEquals('bar', $event->getPreAuthenticationToken()->getCredentials());
184195
$this->assertEquals('bar', $event->getAuthenticationToken()->getCredentials());
185196
});
186197
$dispatcher->addListener(AuthenticationEvents::AUTHENTICATION_SUCCESS, function (AuthenticationEvent $event) {
187198
$this->assertEquals('', $event->getAuthenticationToken()->getCredentials());
188199
});
189200

190-
$manager = new AuthenticationProviderManager(array(
191-
$this->getAuthenticationProvider(true, $token = new UsernamePasswordToken('foo', 'bar', 'key')),
192-
));
201+
$manager = new AuthenticationProviderManager(array($provider));
193202
$manager->setEventDispatcher($dispatcher);
194-
$manager->authenticate($token);
203+
$manager->authenticate($priorToken);
195204
}
196205

197206
protected function getAuthenticationProvider($supports, $token = null, $exception = null)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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\Core\Tests\Authentication;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\EventDispatcher\EventDispatcher;
16+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
17+
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
18+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
19+
use Symfony\Component\Security\Core\AuthenticationEvents;
20+
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
21+
use Symfony\Component\Security\Core\Event\AuthenticationSensitiveEvent;
22+
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
23+
24+
class AuthenticationSensitiveEventTest extends TestCase
25+
{
26+
public function testAuthenticateDispatchesAuthenticationSuccessEvents()
27+
{
28+
$finalToken = new UsernamePasswordToken('foo', 'bar', 'baz', array('role-01', 'role-02'));
29+
$priorToken = new UsernamePasswordToken('foo', 'bar', 'baz');
30+
31+
$provider = $this->getAuthenticationProvider(true, $finalToken);
32+
$providerCN = get_class($provider);
33+
34+
$dispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock();
35+
$dispatcher
36+
->expects($this->exactly(2))
37+
->method('dispatch')
38+
->withConsecutive(array(
39+
AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, $this->equalTo(new AuthenticationSensitiveEvent($finalToken, $priorToken, $providerCN)),
40+
), array(
41+
AuthenticationEvents::AUTHENTICATION_SUCCESS, $this->equalTo(new AuthenticationEvent($finalToken)),
42+
));
43+
44+
$manager = new AuthenticationProviderManager(array($provider));
45+
$manager->setEventDispatcher($dispatcher);
46+
47+
$this->assertSame($finalToken, $manager->authenticate($priorToken));
48+
}
49+
50+
public function testAuthenticateDispatchesAuthenticationSuccessEventsWithCredentialsAvailableAndRemovedForSuccessiveDispatches()
51+
{
52+
$finalToken = $this->getTokenInterfaceMock();
53+
$priorToken = $this->getTokenInterfaceMock($credentials = array('password' => 'foobar'));
54+
55+
$provider = $this->getAuthenticationProvider(true, $finalToken);
56+
$providerCN = get_class($provider);
57+
58+
$dispatcher = new EventDispatcher();
59+
$dispatcher->addListener(AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, function (AuthenticationSensitiveEvent $event) use ($credentials, $providerCN) {
60+
$this->assertSame($providerCN, $event->getAuthenticationProviderClassName());
61+
$this->assertSame('foobar', $event->getAuthenticationTokenPassword());
62+
$this->assertEquals($credentials, $event->getPreAuthenticationToken()->getCredentials());
63+
});
64+
$dispatcher->addListener(AuthenticationEvents::AUTHENTICATION_SUCCESS, function (AuthenticationEvent $event) {
65+
$this->assertEquals('', $event->getAuthenticationToken()->getCredentials());
66+
});
67+
68+
$manager = new AuthenticationProviderManager(array($provider));
69+
$manager->setEventDispatcher($dispatcher);
70+
$manager->authenticate($priorToken);
71+
}
72+
73+
private function getTokenInterfaceMock($credentials = null): TokenInterface
74+
{
75+
$token = $this->getMockBuilder(TokenInterface::class)->getMock();
76+
77+
if (null !== $credentials) {
78+
$token
79+
->expects($this->atLeastOnce())
80+
->method('getCredentials')
81+
->will($this->returnValue($credentials));
82+
}
83+
84+
return $token;
85+
}
86+
87+
private function getAuthenticationProvider($supports, $token = null, $exception = null)
88+
{
89+
$provider = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface')->getMock();
90+
$provider->expects($this->once())
91+
->method('supports')
92+
->will($this->returnValue($supports))
93+
;
94+
95+
if (null !== $token) {
96+
$provider->expects($this->once())
97+
->method('authenticate')
98+
->will($this->returnValue($token))
99+
;
100+
} elseif (null !== $exception) {
101+
$provider->expects($this->once())
102+
->method('authenticate')
103+
->will($this->throwException($this->getMockBuilder($exception)->setMethods(null)->getMock()))
104+
;
105+
}
106+
107+
return $provider;
108+
}
109+
}

0 commit comments

Comments
 (0)