Problem/Motivation

With page cache enabled and homepage properly cached, when a user logs in with a persistent session, closes and re-open the browser, the homepage is being server from the cache, also preventing the automatic re-login of the user.

This happens on the page cache middleware. Since the session cookie has been deleted when the browser was closed, the policy rules that are checked in order to determine the cacheability of a request do not prevent said caching.
Normally \Drupal\Core\PageCache\RequestPolicy\NoSessionOpen would skip the cache when the session cookie is present.

Proposed resolution

Create a policy rule similar to the above. This policy should check if the persistent login cookie is present and the session one is not. This would mean that the token wasn't yet validated, so we need to avoid caching in order to allow said validation and automatic relogging of the user.
If the token is valid, the session cookie will be created and the normal NoSession policy rule will start applying. If the token is not valid, it will be removed and caching will be again possible for the subsequent requests.

Remaining tasks

Provide tests. The bug was actually discovered running a Behat test on the project I'm working on (see here).
It should be possible to write a browser test with the same steps as this one.

User interface changes

None.

API changes

Moved the cookie name generation to a TokenHelper service because the request policy rule also needed it.

Data model changes

None

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

sardara created an issue. See original summary.

sardara’s picture

Assigned: sardara » Unassigned
Status: Active » Needs review
FileSize
9.81 KB
claudiu.cristea’s picture

Issue tags: +Needs tests

Seems that needs some tests :)

pfrenssen’s picture

  1. +++ b/src/PageCache/RequestPolicy/PendingPersistentLogin.php
    @@ -0,0 +1,60 @@
    + * a persistent login one. This should happen only when the user reopened the
    + * browser where he was logged with a persistent login session. In the first
    + * kernel request event, the cookie will be validated and the user will be
    

    Let's make this sentence gender neutral, not all visitors are male.

    Also "This should happen only" sounds quite strong, maybe just say "A common situation"?

  2. +++ b/src/PageCache/RequestPolicy/PendingPersistentLogin.php
    @@ -0,0 +1,60 @@
    + * present again and this policy won't apply anymore. If the cookie is invalid,
    + * it will be removed during the response event, again making this policy not
    + * applicable on next page loads.
    

    It would sound better to say "..not applicable on _subsequent_ page loads."

  3. +++ b/src/PageCache/RequestPolicy/PendingPersistentLogin.php
    @@ -0,0 +1,60 @@
    +  /**
    +   * Instantiate a new PendingPersistentLogin object.
    +   *
    

    Use third person verb.

  4. +++ b/src/PageCache/RequestPolicy/PendingPersistentLogin.php
    @@ -0,0 +1,60 @@
    +   * @param \Drupal\persistent_login\TokenHelper $token_helper
    +   *   The token helper service.
    

    When injecting services, it's good practice to provide an interface instead of a concrete implementation.

  5. +++ b/src/PageCache/RequestPolicy/PendingPersistentLogin.php
    @@ -0,0 +1,60 @@
    +  public function check(Request $request) {
    +    if (!$this->sessionConfiguration->hasSession($request) && $this->tokenHelper->hasCookie($request)) {
    +      return static::DENY;
    +    }
    +  }
    

    This is the actual bugfix. Let's add some documentation here why we are returning DENY if our cookie is present but there is no session.

  6. +++ b/src/TokenHelper.php
    @@ -0,0 +1,59 @@
    +/**
    + * Token helper service.
    + */
    +class TokenHelper {
    

    Good idea to put these helper methods in a separate service. Let's add an interface for it so the methods can be swapped out with a custom implementation when needed.

  7. +++ b/src/TokenHelper.php
    @@ -0,0 +1,59 @@
    +  /**
    +   * Instantiate a new TokenHelper instance.
    +   *
    

    Use a third person verb to start the summary of a class, interface, or method: "Instantiates a new...".

  8. +++ b/src/TokenHelper.php
    @@ -0,0 +1,59 @@
    +  /**
    +   * Get the name of the persistent login cookie.
    +   *
    

    Use third person verb.

  9. +++ b/src/TokenHelper.php
    @@ -0,0 +1,59 @@
    +  /**
    +   * Check if a request contains a persistent login cookie.
    +   *
    

    Use third person verb.

pfrenssen’s picture

Priority: Normal » Major

Bumping this to major, since the main functionality of the module (staying logged in) is broken on websites that use the page cache, which I presume is the most common situation for sites that are in production.

gapple’s picture

Status: Needs review » Needs work

Thank you @sardara for the detailed report and patch, and @pfrenssen for the review.

The name TokenHelper didn't indicate to me the need for a new class or its role before reading the code. Given its scope, I think CookieHelper may be more appropriate.

I generally agree with, but am less strict about having an interface for each service.

pfrenssen’s picture

Assigned: Unassigned » pfrenssen

CookieHelper sounds like a really good name for the service.

Assigning to me, I'll address the remarks, and port the Behat test that is linked in the issue summary to BrowserTestBase. I very much like the way the Behat test is set up since it also covers the base functionality of the module.

pfrenssen’s picture

FileSize
10.9 KB
6.55 KB

Addressed the remarks. Going to start on the test coverage now.

claudiu.cristea’s picture

Status: Needs work » Needs review
pfrenssen’s picture

Assigned: pfrenssen » Unassigned
Issue tags: -Needs tests
FileSize
15.24 KB
4.35 KB
9.88 KB

Here is the test + some renames that I missed in the previous patch.

Full credit for this work should go to @sardara, I shamelessly ripped off the code + general approach from the Behat test posted in the issue summary.

@gapple, can you enable the test coverage in the Automated Tests tab on the project homepage so we can see the test results?

pfrenssen’s picture

Awaiting automated testing to be enabled for the projects, here are the test results from a local test run. The first test run passes with the patch applied, and the second test run is done on the 8.x-1.x branch without the patch, and this test fails. This indicates that the patch fixes the problem correctly.

[pieter@thinkarch web]$ cd modules/contrib/persistent_login/
[pieter@thinkarch persistent_login (2853553)]$ ../../../../vendor/bin/phpunit -c ../../../phpunit.xml tests/
PHPUnit 4.8.35 by Sebastian Bergmann and contributors.

..

Time: 23.44 seconds, Memory: 4.00MB

OK (2 tests, 10 assertions)
[pieter@thinkarch persistent_login (2853553)]$ git checkout 8.x-1.x 
Switched to branch '8.x-1.x'
Your branch is up-to-date with 'origin/8.x-1.x'.
[pieter@thinkarch persistent_login (8.x-1.x=)]$ git apply --index /tmp/2853553-10-test_only.patch
[pieter@thinkarch persistent_login (8.x-1.x +=)]$ ../../../../vendor/bin/phpunit -c ../../../phpunit.xml tests/
PHPUnit 4.8.35 by Sebastian Bergmann and contributors.

.F

Time: 23.14 seconds, Memory: 4.00MB

There was 1 failure:

1) Drupal\Tests\persistent_login\Functional\PersistentLoginTest::testPersistentLogin with data set #1 (true, 0, SplObjectStorage Object ())
Failed asserting that false matches expected true.

/home/pieter/v/joinup/web/core/tests/Drupal/Tests/BrowserTestBase.php:1343
/home/pieter/v/joinup/web/modules/contrib/persistent_login/tests/src/Functional/PersistentLoginTest.php:71

FAILURES!
Tests: 2, Assertions: 10, Failures: 1.
sardara’s picture

Assigned: Unassigned » sardara
sardara’s picture

The test was missing to mimic the setup required by the module, which is setting the cookie lifetime to 0.
I added that to the setup(), added a few assertion messages and improved a function comment which I wrote before and sounded a bit weird.

Thanks for supplying the test @pfrenssen !

The last submitted patch, 13: 2853553-13-test_only.patch, failed testing.

pfrenssen’s picture

Thanks @sardara, the cookie life time is critical for the functioning of the module. This is very useful to have added this to the test setup.

  1. +++ b/tests/src/Functional/PersistentLoginTest.php
    @@ -132,11 +140,13 @@
    +        'The user should not be logged in after starting a new session.'
    

    Missing comma at the end.

  2. +++ b/tests/src/Functional/PersistentLoginTest.php
    @@ -132,11 +140,13 @@
    +        'The user should be logged in after starting a new session.'
    

    Missing comma at the end.

sardara’s picture

Oops!
I've found the missing commas.

The last submitted patch, 16: 2853553-16-test_only.patch, failed testing.

pfrenssen’s picture

Status: Needs review » Reviewed & tested by the community

Thanks! Looks good now.

I have written the test in this patch, but this has been approved by @sardara in comment #13. I have reviewed the original fix. So seeing that all work has been reviewed properly I think it's fine to RTBC this :)

The last submitted patch, 10: 2853553-10.patch, failed testing.

The last submitted patch, 10: 2853553-10-test_only.patch, failed testing.

gapple’s picture

Status: Reviewed & tested by the community » Fixed

There was a related security report submitted.

If after a cache rebuild a user returns to the site with a persistent login token, the response is cached including their updated persistent login cookie. The next visitor to that url will also receive the cookie and may be authenticated as another user.

Since this patch turns off page cache behaviour for the entire request, it also excludes page cache from storing the response, resolving the issue. The only other change I've introduced is to mark the response as private if the persistent login cookie is set, updated, or cleared to ensure that no other intermediary caches attempt to store the response.

Since the module is still in alpha and did not support the page cache prior to this patch there won't be a security announcement, but I will tag the next alpha release as containing a security fix.

  • gapple committed 1fb8a19 on 8.x-1.x authored by sardara
    Issue #2853553 by sardara, pfrenssen, gapple: Wrong cached page is being...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.