Skip to content

ESI requests break persistent remember me tokens #31078

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
leofeyer opened this issue Apr 11, 2019 · 8 comments
Closed

ESI requests break persistent remember me tokens #31078

leofeyer opened this issue Apr 11, 2019 · 8 comments

Comments

@leofeyer
Copy link
Contributor

We are using the PersistentTokenBasedRememberMeServices service together with the DoctrineTokenProvider class to manage "remember me" tokens. The idea behind the service is to update the token hash after each usage, so the same token cannot be used twice.

Now, if you have a "remember me" cookie and request a page that includes an ESI fragment, both the main request and the ESI request will trigger the auto-login process. However, after the main request has used the token, the hash is changed and the cookie is regenerated. By the time the ESI request is processed, the "remember me" token can no longer be verified (CookieTheftException) and the cookie is deleted in the loginFail method.

If the user was successfully authenticated based on the "remember me" cookie in the main request, the response will contain the updated "remember me" cookie as well as a session cookie. Should these cookies not be passed to the ESI request?

@leofeyer
Copy link
Contributor Author

Related: #27910 @paragonie-scott

@stof
Copy link
Member

stof commented Apr 11, 2019

No, they should not, as that's not how ESI works when using Varnish or other reverse proxies. Reading response cookies to update the cookies sent in subsequent requests is something done by the browser, not by ESI-capable reverse proxies.

@stof
Copy link
Member

stof commented Apr 11, 2019

#27910 is not related to this. Storing a hash of the value rather than the value itself is entirely orthogonal to the fact of changing the value on each usage.

@leofeyer
Copy link
Contributor Author

leofeyer commented Apr 11, 2019

Ok, but then the ESI request will trigger the auto-login process again and delete the "remember me" cookie (see above). Are you implying that we cannot use persistent tokens together with ESI?

@leofeyer
Copy link
Contributor Author

leofeyer commented May 8, 2019

Any updates on this one? @nicolas-grekas Can you confirm that Symfony intentionally does not support persistent rememberme tokens with series in combination with ESI?

@nicolas-grekas
Copy link
Member

Should these cookies not be passed to the ESI request?

as @stof explained, Varnish doesn't do that. Maybe you could make it do so but that's out of scope.
If you do, that'd mean each ESI request would have a different rememberme cookie, since they can be used only once?
This wouldn't work because there would be race conditions. Your requirement of having cookies valid once mandates making requests on after the other. That'd defeat a significant benefit of ESI.

Base on your description above, I would look for a way to accept already consumed token from ESI responses (for some duration). Since they're signed, that'd be safe.

All that is on you, nothing we can do in Symfony to me.

@leofeyer
Copy link
Contributor Author

leofeyer commented May 8, 2019

Agreed. Thank you for your explanation. 😊

@aschempp
Copy link
Contributor

Also see #18384

leofeyer pushed a commit to contao/contao that referenced this issue Jun 12, 2019
Description
-----------

**Warning: This is currently POC and completely untested!**

This is our current take at fixing #400

As suggested by @nicolas-grekas in symfony/symfony#31078 (comment), we need to add a short expiration time to the persistent rememberme tokens. This also fixes concurrent requests mentioned in symfony/symfony#18384.

**Background Info:**
 - A compiler pass replaces the existing rememberme service implementation. This could be replaced by symfony/symfony#31309, but that would not allow new constructor arguments to the service.
 - Because we have our own service implementation, there's not need for a token provider. Also,  `PersistentToken` is only used to communicate between the service and token provider.

**Authentication flow:**
 1. A request with rememberme cookie is authenticated in the firewall.
 2. The rememberme service will mark all existing database tokens of the current series as *to expire* in 60 seconds.
 3. Also adds a new token without an expiration time to the database.
 4. The new cookie is added to the request attribute. If the request is ESI, the response cookie will never make it to the browser, but that's fine.

On the second parallel request, there will be two tokens in the database (thanks to table locking). If the expiring one is still valid, it will be accepted. Instead of generating another new one, it will however send the already created new one back to the client.

As long as there is one browser request (non-ESI) within that minute, the new rememberme cookie as well as the session will correctly make it to the client. This will be accomplised through:
 1. The main response will never be in the cache (because there are cookies).
 2. Should we implement an option to cache sites regardless of cookies (e.g. via checkbox as in #389), we can add a 1px blank image to the page that will fetch the session/rememberme cookie.


I have also implemented symfony/symfony#27910 and now hashed the token value instead of the series. Both ways would work. Changing this will break existing rememberme cookies in Contao, but as everything is broken atm that should be fine.

**TODO:**
 - [x] ~make the expiration time configurable~
 - [x] update and add unit tests

Commits
-------

92bcb51 Add expiration based persistent rememberme token
9a6a8ba Expire old rememberme series after 5 minutes
5121541 Code adjustments from PR feedback
3807ca3 Use Doctrine ORM entity to define tl_remember_me table
b1b334b Rewrite rememberme services to use ORM
a6b675f Implemented unit tests for everything except rememberme services
438f614 Implemented unit tests for ExpiringTokenBasedRememberMeServices
a5fb20e Add ORM config for travis tests
4bce3e6 Fixed RemembermeServicesPass not correctly overriding arguments
f81c940 Fixed composer.json validation
9ad2251 Fixed PHPStan issues
004489b Fixed several DQL query issues
1f1875e Delete database records of same series before migration
5709870 Fix the coding style
97881a5 Fix the --prefer-lowest tests
leofeyer pushed a commit to contao/manager-bundle that referenced this issue Jun 12, 2019
Description
-----------

**Warning: This is currently POC and completely untested!**

This is our current take at fixing contao/contao#400

As suggested by @nicolas-grekas in symfony/symfony#31078 (comment), we need to add a short expiration time to the persistent rememberme tokens. This also fixes concurrent requests mentioned in symfony/symfony#18384.

**Background Info:**
 - A compiler pass replaces the existing rememberme service implementation. This could be replaced by symfony/symfony#31309, but that would not allow new constructor arguments to the service.
 - Because we have our own service implementation, there's not need for a token provider. Also,  `PersistentToken` is only used to communicate between the service and token provider.

**Authentication flow:**
 1. A request with rememberme cookie is authenticated in the firewall.
 2. The rememberme service will mark all existing database tokens of the current series as *to expire* in 60 seconds.
 3. Also adds a new token without an expiration time to the database.
 4. The new cookie is added to the request attribute. If the request is ESI, the response cookie will never make it to the browser, but that's fine.

On the second parallel request, there will be two tokens in the database (thanks to table locking). If the expiring one is still valid, it will be accepted. Instead of generating another new one, it will however send the already created new one back to the client.

As long as there is one browser request (non-ESI) within that minute, the new rememberme cookie as well as the session will correctly make it to the client. This will be accomplised through:
 1. The main response will never be in the cache (because there are cookies).
 2. Should we implement an option to cache sites regardless of cookies (e.g. via checkbox as in contao/contao#389), we can add a 1px blank image to the page that will fetch the session/rememberme cookie.


I have also implemented symfony/symfony#27910 and now hashed the token value instead of the series. Both ways would work. Changing this will break existing rememberme cookies in Contao, but as everything is broken atm that should be fine.

**TODO:**
 - [x] ~make the expiration time configurable~
 - [x] update and add unit tests

Commits
-------

92bcb515 Add expiration based persistent rememberme token
9a6a8ba8 Expire old rememberme series after 5 minutes
5121541c Code adjustments from PR feedback
3807ca38 Use Doctrine ORM entity to define tl_remember_me table
b1b334b2 Rewrite rememberme services to use ORM
a6b675f3 Implemented unit tests for everything except rememberme services
438f6144 Implemented unit tests for ExpiringTokenBasedRememberMeServices
a5fb20e6 Add ORM config for travis tests
4bce3e64 Fixed RemembermeServicesPass not correctly overriding arguments
f81c9403 Fixed composer.json validation
9ad2251e Fixed PHPStan issues
004489b2 Fixed several DQL query issues
1f1875ef Delete database records of same series before migration
57098706 Fix the coding style
97881a50 Fix the --prefer-lowest tests
leofeyer pushed a commit to contao/core-bundle that referenced this issue Jun 12, 2019
Description
-----------

**Warning: This is currently POC and completely untested!**

This is our current take at fixing contao/contao#400

As suggested by @nicolas-grekas in symfony/symfony#31078 (comment), we need to add a short expiration time to the persistent rememberme tokens. This also fixes concurrent requests mentioned in symfony/symfony#18384.

**Background Info:**
 - A compiler pass replaces the existing rememberme service implementation. This could be replaced by symfony/symfony#31309, but that would not allow new constructor arguments to the service.
 - Because we have our own service implementation, there's not need for a token provider. Also,  `PersistentToken` is only used to communicate between the service and token provider.

**Authentication flow:**
 1. A request with rememberme cookie is authenticated in the firewall.
 2. The rememberme service will mark all existing database tokens of the current series as *to expire* in 60 seconds.
 3. Also adds a new token without an expiration time to the database.
 4. The new cookie is added to the request attribute. If the request is ESI, the response cookie will never make it to the browser, but that's fine.

On the second parallel request, there will be two tokens in the database (thanks to table locking). If the expiring one is still valid, it will be accepted. Instead of generating another new one, it will however send the already created new one back to the client.

As long as there is one browser request (non-ESI) within that minute, the new rememberme cookie as well as the session will correctly make it to the client. This will be accomplised through:
 1. The main response will never be in the cache (because there are cookies).
 2. Should we implement an option to cache sites regardless of cookies (e.g. via checkbox as in contao/contao#389), we can add a 1px blank image to the page that will fetch the session/rememberme cookie.


I have also implemented symfony/symfony#27910 and now hashed the token value instead of the series. Both ways would work. Changing this will break existing rememberme cookies in Contao, but as everything is broken atm that should be fine.

**TODO:**
 - [x] ~make the expiration time configurable~
 - [x] update and add unit tests

Commits
-------

92bcb515 Add expiration based persistent rememberme token
9a6a8ba8 Expire old rememberme series after 5 minutes
5121541c Code adjustments from PR feedback
3807ca38 Use Doctrine ORM entity to define tl_remember_me table
b1b334b2 Rewrite rememberme services to use ORM
a6b675f3 Implemented unit tests for everything except rememberme services
438f6144 Implemented unit tests for ExpiringTokenBasedRememberMeServices
a5fb20e6 Add ORM config for travis tests
4bce3e64 Fixed RemembermeServicesPass not correctly overriding arguments
f81c9403 Fixed composer.json validation
9ad2251e Fixed PHPStan issues
004489b2 Fixed several DQL query issues
1f1875ef Delete database records of same series before migration
57098706 Fix the coding style
97881a50 Fix the --prefer-lowest tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants