-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Automatic logout after changing password on another session #19033
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
You probably have to rebase, you have a bunch of commits in here that are not related to your PR |
Yes sorry rebased it now onto fresh master, the patch was based on current 3.1 branch. |
@@ -56,7 +57,7 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke | |||
$currentUser = $token->getUser(); | |||
if ($currentUser instanceof UserInterface) { | |||
if ($currentUser->getPassword() !== $user->getPassword()) { | |||
throw new BadCredentialsException('The credentials were changed from another session.'); | |||
throw new AccountPasswordChangedException('The credentials were changed from another session.'); |
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.
As the new Exception extends a different base Exception (account status instead of bad credentials), this is a pretty big BC break
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.
The exception should extend this other type in order to avoid a redirect loop and unset the wrong(old) token from the session.
Both exceptions are correctly handled, checkPreAuth and checkPostAuth in DaoAuthenticationProvider both throw exceptions that extend this base exception class.
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.
The DaoAuthenticationProvider
can be extended or used elsewhere, thus can break user-land implementations 😢
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.
What if we remove the token on the BadCredentialsException as well? This change will be done on ExceptionListener.php L197 ??
@iltar I've changed the PR and removed the custom exception. Instead of it, I added code that removes the token for the case when there is a BadCredentialsException in order to prevent a infinite loop. |
$this->tokenStorage->setToken(null); | ||
|
||
if (null !== $this->logger) { | ||
$this->logger->info('The security token was removed due to an BadCredentialsException.', array('exception' => $authException)); |
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.
a BadCrendentialsException.
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.
@fabpot fixed
} | ||
|
||
$this->user = $user; |
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.
Won't this break applications that rely on the current behaviour?
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.
Well as I stated at the top of my initial comment, this is the point that is actually the most problematic. And I need input frim somebody that knows really well the security component and it's usage.
This setter as it stands does too much on it's own, and should be refactored. Most likely separating the logic for marking a token as not authenticated and doing a check elsewhere before setting the token.
The logout fix will not work if we overwrite the user in the token with the refreshed user.
Should / could this be optional and driven by a security.yml config that defaults to false? |
Any update on this? It's an important issue as changing a password to log out any other session is a very common use case and should be the default behaviour on any website. |
So after some debugging of mentioned "incorrect" behavior (not being logged out on other sessions after changing the password) I came to the same conclusions: However, I have not yet tested the patch here. |
Any update here? |
I don't think this PR should be accepted, imo the flow is working as intended. The current behavior is to log the user out whenever anything changes. This is correct because the user is not the same anymore. If you want to divert from this behavior, implement the EquatableInterface and customize behavior. |
@iltar have you seen my comment above? It should log out whenever anything (or, like, a password) changes but it does not. |
No, that is not the current behavior. But I would expect this, too. See the comment of @eXtreme. |
@eXtreme I've been digging in the security component quite often last weeks and it works exactly as I just described, with the difference that I have the |
@iltar ok I will try later today or tomorrow. The app runs on 2.8 so I will use the same version. |
@iltar have you followed the entire trail of this back to the original issue report? As others mentioned, this is not the current behaviour. #17023 - might be it also works for ID (can you share your fork), same as for enabled field, but doesn't for other properties. Security is such an important aspect and shouldn't be ignored - imo this should be picked up by the core team as a high priority (it has been almost 2 years already) |
@iltar you can use this branch: https://github.com/gharlan/symfony-demo/tree/issue-19033 Go to backend, login, click on the "CHANGE PASSWORD" link. |
I don't have a proper environment to run or test stuff at the moment. Digging into the code, this part looks like the problem to me: https://github.com/symfony/symfony/blob/2.8/src/Symfony/Component/Security/Core/Authentication/Provider/UserAuthenticationProvider.php#L70-L86 This part in the authenticator, is what causes the problem imo. There's no distinction between the current and actual user, because it always fetches the current user from the current token: What should happen imo, is a |
@iltar simple reproduce:
I have not managed to reproduce the same behavior I debugged in my real app. BUT. Changing the passwords sets authenticated to false on the second browser. Which... logically makes sense, well, you are not authenticated but still logged in (?). So checking IS_AUTHENTICATED_FULLY should fail right? You can guard your critical app parts by this check and prevent abuse, right? Nope. After adding if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
throw $this->createAccessDeniedException();
} to In my real app I mentioned in the comment above it was AccessListener which reset the token to authenticated. But maybe it is because I use JMS SecurityExtraBundle? Not sure. |
There's a flag that might alter behavior of the the access decision manager, could that change this particular behavior? |
@iltar I'm out of office so I will test my fork tomorrow. |
@iltar |
@eXtreme I mean this line of code, by default |
@iltar it's symfony-demo, so it should be default false |
@iltar I checked this. It is |
So the issue is really just within the DaoAuthenticator then. What if you would remove just those lines, would that fix the issue as well? I see no reason to not always fetch the new data tbh. |
@iltar still no luck. btw. we can move to Slack, not to spam here |
So after some fiddling around with @eXtreme on slack, we have found out the following:
if ($this->alwaysAuthenticate || !$token->isAuthenticated()) {
$this->tokenStorage->setToken($token = $this->authenticationManager->authenticate($token));
}
Possible solutionIn theory a check could be added to the diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php
index b443fa1..42b950d 100644
--- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php
@@ -157,6 +157,12 @@ class ContextListener implements ListenerInterface
$refreshedUser = $provider->refreshUser($user);
$token->setUser($refreshedUser);
+ // tokens can be deauthenticated if the user has been changed.
+ if (!$token->isAuthenticated()) {
+ $this->logger->debug('Token was deauthenticated after trying to refresh it.', array('username' => $refreshedUser->getUsername(), 'provider' => get_class($provider)));
+ return null;
+ }
+
if (null !== $this->logger) {
$this->logger->debug('User was reloaded from a user provider.', array('username' => $refreshedUser->getUsername(), 'provider' => get_class($provider)));
} This is how Guard solved this. The |
@iltar haha, yes, someone finally noticed this in Guard :). I noticed this problem when making Guard, and so I made sure to make Guard not suffer from this. But, it's a workaround in Guard for sure. About your patch, initially, I like it, but I'd need to review this again in more detail - I can't remember all the moving pieces. In the Guard provider, I throw an instance of
But in if ($token instanceof TokenInterface) {
$token = $this->refreshUser($token);
} elseif (null !== $token) {
if (null !== $this->logger) {
$this->logger->warning('Expected a security token from the session, got something else.', array('key' => $this->sessionKey, 'received' => $token));
}
$token = null;
}
// -> right HERE, check if the token is authenticated, and if not, $token = null.
$this->tokenStorage->setToken($token); Hopefully that helps... a bit :) |
Hopefully someone on this PR can answer this clearly. From my investigations, refreshUser isn't being called, and so ANY field changing in a user entity, causes the session to be forced to log back in. But everywhere I'm reading, I'm seeing conflicting information about it should refresh the user, vs logout the user. |
So? Is anyone interested in picking this one up? |
I would love if someone could open a PR for this. I don't think this PR is the correct way - it would only fix it for some parts of the system. I think the solution proposed by @iltar and I is a good approach to try: (#19033 (comment) and comment above). Anyone want to open a PR? |
@weaverryan I can try, but I'm a bit concerned about the change as it does 2 things:
First point indicates 3.4, second point indicates 2.7, which one should I open it against? |
I would definitely do this change in 3.4. |
Closing in favor of #23882. |
…change (iltar) This PR was squashed before being merged into the 3.4 branch (closes #23882). Discussion ---------- [Security] Deprecated not being logged out after user change | Q | A | ------------- | --- | Branch? | 3.4 | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | yes | Tests pass? | yes | Fixed tickets | #17023 | License | MIT | Doc PR | ~ This PR is an alternative approach to #19033. Due to a behavioral change that could break a lot of applications and websites, I've decided to trigger a deprecation instead of actually changing the behavior as that can be done for 4.0. Whenever a user object is considered changed (`AbstractToken::hasUserChanged`) when setting a new user object after refreshing, it will now throw a deprecation, paving the way for a behavioral change in 4.0. The idea is that in 4.0 Symfony will simply trigger a logout when this case is encountered. Commits ------- 22f525b [Security] Deprecated not being logged out after user change
Although this PR does not break BC, the change in AbstractToken that relates to setUser functionality is highly volatile and should be thoughtfully inspected for possible regressions.
The flow that causes this bug:
As a result the user remains logged in although the serialized data from the session he has, contains an old password. After first refresh of the page, his session data is overwritten with fresh data from the refreshed user.