BasicAuthTest.php 10.2 KB
Newer Older
1 2
<?php

3
namespace Drupal\Tests\basic_auth\Functional;
4

5
use Drupal\Component\Render\FormattableMarkup;
6
use Drupal\Core\Url;
7
use Drupal\Tests\basic_auth\Traits\BasicAuthTestTrait;
8
use Drupal\language\Entity\ConfigurableLanguage;
9
use Drupal\Tests\BrowserTestBase;
10
use Drupal\user\Entity\Role;
11 12

/**
13 14 15
 * Tests for BasicAuth authentication provider.
 *
 * @group basic_auth
16
 */
17
class BasicAuthTest extends BrowserTestBase {
18

19 20
  use BasicAuthTestTrait;

21
  /**
22
   * Modules installed for all tests.
23 24 25
   *
   * @var array
   */
26
  public static $modules = ['basic_auth', 'router_test', 'locale', 'basic_auth_test'];
27 28 29 30

  /**
   * Test http basic authentication.
   */
31
  public function testBasicAuth() {
32 33 34 35 36
    // Enable page caching.
    $config = $this->config('system.performance');
    $config->set('cache.page.max_age', 300);
    $config->save();

37
    $account = $this->drupalCreateUser();
38
    $url = Url::fromRoute('router_test.11');
39

40
    $this->basicAuthGet($url, $account->getUsername(), $account->pass_raw);
41
    $this->assertText($account->getUsername(), 'Account name is displayed.');
42
    $this->assertResponse('200', 'HTTP response is OK');
43
    $this->mink->resetSessions();
44 45
    $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'));
    $this->assertIdentical(strpos($this->drupalGetHeader('Cache-Control'), 'public'), FALSE, 'Cache-Control is not set to public');
46

47
    $this->basicAuthGet($url, $account->getUsername(), $this->randomMachineName());
48
    $this->assertNoText($account->getUsername(), 'Bad basic auth credentials do not authenticate the user.');
49
    $this->assertResponse('403', 'Access is not granted.');
50
    $this->mink->resetSessions();
51

52
    $this->drupalGet($url);
53
    $this->assertEqual($this->drupalGetHeader('WWW-Authenticate'), new FormattableMarkup('Basic realm="@realm"', ['@realm' => \Drupal::config('system.site')->get('name')]));
54
    $this->assertResponse('401', 'Not authenticated on the route that allows only basic_auth. Prompt to authenticate received.');
55 56 57 58

    $this->drupalGet('admin');
    $this->assertResponse('403', 'No authentication prompt for routes not explicitly defining authentication providers.');

59
    $account = $this->drupalCreateUser(['access administration pages']);
60

61
    $this->basicAuthGet(Url::fromRoute('system.admin'), $account->getUsername(), $account->pass_raw);
62
    $this->assertNoLink('Log out', 'User is not logged in');
63
    $this->assertResponse('403', 'No basic authentication for routes not explicitly defining authentication providers.');
64
    $this->mink->resetSessions();
65 66 67 68 69 70 71 72 73

    // Ensure that pages already in the page cache aren't returned from page
    // cache if basic auth credentials are provided.
    $url = Url::fromRoute('router_test.10');
    $this->drupalGet($url);
    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
    $this->basicAuthGet($url, $account->getUsername(), $account->pass_raw);
    $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'));
    $this->assertIdentical(strpos($this->drupalGetHeader('Cache-Control'), 'public'), FALSE, 'No page cache response when requesting a cached page with basic auth credentials.');
74 75
  }

76 77 78
  /**
   * Test the global login flood control.
   */
79
  public function testGlobalLoginFloodControl() {
80
    $this->config('user.flood')
81 82 83 84 85
      ->set('ip_limit', 2)
      // Set a high per-user limit out so that it is not relevant in the test.
      ->set('user_limit', 4000)
      ->save();

86
    $user = $this->drupalCreateUser([]);
87 88
    $incorrect_user = clone $user;
    $incorrect_user->pass_raw .= 'incorrect';
89
    $url = Url::fromRoute('router_test.11');
90 91 92

    // Try 2 failed logins.
    for ($i = 0; $i < 2; $i++) {
93
      $this->basicAuthGet($url, $incorrect_user->getUsername(), $incorrect_user->pass_raw);
94 95 96
    }

    // IP limit has reached to its limit. Even valid user credentials will fail.
97
    $this->basicAuthGet($url, $user->getUsername(), $user->pass_raw);
98 99 100 101 102 103
    $this->assertResponse('403', 'Access is blocked because of IP based flood prevention.');
  }

  /**
   * Test the per-user login flood control.
   */
104
  public function testPerUserLoginFloodControl() {
105
    $this->config('user.flood')
106 107 108 109 110
      // Set a high global limit out so that it is not relevant in the test.
      ->set('ip_limit', 4000)
      ->set('user_limit', 2)
      ->save();

111
    $user = $this->drupalCreateUser([]);
112 113
    $incorrect_user = clone $user;
    $incorrect_user->pass_raw .= 'incorrect';
114
    $user2 = $this->drupalCreateUser([]);
115
    $url = Url::fromRoute('router_test.11');
116 117

    // Try a failed login.
118
    $this->basicAuthGet($url, $incorrect_user->getUsername(), $incorrect_user->pass_raw);
119 120

    // A successful login will reset the per-user flood control count.
121
    $this->basicAuthGet($url, $user->getUsername(), $user->pass_raw);
122 123 124 125
    $this->assertResponse('200', 'Per user flood prevention gets reset on a successful login.');

    // Try 2 failed logins for a user. They will trigger flood control.
    for ($i = 0; $i < 2; $i++) {
126
      $this->basicAuthGet($url, $incorrect_user->getUsername(), $incorrect_user->pass_raw);
127 128 129
    }

    // Now the user account is blocked.
130
    $this->basicAuthGet($url, $user->getUsername(), $user->pass_raw);
131 132 133 134
    $this->assertResponse('403', 'The user account is blocked due to per user flood prevention.');

    // Try one successful attempt for a different user, it should not trigger
    // any flood control.
135
    $this->basicAuthGet($url, $user2->getUsername(), $user2->pass_raw);
136 137 138
    $this->assertResponse('200', 'Per user flood prevention does not block access for other users.');
  }

139 140 141
  /**
   * Tests compatibility with locale/UI translation.
   */
142
  public function testLocale() {
143
    ConfigurableLanguage::createFromLangcode('de')->save();
144
    $this->config('system.site')->set('default_langcode', 'de')->save();
145 146

    $account = $this->drupalCreateUser();
147
    $url = Url::fromRoute('router_test.11');
148

149
    $this->basicAuthGet($url, $account->getUsername(), $account->pass_raw);
150 151 152 153
    $this->assertText($account->getUsername(), 'Account name is displayed.');
    $this->assertResponse('200', 'HTTP response is OK');
  }

154 155 156
  /**
   * Tests if a comprehensive message is displayed when the route is denied.
   */
157
  public function testUnauthorizedErrorMessage() {
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
    $account = $this->drupalCreateUser();
    $url = Url::fromRoute('router_test.11');

    // Case when no credentials are passed.
    $this->drupalGet($url);
    $this->assertResponse('401', 'The user is blocked when no credentials are passed.');
    $this->assertNoText('Exception', "No raw exception is displayed on the page.");
    $this->assertText('Please log in to access this page.', "A user friendly access unauthorized message is displayed.");

    // Case when empty credentials are passed.
    $this->basicAuthGet($url, NULL, NULL);
    $this->assertResponse('403', 'The user is blocked when empty credentials are passed.');
    $this->assertText('Access denied', "A user friendly access denied message is displayed");

    // Case when wrong credentials are passed.
    $this->basicAuthGet($url, $account->getUsername(), $this->randomMachineName());
    $this->assertResponse('403', 'The user is blocked when wrong credentials are passed.');
    $this->assertText('Access denied', "A user friendly access denied message is displayed");
176 177 178 179 180 181

    // Case when correct credentials but hasn't access to the route.
    $url = Url::fromRoute('router_test.15');
    $this->basicAuthGet($url, $account->getUsername(), $account->pass_raw);
    $this->assertResponse('403', 'The used authentication method is not allowed on this route.');
    $this->assertText('Access denied', "A user friendly access denied message is displayed");
182 183
  }

184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
  /**
   * Tests the cacheability of Basic Auth's 401 response.
   *
   * @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
   */
  public function testCacheabilityOf401Response() {
    $session = $this->getSession();
    $url = Url::fromRoute('router_test.11');

    $assert_response_cacheability = function ($expected_page_cache_header_value, $expected_dynamic_page_cache_header_value) use ($session, $url) {
      $this->drupalGet($url);
      $this->assertSession()->statusCodeEquals(401);
      $this->assertSame($expected_page_cache_header_value, $session->getResponseHeader('X-Drupal-Cache'));
      $this->assertSame($expected_dynamic_page_cache_header_value, $session->getResponseHeader('X-Drupal-Dynamic-Cache'));
    };

    // 1. First request: cold caches, both Page Cache and Dynamic Page Cache are
    // now primed.
    $assert_response_cacheability('MISS', 'MISS');
    // 2. Second request: Page Cache HIT, we don't even hit Dynamic Page Cache.
    // This is going to keep happening.
    $assert_response_cacheability('HIT', 'MISS');
    // 3. Third request: after clearing Page Cache, we now see that Dynamic Page
    // Cache is a HIT too.
    $this->container->get('cache.page')->deleteAll();
    $assert_response_cacheability('MISS', 'HIT');
    // 4. Fourth request: warm caches.
    $assert_response_cacheability('HIT', 'HIT');

    // If the permissions of the 'anonymous' role change, it may no longer be
    // necessary to be authenticated to access this route. Therefore the cached
    // 401 responses should be invalidated.
    $this->grantPermissions(Role::load(Role::ANONYMOUS_ID), [$this->randomMachineName()]);
    $assert_response_cacheability('MISS', 'MISS');
    $assert_response_cacheability('HIT', 'MISS');
    // Idem for when the 'system.site' config changes.
    $this->config('system.site')->save();
    $assert_response_cacheability('MISS', 'MISS');
    $assert_response_cacheability('HIT', 'MISS');
  }

225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
  /**
   * Tests if the controller is called before authentication.
   *
   * @see https://www.drupal.org/node/2817727
   */
  public function testControllerNotCalledBeforeAuth() {
    $this->drupalGet('/basic_auth_test/state/modify');
    $this->assertResponse(401);
    $this->drupalGet('/basic_auth_test/state/read');
    $this->assertResponse(200);
    $this->assertRaw('nope');

    $account = $this->drupalCreateUser();
    $this->basicAuthGet('/basic_auth_test/state/modify', $account->getUsername(), $account->pass_raw);
    $this->assertResponse(200);
    $this->assertRaw('Done');
241 242

    $this->mink->resetSessions();
243 244 245 246 247
    $this->drupalGet('/basic_auth_test/state/read');
    $this->assertResponse(200);
    $this->assertRaw('yep');
  }

248
}