You are here

trait CookieResourceTestTrait in Drupal 9

Same name and namespace in other branches
  1. 8 core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php \Drupal\Tests\rest\Functional\CookieResourceTestTrait

Trait for ResourceTestBase subclasses testing $auth=cookie.

Characteristics:

  • After performing a valid "log in" request, the server responds with a 2xx status code and a 'Set-Cookie' response header. This cookie is what continues to identify the user in subsequent requests.
  • When accessing a URI that requires authentication without being authenticated, a standard 403 response must be sent.
  • Because of the reliance on cookies, and the fact that user agents send cookies with every request, this is vulnerable to CSRF attacks. To mitigate this, the response for the "log in" request contains a CSRF token that must be sent with every unsafe (POST/PATCH/DELETE) HTTP request.

Hierarchy

155 files declare their use of CookieResourceTestTrait
ActionHalJsonCookieTest.php in core/modules/system/tests/src/Functional/Hal/ActionHalJsonCookieTest.php
ActionJsonCookieTest.php in core/modules/system/tests/src/Functional/Rest/ActionJsonCookieTest.php
ActionXmlCookieTest.php in core/modules/system/tests/src/Functional/Rest/ActionXmlCookieTest.php
BaseFieldOverrideHalJsonCookieTest.php in core/tests/Drupal/FunctionalTests/Hal/BaseFieldOverrideHalJsonCookieTest.php
BaseFieldOverrideJsonCookieTest.php in core/tests/Drupal/FunctionalTests/Rest/BaseFieldOverrideJsonCookieTest.php

... See full list

File

core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php, line 23

Namespace

Drupal\Tests\rest\Functional
View source
trait CookieResourceTestTrait {

  /**
   * The session cookie.
   *
   * @see ::initAuthentication
   *
   * @var string
   */
  protected $sessionCookie;

  /**
   * The CSRF token.
   *
   * @see ::initAuthentication
   *
   * @var string
   */
  protected $csrfToken;

  /**
   * The logout token.
   *
   * @see ::initAuthentication
   *
   * @var string
   */
  protected $logoutToken;

  /**
   * {@inheritdoc}
   */
  protected function initAuthentication() {
    $user_login_url = Url::fromRoute('user.login.http')
      ->setRouteParameter('_format', static::$format);
    $request_body = [
      'name' => $this->account->name->value,
      'pass' => $this->account->passRaw,
    ];
    $request_options[RequestOptions::BODY] = $this->serializer
      ->encode($request_body, static::$format);
    $request_options[RequestOptions::HEADERS] = [
      'Content-Type' => static::$mimeType,
    ];
    $response = $this
      ->request('POST', $user_login_url, $request_options);

    // Parse and store the session cookie.
    $this->sessionCookie = explode(';', $response
      ->getHeader('Set-Cookie')[0], 2)[0];

    // Parse and store the CSRF token and logout token.
    $data = $this->serializer
      ->decode((string) $response
      ->getBody(), static::$format);
    $this->csrfToken = $data['csrf_token'];
    $this->logoutToken = $data['logout_token'];
  }

  /**
   * {@inheritdoc}
   */
  protected function getAuthenticationRequestOptions($method) {
    $request_options[RequestOptions::HEADERS]['Cookie'] = $this->sessionCookie;

    // @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
    if (!in_array($method, [
      'HEAD',
      'GET',
      'OPTIONS',
      'TRACE',
    ])) {
      $request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = $this->csrfToken;
    }
    return $request_options;
  }

  /**
   * {@inheritdoc}
   */
  protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {

    // Requests needing cookie authentication but missing it results in a 403
    // response. The cookie authentication mechanism sets no response message.
    // Hence, effectively, this is just the 403 response that one gets as the
    // anonymous user trying to access a certain REST resource.
    // @see \Drupal\user\Authentication\Provider\Cookie
    // @todo https://www.drupal.org/node/2847623
    if ($method === 'GET') {
      $expected_cookie_403_cacheability = $this
        ->getExpectedUnauthorizedAccessCacheability()
        ->addCacheableDependency($this
        ->getExpectedUnauthorizedEntityAccessCacheability(FALSE));

      // - \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber applies
      //   to cacheable anonymous responses: it updates their cacheability.
      // - A 403 response to a GET request is cacheable.
      // Therefore we must update our cacheability expectations accordingly.
      if (in_array('user.permissions', $expected_cookie_403_cacheability
        ->getCacheContexts(), TRUE)) {
        $expected_cookie_403_cacheability
          ->addCacheTags([
          'config:user.role.anonymous',
        ]);
      }

      // @todo Fix \Drupal\block\BlockAccessControlHandler::mergeCacheabilityFromConditions() in https://www.drupal.org/node/2867881
      if (static::$entityTypeId === 'block') {
        $expected_cookie_403_cacheability
          ->setCacheTags(str_replace('user:2', 'user:0', $expected_cookie_403_cacheability
          ->getCacheTags()));
      }
      $this
        ->assertResourceErrorResponse(403, FALSE, $response, $expected_cookie_403_cacheability
        ->getCacheTags(), $expected_cookie_403_cacheability
        ->getCacheContexts(), 'MISS', FALSE);
    }
    else {
      $this
        ->assertResourceErrorResponse(403, FALSE, $response);
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options) {

    // X-CSRF-Token request header is unnecessary for safe and side effect-free
    // HTTP methods. No need for additional assertions.
    // @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
    if (in_array($method, [
      'HEAD',
      'GET',
      'OPTIONS',
      'TRACE',
    ])) {
      return;
    }
    unset($request_options[RequestOptions::HEADERS]['X-CSRF-Token']);

    // DX: 403 when missing X-CSRF-Token request header.
    $response = $this
      ->request($method, $url, $request_options);
    $this
      ->assertResourceErrorResponse(403, 'X-CSRF-Token request header is missing', $response);
    $request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = 'this-is-not-the-token-you-are-looking-for';

    // DX: 403 when invalid X-CSRF-Token request header.
    $response = $this
      ->request($method, $url, $request_options);
    $this
      ->assertResourceErrorResponse(403, 'X-CSRF-Token request header is invalid', $response);
    $request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = $this->csrfToken;
  }

}

Members