Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bfd20b0

Browse files
robfrawleycurry684
authored andcommitted
[Security] Add authentication success sensitive event to authentication provider manager
1 parent 680074d commit bfd20b0

File tree

5 files changed

+346
-11
lines changed

5 files changed

+346
-11
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1818
use Symfony\Component\Security\Core\AuthenticationEvents;
1919
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
20+
use Symfony\Component\Security\Core\Event\AuthenticationSensitiveEvent;
2021
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
2122
use Symfony\Component\Security\Core\Exception\AccountStatusException;
2223
use Symfony\Component\Security\Core\Exception\AuthenticationException;
@@ -66,6 +67,7 @@ public function authenticate(TokenInterface $token)
6667
{
6768
$lastException = null;
6869
$result = null;
70+
$providerClassName = null;
6971

7072
foreach ($this->providers as $provider) {
7173
if (!$provider instanceof AuthenticationProviderInterface) {
@@ -80,6 +82,7 @@ public function authenticate(TokenInterface $token)
8082
$result = $provider->authenticate($token);
8183

8284
if (null !== $result) {
85+
$providerClassName = \get_class($provider);
8386
break;
8487
}
8588
} catch (AccountStatusException $e) {
@@ -92,6 +95,10 @@ public function authenticate(TokenInterface $token)
9295
}
9396

9497
if (null !== $result) {
98+
if (null !== $this->eventDispatcher) {
99+
$this->eventDispatcher->dispatch(new AuthenticationSensitiveEvent($token, $result, $providerClassName), AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE);
100+
}
101+
95102
if (true === $this->eraseCredentials) {
96103
$result->eraseCredentials();
97104
}

src/Symfony/Component/Security/Core/AuthenticationEvents.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,32 @@
1313

1414
final class AuthenticationEvents
1515
{
16+
/**
17+
* The AUTHENTICATION_SUCCESS_SENSITIVE event occurs after a user is
18+
* authenticated by one provider. It is dispatched immediately *prior* to
19+
* the companion AUTHENTICATION_SUCCESS event.
20+
*
21+
* This event *does* contain user credentials and other sensitive data. This
22+
* enables rehashing and other credentials-aware actions. Listeners and
23+
* subscribers of this event carry the added responsibility of passing
24+
* around sensitive data and usage should be limited to cases where this
25+
* extra information is explicitly utilized; otherwise, use the
26+
* AUTHENTICATION_SUCCESS event instead.
27+
*
28+
* @Event("Symfony\Component\Security\Core\Event\AuthenticationSensitiveEvent")
29+
*/
30+
const AUTHENTICATION_SUCCESS_SENSITIVE = 'security.authentication.success_sensitive';
31+
1632
/**
1733
* The AUTHENTICATION_SUCCESS event occurs after a user is authenticated
18-
* by one provider.
34+
* by one provider. It is dispatched immediately *after* the companion
35+
* AUTHENTICATION_SUCCESS_SENSITIVE event.
36+
*
37+
* This event does *not* contain user credentials and other sensitive data
38+
* by default. Listeners and subscribers of this event are shielded from
39+
* the added responsibility of passing around sensitive data and this event
40+
* should be used unless such extra information is required; use the
41+
* AUTHENTICATION_SUCCESS_SENSITIVE event instead if this is the case.
1942
*
2043
* @Event("Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent")
2144
*/
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Event;
13+
14+
use Symfony\Component\EventDispatcher\Event;
15+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
16+
17+
/**
18+
* This is an authentication event that includes sensitive data.
19+
*
20+
* @author Rob Frawley 2nd <[email protected]>
21+
*/
22+
class AuthenticationSensitiveEvent extends Event
23+
{
24+
private $preAuthenticationToken;
25+
private $authenticationToken;
26+
private $authenticationProviderClassName;
27+
28+
public function __construct(TokenInterface $preAuthenticationToken, TokenInterface $authenticationToken, ?string $authenticationProviderClassName = null)
29+
{
30+
$this->preAuthenticationToken = $preAuthenticationToken;
31+
$this->authenticationToken = $authenticationToken;
32+
$this->authenticationProviderClassName = $authenticationProviderClassName;
33+
}
34+
35+
public function getPreAuthenticationToken(): TokenInterface
36+
{
37+
return $this->preAuthenticationToken;
38+
}
39+
40+
public function getAuthenticationToken(): TokenInterface
41+
{
42+
return $this->authenticationToken;
43+
}
44+
45+
public function getAuthenticationProviderClassName(): ?string
46+
{
47+
return $this->authenticationProviderClassName;
48+
}
49+
50+
/**
51+
* Tries to extract the credentials password, first from the post-auth token and second from the pre-auth token.
52+
* It uses either a custom extraction closure (optionally passed as its first and only argument) or the default
53+
* extraction implementation. The default extractor fetches the token's credentials and directly returns it if
54+
* the value is a scalar or object that implements a "__toString()" method. If the credentials val is an array
55+
* the first "password", "api_key", "api-key", or "secret" index value (that exists and is non-false after being
56+
* cast to a sting using the prior described method) is returned. Lastly, if none of the previous conditions are
57+
* met, "null" is returned.
58+
*
59+
* @param \Closure|null $extractor An optional custom token credentials password extraction \Closure that is
60+
* provided an auth token (as an instance of TokenInterface) and an auth event
61+
* (as an instance of AuthenticationSensitiveEvent). This closure is called
62+
* first with the final-auth token and second with the pre-auth token, returning
63+
* early if a non-null/non-empty scalar/castable-object value is returned.
64+
*
65+
* @return string|null Either a credentials password/secret/auth_key is returned or null on extraction failure
66+
*/
67+
public function getAuthenticationTokenPassword(?\Closure $extractor = null): ?string
68+
{
69+
$extractor = $extractor ?? function (TokenInterface $token): ?string {
70+
return $this->tryCoercibleCredentialsPasswordToString($credentials = $token->getCredentials())
71+
?: $this->tryArrayFindCredentialsPasswordToString($credentials);
72+
};
73+
74+
return ($extractor($this->authenticationToken, $this) ?: null)
75+
?: ($extractor($this->preAuthenticationToken, $this) ?: null);
76+
}
77+
78+
private function tryCoercibleCredentialsPasswordToString($credentials): ?string
79+
{
80+
return is_scalar($credentials) || method_exists($credentials, '__toString')
81+
? $credentials
82+
: null;
83+
}
84+
85+
private function tryArrayFindCredentialsPasswordToString($credentials): ?string
86+
{
87+
if (\is_array($credentials)) {
88+
foreach (['password', 'api_key', 'api-key', 'secret'] as $index) {
89+
if ($c = $this->tryCoercibleCredentialsPasswordToString($credentials[$index] ?? null)) {
90+
return $c;
91+
}
92+
}
93+
}
94+
95+
return null;
96+
}
97+
}

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

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@
1212
namespace Symfony\Component\Security\Core\Tests\Authentication;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\EventDispatcher\EventDispatcher;
16+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1517
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
18+
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
1619
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1720
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
1821
use Symfony\Component\Security\Core\AuthenticationEvents;
1922
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
23+
use Symfony\Component\Security\Core\Event\AuthenticationSensitiveEvent;
2024
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
2125
use Symfony\Component\Security\Core\Exception\AccountStatusException;
2226
use Symfony\Component\Security\Core\Exception\AuthenticationException;
@@ -152,29 +156,58 @@ public function testAuthenticateDispatchesAuthenticationFailureEvent()
152156
}
153157
}
154158

155-
public function testAuthenticateDispatchesAuthenticationSuccessEvent()
159+
public function testAuthenticateDispatchesAuthenticationSuccessEvents()
156160
{
157-
$token = new UsernamePasswordToken('foo', 'bar', 'key');
161+
$finalToken = new UsernamePasswordToken('foo', 'bar', 'baz', ['role-01', 'role-02']);
162+
$priorToken = new UsernamePasswordToken('foo', 'bar', 'baz');
158163

159-
$provider = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface')->getMock();
160-
$provider->expects($this->once())->method('supports')->willReturn(true);
161-
$provider->expects($this->once())->method('authenticate')->willReturn($token);
164+
$provider = $this->getAuthenticationProvider(true, $finalToken);
165+
$providerCN = \get_class($provider);
162166

163-
$dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock();
167+
$dispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock();
164168
$dispatcher
165-
->expects($this->once())
169+
->expects($this->exactly(2))
166170
->method('dispatch')
167-
->with($this->equalTo(new AuthenticationSuccessEvent($token)), AuthenticationEvents::AUTHENTICATION_SUCCESS);
171+
->withConsecutive([
172+
$this->equalTo(new AuthenticationSensitiveEvent($priorToken, $finalToken, $providerCN)), AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE,
173+
], [
174+
$this->equalTo(new AuthenticationSuccessEvent($finalToken)), AuthenticationEvents::AUTHENTICATION_SUCCESS,
175+
]);
168176

169177
$manager = new AuthenticationProviderManager([$provider]);
170178
$manager->setEventDispatcher($dispatcher);
171179

172-
$this->assertSame($token, $manager->authenticate($token));
180+
$this->assertSame($finalToken, $manager->authenticate($priorToken));
181+
}
182+
183+
public function testAuthenticateDispatchesAuthenticationSuccessEventsWithCredentialsAvailableAndRemovedForSuccessiveDispatches()
184+
{
185+
$finalToken = new UsernamePasswordToken('foo', 'bar', 'baz', ['role-01', 'role-02']);
186+
$priorToken = new UsernamePasswordToken('foo', 'bar', 'baz');
187+
188+
$provider = $this->getAuthenticationProvider(true, $finalToken);
189+
$providerCN = \get_class($provider);
190+
191+
$dispatcher = new EventDispatcher();
192+
$dispatcher->addListener(AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, function (AuthenticationSensitiveEvent $event) use ($providerCN) {
193+
$this->assertSame($providerCN, $event->getAuthenticationProviderClassName());
194+
$this->assertSame('bar', $event->getAuthenticationTokenPassword());
195+
$this->assertEquals('bar', $event->getPreAuthenticationToken()->getCredentials());
196+
$this->assertEquals('bar', $event->getAuthenticationToken()->getCredentials());
197+
});
198+
$dispatcher->addListener(AuthenticationEvents::AUTHENTICATION_SUCCESS, function (AuthenticationSuccessEvent $event) {
199+
$this->assertEquals('', $event->getAuthenticationToken()->getCredentials());
200+
});
201+
202+
$manager = new AuthenticationProviderManager([$provider]);
203+
$manager->setEventDispatcher($dispatcher);
204+
205+
$this->assertSame($finalToken, $manager->authenticate($priorToken));
173206
}
174207

175208
protected function getAuthenticationProvider($supports, $token = null, $exception = null)
176209
{
177-
$provider = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface')->getMock();
210+
$provider = $this->getMockBuilder(AuthenticationProviderInterface::class)->getMock();
178211
$provider->expects($this->once())
179212
->method('supports')
180213
->will($this->returnValue($supports))

0 commit comments

Comments
 (0)