From 166a3f6ace5ad56473b00cf0cd1c499a654a9bd2 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Sat, 29 Mar 2014 15:08:21 +0100
Subject: [PATCH] Issue #2160021 by juampy, damiankloip, klausi, vijaycs85:
 Basic auth has no login flood control.

---
 core/core.services.yml                        |  4 +-
 core/lib/Drupal/Core/Session/AccountProxy.php | 16 +----
 .../basic_auth/basic_auth.services.yml        |  2 +-
 .../Authentication/Provider/BasicAuth.php     | 70 +++++++++++++++++--
 .../Tests/Authentication/BasicAuthTest.php    | 64 ++++++++++++++++-
 5 files changed, 132 insertions(+), 24 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index 51ce4c4ca271..18e37f98ebea 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -715,9 +715,7 @@ services:
     arguments: ['@authentication']
   current_user:
     class: Drupal\Core\Session\AccountProxy
-    arguments: ['@authentication']
-    calls:
-      - [setRequest, ['@?request=']]
+    arguments: ['@authentication', '@request']
   asset.css.collection_renderer:
     class: Drupal\Core\Asset\CssCollectionRenderer
     arguments: [ '@state' ]
diff --git a/core/lib/Drupal/Core/Session/AccountProxy.php b/core/lib/Drupal/Core/Session/AccountProxy.php
index c3f10adda895..9f002180bcbc 100644
--- a/core/lib/Drupal/Core/Session/AccountProxy.php
+++ b/core/lib/Drupal/Core/Session/AccountProxy.php
@@ -49,22 +49,12 @@ class AccountProxy implements AccountProxyInterface {
    *
    * @param \Drupal\Core\Authentication\AuthenticationManagerInterface $authentication_manager
    *   The authentication manager.
-   */
-  public function __construct(AuthenticationManagerInterface $authentication_manager) {
-    $this->authenticationManager = $authentication_manager;
-  }
-
-  /**
-   * Sets the current request.
-   *
    * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The current request.
+   *   The request object used for authenticating.
    */
-  public function setRequest(Request $request) {
+  public function __construct(AuthenticationManagerInterface $authentication_manager, Request $request) {
+    $this->authenticationManager = $authentication_manager;
     $this->request = $request;
-    // Reset the current user to ensure that new calls will return the correct
-    // user based on the request.
-    $this->account = NULL;
   }
 
   /**
diff --git a/core/modules/basic_auth/basic_auth.services.yml b/core/modules/basic_auth/basic_auth.services.yml
index 4c7a4493d2a9..a0d436866a66 100644
--- a/core/modules/basic_auth/basic_auth.services.yml
+++ b/core/modules/basic_auth/basic_auth.services.yml
@@ -1,6 +1,6 @@
 services:
   authentication.basic_auth:
     class: Drupal\basic_auth\Authentication\Provider\BasicAuth
-    arguments: ['@config.factory', '@user.auth']
+    arguments: ['@config.factory', '@user.auth', '@flood', '@entity.manager']
     tags:
       - { name: authentication_provider, priority: 100 }
diff --git a/core/modules/basic_auth/lib/Drupal/basic_auth/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/lib/Drupal/basic_auth/Authentication/Provider/BasicAuth.php
index 579d8a0236f1..660fff16c568 100644
--- a/core/modules/basic_auth/lib/Drupal/basic_auth/Authentication/Provider/BasicAuth.php
+++ b/core/modules/basic_auth/lib/Drupal/basic_auth/Authentication/Provider/BasicAuth.php
@@ -9,8 +9,9 @@
 
 use \Drupal\Component\Utility\String;
 use Drupal\Core\Authentication\AuthenticationProviderInterface;
-use Drupal\Core\Config\Config;
 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\Event\GetResponseForExceptionEvent;
@@ -36,15 +37,37 @@ class BasicAuth implements AuthenticationProviderInterface {
    */
   protected $userAuth;
 
+  /**
+   * The flood service.
+   *
+   * @var \Drupal\Core\Flood\FloodInterface
+   */
+  protected $flood;
+
+  /**
+   * The user storage.
+   *
+   * @var \Drupal\user\UserStorageInterface
+   */
+  protected $userStorage;
+
   /**
    * 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) {
+  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->userStorage = $entity_manager->getStorage('user');
   }
 
   /**
@@ -60,12 +83,48 @@ public function applies(Request $request) {
    * {@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');
-    $uid = $this->userAuth->authenticate($username, $password);
-    if ($uid) {
-      return user_load($uid);
+    // 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->userStorage->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->userStorage->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 NULL;
   }
 
@@ -92,4 +151,5 @@ public function handleException(GetResponseForExceptionEvent $event) {
     }
     return FALSE;
   }
+
 }
diff --git a/core/modules/basic_auth/lib/Drupal/basic_auth/Tests/Authentication/BasicAuthTest.php b/core/modules/basic_auth/lib/Drupal/basic_auth/Tests/Authentication/BasicAuthTest.php
index b8f580556e1a..134ddb2a7aa4 100644
--- a/core/modules/basic_auth/lib/Drupal/basic_auth/Tests/Authentication/BasicAuthTest.php
+++ b/core/modules/basic_auth/lib/Drupal/basic_auth/Tests/Authentication/BasicAuthTest.php
@@ -7,9 +7,7 @@
 
 namespace Drupal\basic_auth\Tests\Authentication;
 
-use Drupal\Core\Authentication\Provider\BasicAuth;
 use Drupal\simpletest\WebTestBase;
-use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Test for http basic authentication.
@@ -61,6 +59,67 @@ public function testBasicAuth() {
     $this->curlClose();
   }
 
+  /**
+   * Test the global login flood control.
+   */
+  function testGlobalLoginFloodControl() {
+    \Drupal::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';
+
+    // Try 2 failed logins.
+    for ($i = 0; $i < 2; $i++) {
+      $this->basicAuthGet('router_test/test11', $incorrect_user->getUsername(), $incorrect_user->pass_raw);
+    }
+
+    // IP limit has reached to its limit. Even valid user credentials will fail.
+    $this->basicAuthGet('router_test/test11', $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() {
+    \Drupal::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());
+
+    // Try a failed login.
+    $this->basicAuthGet('router_test/test11', $incorrect_user->getUsername(), $incorrect_user->pass_raw);
+
+    // A successful login will reset the per-user flood control count.
+    $this->basicAuthGet('router_test/test11', $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('router_test/test11', $incorrect_user->getUsername(), $incorrect_user->pass_raw);
+    }
+
+    // Now the user account is blocked.
+    $this->basicAuthGet('router_test/test11', $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('router_test/test11', $user2->getUsername(), $user2->pass_raw);
+    $this->assertResponse('200', 'Per user flood prevention does not block access for other users.');
+  }
+
   /**
    * Does HTTP basic auth request.
    *
@@ -93,4 +152,5 @@ protected function basicAuthGet($path, $username, $password) {
 
     return $out;
   }
+
 }
-- 
GitLab