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 (enabled separation of subscribers with credentials access) #26050

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

Conversation

robfrawley
Copy link
Contributor

@robfrawley robfrawley commented Feb 5, 2018

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #18494
License MIT
Doc PR n/a

This PR adds an AUTHENTICATION_SUCCESS_SENSITIVE authentication event that is dispatched immediately prior to the existing AUTHENTICATION_SUCCESS event.

By default, the token provided to the authentication success event is sanitized and does not contain any sensitive data (for example, the user's raw password). The existing success event can be configured such that the token is not sanitized of sensitive data, but then we have the problem of providing this data to all authentication success subscribers, when the vast majority should not have access to such data.

The newly added sensitive success event includes the original, unmodified token, including the raw password. These two events now provide a clear separation between subscribers that shoulder the added responsibility of handling sensitive user data and those that do not. The new event can be used for actions such as rehashing passwords and other credentials-aware actions.

@nicolas-grekas
Copy link
Member

What's the alternative right now for rehashing? Is there any?

AUTHENTICATION_SUCCESS_SENSITIVE

makes sense, the current name is too specific to one use case, isn't it?
AUTHENTICATION_SUCCESS_WITH_CREDENTIALS?

@nicolas-grekas nicolas-grekas modified the milestones: 4.1, next Apr 20, 2018
@wouterj
Copy link
Member

wouterj commented Apr 21, 2018

👍 For adding this event. I agree with @nicolas-grekas that the name is a bit specific atm. I like AUTHENTICATION_SUCCESS_SENSITIVE the most from the suggestions (passwords still are credentials while hashed imo, so I think the second suggestion doesn't really warn the user that's quite sensitive data).

@robfrawley robfrawley force-pushed the feature-add-rehash-authentication-event branch 2 times, most recently from 9f75712 to 2d69374 Compare May 15, 2018 03:15
@robfrawley
Copy link
Contributor Author

robfrawley commented May 15, 2018

"What's the alternative right now for rehashing? Is there any?" -@nicolas-grekas

I've seen implementations handle it inside the password encoder, but this requires injection an entity manager or other persistence layer inside the password encoder implementation, which is a deal breaker for me. IMHO, the password encoder should not be in the business of persisting anything.

"The name is a bit specific atm. AUTHENTICATION_SUCCESS_SENSITIVE?" -@wouterj
"Current name is too specific. AUTHENTICATION_SUCCESS_WITH_CREDENTIALS?" -@nicolas-grekas

I prefer AUTHENTICATION_SUCCESS_SENSITIVE and that is what I've gone ahead and edited this PR to implement. If there is a strong preference otherwise, please advise. I've also included unit tests now. Thoughts on this implementation? Any changes I should make?

Otherwise, I think this is pretty much set, pending any additional reviews.

@robfrawley robfrawley force-pushed the feature-add-rehash-authentication-event branch from 2d69374 to 21cf149 Compare May 15, 2018 03:39
@robfrawley robfrawley changed the title [Security] Add authentication rehash event to authentication provider manager [Security] Add authentication success sensitive event to authentication provider manager (enabled separation of subscribers with credentials access) May 15, 2018
@linaori
Copy link
Contributor

linaori commented May 15, 2018

This will be a bit difficult to do for guard. A guard authenticator has getCredentials in the PreAuthenticatedGuardToken, which usually returns an array of which 1 key is password (average use-case). In my case, I have 3 authenticators on a single firewall. 2/3 use password, the 3rd doesn't have a password (one time token). They are on the same firewall, same firewall context and use the same PreAuthenticatedGuardToken. The only difference is that I have no password key in the 3rd case. I know I can make a bunch of if-statements in my listener, but it feels nasty.

Also in your code I noticed that you use $result instead of $token, which means with guard, you can never get the credentials. Here's a scenario where I have a two factor code (scheb/two-factor-bundle). If it wasn't a two factor token, it would've been a PostAuthenticatedGuardToken directly:

image

@robfrawley
Copy link
Contributor Author

"I know I can make a bunch of if statements in my listener, but it feels nasty." -@iltar

Effectively, whether the control statement is in your own listener or within a symfony-core implementation, such logic does need to exist somewhere, right? What are you suggesting as an alternative?

"Also in your code I noticed that you use $result instead of $token." -@iltar

You're referring to diff-70fc43e7af23f097f94d4a0036888896R97, correct? My usage of $result instead of $token was taken directly from the implementation of AUTHENTICATION_SUCCESS. I thought that the authenticate() method on a provider returned a token? You're saying for guard auth providers they do not? Why is that?

@linaori
Copy link
Contributor

linaori commented May 15, 2018

Effectively, whether the control statement is in your own listener or within a symfony-core implementation, such logic does need to exist somewhere, right? What are you suggesting as an alternative?

I'm not exactly sure if this can be solved in an easy fashion. I just felt it was worthy of mentioning, either for the docs or perhaps to inspire an idea.

You're referring to diff-70fc43e7af23f097f94d4a0036888896R97, correct?

Correct.

I thought that the authenticate() method on a provider returned a token? You're saying for guard auth providers they do not? Why is that?

They do return a token, this token just doesn't have the password in it.

In your change, you're using the authenticated token, not the token that contains the information to authenticated. The even you've borrowed the code from, uses the authenticated token as it would not make sense to use the raw data.

edit:

public function getCredentials()
{
return array();
}

@robfrawley
Copy link
Contributor Author

Thanks for the clarifications @iltar. Keeping some of the concerns you mentioned earlier at the forefront of my mind, perhaps it would make sense to create a more feature-rich AuthenticationSensitiveEvent class implementation that offloads some of the more complex tasks away from the user.

Here is an example draft of one such event implementation, where some of the most common operations for this new event (such as password rehashing) should be relatively easy to implement thanks to the expanded context provided to the constructor and the additional helper methods exposed on it:

<?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\EventDispatcher\Event;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

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

    public function __construct(TokenInterface $finalToken, TokenInterface $priorToken, string $authenticationProvider = null)
    {
        $this->priorToken = $priorToken;
        $this->finalToken = $finalToken;
        $this->authenticationProvider = $authenticationProvider;
    }

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

    public function getPriorToken(): TokenInterface
    {
        return $this->priorToken;
    }

    public function getFinalToken(): TokenInterface
    {
        return $this->finalToken;
    }

    public function isTokenAuthenticated(bool $considerPriorToken = false): bool
    {
        return $this->finalToken->isAuthenticated() || ($considerPriorToken && $this->priorToken->isAuthenticated());
    }

    public function getAuthenticationProvider(): ?string
    {
        return $this->authenticationProvider;
    }

    public function isAuthenticationProvider(string $provider): bool
    {
        return $this->authenticationProvider === $provider;
    }

    public function getTokenPasswordCredentials(\Closure $extractor = null, bool $considerRequestedToken = true)
    {
        return $this->normalizeTokenPasswordCredentials($this->finalToken, $extractor) ?: (
            true === $considerRequestedToken
                ? $this->normalizeTokenPasswordCredentials($this->priorToken, $extractor)
                : null
        );
    }

    private function normalizeTokenPasswordCredentials(TokenInterface $token, \Closure $extractor = null): ?string
    {
        return ($extractor ?? function (TokenInterface $token): ?string {
            return $this->extractTokenPasswordCredentials($token);
        })($token, $this);
    }

    private function extractTokenPasswordCredentials(TokenInterface $token): ?string
    {
        $cred = $token->getCredentials();

        if (is_array($cred) && null !== $pass = $this->searchTokenCredentialsArrayForPasswordElement($cred)) {
            return $pass;
        }

        return $cred ?: null;
    }

    private function searchTokenCredentialsArrayForPasswordElement(array $credentials): ?string
    {
        foreach (['password', 'secret', 'api_key'] as $index) {
            if (isset($credentials[$index]) && false === empty($credentials[$index])) {
                return $credentials[$index];
            }
        }

        return null;
    }
}

I believe most of the above functionsality is generic enough for countless purposes while still providing meaningful impact on lessoning the burden on developers looking to handle common tasks like password rehashing. Given, the above is a rough draft and I'm just looking for feedback ATM, but this does work relatively well for the original goal of this PR: to streamline password rehashing and expose an event for this use-case.

As a further illustration, browse the below event listener utilizing a AuthenticationSensitiveEvent event. Operations such as retrieving the prior/final token object, checking authentication state, exposing the authentication provider responsible for creating the final token, and password extraction from the multiple possible formats of a token's credentials value, should all help to create a better DX experience.

<?php

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Security\Core\AuthenticationEvents;
use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Event\AuthenticationSensitiveEvent;

$listener = function (AuthenticationSensitiveEvent $event) {
    ### Token Objects: Final and Prior

    // Easily get the final authentication token using either method (they are synonyms atm).
    $finalToken = $event->getAuthenticationToken();
    $finalToken = $event->getFinalToken();

    // Or, get the prior authentication token as originally passed into the authentication
    // manager, before any authentication provider performed operations on it.
    $priorToken = $event->getPriorToken();

    ### Token Authentication: State

    // Check if the authentication token (the final token) is authenticated.
    $isAuthenticated = $event->isTokenAuthenticated();
    // Or, check if *either* the final or the prior authentication token is authenticated.
    $isAuthenticated = $event->isTokenAuthenticated(true);

    ### Token Credentials: Extract Password

    // Easily extract the token's password from its credentials, regardless of format.
    // All core token credential types are supported, including strings (the most common)
    // and credential arrays (as seen in the guard tokens). Supported indexes for pass
    // when token credentials is an array are 'password', 'secret', and 'api_key'.
    $password = $event->getTokenPasswordCredentials();

    // Or, pass an "extraction" closure as the first argument and perform any custom
    // logic required on the token to resolve the password.
    $password = $event->getTokenPasswordCredentials(function (TokenInterface $token): ?string {
        return $token->getCredentials()['some_custom_index_string'] ?? null;
    });

    // For both examples above, be sure to check for a `null` return to ensure the
    // password resolution was successful.
    if (null !== $password) {
        // Perform work if password was extracted...
    }

    ### Authentication Provider: Fully Qualified Class Name

    // Get the fully qualified class name for the authentication provider that resolved
    // the final token value (use in a `switch` or other control structure to perform
    // specific actions depending on the authentication provider that handled the token).
    $providerClassName = $event->getAuthenticationProvider();

    // Or, quickly check if the authentication provider equals your passed class name
    // argument.
    if ($event->isAuthenticationProvider(UserAuthenticationProvider::class)) {
        // Do work if `UserAuthenticationProvider::class` is the authentication provider
        // that resolved the final token...
    }

    ### Everything Else: Use Your Imagination

    // Lastly, perform any other operations on either token that regularly occur in
    // existing code...
    $event->getFinalToken()->getUser()->callMyCustomMethodOnUserObject();

};

// Start, setup, and run dispatcher manually using above defined `$listener` anonymous
// function (normally handled internally by Symfony; only to illustrate example usage).
$dispatcher = new EventDispatcher();
$dispatcher->addListener(
    AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE,
    $listener
);
$dispatcher->dispatch(
    AuthenticationEvents::AUTHENTICATION_SUCCESS_SENSITIVE,
    new AuthenticationSensitiveEvent($finalToken, $priorToken, $providerClassName)
);

@iltar, @wouterj, @nicolas-grekas: Any thoughts on the current implementation and the drafted additions here?

@linaori
Copy link
Contributor

linaori commented May 15, 2018

Some feedback:

  • I don't know if it's useful to pass the authentication provider, in terms for Guard, this would be guard, thus still of no use, I don't see a way of fetching the internal authenticator either, so this might be a bit problematic to solve.

Or, check if either the final or the prior authentication token is authenticated.

  • The previous token (at least in guard), is never authenticated. You're never authenticated when you're trying to authenticate, so I don't think this check makes sense to have

$event->getTokenPasswordCredentials(); I like this idea, but it's convention based. In case of the "old" tokens like UsernamePasswordToken, the contents of the credentials is a string, the old password, where the user(name) is the username filled in. It's really hard to facilitate all edge cases in here. Perhaps it would be enough to explain in the documentation that you'll need to manually check this value instead? The event as is, kinda mixes a bit of domain logic, which is error prone.

One of the issues I foresee, is the ability to determine which guard authenticator was used to generate the token. @weaverryan made it so that secured_area would have secured_area_$i as provider key in the Pre token. It would have secured_area in the Post token. If we would be able to track back which guard authenticator was used via the token, things might be easier. It would probably mean that the Pre token should get a more user-friendly name that can be referred back to the actual guard implementation used. Perhaps the class name or service id?

I'm personally okay with the event looking like this:

public function __construct(TokenInterface $authenticationAttemptToken, TokenInterface $authenticatedToken)
// or if you wish to follow the guard naming convention
public function __construct(TokenInterface $preAuthenticationToken, TokenInterface $postAuthenticationToken)

So long story short: this would have to be fixed in guard to be a lot more useful, as currently the providers are sequence based, this is fragile. On your current PR, If you'd fix the event to have pre and post tokens, it should be good enough for the core imo. Judging on this being a feature for 4.2, we have enough time to fix Guard along with it.

Edit: perhaps the authenticator class would be an option to pass along?

@robfrawley
Copy link
Contributor Author

robfrawley commented May 15, 2018

perhaps the authenticator class would be an option to pass along?

@iltar: The current version is passing along the authentication provider class name; that's the third constructor argument. Is that what you mean? This allows you to easily compare it with whatever provider class name you need to to determine the logic.

Here is the latest version, updated per some of your other comments and generally simplified/refactored (I've also edited all properties/methods to be much more explicit in their naming, so they should be clearer):

Edit: Removed huge code block as recent commit #b3d37d2bfae1f61a6cd1450ed5e3e1e2e6bb7124 provides the same context.

@robfrawley
Copy link
Contributor Author

I've pushed some working changes as #b3d37d2bfae1f61a6cd1450ed5e3e1e2e6bb7124 so taking a look at that should provide some insight into how this is coming together (especially the tests, which are incomplete but helpful).

Copy link
Contributor

@linaori linaori left a comment

Choose a reason for hiding this comment

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

I think this feature is pretty reliable at this point, nice job!

private $preAuthenticationToken;
private $authenticationProviderClassName;

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

Choose a reason for hiding this comment

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

Should probably be ?string $authenticationProviderClassName = null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed


return is_array($c) && (null !== $p = $this->resolveArrayAuthenticationTokenCredentialsPassword($c))
? $p ?: null
: $c ?: null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you put parenthesis around the nested ternaries? It makes it easier to read and less likely to have unexpected behavior

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed implementation; no longer relevant.

@@ -32,6 +34,10 @@ class AuthenticationProviderManager implements AuthenticationManagerInterface
{
private $providers;
private $eraseCredentials;

/**
* @var EventDispatcherInterface
Copy link
Contributor

Choose a reason for hiding this comment

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

@var EventDispatcherInterface|null, by default it's not set

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

return $this->authenticationProviderClassName;
}

public function getAuthenticationTokenPassword(\Closure $extractor = null): ?string
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you document the default behavior of this method? I understand what it does and what the callable can do, but it might not be too obvious for someone with less experience with the feature 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

@robfrawley robfrawley force-pushed the feature-add-rehash-authentication-event branch 2 times, most recently from f3873c8 to f683092 Compare May 21, 2018 21:06
@robfrawley
Copy link
Contributor Author

I've addressed all reviews up to this point and refactored the event's password extraction handling (to make it both simpler and more capable) and implemented comprehensive unit tests for it, as well.

Does anyone have additional thoughts on how this is coming together?

@robfrawley robfrawley force-pushed the feature-add-rehash-authentication-event branch 3 times, most recently from 7efce69 to 4b44344 Compare May 22, 2018 02:07
…ive user credentials, dispatched

immediately prior to the existing "AUTHENTICATION_SUCCESS" event (which does not include sensitive
user credentials, by default).

The existing "AUTHENTICATION_SUCCESS" event can be configured such that the token is not sanitized of
sensitive data, but that introduces the problem of exposing this data to *all* authentication success
subscribers/listeners when the vast majority do not need, and should not have, access to such data.
The newly added "AUTHENTICATION_SUCCESS_SENSITIVE" event includes the unsanitized token, leaving the
raw password/api-key), enabling actions such as password re-hashing and other credentials-aware tasks.

Between the existing and newly added authentication events, there is now a *clear* separation between
subscribers/listeners that *should* have user credentials (and must shoulder the added responsibility
of handling it), and those that *should not* have access to user credentials.
@robfrawley robfrawley force-pushed the feature-add-rehash-authentication-event branch from 4b44344 to cfcd1c7 Compare May 22, 2018 02:32
@timwhite
Copy link

timwhite commented Aug 1, 2018

@iltar any idea when this will be merged in? Is it waiting on more reviews?

@linaori
Copy link
Contributor

linaori commented Aug 1, 2018

@timwhite I have no idea, I'm merely a contributor and I have no say in those things. Before it can be merged, you'll have to solve the conflicts (and rebase your branch). @chalasr can do more with this PR 👍

@chalasr chalasr self-requested a review August 1, 2018 09:24
@rjd22
Copy link
Contributor

rjd22 commented Aug 29, 2018

IMO this is still a much needed feature for the people that want to upgrade their legacy passwords or rehash them when the cost is lower than the required cost.

Would it be possible to pick this up? Or would it be okay that I make a new PR for this if @robfrawley is okay with that?

cc @chalasr

Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

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

Looks good to me. @robfrawley could you please rebase?

@rjd22
Copy link
Contributor

rjd22 commented Sep 10, 2018

@chalasr what branch you you want me to rebase it from? I will make a new PR so this can be merged.

@chalasr
Copy link
Member

chalasr commented Sep 10, 2018

@rjd22 should be master, this one has the good target but is outdated. No emergency though

Copy link
Contributor

@ndench ndench left a comment

Choose a reason for hiding this comment

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

This looks like a great addition since there doesn't appear to be any other way to rehash users passwords if you change your hashing algorithm or cost factors. Do you have time to rebase onto master @robfrawley so we can merge this?

@curry684
Copy link
Contributor

curry684 commented Apr 6, 2019

Will pick this up tomorrow at EUFOSSA 👍

@xabbuh
Copy link
Member

xabbuh commented Apr 7, 2019

Thank you very much for your pull request!

As part of the Symfony EU funded hackathon (https://symfony.com/blog/the-symfony-and-api-platform-hackathon-is-coming), we were able to assemble most of the core team and big contributors in one place. Our goal is to finish as many open issues and PRs as possible.

Your commits will not be lost and you will therefore get credit for your work. All that will happen is that your commits will be moved to a new PR (see #30955) where all remaining concerns will be addressed.

Without your work this would not have been possible. So thank you once again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

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