Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 7efce69

Browse filesBrowse files
committed
In the authentication provider manager handling, add authentication success event for credentials-aware listeners to complement existing, sanitized authentication success event
1 parent 4a7e6fa commit 7efce69
Copy full SHA for 7efce69

File tree

5 files changed

+353
-11
lines changed
Filter options

5 files changed

+353
-11
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authentication/AuthenticationProviderManager.php
+11Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
1616
use Symfony\Component\Security\Core\AuthenticationEvents;
1717
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
18+
use Symfony\Component\Security\Core\Event\AuthenticationSensitiveEvent;
1819
use Symfony\Component\Security\Core\Exception\AccountStatusException;
1920
use Symfony\Component\Security\Core\Exception\AuthenticationException;
2021
use Symfony\Component\Security\Core\Exception\ProviderNotFoundException;
@@ -32,6 +33,10 @@ class AuthenticationProviderManager implements AuthenticationManagerInterface
3233
{
3334
private $providers;
3435
private $eraseCredentials;
36+
37+
/**
38+
* @var EventDispatcherInterface|null
39+
*/
3540
private $eventDispatcher;
3641

3742
/**
@@ -62,6 +67,7 @@ public function authenticate(TokenInterface $token)
6267
{
6368
$lastException = null;
6469
$result = null;
70+
$providerClassName = null;
6571

6672
foreach ($this->providers as $provider) {
6773
if (!$provider instanceof AuthenticationProviderInterface) {
@@ -76,6 +82,7 @@ public function authenticate(TokenInterface $token)
7682
$result = $provider->authenticate($token);
7783

7884
if (null !== $result) {
85+
$providerClassName = get_class($provider);
7986
break;
8087
}
8188
} catch (AccountStatusException $e) {
@@ -88,6 +95,10 @@ public function authenticate(TokenInterface $token)
8895
}
8996

9097
if (null !== $result) {
98+
if (null !== $this->eventDispatcher) {
99+
$this->eventDispatcher->dispatch(AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, new AuthenticationSensitiveEvent($token, $result, $providerClassName));
100+
}
101+
91102
if (true === $this->eraseCredentials) {
92103
$result->eraseCredentials();
93104
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/AuthenticationEvents.php
+24-1Lines 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\AuthenticationEvent")
2144
*/
+96Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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\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 <rmf@src.run>
21+
*/
22+
class AuthenticationSensitiveEvent extends Event
23+
{
24+
private $authenticationToken;
25+
private $preAuthenticationToken;
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 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+
/**
51+
* Attempts to extract the credentials password, first from the authentication token and second from the pre-
52+
* authentication token. It uses either a custom extractor closure (optionally passed as its only argument) or
53+
* the default extractor implementation, which returns the token's credentials string representation if it is
54+
* not an array, or if it is, searches for the index "password", "secret", or "api_key" (in that order) if the
55+
* token's credentials is an array.
56+
*
57+
* @param \Closure|null $extractor An optional custom token credentials password extraction \Closure that is
58+
* provided an auth token (as an instance of TokenInterface) and an auth event
59+
* (as an instance of AuthenticationSensitiveEvent). This closure is called
60+
* first with the final-auth token and second with the pre-auth token, returning
61+
* early if a non-null/non-empty scalar/castable-object value is returned.
62+
*
63+
* @return null|string Either a credentials password/secret/auth_key is returned or null on extraction failure
64+
*/
65+
public function getAuthenticationTokenPassword(\Closure $extractor = null): ?string
66+
{
67+
$extractor = $extractor ?? function (TokenInterface $token): ?string {
68+
return $this->tryCoercibleCredentialsPassToString($credentials = $token->getCredentials())
69+
?: $this->tryArrayFindCredentialsPassToString($credentials);
70+
};
71+
72+
return $extractor($this->authenticationToken, $this) ?: $extractor($this->preAuthenticationToken, $this);
73+
}
74+
75+
private function tryCoercibleCredentialsPassToString($credentials): ?string
76+
{
77+
if (is_object($credentials) && method_exists($credentials, '__toString')) {
78+
return $credentials->__toString() ?: null;
79+
}
80+
81+
return is_scalar($credentials) ? ($credentials ?: null) : null;
82+
}
83+
84+
private function tryArrayFindCredentialsPassToString($credentials): ?string
85+
{
86+
if (is_array($credentials)) {
87+
foreach (array('password', 'api_key', 'api-key', 'secret') as $index) {
88+
if (null !== $c = $this->tryCoercibleCredentialsPassToString($credentials[$index] ?? null)) {
89+
return $c;
90+
}
91+
}
92+
}
93+
94+
return null;
95+
}
96+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Tests/Authentication/AuthenticationProviderManagerTest.php
+42-10Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
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\AuthenticationEvents;
1720
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
1821
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
22+
use Symfony\Component\Security\Core\Event\AuthenticationSensitiveEvent;
1923
use Symfony\Component\Security\Core\Exception\ProviderNotFoundException;
2024
use Symfony\Component\Security\Core\Exception\AuthenticationException;
2125
use Symfony\Component\Security\Core\Exception\AccountStatusException;
@@ -152,29 +156,57 @@ 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', array('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(AuthenticationEvents::AUTHENTICATION_SUCCESS, $this->equalTo(new AuthenticationEvent($token)));
171+
->withConsecutive(array(
172+
AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, $this->equalTo(new AuthenticationSensitiveEvent($priorToken, $finalToken, $providerCN)),
173+
), array(
174+
AuthenticationEvents::AUTHENTICATION_SUCCESS, $this->equalTo(new AuthenticationEvent($finalToken)),
175+
));
168176

169177
$manager = new AuthenticationProviderManager(array($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', array('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 (AuthenticationEvent $event) {
199+
$this->assertEquals('', $event->getAuthenticationToken()->getCredentials());
200+
});
201+
202+
$manager = new AuthenticationProviderManager(array($provider));
203+
$manager->setEventDispatcher($dispatcher);
204+
$manager->authenticate($priorToken);
173205
}
174206

175207
protected function getAuthenticationProvider($supports, $token = null, $exception = null)
176208
{
177-
$provider = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface')->getMock();
209+
$provider = $this->getMockBuilder(AuthenticationProviderInterface::class)->getMock();
178210
$provider->expects($this->once())
179211
->method('supports')
180212
->will($this->returnValue($supports))

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.