Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,141 @@
<?php
/**
* @file
* Contains \Drupal\basic_auth\Authentication\Provider\BasicAuth.
*/
namespace Drupal\basic_auth\Authentication\Provider;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\Core\Authentication\AuthenticationProviderChallengeInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Flood\FloodInterface;
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
*/
protected $userAuth;
/**
* The flood service.
*
* @var \Drupal\Core\Flood\FloodInterface
*/
protected $flood;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a HTTP basic authentication provider object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\user\UserAuthInterface $user_auth
* The user authentication service.
* @param \Drupal\Core\Flood\FloodInterface $flood
* The flood service.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
*/
public function __construct(ConfigFactoryInterface $config_factory, UserAuthInterface $user_auth, FloodInterface $flood, EntityManagerInterface $entity_manager) {
$this->configFactory = $config_factory;
$this->userAuth = $user_auth;
$this->flood = $flood;
$this->entityManager = $entity_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'))) {
$accounts = $this->entityManager->getStorage('user')->loadByProperties(array('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 = $this->userAuth->authenticate($username, $password);
if ($uid) {
$this->flood->clear('basic_auth.failed_login_user', $identifier);
return $this->entityManager->getStorage('user')->load($uid);
}
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 [];
}
/**
* {@inheritdoc}
*/
public function challengeException(Request $request, \Exception $previous) {
$site_name = $this->configFactory->get('system.site')->get('name');
$challenge = SafeMarkup::format('Basic realm="@realm"', array(
'@realm' => !empty($site_name) ? $site_name : 'Access restricted',
));
return new UnauthorizedHttpException($challenge, 'No authentication credentials provided.', $previous);
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\basic_auth\PageCache\DisallowBasicAuthRequests.
*/
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;
}
}
}

View file

@ -0,0 +1,183 @@
<?php
/**
* @file
* Contains \Drupal\basic_auth\Tests\Authentication\BasicAuthTest.
*/
namespace Drupal\basic_auth\Tests\Authentication;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Url;
use Drupal\basic_auth\Tests\BasicAuthTestTrait;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
/**
* Tests for BasicAuth authentication provider.
*
* @group basic_auth
*/
class BasicAuthTest extends WebTestBase {
use BasicAuthTestTrait;
/**
* Modules installed for all tests.
*
* @var array
*/
public static $modules = array('basic_auth', 'router_test', 'locale');
/**
* Test http basic authentication.
*/
public function testBasicAuth() {
// 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');
$this->basicAuthGet($url, $account->getUsername(), $account->pass_raw);
$this->assertText($account->getUsername(), 'Account name is displayed.');
$this->assertResponse('200', 'HTTP response is OK');
$this->curlClose();
$this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'));
$this->assertIdentical(strpos($this->drupalGetHeader('Cache-Control'), 'public'), FALSE, 'Cache-Control is not set to public');
$this->basicAuthGet($url, $account->getUsername(), $this->randomMachineName());
$this->assertNoText($account->getUsername(), 'Bad basic auth credentials do not authenticate the user.');
$this->assertResponse('403', 'Access is not granted.');
$this->curlClose();
$this->drupalGet($url);
$this->assertEqual($this->drupalGetHeader('WWW-Authenticate'), SafeMarkup::format('Basic realm="@realm"', ['@realm' => \Drupal::config('system.site')->get('name')]));
$this->assertResponse('401', 'Not authenticated on the route that allows only basic_auth. Prompt to authenticate received.');
$this->drupalGet('admin');
$this->assertResponse('403', 'No authentication prompt for routes not explicitly defining authentication providers.');
$account = $this->drupalCreateUser(array('access administration pages'));
$this->basicAuthGet(Url::fromRoute('system.admin'), $account->getUsername(), $account->pass_raw);
$this->assertNoLink('Log out', 'User is not logged in');
$this->assertResponse('403', 'No basic authentication for routes not explicitly defining authentication providers.');
$this->curlClose();
// 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.');
}
/**
* Test the global login flood control.
*/
function testGlobalLoginFloodControl() {
$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(array());
$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->getUsername(), $incorrect_user->pass_raw);
}
// IP limit has reached to its limit. Even valid user credentials will fail.
$this->basicAuthGet($url, $user->getUsername(), $user->pass_raw);
$this->assertResponse('403', 'Access is blocked because of IP based flood prevention.');
}
/**
* Test the per-user login flood control.
*/
function testPerUserLoginFloodControl() {
$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(array());
$incorrect_user = clone $user;
$incorrect_user->pass_raw .= 'incorrect';
$user2 = $this->drupalCreateUser(array());
$url = Url::fromRoute('router_test.11');
// Try a failed login.
$this->basicAuthGet($url, $incorrect_user->getUsername(), $incorrect_user->pass_raw);
// A successful login will reset the per-user flood control count.
$this->basicAuthGet($url, $user->getUsername(), $user->pass_raw);
$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++) {
$this->basicAuthGet($url, $incorrect_user->getUsername(), $incorrect_user->pass_raw);
}
// Now the user account is blocked.
$this->basicAuthGet($url, $user->getUsername(), $user->pass_raw);
$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.
$this->basicAuthGet($url, $user2->getUsername(), $user2->pass_raw);
$this->assertResponse('200', 'Per user flood prevention does not block access for other users.');
}
/**
* Tests compatibility with locale/UI translation.
*/
function testLocale() {
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->getUsername(), $account->pass_raw);
$this->assertText($account->getUsername(), 'Account name is displayed.');
$this->assertResponse('200', 'HTTP response is OK');
$this->curlClose();
}
/**
* Tests if a comprehensive message is displayed when the route is denied.
*/
function testUnauthorizedErrorMessage() {
$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");
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* @file
* Contains \Drupal\basic_auth\Tests\BasicAuthTestTrait.
*/
namespace Drupal\basic_auth\Tests;
/**
* 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));
}
/**
* Executes a form submission using basic authentication.
*
* @param string $path
* Location of the post form.
* @param array $edit
* Field data in an associative array.
* @param string $submit
* Value of the submit button whose click is to be emulated.
* @param string $username
* The username to use for basic authentication.
* @param string $password
* The password to use for basic authentication.
* @param array $options
* Options to be forwarded to the url generator.
* @param string $form_html_id
* (optional) HTML ID of the form to be submitted.
* @param string $extra_post
* (optional) A string of additional data to append to the POST submission.
*
* @return string
* The retrieved HTML string.
*
* @see \Drupal\simpletest\WebTestBase::drupalPostForm()
*/
protected function basicAuthPostForm($path, $edit, $submit, $username, $password, array $options = array(), $form_html_id = NULL, $extra_post = NULL) {
return $this->drupalPostForm($path, $edit, $submit, $options, $this->getBasicAuthHeaders($username, $password), $form_html_id, $extra_post);
}
/**
* 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) {
// Set up Curl to use basic authentication with the test user's credentials.
return ['Authorization: Basic ' . base64_encode("$username:$password")];
}
}