Skip to content

[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

Merged
merged 1 commit into from
Oct 10, 2018
Merged

[Security] Do not deauthenticate user when the first refreshed user has changed #28072

merged 1 commit into from
Oct 10, 2018

Conversation

watashi-djo
Copy link
Contributor

@watashi-djo watashi-djo commented Jul 26, 2018

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.

@nicolas-grekas
Copy link
Member

what is the lowest branch where this bug exists? can you check 2.8 please? maybe 3.4 otherwise?

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.

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)));
Copy link
Member

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?

Copy link
Contributor Author

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;
Copy link
Member

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

@chalasr chalasr added this to the 3.4 milestone Jul 28, 2018

return null;
continue;
}

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

@@ -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.

This comment was marked as resolved.

@chalasr
Copy link
Member

chalasr commented Jul 29, 2018

ping @iltar @weaverryan

@chalasr
Copy link
Member

chalasr commented Jul 29, 2018

Can you rebase your PR on the 3.4 branch and change the PR target branch accordingly?

@chalasr chalasr removed the BC Break label Jul 29, 2018
@watashi-djo watashi-djo requested a review from dunglas as a code owner July 29, 2018 18:46
@watashi-djo watashi-djo changed the base branch from master to 3.4 July 29, 2018 18:46
@watashi-djo watashi-djo reopened this Jul 29, 2018
@watashi-djo
Copy link
Contributor Author

@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 !

@chalasr
Copy link
Member

chalasr commented Jul 30, 2018

@gpekz No worries :)

@@ -209,6 +210,14 @@ protected function refreshUser(TokenInterface $token)
}
}

if (isset($deauthenticationContext)) {
Copy link
Member

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

@watashi-djo
Copy link
Contributor Author

@chalasr Thanks for reviewing :)

@@ -169,21 +170,22 @@ protected function refreshUser(TokenInterface $token)

try {
$refreshedUser = $provider->refreshUser($user);
$token->setUser($refreshedUser);
$newToken = clone $token;
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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?

Copy link
Member

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

Copy link
Member

@chalasr chalasr Sep 21, 2018

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)

Copy link
Contributor Author

@watashi-djo watashi-djo Sep 21, 2018

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 ?

Copy link
Member

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.

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 :)

Copy link
Member

@weaverryan weaverryan left a 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.

Copy link
Member

@nicolas-grekas nicolas-grekas left a 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 :)

@nicolas-grekas
Copy link
Member

friendly ping @gpekz

@watashi-djo
Copy link
Contributor Author

friendly ping @gpekz

@nicolas-grekas Before continuing, I would like your opinion on the unresolve discussion.

@nicolas-grekas
Copy link
Member

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));
Copy link
Member

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?

@chalasr
Copy link
Member

chalasr commented Oct 10, 2018

Nice first contribution @gpekz, thank you.

@chalasr chalasr merged commit 95dce67 into symfony:3.4 Oct 10, 2018
chalasr pushed a commit that referenced this pull request Oct 10, 2018
…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
This was referenced Nov 3, 2018
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.

7 participants