-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Security] Do not deauthenticate user when the first refreshed user has changed #28072
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 is the lowest branch where this bug exists? can you check 2.8 please? maybe 3.4 otherwise? |
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.
Needs rebase on 3.4
|
||
// tokens can be deauthenticated if the user has been changed. | ||
if (!$token->isAuthenticated()) { | ||
if (!$newToken->isAuthenticated()) { | ||
if (null !== $this->logger) { | ||
$this->logger->debug('Token was deauthenticated after trying to refresh it.', array('username' => $refreshedUser->getUsername(), 'provider' => \get_class($provider))); |
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.
Becomes irrelevant in case another provider succeeds, right?
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.
Yes, I will move the log at the end of the process
@@ -193,7 +196,7 @@ protected function refreshUser(TokenInterface $token) | |||
$this->logger->debug('User was reloaded from a user provider.', $context); | |||
} | |||
|
|||
return $token; | |||
return $newToken; |
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.
better avoid returning a different instance if possible
|
||
return null; | ||
continue; | ||
} |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
@@ -169,15 +169,14 @@ protected function refreshUser(TokenInterface $token) | |||
|
|||
try { | |||
$refreshedUser = $provider->refreshUser($user); | |||
$token->setUser($refreshedUser); | |||
$newToken = clone $token; | |||
$newToken->setUser($refreshedUser); |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
ping @iltar @weaverryan |
Can you rebase your PR on the 3.4 branch and change the PR target branch accordingly? |
@chalasr Rebase is done. I made a mistake during this one, it removed some of your comments and automatically requested a review of dunglas. Sorry for this ! |
@gpekz No worries :) |
@@ -209,6 +210,14 @@ protected function refreshUser(TokenInterface $token) | |||
} | |||
} | |||
|
|||
if (isset($deauthenticationContext)) { |
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.
we prefer assigning to null
by default, checking for if ($deauthenticationContext)
instead of isset
@chalasr Thanks for reviewing :) |
@@ -169,21 +170,22 @@ protected function refreshUser(TokenInterface $token) | |||
|
|||
try { | |||
$refreshedUser = $provider->refreshUser($user); | |||
$token->setUser($refreshedUser); | |||
$newToken = clone $token; |
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.
clone
is not always possible. I suppose that we won't have any issues here, but just wanted to point it out.
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 we should not clone here. We can just keep the refreshed user and replace the one in the token after all user providers have been processed.
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'm not sure to understand what you mean.
Are you suggesting to call $token->setUser()
only once with the user returned by the last user provider of the loop? The right ("unchanged") user could be in the middle so we need to compare them all, right?
I mean, the "authenticated" state is part of the token. If we don't call setUser()
on the original token (which contains the original user) then we can't call isAuthenticated()
and break the loop in case it returns true as done below.
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.
You are right here. I forgot that we need to check the token with the refreshed user.
But since the TokenInterface
is serializable we could maybe stick to $newToken = unserialize(serialize($token));
instead. This should always be safe, right?
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.
Probably more expensive in term of memory and CPU time but feels a bit more robust.
@nicolas-grekas any thought?
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.
@gpekz I forgot to mention the signature change:
public static function hasUserChanged(UserInterface $originalUser, UserInterface $user);
so that AbstractToken
would pass $this->user
as $originalUser
for its internal use and we could use it externally without creating dummy token instances
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.
actually we also need to move this part https://github.com/symfony/security-core/blob/master/Authentication/Token/AbstractToken.php#L87-L99 in hasUserChanged()
and remove any UserInterface typehint from the signature in order to handle strings (and objects implementing __toString()
, see https://github.com/symfony/security-core/blob/master/Authentication/Token/TokenInterface.php#L60)
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.
But tokens that do not extend AbstractToken
use their own setUser
and isAuthenticated
methods here. Doing this change will break it. Isn't it a problem ?
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.
@gpekz I think you're right, it's not good to rely on hasUserChanged as it's AbstractToken specific.
Compared clone to serialize on 10 providers with 9/10 failing to refresh and got no significative difference https://blackfire.io/profiles/compare/5f7a76a9-7786-4bd0-b360-96e43ad07b1b/graph.
+1 for using unserialize(serialize($newToken))
instead of clone $newToken
given the token is already safely unserialized beforehand.
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 :)
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 had to stare at it awhile, but it makes sense to me: don't call setUser()
on the token until you actually successfully refresh the user. And allow subsequent user providers an opportunity to refresh, using the original, unmodified token.
And yes, looks like 3.4 is the right branch - the "logging out on user change" wasn't in 2.8.
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.
We can just keep the refreshed user and replace the one in the token after all user providers have been processed.
Possible? Putting a "changes required" review to not merge before resolving :)
friendly ping @gpekz |
@nicolas-grekas Before continuing, I would like your opinion on the unresolve discussion. |
I'd need @chalasr @xabbuh @weaverryan to chime in :) |
if (null !== $this->logger) { | ||
$this->logger->debug('Token was deauthenticated after trying to refresh it.', array('username' => $refreshedUser->getUsername(), 'provider' => \get_class($provider))); | ||
} | ||
$deauthenticationContext = array('username' => $refreshedUser->getUsername(), 'provider' => \get_class($provider)); |
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.
Would it make sense to collect these data from all providers that reach this point instead of overwriting it every time?
Nice first contribution @gpekz, thank you. |
…shed user has changed (gpekz) This PR was squashed before being merged into the 3.4 branch (closes #28072). Discussion ---------- [Security] Do not deauthenticate user when the first refreshed user has changed | Q | A | ------------- | --- | Branch? | 3.4 | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT | Doc PR | Currently the token is deauthenticated when the first refreshed user has changed. In theory, a second user provider could find a user that is the same than the user stored in the token. Also, the deauthentication is currently affected by the order of the user providers in the security.yaml and IMHO it does not make sense. Commits ------- 95dce67 [Security] Do not deauthenticate user when the first refreshed user has changed
Currently the token is deauthenticated when the first refreshed user has changed. In theory, a second user provider could find a user that is the same than the user stored in the token.
Also, the deauthentication is currently affected by the order of the user providers in the security.yaml and IMHO it does not make sense.