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

[Security] Add authentication success sensitive event to authentication provider manager (rebase #26050) #30955

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Event\AuthenticationSensitiveEvent;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
Expand Down Expand Up @@ -66,6 +67,7 @@ public function authenticate(TokenInterface $token)
{
$lastException = null;
$result = null;
$providerClassName = null;

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

if (null !== $result) {
$providerClassName = \get_class($provider);
break;
}
} catch (AccountStatusException $e) {
Expand All @@ -92,6 +95,10 @@ public function authenticate(TokenInterface $token)
}

if (null !== $result) {
if (null !== $this->eventDispatcher) {
$this->eventDispatcher->dispatch(new AuthenticationSensitiveEvent($token, $result, $providerClassName), AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE);
}

if (true === $this->eraseCredentials) {
$result->eraseCredentials();
}
Expand Down
25 changes: 24 additions & 1 deletion 25 src/Symfony/Component/Security/Core/AuthenticationEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,32 @@

final class AuthenticationEvents
{
/**
* The AUTHENTICATION_SUCCESS_SENSITIVE event occurs after a user is
* authenticated by one provider. It is dispatched immediately *prior* to
* the companion AUTHENTICATION_SUCCESS event.
*
* This event *does* contain user credentials and other sensitive data. This
* enables rehashing and other credentials-aware actions. Listeners and
* subscribers of this event carry the added responsibility of passing
* around sensitive data and usage should be limited to cases where this
* extra information is explicitly utilized; otherwise, use the
* AUTHENTICATION_SUCCESS event instead.
*
* @Event("Symfony\Component\Security\Core\Event\AuthenticationSensitiveEvent")
*/
const AUTHENTICATION_SUCCESS_SENSITIVE = 'security.authentication.success_sensitive';

/**
* The AUTHENTICATION_SUCCESS event occurs after a user is authenticated
* by one provider.
* by one provider. It is dispatched immediately *after* the companion
* AUTHENTICATION_SUCCESS_SENSITIVE event.
*
* This event does *not* contain user credentials and other sensitive data
* by default. Listeners and subscribers of this event are shielded from
* the added responsibility of passing around sensitive data and this event
* should be used unless such extra information is required; use the
* AUTHENTICATION_SUCCESS_SENSITIVE event instead if this is the case.
*
* @Event("Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent")
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

namespace Symfony\Component\Security\Core\Event;

use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Contracts\EventDispatcher\Event;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be reverted: BC break :)


/**
* This is a general purpose authentication event.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Core\Event;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Contracts\EventDispatcher\Event;

/**
* This is an authentication event that includes sensitive data.
*
* @author Rob Frawley 2nd <rmf@src.run>
*/
class AuthenticationSensitiveEvent extends Event
{
private $preAuthenticationToken;
private $authenticationToken;
private $authenticationProviderClassName;

public function __construct(TokenInterface $preAuthenticationToken, TokenInterface $authenticationToken, ?string $authenticationProviderClassName = null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove the? as our CS is to consider the null default means the same (it does)

{
$this->preAuthenticationToken = $preAuthenticationToken;
$this->authenticationToken = $authenticationToken;
$this->authenticationProviderClassName = $authenticationProviderClassName;
}

public function getPreAuthenticationToken(): TokenInterface
{
return $this->preAuthenticationToken;
}

public function getAuthenticationToken(): TokenInterface
{
return $this->authenticationToken;
}

public function getAuthenticationProviderClassName(): ?string
{
return $this->authenticationProviderClassName;
}

/**
* Tries to extract the credentials password, first from the post-auth token and second from the pre-auth token.
* It uses either a custom extraction closure (optionally passed as its first and only argument) or the default
* extraction implementation. The default extractor fetches the token's credentials and directly returns it if
* the value is a scalar or object that implements a "__toString()" method. If the credentials val is an array
* the first "password", "api_key", "api-key", or "secret" index value (that exists and is non-false after being
* cast to a sting using the prior described method) is returned. Lastly, if none of the previous conditions are
* met, "null" is returned.
*
* @param \Closure|null $extractor An optional custom token credentials password extraction \Closure that is
* provided an auth token (as an instance of TokenInterface) and an auth event
* (as an instance of AuthenticationSensitiveEvent). This closure is called
* first with the final-auth token and second with the pre-auth token, returning
* early if a non-null/non-empty scalar/castable-object value is returned.
*
* @return string|null Either a credentials password/secret/auth_key is returned or null on extraction failure
*/
public function getAuthenticationTokenPassword(?\Closure $extractor = null): ?string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

{
$extractor = $extractor ?? function (TokenInterface $token): ?string {
return $this->tryCoercibleCredentialsPasswordToString($credentials = $token->getCredentials())
?: $this->tryArrayFindCredentialsPasswordToString($credentials);
};

return ($extractor($this->authenticationToken, $this) ?: null)
?: ($extractor($this->preAuthenticationToken, $this) ?: null);
}

private function tryCoercibleCredentialsPasswordToString($credentials): ?string
{
return is_scalar($credentials) || method_exists($credentials, '__toString')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be on one line as we usually prefer long line in similar cases

? $credentials
: null;
}

private function tryArrayFindCredentialsPasswordToString($credentials): ?string
{
if (\is_array($credentials)) {
foreach (['password', 'api_key', 'api-key', 'secret'] as $index) {
if ($c = $this->tryCoercibleCredentialsPasswordToString($credentials[$index] ?? null)) {
return $c;
}
}
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@
namespace Symfony\Component\Security\Core\Tests\Authentication;

use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Event\AuthenticationSensitiveEvent;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\ProviderNotFoundException;
use Symfony\Contracts\EventDispatcher\Event;

class AuthenticationProviderManagerTest extends TestCase
{
Expand Down Expand Up @@ -152,29 +157,68 @@ public function testAuthenticateDispatchesAuthenticationFailureEvent()
}
}

public function testAuthenticateDispatchesAuthenticationSuccessEvent()
public function testAuthenticateDispatchesAuthenticationSuccessEvents()
{
$token = new UsernamePasswordToken('foo', 'bar', 'key');
$finalToken = new UsernamePasswordToken('foo', 'bar', 'baz', ['role-01', 'role-02']);
$priorToken = new UsernamePasswordToken('foo', 'bar', 'baz');

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

$dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock();
$dispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock();
$dispatcher
->expects($this->once())
->expects($this->exactly(2))
->method('dispatch')
->with($this->equalTo(new AuthenticationSuccessEvent($token)), AuthenticationEvents::AUTHENTICATION_SUCCESS);
->withConsecutive([
$this->equalTo(new AuthenticationSensitiveEvent($priorToken, $finalToken, $providerCN)), AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE,
], [
$this->equalTo(new AuthenticationSuccessEvent($finalToken)), AuthenticationEvents::AUTHENTICATION_SUCCESS,
]);

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

$this->assertSame($token, $manager->authenticate($token));
$this->assertSame($finalToken, $manager->authenticate($priorToken));
}

public function testAuthenticateDispatchesAuthenticationSuccessEventsWithCredentialsAvailableAndRemovedForSuccessiveDispatches()
{
$finalToken = new UsernamePasswordToken('foo', 'bar', 'baz', ['role-01', 'role-02']);
$priorToken = new UsernamePasswordToken('foo', 'bar', 'baz');

$provider = $this->getAuthenticationProvider(true, $finalToken);
$providerCN = \get_class($provider);

$dispatcher = new EventDispatcher();
$dispatcher->addListener(AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE, function ($event) use ($providerCN) {
if ('Symfony\Component\EventDispatcher\WrappedEvent' === \get_class($event)) {
$event = $event->getWrappedEvent();
}

/* @var AuthenticationSensitiveEvent $event */
$this->assertSame($providerCN, $event->getAuthenticationProviderClassName());
$this->assertSame('bar', $event->getAuthenticationTokenPassword());
$this->assertEquals('bar', $event->getPreAuthenticationToken()->getCredentials());
$this->assertEquals('bar', $event->getAuthenticationToken()->getCredentials());
});
$dispatcher->addListener(AuthenticationEvents::AUTHENTICATION_SUCCESS, function ($event) {
if ('Symfony\Component\EventDispatcher\WrappedEvent' === \get_class($event)) {
$event = $event->getWrappedEvent();
}

/* @var AuthenticationSuccessEvent $event */
$this->assertEquals('', $event->getAuthenticationToken()->getCredentials());
});

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

$this->assertSame($finalToken, $manager->authenticate($priorToken));
}

protected function getAuthenticationProvider($supports, $token = null, $exception = null)
{
$provider = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface')->getMock();
$provider = $this->getMockBuilder(AuthenticationProviderInterface::class)->getMock();
$provider->expects($this->once())
->method('supports')
->will($this->returnValue($supports))
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.