This child issue of #2403307: RPC endpoints for user authentication: log in, check login status, log out

In that issue the http logout route needs CSRF protect but the logic for including the CSRF token in the header is in the REST module so is not usable unless we made the user module dependent on the REST module.

This issue will
Create route requirement _csrf_request_header_token which replace the current rest _access_rest_csrf(which will be deprecated)
Create new CsrfRequestHeaderAccessCheck which replace \Drupal\rest\Access\CSRFAccessCheck

Comments

tedbow created an issue. See original summary.

tedbow’s picture

Status: Active » Needs review
StatusFileSize
new4.76 KB

Ok here is the first patch.

Remaining issues that I can think of:

  1. Move route rest.csrftoken to core. But I am not sure where
  2. Create a test module that has a route uses deprecated _access_rest_csrf. Write a test to make sure csrf protection still works.
wim leers’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests
+++ b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
@@ -36,8 +34,12 @@ public function __construct(SessionConfigurationInterface $session_configuration
+    $applicable_requirements = ['_csrf_request_header_token', '_access_rest_csrf'];

We want a @todo to remove the second one in Drupal 9.0.0.

#2:

  1. Tricky! I think system module is the only fit, sadly.
  2. Sounds great!

The last submitted patch, 2: 2753681-2.patch, failed testing.

wim leers’s picture

Title: Move CSRF header token out of REST model so that user module can use it. » Move CSRF header token out of REST module so that user module can use it, as well as any contrib module
Issue tags: +blocker
dawehner’s picture

+++ b/core/modules/rest/rest.services.yml
@@ -8,11 +8,6 @@ services:
-  access_check.rest.csrf:
-    class: Drupal\rest\Access\CSRFAccessCheck
-    arguments: ['@session_configuration']
-    tags:
-      - { name: access_check }

Can we keep using access_check.rest.csrf to be an alias of the core service? Its removes some BC break

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new13.82 KB
new10.77 KB

Ok here is patch that adds the test module csrf_test.

This has to 2 routes one route uses _csrf_request_header_token and one that uses the deprecated _access_rest_csrf.

It also has \Drupal\tests\system\Functional\CsrfHeaderTest that tests these routes.

It also adds a new route at 'session/token' to retrieve the token.

I deprecated '/rest/session/token' but not sure I did it right. It is now pointing to the same method as 'session/token'

Can we keep using access_check.rest.csrf to be an alias of the core service? Its removes some BC break

I did this and added a @todo to remove in Drupal 9.0.0

wim leers’s picture

Issue tags: -Needs tests

#6++


  1. +++ b/core/modules/rest/rest.routing.yml
    @@ -1,6 +1,9 @@
    +# @deprecated This path is deprectated use the system.csrftoken route from the
    

    s/path/route/

    s/deprectated use/deprecated, use/

  2. +++ b/core/modules/system/src/Controller/CSRFTokenController.php
    @@ -0,0 +1,51 @@
    +class CSRFTokenController extends ControllerBase {
    

    s/CSRF/Csrf/

  3. +++ b/core/modules/system/src/Controller/CSRFTokenController.php
    @@ -0,0 +1,51 @@
    +   * The CSRF token manager.
    

    s/manager/generator/

  4. +++ b/core/modules/system/src/Controller/CSRFTokenController.php
    @@ -0,0 +1,51 @@
    +   * Constructs a new CsrfTokenGenerator object.
    

    C/P remnant.

  5. +++ b/core/modules/system/src/Controller/CSRFTokenController.php
    @@ -0,0 +1,51 @@
    +    return new Response($this->tokenGenerator->get('header'), 200, array('Content-Type' => 'text/plain'));
    

    s/array()/[]/

  6. +++ b/core/modules/system/tests/modules/csrf_test/csrf_test.routing.yml
    @@ -0,0 +1,18 @@
    +# Tests CSRF Header Token protection.
    

    s/CSRF Header Token/CSRF request header token/

  7. +++ b/core/modules/system/tests/modules/csrf_test/csrf_test.routing.yml
    @@ -0,0 +1,18 @@
    +# This originally was in the REST module but now is supported in Core/lib.
    

    s/Core/core/

  8. +++ b/core/modules/system/tests/modules/csrf_test/csrf_test.routing.yml
    @@ -0,0 +1,18 @@
    +  #methods:  [POST]
    

    ?

  9. +++ b/core/modules/system/tests/modules/csrf_test/src/Controller/TestController.php
    @@ -0,0 +1,22 @@
    +    return new Response('Sometimes it is hard to think of test content!');
    

    :P

  10. +++ b/core/modules/system/tests/src/Functional/CsrfHeaderTest.php
    @@ -0,0 +1,75 @@
    + * Tests protecting routes by requiring CSRF token in the header.
    

    s/header/request header/

  11. +++ b/core/modules/system/tests/src/Functional/CsrfHeaderTest.php
    @@ -0,0 +1,75 @@
    +class CsrfHeaderTest extends BrowserTestBase {
    

    CsrfRequestHeaderTest

  12. +++ b/core/modules/system/tests/src/Functional/CsrfHeaderTest.php
    @@ -0,0 +1,75 @@
    +  /**
    +   * Modules to enable.
    +   *
    +   * @var array
    +   */
    

    {@inheritdoc}

  13. +++ b/core/modules/system/tests/src/Functional/CsrfHeaderTest.php
    @@ -0,0 +1,75 @@
    +      // Add cookies to post options so that all other requests are for the
    

    s/post/POST/

  14. +++ b/core/modules/system/tests/src/Functional/CsrfHeaderTest.php
    @@ -0,0 +1,75 @@
    +      $post_options['headers']['X-CSRF-Token'] = 'this-is-not-the-token-you-are-looking-for';
    

    :D

wim leers’s picture

Status: Needs review » Needs work
tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new13.87 KB
new5.39 KB

Uploading patch that addresses @Wim Leers' review in #8

The only one I am not changing is #8.12 regarding using {@inheritdoc}. The $modules property is not actually in the parent class like you might think.

See \Drupal\Tests\BrowserTestBase::installDrupal

    // Collect modules to install.
    $class = get_class($this);
    $modules = array();
    while ($class) {
      if (property_exists($class, 'modules')) {
        $modules = array_merge($modules, $class::$modules);
      }
      $class = get_parent_class($class);
    }

Not really sure why it is done this way. I assuming I can't use {@inheritdoc} in this case.

tedbow’s picture

StatusFileSize
new5.39 KB

Whoops named interdiff wrong not seeing how to cancel it now

Status: Needs review » Needs work

The last submitted patch, 10: interdiff-2753681-7-10.patch, failed testing.

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

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new13.87 KB

Argggg git case file name change problem
Ok this is the exact patch as 2753681-10.patch except with case of CSRFTokenController.php correct to CsrfTokenController.php

dawehner’s picture

Status: Needs review » Needs work

I'm feeling a bit bad about it.

  1. +++ b/core/modules/rest/rest.services.yml
    @@ -8,11 +8,9 @@ services:
       access_check.rest.csrf:
    -    class: Drupal\rest\Access\CSRFAccessCheck
    -    arguments: ['@session_configuration']
    -    tags:
    -      - { name: access_check }
    +    alias: access_check.header.csrf
    

    Cool, thank you!

  2. +++ b/core/modules/system/tests/src/Functional/CsrfRequestHeaderTest.php
    @@ -0,0 +1,75 @@
    +
    +namespace Drupal\tests\system\Functional;
    

    The namespace should be Drupal\Tests\system\Functional ... aka. this will not run on the testbot. This is kinda of a bummer

  3. +++ b/core/modules/system/tests/src/Functional/CsrfRequestHeaderTest.php
    @@ -0,0 +1,75 @@
    +      $post_options['cookies']  = $cookies;
    

    Nitpick of the month: 2 spaces!

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new864 bytes
new13.87 KB

I'm feeling a bit bad about it.

No need, thanks for review!

Here is patch that fixes the 2 issues

dawehner’s picture

+++ b/core/modules/system/src/Controller/CsrfTokenController.php
@@ -0,0 +1,51 @@
+ * Returns responses for CSRF token routes.
+ */
+class CsrfTokenController extends ControllerBase {
+

Let's not extend the controller base as long we don't need it.

tedbow’s picture

StatusFileSize
new13.91 KB
new833 bytes

Good point!

wim leers’s picture

  1. +++ b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
    @@ -36,8 +34,13 @@ public function __construct(SessionConfigurationInterface $session_configuration
    +    // @todo Remove _access_rest_csrf in Drupal 9.0.0.
    +    $applicable_requirements = ['_csrf_request_header_token', '_access_rest_csrf'];
    

    SuperNit: I'd format this array across multiple lines, then you can add the @todo comment above the relevant line.

  2. +++ b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
    @@ -77,7 +80,7 @@ public function access(Request $request, AccountInterface $account) {
    -      if (!\Drupal::csrfToken()->validate($csrf_token, 'rest')) {
    +      if (!\Drupal::csrfToken()->validate($csrf_token, 'header')) {
    

    +1 to not tie this to rest in any way.

    I wonder if 'X-CSRF-Token request header' would be a better value though.

    In any case, this should be moved into a class constant.

  3. +++ b/core/modules/system/tests/modules/csrf_test/csrf_test.routing.yml
    @@ -0,0 +1,18 @@
    +# Tests deprecated _access_rest_csrf protection.
    +# This originally was in the REST module but now is supported in core/lib.
    +# @todo Remove this test route in Drupal 9.0.0.
    

    SuperNit: I think this could use an @see.

tedbow’s picture

StatusFileSize
new14.21 KB
new3.29 KB

@Wim Leers thanks for review. Here is a patch that addresses those issues.

dawehner’s picture

+++ b/core/core.services.yml
similarity index 78%
rename from core/modules/rest/src/Access/CSRFAccessCheck.php

rename from core/modules/rest/src/Access/CSRFAccessCheck.php
rename to core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php

+++ b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
@@ -12,7 +10,12 @@
  */
-class CSRFAccessCheck implements AccessCheckInterface {
+class CsrfRequestHeaderAccessCheck implements AccessCheckInterface {

I tried to check the BC policy around those classes and it seems to be not 100% clear at the moment, see https://www.drupal.org/node/2550249#comment-11344307 for a clarification.

wim leers’s picture

Status: Needs review » Reviewed & tested by the community
neclimdul’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/rest/src/Tests/CsrfTest.php
@@ -87,7 +87,7 @@ public function testCookieAuth() {
-    $token = $this->drupalGet('rest/session/token');
+    $token = $this->drupalGet('session/token');

With the new test, why are we changing this? Shouldn't we leave it the same and deprecate the test aligned with the route?

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new1.06 KB
new14.13 KB

@neclimdul nice catch!

I am sure about the policy for deprecating tests but this seems like good idea.

Access to routes is already tested in the new test \Drupal\Tests\system\Functional\CsrfRequestHeaderTest::testRouteAccess

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

RTBC under the assumption that https://www.drupal.org/node/2550249#comment-11344307 is fine.

neclimdul’s picture

Works for me, +1 on RTBC. Thanks guys!

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 24: 2753681-24.patch, failed testing.

neclimdul’s picture

Status: Needs work » Reviewed & tested by the community

Looks like bot problems. Been fixing a lot of those on RTBC issues this weekend.

tedbow’s picture

Assigned: tedbow » Unassigned
catch’s picture

Status: Reviewed & tested by the community » Needs review

Few questions on this one, code itself looks good:

  1. +++ b/core/modules/rest/rest.routing.yml
    @@ -1,6 +1,9 @@
    +# @deprecated This route is deprecated, use the system.csrftoken route from the
    

    What does this mean for the actual REST API itself. Should we be adding a 301 here (not necessarily this patch, but in a later minor release?). Or should 9.x keep the path and add the redirect?

  2. +++ b/core/modules/rest/rest.services.yml
    @@ -8,11 +8,9 @@ services:
    +    alias: access_check.header.csrf
    

    +1.

  3. +++ b/core/modules/rest/src/RequestHandler.php
    @@ -137,16 +137,6 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
    -    return new Response(\Drupal::csrfToken()->get('rest'), 200, array('Content-Type' => 'text/plain'));
    

    This was using 'rest' as the key.

  4. +++ b/core/modules/system/src/Controller/CsrfTokenController.php
    @@ -0,0 +1,52 @@
    +    return new Response($this->tokenGenerator->get(CsrfRequestHeaderAccessCheck::TOKEN_KEY), 200, ['Content-Type' => 'text/plain']);
    

    Now using the constant, which is not 'rest'. What happens to a rest client if they access the site just as it updates? Won't they fail the CSRF check? If so is there anything we should do about that? Also brings up the question whether the token key should be set as a route option rather than a fixed constant - then REST could still use 'rest' then.

  5. +++ b/core/modules/system/system.routing.yml
    @@ -492,3 +492,10 @@ system.entity_autocomplete:
    +system.csrftoken:
    

    Makes me wish we'd split system module up a bit more in 8.x, but for now can't think of anywhere else it could live.

  6. +++ b/core/modules/system/tests/modules/csrf_test/src/Controller/TestController.php
    @@ -0,0 +1,22 @@
    +    return new Response('Sometimes it is hard to think of test content!');
    

    :)

wim leers’s picture

  1. What does this mean for the actual REST API itself. Should we be adding a 301 here (not necessarily this patch, but in a later minor release?). Or should 9.x keep the path and add the redirect?

    IMO the latter. The former would cause that to become two round trips rather than one.

  1. Great point! But AFAICT this is exactly the same as a session having expired? Then your token suddenly is no longer valid either. Furthermore, since this is a minor version bump (8.1.x -> 8.2.x), we'll be terminating sessions anywayapparently we don't terminate sessions upon updating? Should we add an update hook to REST to terminate all sessions?.
catch’s picture

If your session has just expired then you'll be logged out, the situation here would be you're logged in but get a CSRF validation error (at least, that's what would happen with a form submission if the token check changed between rendering the form and submitting it, haven't tested it with REST). That's a bit different from getting logged out - you'd be forced to re-authenticate in that case, but not if you just have a wrong token.

Terminating sessions on update would 'fix' this, but not sure if that's acceptable - might be worse than the wrong REST tokens (since that'd affect regular site visitors too).

tedbow’s picture

@catch good point in #30.4

We could change it something like

// @todo Remove 'rest' value validate in 8.3.
      //   Kept here for session active during update.
      if (!\Drupal::csrfToken()->validate($csrf_token, self::TOKEN_KEY)
        && !\Drupal::csrfToken()->validate($csrf_token, 'rest')
      ) {
        return AccessResult::forbidden()->setCacheMaxAge(0);
      }

That way in any sessions that just got the token using the old method during the update will work. After the update even the 'rest' would still work it would still have to be token generated by the system so that doesn't seem like a security problem.

Performance wise after the update for any valid requests using self::TOKEN_KEY the first part of the && condition will not be true so it won't make the second validate call. So it would only be performance hit for requests for invalid tokens.

We can then safely remove it in 8.3 because 8.1 won't have been support for a while, meaning core doesn't support a 8.1 to 8.3 upgrade(am I correct about that?).

catch’s picture

#33 seems like a good workaround/bc layer, and it's right that'd we'd only need to keep it in for one minor release.

tedbow’s picture

StatusFileSize
new15.13 KB
new2.19 KB

Ok here is patch that does my suggest in #33

If also gets rid of "\Drupal::csrfToken()" by making the token generator an argument to the service. I hope there wasn't a reason we weren't already doing that.

Status: Needs review » Needs work

The last submitted patch, 35: 2753681-35.patch, failed testing.

wim leers’s picture

Status: Needs work » Reviewed & tested by the community

#35 had a random fail, so re-testing, should come back green.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 35: 2753681-35.patch, failed testing.

wim leers’s picture

Status: Needs work » Reviewed & tested by the community

Gah again?! UpdatePathTestBaseFilledTest

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests
+++ b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php
@@ -88,7 +98,10 @@ public function access(Request $request, AccountInterface $account) {
+      // @todo Remove validate call using 'rest' in 8.3.
+      //   Kept here for sessions active during update.
+      if (!$this->csrfToken->validate($csrf_token, self::TOKEN_KEY)
+        && !$this->csrfToken->validate($csrf_token, 'rest')) {

Can this new logic be tested? Probably.

alexpott’s picture

Also I think this change could do with a change record.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new17.79 KB
new7.13 KB

Can this new logic be tested? Probably.

Good call!
In \Drupal\Tests\system\Functional\CsrfRequestHeaderTest

added outer loop

$csrf_token_paths = ['deprecated/session/token', 'session/token'];
    // Test using the both the current path and a test path that returns
    // a token using the deprecated 'rest' value.
    // Checking /deprecated/session/token can be removed in 8.3.
    // @see \Drupal\Core\Access\CsrfRequestHeaderAccessCheck::access()
    foreach ($csrf_token_paths as $csrf_token_path) {

This uses \Drupal\csrf_test\Controller\DeprecatedCsrfTokenController
which creates the csrf token using the 'rest' value

Status: Needs review » Needs work

The last submitted patch, 42: 2753681-42.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review

Random fail in UpdatePathTestBaseFilledTest.

wim leers’s picture

Status: Needs review » Reviewed & tested by the community
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed 62b7392 and pushed to 8.2.x. Thanks!

  • catch committed 62b7392 on 8.2.x
    Issue #2753681 by tedbow, Wim Leers, dawehner, neclimdul, catch: Move...
wim leers’s picture

Issue tags: -Needs tests

OMG YES YES YES! This unblocks #2403307: RPC endpoints for user authentication: log in, check login status, log out :)

@tedbow can you still create a change record?

catch’s picture

Sorry I saw alexpott's request for a CR but convinced myself it had been added after reviewing the new tests - that'd be good to have indeed for this.

tedbow’s picture

Here is the draft change record: https://www.drupal.org/node/2772399
Please look over and publish if ok

This handbook page will also probably need to be updated: https://www.drupal.org/node/2122195

  • catch committed 62b7392 on 8.3.x
    Issue #2753681 by tedbow, Wim Leers, dawehner, neclimdul, catch: Move...

  • catch committed 62b7392 on 8.3.x
    Issue #2753681 by tedbow, Wim Leers, dawehner, neclimdul, catch: Move...

Status: Fixed » Closed (fixed)

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