Initial Drupal 11 with DDEV setup
This commit is contained in:
7
web/core/modules/basic_auth/basic_auth.info.yml
Normal file
7
web/core/modules/basic_auth/basic_auth.info.yml
Normal file
@ -0,0 +1,7 @@
|
||||
name: 'HTTP Basic Authentication'
|
||||
type: module
|
||||
description: 'Provides an HTTP Basic authentication provider.'
|
||||
package: Web services
|
||||
version: VERSION
|
||||
dependencies:
|
||||
- drupal:user
|
||||
16
web/core/modules/basic_auth/basic_auth.services.yml
Normal file
16
web/core/modules/basic_auth/basic_auth.services.yml
Normal file
@ -0,0 +1,16 @@
|
||||
parameters:
|
||||
basic_auth.skip_procedural_hook_scan: true
|
||||
|
||||
services:
|
||||
_defaults:
|
||||
autoconfigure: true
|
||||
basic_auth.authentication.basic_auth:
|
||||
class: Drupal\basic_auth\Authentication\Provider\BasicAuth
|
||||
arguments: ['@config.factory', '@user.auth', '@flood', '@entity_type.manager']
|
||||
tags:
|
||||
- { name: authentication_provider, provider_id: 'basic_auth', priority: 100 }
|
||||
basic_auth.page_cache_request_policy.disallow_basic_auth_requests:
|
||||
class: Drupal\basic_auth\PageCache\DisallowBasicAuthRequests
|
||||
public: false
|
||||
tags:
|
||||
- { name: page_cache_request_policy }
|
||||
@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\basic_auth\Authentication\Provider;
|
||||
|
||||
use Drupal\Component\Render\FormattableMarkup;
|
||||
use Drupal\Core\Authentication\AuthenticationProviderInterface;
|
||||
use Drupal\Core\Authentication\AuthenticationProviderChallengeInterface;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Flood\FloodInterface;
|
||||
use Drupal\Core\Http\Exception\CacheableUnauthorizedHttpException;
|
||||
use Drupal\user\UserAuthenticationInterface;
|
||||
use Drupal\user\UserAuthInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
|
||||
/**
|
||||
* HTTP Basic authentication provider.
|
||||
*/
|
||||
class BasicAuth implements AuthenticationProviderInterface, AuthenticationProviderChallengeInterface {
|
||||
|
||||
/**
|
||||
* The config factory.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* The user auth service.
|
||||
*
|
||||
* @var \Drupal\user\UserAuthInterface|\Drupal\user\UserAuthenticationInterface
|
||||
*/
|
||||
protected $userAuth;
|
||||
|
||||
/**
|
||||
* The flood service.
|
||||
*
|
||||
* @var \Drupal\Core\Flood\FloodInterface
|
||||
*/
|
||||
protected $flood;
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs a HTTP basic authentication provider object.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory.
|
||||
* @param \Drupal\user\UserAuthInterface|\Drupal\user\UserAuthenticationInterface $user_auth
|
||||
* The user authentication service.
|
||||
* @param \Drupal\Core\Flood\FloodInterface $flood
|
||||
* The flood service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, UserAuthInterface|UserAuthenticationInterface $user_auth, FloodInterface $flood, EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->configFactory = $config_factory;
|
||||
if (!$user_auth instanceof UserAuthenticationInterface) {
|
||||
@trigger_error('The $user_auth parameter implementing UserAuthInterface is deprecated in drupal:10.3.0 and will be removed in drupal:12.0.0. Implement UserAuthenticationInterface instead. See https://www.drupal.org/node/3411040');
|
||||
}
|
||||
$this->userAuth = $user_auth;
|
||||
$this->flood = $flood;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applies(Request $request) {
|
||||
$username = $request->headers->get('PHP_AUTH_USER');
|
||||
$password = $request->headers->get('PHP_AUTH_PW');
|
||||
return isset($username) && isset($password);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function authenticate(Request $request) {
|
||||
$flood_config = $this->configFactory->get('user.flood');
|
||||
$username = $request->headers->get('PHP_AUTH_USER');
|
||||
$password = $request->headers->get('PHP_AUTH_PW');
|
||||
// Flood protection: this is very similar to the user login form code.
|
||||
// @see \Drupal\user\Form\UserLoginForm::validateAuthentication()
|
||||
// Do not allow any login from the current user's IP if the limit has been
|
||||
// reached. Default is 50 failed attempts allowed in one hour. This is
|
||||
// independent of the per-user limit to catch attempts from one IP to log
|
||||
// in to many different user accounts. We have a reasonably high limit
|
||||
// since there may be only one apparent IP for all users at an institution.
|
||||
if ($this->flood->isAllowed('basic_auth.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
|
||||
$account = FALSE;
|
||||
if ($this->userAuth instanceof UserAuthenticationInterface) {
|
||||
$lookup = $this->userAuth->lookupAccount($username);
|
||||
if ($lookup && !$lookup->isBlocked()) {
|
||||
$account = $lookup;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$accounts = $this->entityTypeManager->getStorage('user')->loadByProperties(['name' => $username, 'status' => 1]);
|
||||
$account = reset($accounts);
|
||||
}
|
||||
if ($account) {
|
||||
if ($flood_config->get('uid_only')) {
|
||||
// Register flood events based on the uid only, so they apply for any
|
||||
// IP address. This is the most secure option.
|
||||
$identifier = $account->id();
|
||||
}
|
||||
else {
|
||||
// The default identifier is a combination of uid and IP address. This
|
||||
// is less secure but more resistant to denial-of-service attacks that
|
||||
// could lock out all users with public user names.
|
||||
$identifier = $account->id() . '-' . $request->getClientIP();
|
||||
}
|
||||
// Don't allow login if the limit for this user has been reached.
|
||||
// Default is to allow 5 failed attempts every 6 hours.
|
||||
if ($this->flood->isAllowed('basic_auth.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
|
||||
$uid = FALSE;
|
||||
if ($this->userAuth instanceof UserAuthenticationInterface) {
|
||||
$uid = $this->userAuth->authenticateAccount($account, $password) ? $account->id() : FALSE;
|
||||
}
|
||||
else {
|
||||
$uid = $this->userAuth->authenticate($username, $password);
|
||||
}
|
||||
if ($uid) {
|
||||
$this->flood->clear('basic_auth.failed_login_user', $identifier);
|
||||
return $account;
|
||||
}
|
||||
else {
|
||||
// Register a per-user failed login event.
|
||||
$this->flood->register('basic_auth.failed_login_user', $flood_config->get('user_window'), $identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Always register an IP-based failed login event.
|
||||
$this->flood->register('basic_auth.failed_login_ip', $flood_config->get('ip_window'));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function challengeException(Request $request, \Exception $previous) {
|
||||
$site_config = $this->configFactory->get('system.site');
|
||||
$site_name = $site_config->get('name');
|
||||
$challenge = new FormattableMarkup('Basic realm="@realm"', [
|
||||
'@realm' => !empty($site_name) ? $site_name : 'Access restricted',
|
||||
]);
|
||||
|
||||
// A 403 is converted to a 401 here, but it doesn't matter what the
|
||||
// cacheability was of the 403 exception: what matters here is that
|
||||
// authentication credentials are missing, i.e. this request was made
|
||||
// as an anonymous user.
|
||||
// Therefore, the following actions will be taken:
|
||||
// 1. Verify whether the current user has the 'anonymous' role or not. This
|
||||
// works fine because:
|
||||
// - Thanks to \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests,
|
||||
// Page Cache never caches a response whose request has Basic Auth
|
||||
// credentials.
|
||||
// - Dynamic Page Cache will cache a different result for when the
|
||||
// request is unauthenticated (this 401) versus authenticated (some
|
||||
// other response)
|
||||
// 2. Have the 'config:user.role.anonymous' cache tag, because the only
|
||||
// reason this 401 would no longer be a 401 is if permissions for the
|
||||
// 'anonymous' role change, causing the cache tag to be invalidated.
|
||||
// @see \Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
|
||||
// @see \Drupal\Core\EventSubscriber\ClientErrorResponseSubscriber()
|
||||
// @see \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onAllResponds()
|
||||
$cacheability = CacheableMetadata::createFromObject($site_config)
|
||||
->addCacheTags(['config:user.role.anonymous'])
|
||||
->addCacheContexts(['user.roles:anonymous']);
|
||||
return $request->isMethodCacheable()
|
||||
? new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous)
|
||||
: new UnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
|
||||
}
|
||||
|
||||
}
|
||||
37
web/core/modules/basic_auth/src/Hook/BasicAuthHooks.php
Normal file
37
web/core/modules/basic_auth/src/Hook/BasicAuthHooks.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\basic_auth\Hook;
|
||||
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
|
||||
/**
|
||||
* Hook implementations for basic_auth.
|
||||
*/
|
||||
class BasicAuthHooks {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
#[Hook('help')]
|
||||
public function help($route_name, RouteMatchInterface $route_match): ?string {
|
||||
switch ($route_name) {
|
||||
case 'help.page.basic_auth':
|
||||
$output = '';
|
||||
$output .= '<h2>' . $this->t('About') . '</h2>';
|
||||
$output .= '<p>' . $this->t('The HTTP Basic Authentication module supplies an <a href="http://en.wikipedia.org/wiki/Basic_access_authentication">HTTP Basic authentication</a> provider for web service requests. This authentication provider authenticates requests using the HTTP Basic Authentication username and password, as an alternative to using Drupal\'s standard cookie-based authentication system. It is only useful if your site provides web services configured to use this type of authentication (for instance, the <a href=":rest_help">RESTful Web Services module</a>). For more information, see the <a href=":hba_do">online documentation for the HTTP Basic Authentication module</a>.', [
|
||||
':hba_do' => 'https://www.drupal.org/documentation/modules/basic_auth',
|
||||
':rest_help' => \Drupal::moduleHandler()->moduleExists('rest') ? Url::fromRoute('help.page', [
|
||||
'name' => 'rest',
|
||||
])->toString() : '#',
|
||||
]) . '</p>';
|
||||
return $output;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\basic_auth\PageCache;
|
||||
|
||||
use Drupal\Core\PageCache\RequestPolicyInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Cache policy for pages served from basic auth.
|
||||
*
|
||||
* This policy disallows caching of requests that use basic_auth for security
|
||||
* reasons. Otherwise responses for authenticated requests can get into the
|
||||
* page cache and could be delivered to unprivileged users.
|
||||
*/
|
||||
class DisallowBasicAuthRequests implements RequestPolicyInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function check(Request $request) {
|
||||
$username = $request->headers->get('PHP_AUTH_USER');
|
||||
$password = $request->headers->get('PHP_AUTH_PW');
|
||||
if (isset($username) && isset($password)) {
|
||||
return self::DENY;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
name: 'HTTP Basic Authentication test'
|
||||
type: module
|
||||
description: 'Support module for HTTP Basic Authentication testing.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
@ -0,0 +1,16 @@
|
||||
basic_auth_test.state.modify:
|
||||
path: '/basic_auth_test/state/modify'
|
||||
defaults:
|
||||
_controller: '\Drupal\basic_auth_test\BasicAuthTestController::modifyState'
|
||||
options:
|
||||
_auth:
|
||||
- basic_auth
|
||||
requirements:
|
||||
_user_is_logged_in: 'TRUE'
|
||||
|
||||
basic_auth_test.state.read:
|
||||
path: '/basic_auth_test/state/read'
|
||||
defaults:
|
||||
_controller: '\Drupal\basic_auth_test\BasicAuthTestController::readState'
|
||||
requirements:
|
||||
_access: 'TRUE'
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\basic_auth_test;
|
||||
|
||||
/**
|
||||
* Provides routes for HTTP Basic Authentication testing.
|
||||
*/
|
||||
class BasicAuthTestController {
|
||||
|
||||
/**
|
||||
* @see \Drupal\basic_auth\Tests\Authentication\BasicAuthTest::testControllerNotCalledBeforeAuth()
|
||||
*/
|
||||
public function modifyState() {
|
||||
\Drupal::state()->set('basic_auth_test.state.controller_executed', TRUE);
|
||||
return ['#markup' => 'Done'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @see \Drupal\basic_auth\Tests\Authentication\BasicAuthTest::testControllerNotCalledBeforeAuth()
|
||||
*/
|
||||
public function readState() {
|
||||
// Mark this page as being uncacheable.
|
||||
\Drupal::service('page_cache_kill_switch')->trigger();
|
||||
|
||||
return [
|
||||
'#markup' => \Drupal::state()->get('basic_auth_test.state.controller_executed') ? 'yep' : 'nope',
|
||||
'#cache' => [
|
||||
'max-age' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\basic_auth\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Tests\basic_auth\Traits\BasicAuthTestTrait;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
|
||||
/**
|
||||
* Tests for BasicAuth authentication provider.
|
||||
*
|
||||
* @group basic_auth
|
||||
*/
|
||||
class BasicAuthTest extends BrowserTestBase {
|
||||
|
||||
use BasicAuthTestTrait;
|
||||
|
||||
/**
|
||||
* Modules installed for all tests.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $modules = [
|
||||
'basic_auth',
|
||||
'router_test',
|
||||
'locale',
|
||||
'basic_auth_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Tests http basic authentication.
|
||||
*/
|
||||
public function testBasicAuth(): void {
|
||||
// Enable page caching.
|
||||
$config = $this->config('system.performance');
|
||||
$config->set('cache.page.max_age', 300);
|
||||
$config->save();
|
||||
|
||||
$account = $this->drupalCreateUser();
|
||||
$url = Url::fromRoute('router_test.11');
|
||||
|
||||
// Ensure we can log in with valid authentication details.
|
||||
$this->basicAuthGet($url, $account->getAccountName(), $account->pass_raw);
|
||||
$this->assertSession()->pageTextContains($account->getAccountName());
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->mink->resetSessions();
|
||||
$this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)');
|
||||
// Check that Cache-Control is not set to public.
|
||||
$this->assertSession()->responseHeaderNotContains('Cache-Control', 'public');
|
||||
|
||||
// Ensure that invalid authentication details give access denied.
|
||||
$this->basicAuthGet($url, $account->getAccountName(), $this->randomMachineName());
|
||||
$this->assertSession()->pageTextNotContains($account->getAccountName());
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
$this->mink->resetSessions();
|
||||
|
||||
// Ensure that the user is prompted to authenticate if they are not yet
|
||||
// authenticated and the route only allows basic auth.
|
||||
$this->drupalGet($url);
|
||||
$this->assertSession()->responseHeaderEquals('WWW-Authenticate', 'Basic realm="' . \Drupal::config('system.site')->get('name') . '"');
|
||||
$this->assertSession()->statusCodeEquals(401);
|
||||
|
||||
// Ensure that a route without basic auth defined doesn't prompt for auth.
|
||||
$this->drupalGet('admin');
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
|
||||
$account = $this->drupalCreateUser(['access administration pages']);
|
||||
|
||||
// Ensure that a route without basic auth defined doesn't allow login.
|
||||
$this->basicAuthGet(Url::fromRoute('system.admin'), $account->getAccountName(), $account->pass_raw);
|
||||
$this->assertSession()->linkNotExists('Log out', 'User is not logged in');
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
$this->mink->resetSessions();
|
||||
|
||||
// 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->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS');
|
||||
$this->basicAuthGet($url, $account->getAccountName(), $account->pass_raw);
|
||||
$this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'UNCACHEABLE (request policy)');
|
||||
// Check that Cache-Control is not set to public.
|
||||
$this->assertSession()->responseHeaderNotContains('Cache-Control', 'public');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the global login flood control.
|
||||
*/
|
||||
public function testGlobalLoginFloodControl(): void {
|
||||
$this->config('user.flood')
|
||||
->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();
|
||||
|
||||
$user = $this->drupalCreateUser([]);
|
||||
$incorrect_user = clone $user;
|
||||
$incorrect_user->pass_raw .= 'incorrect';
|
||||
$url = Url::fromRoute('router_test.11');
|
||||
|
||||
// Try 2 failed logins.
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$this->basicAuthGet($url, $incorrect_user->getAccountName(), $incorrect_user->pass_raw);
|
||||
}
|
||||
|
||||
// IP limit has reached to its limit. Even valid user credentials will fail.
|
||||
$this->basicAuthGet($url, $user->getAccountName(), $user->pass_raw);
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the per-user login flood control.
|
||||
*/
|
||||
public function testPerUserLoginFloodControl(): void {
|
||||
$this->config('user.flood')
|
||||
// Set a high global limit out so that it is not relevant in the test.
|
||||
->set('ip_limit', 4000)
|
||||
->set('user_limit', 2)
|
||||
->save();
|
||||
|
||||
$user = $this->drupalCreateUser([]);
|
||||
$incorrect_user = clone $user;
|
||||
$incorrect_user->pass_raw .= 'incorrect';
|
||||
$user2 = $this->drupalCreateUser([]);
|
||||
$url = Url::fromRoute('router_test.11');
|
||||
|
||||
// Try a failed login.
|
||||
$this->basicAuthGet($url, $incorrect_user->getAccountName(), $incorrect_user->pass_raw);
|
||||
|
||||
// A successful login will reset the per-user flood control count.
|
||||
$this->basicAuthGet($url, $user->getAccountName(), $user->pass_raw);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Try 2 failed logins for a user. They will trigger flood control.
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$this->basicAuthGet($url, $incorrect_user->getAccountName(), $incorrect_user->pass_raw);
|
||||
}
|
||||
|
||||
// Now the user account is blocked.
|
||||
$this->basicAuthGet($url, $user->getAccountName(), $user->pass_raw);
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
|
||||
// Try one successful attempt for a different user, it should not trigger
|
||||
// any flood control.
|
||||
$this->basicAuthGet($url, $user2->getAccountName(), $user2->pass_raw);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests compatibility with locale/UI translation.
|
||||
*/
|
||||
public function testLocale(): void {
|
||||
ConfigurableLanguage::createFromLangcode('de')->save();
|
||||
$this->config('system.site')->set('default_langcode', 'de')->save();
|
||||
|
||||
$account = $this->drupalCreateUser();
|
||||
$url = Url::fromRoute('router_test.11');
|
||||
|
||||
$this->basicAuthGet($url, $account->getAccountName(), $account->pass_raw);
|
||||
$this->assertSession()->pageTextContains($account->getAccountName());
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a comprehensive message is displayed when the route is denied.
|
||||
*/
|
||||
public function testUnauthorizedErrorMessage(): void {
|
||||
$account = $this->drupalCreateUser();
|
||||
$url = Url::fromRoute('router_test.11');
|
||||
|
||||
// Case when no credentials are passed, a user friendly access
|
||||
// unauthorized message is displayed.
|
||||
$this->drupalGet($url);
|
||||
$this->assertSession()->statusCodeEquals(401);
|
||||
$this->assertSession()->pageTextNotContains('Exception');
|
||||
$this->assertSession()->pageTextContains('Log in to access this page.');
|
||||
|
||||
// Case when empty credentials are passed, a user friendly access denied
|
||||
// message is displayed.
|
||||
$this->basicAuthGet($url, NULL, NULL);
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Case when wrong credentials are passed, a user friendly access denied
|
||||
// message is displayed.
|
||||
$this->basicAuthGet($url, $account->getAccountName(), $this->randomMachineName());
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Case when correct credentials but hasn't access to the route, an user
|
||||
// friendly access denied message is displayed.
|
||||
$url = Url::fromRoute('router_test.15');
|
||||
$this->basicAuthGet($url, $account->getAccountName(), $account->pass_raw);
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the cacheability of the Basic Auth 401 response.
|
||||
*
|
||||
* @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
|
||||
*/
|
||||
public function testCacheabilityOf401Response(): void {
|
||||
$url = Url::fromRoute('router_test.11');
|
||||
|
||||
$assert_response_cacheability = function ($expected_page_cache_header_value, $expected_dynamic_page_cache_header_value) use ($url) {
|
||||
$this->drupalGet($url);
|
||||
$this->assertSession()->statusCodeEquals(401);
|
||||
$this->assertSession()->responseHeaderEquals('X-Drupal-Cache', $expected_page_cache_header_value);
|
||||
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', $expected_dynamic_page_cache_header_value);
|
||||
};
|
||||
|
||||
// 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), ['access content']);
|
||||
$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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the controller is called before authentication.
|
||||
*
|
||||
* @see https://www.drupal.org/node/2817727
|
||||
*/
|
||||
public function testControllerNotCalledBeforeAuth(): void {
|
||||
$this->drupalGet('/basic_auth_test/state/modify');
|
||||
$this->assertSession()->statusCodeEquals(401);
|
||||
$this->drupalGet('/basic_auth_test/state/read');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->pageTextContains('nope');
|
||||
|
||||
$account = $this->drupalCreateUser();
|
||||
$this->basicAuthGet('/basic_auth_test/state/modify', $account->getAccountName(), $account->pass_raw);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->pageTextContains('Done');
|
||||
|
||||
$this->mink->resetSessions();
|
||||
$this->drupalGet('/basic_auth_test/state/read');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->pageTextContains('yep');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\basic_auth\Functional;
|
||||
|
||||
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
|
||||
|
||||
/**
|
||||
* Generic module test for basic_auth.
|
||||
*
|
||||
* @group basic_auth
|
||||
*/
|
||||
class GenericTest extends GenericModuleTestBase {}
|
||||
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\basic_auth\Traits;
|
||||
|
||||
/**
|
||||
* Provides common functionality for Basic Authentication test classes.
|
||||
*/
|
||||
trait BasicAuthTestTrait {
|
||||
|
||||
/**
|
||||
* Retrieves a Drupal path or an absolute path using basic authentication.
|
||||
*
|
||||
* @param \Drupal\Core\Url|string $path
|
||||
* Drupal path or URL to load into the internal browser.
|
||||
* @param string $username
|
||||
* The username to use for basic authentication.
|
||||
* @param string $password
|
||||
* The password to use for basic authentication.
|
||||
* @param array $options
|
||||
* (optional) Options to be forwarded to the URL generator.
|
||||
*
|
||||
* @return string
|
||||
* The retrieved HTML string, also available as $this->getRawContent().
|
||||
*/
|
||||
protected function basicAuthGet($path, $username, $password, array $options = []) {
|
||||
return $this->drupalGet($path, $options, $this->getBasicAuthHeaders($username, $password));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTTP headers that can be used for basic authentication in Curl.
|
||||
*
|
||||
* @param string $username
|
||||
* The username to use for basic authentication.
|
||||
* @param string $password
|
||||
* The password to use for basic authentication.
|
||||
*
|
||||
* @return array
|
||||
* An array of raw request headers as used by curl_setopt().
|
||||
*/
|
||||
protected function getBasicAuthHeaders($username, $password): array {
|
||||
// Set up Curl to use basic authentication with the test user's credentials.
|
||||
return ['Authorization' => 'Basic ' . base64_encode("$username:$password")];
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user