annotate core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Tests\rest\Functional;
Chris@0 4
Chris@0 5 use Drupal\Core\Url;
Chris@0 6 use GuzzleHttp\RequestOptions;
Chris@0 7 use Psr\Http\Message\ResponseInterface;
Chris@0 8
Chris@0 9 /**
Chris@0 10 * Trait for ResourceTestBase subclasses testing $auth=cookie.
Chris@0 11 *
Chris@0 12 * Characteristics:
Chris@0 13 * - After performing a valid "log in" request, the server responds with a 2xx
Chris@0 14 * status code and a 'Set-Cookie' response header. This cookie is what
Chris@0 15 * continues to identify the user in subsequent requests.
Chris@0 16 * - When accessing a URI that requires authentication without being
Chris@0 17 * authenticated, a standard 403 response must be sent.
Chris@0 18 * - Because of the reliance on cookies, and the fact that user agents send
Chris@0 19 * cookies with every request, this is vulnerable to CSRF attacks. To mitigate
Chris@0 20 * this, the response for the "log in" request contains a CSRF token that must
Chris@0 21 * be sent with every unsafe (POST/PATCH/DELETE) HTTP request.
Chris@0 22 */
Chris@0 23 trait CookieResourceTestTrait {
Chris@0 24
Chris@0 25 /**
Chris@0 26 * The session cookie.
Chris@0 27 *
Chris@0 28 * @see ::initAuthentication
Chris@0 29 *
Chris@0 30 * @var string
Chris@0 31 */
Chris@0 32 protected $sessionCookie;
Chris@0 33
Chris@0 34 /**
Chris@0 35 * The CSRF token.
Chris@0 36 *
Chris@0 37 * @see ::initAuthentication
Chris@0 38 *
Chris@0 39 * @var string
Chris@0 40 */
Chris@0 41 protected $csrfToken;
Chris@0 42
Chris@0 43 /**
Chris@0 44 * The logout token.
Chris@0 45 *
Chris@0 46 * @see ::initAuthentication
Chris@0 47 *
Chris@0 48 * @var string
Chris@0 49 */
Chris@0 50 protected $logoutToken;
Chris@0 51
Chris@0 52 /**
Chris@0 53 * {@inheritdoc}
Chris@0 54 */
Chris@0 55 protected function initAuthentication() {
Chris@0 56 $user_login_url = Url::fromRoute('user.login.http')
Chris@0 57 ->setRouteParameter('_format', static::$format);
Chris@0 58
Chris@0 59 $request_body = [
Chris@0 60 'name' => $this->account->name->value,
Chris@0 61 'pass' => $this->account->passRaw,
Chris@0 62 ];
Chris@0 63
Chris@14 64 $request_options[RequestOptions::BODY] = $this->serializer->encode($request_body, static::$format);
Chris@0 65 $request_options[RequestOptions::HEADERS] = [
Chris@0 66 'Content-Type' => static::$mimeType,
Chris@0 67 ];
Chris@0 68 $response = $this->request('POST', $user_login_url, $request_options);
Chris@0 69
Chris@0 70 // Parse and store the session cookie.
Chris@0 71 $this->sessionCookie = explode(';', $response->getHeader('Set-Cookie')[0], 2)[0];
Chris@0 72
Chris@0 73 // Parse and store the CSRF token and logout token.
Chris@0 74 $data = $this->serializer->decode((string) $response->getBody(), static::$format);
Chris@0 75 $this->csrfToken = $data['csrf_token'];
Chris@0 76 $this->logoutToken = $data['logout_token'];
Chris@0 77 }
Chris@0 78
Chris@0 79 /**
Chris@0 80 * {@inheritdoc}
Chris@0 81 */
Chris@0 82 protected function getAuthenticationRequestOptions($method) {
Chris@0 83 $request_options[RequestOptions::HEADERS]['Cookie'] = $this->sessionCookie;
Chris@0 84 // @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
Chris@0 85 if (!in_array($method, ['HEAD', 'GET', 'OPTIONS', 'TRACE'])) {
Chris@0 86 $request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = $this->csrfToken;
Chris@0 87 }
Chris@0 88 return $request_options;
Chris@0 89 }
Chris@0 90
Chris@0 91 /**
Chris@0 92 * {@inheritdoc}
Chris@0 93 */
Chris@14 94 protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
Chris@0 95 // Requests needing cookie authentication but missing it results in a 403
Chris@0 96 // response. The cookie authentication mechanism sets no response message.
Chris@14 97 // Hence, effectively, this is just the 403 response that one gets as the
Chris@14 98 // anonymous user trying to access a certain REST resource.
Chris@14 99 // @see \Drupal\user\Authentication\Provider\Cookie
Chris@0 100 // @todo https://www.drupal.org/node/2847623
Chris@14 101 if ($method === 'GET') {
Chris@17 102 $expected_cookie_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
Chris@17 103 // @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
Chris@17 104 ->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE));
Chris@14 105 // - \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber applies
Chris@14 106 // to cacheable anonymous responses: it updates their cacheability.
Chris@14 107 // - A 403 response to a GET request is cacheable.
Chris@14 108 // Therefore we must update our cacheability expectations accordingly.
Chris@14 109 if (in_array('user.permissions', $expected_cookie_403_cacheability->getCacheContexts(), TRUE)) {
Chris@14 110 $expected_cookie_403_cacheability->addCacheTags(['config:user.role.anonymous']);
Chris@14 111 }
Chris@14 112 // @todo Fix \Drupal\block\BlockAccessControlHandler::mergeCacheabilityFromConditions() in https://www.drupal.org/node/2867881
Chris@14 113 if (static::$entityTypeId === 'block') {
Chris@14 114 $expected_cookie_403_cacheability->setCacheTags(str_replace('user:2', 'user:0', $expected_cookie_403_cacheability->getCacheTags()));
Chris@14 115 }
Chris@17 116 $this->assertResourceErrorResponse(403, FALSE, $response, $expected_cookie_403_cacheability->getCacheTags(), $expected_cookie_403_cacheability->getCacheContexts(), 'MISS', FALSE);
Chris@14 117 }
Chris@14 118 else {
Chris@14 119 $this->assertResourceErrorResponse(403, FALSE, $response);
Chris@14 120 }
Chris@0 121 }
Chris@0 122
Chris@0 123 /**
Chris@0 124 * {@inheritdoc}
Chris@0 125 */
Chris@0 126 protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options) {
Chris@0 127 // X-CSRF-Token request header is unnecessary for safe and side effect-free
Chris@0 128 // HTTP methods. No need for additional assertions.
Chris@0 129 // @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
Chris@0 130 if (in_array($method, ['HEAD', 'GET', 'OPTIONS', 'TRACE'])) {
Chris@0 131 return;
Chris@0 132 }
Chris@0 133
Chris@0 134 unset($request_options[RequestOptions::HEADERS]['X-CSRF-Token']);
Chris@0 135
Chris@0 136 // DX: 403 when missing X-CSRF-Token request header.
Chris@0 137 $response = $this->request($method, $url, $request_options);
Chris@0 138 $this->assertResourceErrorResponse(403, 'X-CSRF-Token request header is missing', $response);
Chris@0 139
Chris@0 140 $request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = 'this-is-not-the-token-you-are-looking-for';
Chris@0 141
Chris@0 142 // DX: 403 when invalid X-CSRF-Token request header.
Chris@0 143 $response = $this->request($method, $url, $request_options);
Chris@0 144 $this->assertResourceErrorResponse(403, 'X-CSRF-Token request header is invalid', $response);
Chris@0 145
Chris@0 146 $request_options[RequestOptions::HEADERS]['X-CSRF-Token'] = $this->csrfToken;
Chris@0 147 }
Chris@0 148
Chris@0 149 }