-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
What's the alternative right now for rehashing? Is there any?
makes sense, the current name is too specific to one use case, isn't it? |
👍 For adding this event. I agree with @nicolas-grekas that the name is a bit specific atm. I like |
9f75712
to
2d69374
Compare
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.
I prefer Otherwise, I think this is pretty much set, pending any additional reviews. |
2d69374
to
21cf149
Compare
This will be a bit difficult to do for guard. A guard authenticator has Also in your code I noticed that you use |
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?
You're referring to diff-70fc43e7af23f097f94d4a0036888896R97, correct? My usage of |
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.
Correct.
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: symfony/src/Symfony/Component/Security/Guard/Token/PostAuthenticationGuardToken.php Lines 59 to 62 in 4a7e6fa
|
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 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 <?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? |
Some feedback:
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 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? |
@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 |
I've pushed some working changes as |
There was a problem hiding this 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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
f3873c8
to
f683092
Compare
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? |
7efce69
to
4b44344
Compare
…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.
4b44344
to
cfcd1c7
Compare
@iltar any idea when this will be merged in? Is it waiting on more reviews? |
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 |
There was a problem hiding this 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?
@chalasr what branch you you want me to rebase it from? I will make a new PR so this can be merged. |
@rjd22 should be master, this one has the good target but is outdated. No emergency though |
There was a problem hiding this 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?
Will pick this up tomorrow at EUFOSSA 👍 |
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! |
This PR adds an
AUTHENTICATION_SUCCESS_SENSITIVE
authentication event that is dispatched immediately prior to the existingAUTHENTICATION_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.