Skip to content

Session lost due to concurrent requests with "Remember Me" #28314

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

Closed
darnel opened this issue Aug 30, 2018 · 13 comments · Fixed by #41175
Closed

Session lost due to concurrent requests with "Remember Me" #28314

darnel opened this issue Aug 30, 2018 · 13 comments · Fixed by #41175

Comments

@darnel
Copy link
Contributor

darnel commented Aug 30, 2018

Symfony version(s) affected: 4.1.3

Description

When using "Remember me" functionality, session is regenerated with every request. Due to concurrent requests done by browser session is effectively lost which leads to e.g. CSRF token mismatch.

Authentication is kept, user is still logged in (because of Remember me), but the session is lost.

In \Symfony\Component\Security\Http\Firewall\RememberMeListener::handle:87 following method is called \Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy::onAuthentication. With default session strategy migrate it leads to calling session_regenerate_id($destroy = true) in every request. Session ID is changed A->B with main request. For other requests done by browser session ID is B, but changed B->C, B->D etc.

How to reproduce

  • use "Remember me" functionality
  • login e.g using form
  • make HTTP request, which makes browser request other resources responded by controller (not assets)
  • you can check session ID changes every request and Cookie/Set-Cookie HTTP headers is set sent accordingly.

fragment from config/packages/security.yaml

            remember_me:
                secret: '%kernel.secret%'
                lifetime: 34128000
                #                secure: true
                always_remember_me: true
                user_provider: entity.user_account.email

Possible Solution

Additional context

@nicolas-grekas
Copy link
Member

Would you like to work on a PR that would follow #27427 (comment)?

@darnel
Copy link
Contributor Author

darnel commented Sep 12, 2018

I would, is preferred solution to skip authentication of already authenticated user?

@nicolas-grekas
Copy link
Member

Any hint @weaverryan or @chalasr, etc.?

@mpdude
Copy link
Contributor

mpdude commented Nov 29, 2020

Do I understand this correctly that with “Remember Me” enabled, the session ID changes for every request?

Asking because I am chasing spurious failures on a CI where concurrent requests (headless browser/AJAX) lead to lost sessions.

@skylord123
Copy link

skylord123 commented Feb 23, 2021

@mpdude yeah that seems to be the case.
EDIT: The session ID stays the same. It's the remember me cookie value that changes.

I'm running into the problem that I have an ajax request that runs every 5 seconds in the back-ground and if the user changed pages before that request finishes it will never get the new value for the remember me cookie so therefor it sends the old value which then Symfony sends a new set-cookie header with the value "deleted".

It really sucks that this functionality cannot be disabled and is on by default. I could make the ajax request blocking but that isn't really a solution and is just a gross work-around. Would be nice if ajax requests were ignored for updating the remember me cookie.

@skylord123
Copy link

skylord123 commented Mar 26, 2021

If you duplicate the service and remove the logic that generates a new token on every request it fixes it. I just posted a blog entry on my website with how to do just this: https://skylar.tech/fix-for-concurrent-requests-breaking-symfonys-rememberme/

I still don't understand why the PersistentTokenBasedRememberMeServices resets the token on every request but the default TokenBasedRememberMeServices does not. It also sucks that this "feature" isn't documented anywhere and PersistentTokenBasedRememberMeServices is only mentioned to be used for storing the remember me tokens in the database (which obviously makes sense from the name).

I think the documentation should be updated and this "feature" should be able to be disabled/enabled without overwriting the service.

EDIT:
I also like the idea of making tokens work for 1-2 minutes after they "expire" as that would prevent concurrent requests breaking things but would also give you the benefit of tokens auto-rotating.

@Seldaek
Copy link
Member

Seldaek commented May 11, 2021

I opened #41175 which aims to fix this issue, if anyone wants to help out or take a look at least.

@Seldaek
Copy link
Member

Seldaek commented May 11, 2021

@skylord123 one part I don't understand about your feedback though, is that in my experience the session id and token only change when remember-me is being used to authenticate. Do you not store sessions at all in your code, and rely purely on persistent tokens for authentication on every request? If so that should be improved already in 5.3.0 by #40972 which will only refresh the token once a minute.

@skylord123
Copy link

skylord123 commented May 11, 2021

@Seldaek PersistentTokenBasedRememberMeServices always refreshes the token on every request whereas the default TokenBasedRememberMeServices does not.

Check the processAutoLoginCookie function in both of those classes. You will see for the PersistentTokenBasedRememberMeServices it has these lines:

        $tokenValue = base64_encode(random_bytes(64));
        $this->tokenProvider->updateToken($series, $this->generateHash($tokenValue), new \DateTime());
        $request->attributes->set(self::COOKIE_ATTR_NAME,
            new Cookie(
                $this->options['name'],
                $this->encodeCookie([$series, $tokenValue]),
                time() + $this->options['lifetime'],
                $this->options['path'],
                $this->options['domain'],
                $this->options['secure'] ?? $request->isSecure(),
                $this->options['httponly'],
                false,
                $this->options['samesite']
            )
        );

Whereas if you look at the exact same function inside of TokenBasedRememberMeServices and it does not set this cookie on every request.

I store sessions in my code. I am using the default authentication system. I just moved to use PersistentTokenBasedRememberMeServices so my sessions would store in the DB and the documentation for this feature doesn't specify at all that the token will be refreshes every request. This makes me to believe this was unintentionally done.

If you check out my blog link I posted you will see that all I do to fix this is copy the class and comment out the part of the PersistentTokenBasedRememberMeServices that sets the cookie on every request. I only care about the cookie being set in onLoginSuccess.

EDIT: As far as I know the processAutoLoginCookie gets ran on every request. Not just when the token is being used.

EDIT 2: Shouldn't these two classes function identically except one stores in the DB? If we are going to auto-rotate the remember me token on every request it should be done for both classes not just for persistent storage.

@Seldaek
Copy link
Member

Seldaek commented May 11, 2021

@skylord123 I can not reproduce this, for me PersistentTokenBasedRememberMeServices::processAutoLoginCookie only gets called if I delete my session cookie, it gets called once while the reauthentication happens and then not anymore on subsequent requests. I'm using Symfony 5.2 and the new Authenticator stuff, not sure if that makes a difference, but IMO the PersistentTokenBasedRememberMeServices should not be called if an active session is present due to https://github.com/symfony/symfony/blob/5.2/src/Symfony/Component/Security/Http/Authenticator/RememberMeAuthenticator.php#L55-L58

@skylord123
Copy link

@Seldaek That is strange. I am running 5.2 using the latest security setup as well. I am for sure logged into my app (deleting the remember me token and I stay logged in). I put a dump on $this->tokenStorage->getToken() and it returns null for some reason (despite when I comment that out I am for sure logged in).

@skylord123
Copy link

@Seldaek So I noticed my firewall had the lazy option set to true. When I set this to false the token stops updating on every request and $this->tokenStorage->getToken() returns my token.

So maybe this is just broken for lazy sessions?

@Seldaek
Copy link
Member

Seldaek commented May 12, 2021

@skylord123 ok that is interesting, indeed I have lazy:false. I'm not familiar enough with lazy mode to know if or how this could be fixable sorry. This does sound to me like a bug though, as recreating the remember me token (or even the cookie if using remember me without token_provider) seems pretty broken.

@fabpot fabpot closed this as completed May 19, 2021
fabpot added a commit that referenced this issue May 19, 2021
…sts doing remember-me re-authentication (Seldaek)

This PR was squashed before being merged into the 5.3-dev branch.

Discussion
----------

[Security] [RememberMe] Add support for parallel requests doing remember-me re-authentication

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | yes
| New feature?  | yes ish <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tickets       | Fix #40971, Fix #28314, Fix #18384
| License       | MIT
| Doc PR        | symfony/symfony-docs#... <!-- required for new features -->

This is a possible implementation to gather feedback mostly..

`TokenVerifierInterface` naming is kinda bad perhaps.. But my goal would be to merge it in TokenProviderInterface for 6.0 so it's not so important. Not sure if/how to best indicate this in terms of deprecation notices.

Anyway wondering if this would be an acceptable implementation (ideally in an application I would probably override the new methods from DoctrineTokenProvider to something like this which is less of a hack and does expiration properly:

```php
    public function verifyToken(PersistentTokenInterface $token, string $tokenValue)
    {
        if (hash_equals($token->getTokenValue(), $tokenValue)) {
            return true;
        }

        if (!$this->cache->hasItem('rememberme-' . $token->getSeries())) {
            return false;
        }

        /** `@var` CacheItem $item */
        $item = $this->cache->getItem('rememberme-' . $token->getSeries());
        $oldToken = $item->get();

        return hash_equals($oldToken, $tokenValue);
    }

    public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void
    {
        $this->updateToken($token->getSeries(), $tokenValue, $lastUsed);

        /** `@var` CacheItem $item */
        $item = $this->cache->getItem('rememberme-'.$token->getSeries());
        $item->set($token->getTokenValue());
        $item->expiresAfter(60);
        $this->cache->save($item);
    }
```

If you think it'd be fine to require optionally the cache inside DoctrineTokenProvider to enable this feature instead of the hackish way I did it, that'd be ok for me too.

The current `DoctrineTokenProvider` implementation of `TokenVerifierInterface` relies on the lucky fact that series are generated using `base64_encode(random_bytes(64))` which always ends in the `==` padding of base64, so that allowed me to store an alternative token value temporarily by replacing `==` with `_`.

Alternative implementation options:

1. Inject cache in `DoctrineTokenProvider` and do a proper implementation (as shown above) that way
2. Do not implement at all in `DoctrineTokenProvider` and let users who care implement this themselves.
3. Implement as a new `token_verifier` option that could be configured on the `firewall->remember_me` key so you can pass an implementation if needed, and possibly ship a default one using cache that could be autoconfigured
4. Add events that allow modifying the token to be verified, and allow receiving the newly updated token incl series, instead of TokenVerifierInterface, but then we need to inject a dispatcher in RememberMeAuthenticator.

`@chalasr` `@wouterj` sorry for the long description but in the hope of getting this included in 5.3.0, if you can provide guidance I will happily work on this further tomorrow to try and wrap it up ASAP.

Commits
-------

1992337 [Security] [RememberMe] Add support for parallel requests doing remember-me re-authentication
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
8 participants