diff --git a/core/modules/basic_auth/basic_auth.services.yml b/core/modules/basic_auth/basic_auth.services.yml
index be85ccc..d985d0f 100644
--- a/core/modules/basic_auth/basic_auth.services.yml
+++ b/core/modules/basic_auth/basic_auth.services.yml
@@ -1,7 +1,7 @@
 services:
   basic_auth.authentication.basic_auth:
     class: Drupal\basic_auth\Authentication\Provider\BasicAuth
-    arguments: ['@config.factory', '@user.auth', '@flood', '@entity.manager']
+    arguments: ['@user.auth_flood', '@config.factory']
     tags:
       - { name: authentication_provider, provider_id: 'basic_auth', priority: 100 }
   basic_auth.page_cache_request_policy.disallow_basic_auth_requests:
diff --git a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
index eac482c..1bb1e54 100644
--- a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
+++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php
@@ -3,12 +3,10 @@
 namespace Drupal\basic_auth\Authentication\Provider;
 
 use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Core\Authentication\AuthenticationProviderInterface;
 use Drupal\Core\Authentication\AuthenticationProviderChallengeInterface;
+use Drupal\Core\Authentication\AuthenticationProviderInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Entity\EntityManagerInterface;
-use Drupal\Core\Flood\FloodInterface;
-use Drupal\user\UserAuthInterface;
+use Drupal\user\Authentication\UserAuthFloodInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
 
@@ -18,50 +16,27 @@
 class BasicAuth implements AuthenticationProviderInterface, AuthenticationProviderChallengeInterface {
 
   /**
-   * The config factory.
-   *
-   * @var \Drupal\Core\Config\ConfigFactoryInterface
-   */
-  protected $configFactory;
-
-  /**
-   * The user auth service.
+   * The user authentication and flood control service.
    *
-   * @var \Drupal\user\UserAuthInterface
+   * @var \Drupal\user\Authentication\UserAuthFloodInterface
    */
-  protected $userAuth;
+  protected $userAuthFlood;
 
   /**
-   * The flood service.
-   *
-   * @var \Drupal\Core\Flood\FloodInterface
-   */
-  protected $flood;
-
-  /**
-   * The entity manager.
+   * The config factory.
    *
-   * @var \Drupal\Core\Entity\EntityManagerInterface
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
    */
-  protected $entityManager;
+  protected $configFactory;
 
   /**
    * 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.
+   * @param $userAuthFlood
    */
-  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;
+  public function __construct(UserAuthFloodInterface $userAuthFlood, ConfigFactoryInterface $configFactory) {
+    $this->userAuthFlood = $userAuthFlood;
+    $this->configFactory = $configFactory;
   }
 
   /**
@@ -77,49 +52,13 @@ 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');
-    // 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);
-          }
-        }
-      }
+
+    $result = $this->userAuthFlood->authenticate($username, $password, 'basic_auth', $request);
+    if ($result->isSuccessful()) {
+      return $result->user;
     }
-    // Always register an IP-based failed login event.
-    $this->flood->register('basic_auth.failed_login_ip', $flood_config->get('ip_window'));
-    return [];
   }
 
   /**
diff --git a/core/modules/user/src/Authentication/AuthResult.php b/core/modules/user/src/Authentication/AuthResult.php
new file mode 100644
index 0000000..8719a5b
--- /dev/null
+++ b/core/modules/user/src/Authentication/AuthResult.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Drupal\user\Authentication;
+
+use Drupal\Component\Render\MarkupInterface;
+use Drupal\user\UserInterface;
+
+/**
+ * Represents the result after an authentication attempt.
+ */
+class AuthResult {
+
+  const ERROR_USER_BLOCKED = 1;
+  const ERROR_INVALID_CREDENTIALS = 2;
+  const ERROR_FLOOD_USER = 3;
+  const ERROR_FLOOD_IP = 4;
+
+  /**
+   * TRUE if the authentication was successful.
+   *
+   * @var bool
+   */
+  protected $success = FALSE;
+
+  /**
+   * The loaded user if the authentication was successful.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $user;
+
+  /**
+   * The error code in case the authentication failed.
+   *
+   * One of the ERROR_* constants on this class.
+   *
+   * @var int
+   */
+  protected $errorCode;
+
+  /**
+   * The translated error message in case the authentication failed.
+   *
+   * @var \Drupal\Component\Render\MarkupInterface
+   */
+  protected $errorMessage;
+
+  /**
+   * Indicates if the authentication attempt was blocked by flood control.
+   *
+   * @var bool
+   */
+  protected $floodControlTriggered = FALSE;
+
+  /**
+   * Returns TRUE if the authentication was successful.
+   *
+   * @return bool
+   *   TRUE if the authentication was successful, FALSE otherwise.
+   */
+  public function isSuccessful() {
+    return $this->success;
+  }
+
+  /**
+   * Returns the loaded user if the authentication was successful.
+   *
+   * @return \Drupal\user\UserInterface|null
+   *   The user object or NULL if the authentication failed.
+   */
+  public function getUser() {
+    return $this->user;
+  }
+
+  /**
+   * Returns the error code in case the authentication failed.
+   *
+   * @return int|null
+   *   One of the ERROR_* constants on this class or NULL if the authentication
+   *   was successful.
+   */
+  public function getErrorCode() {
+    return $this->errorCode;
+  }
+
+  /**
+   * Returns the translated error message in case the authentication failed.
+   *
+   * @return \Drupal\Component\Render\MarkupInterface|null
+   *   The error message or NULL if the authentication was successful.
+   */
+  public function getErrorMessage() {
+    return $this->errorMessage;
+  }
+
+  /**
+   * Returns TRUE if the authentication attempt was blocked by flood control.
+   *
+   * @return bool
+   *   TRUE if the authentication attempt was blocked by flood control, FALSE
+   *   otherwise.
+   */
+  public function isFloodControlTriggered() {
+    return $this->floodControlTriggered;
+  }
+
+  /**
+   * Sets the authentication attempt to be successful.
+   *
+   * @param bool $success
+   *   TRUE if the authentication was successful, FALSE otherwise.
+   */
+  public function setSuccessful($success) {
+    $this->success = $success;
+  }
+
+  /**
+   * Sets the user object of the successful authentication attempt.
+   *
+   * @param \Drupal\user\UserInterface $user
+   *   The loaded user object.
+   */
+  public function setUser(UserInterface $user) {
+    $this->user = $user;
+  }
+
+  /**
+   * Sets the error code of a failed authentication attempt.
+   *
+   * @param int $errorCode
+   *   One of the ERROR_* constants on this class.
+   */
+  public function setErrorCode($errorCode) {
+    $this->errorCode = $errorCode;
+  }
+
+  /**
+   * Sets the error message of a failed authentication attempt.
+   *
+   * @param \Drupal\Component\Render\MarkupInterface $errorMessage
+   *   The translated error message.
+   */
+  public function setErrorMessage(MarkupInterface $errorMessage) {
+    $this->errorMessage = $errorMessage;
+  }
+
+  /**
+   * Sets the flood control to have triggered in failed authentication attempts.
+   *
+   * @param bool $floodControlTriggered
+   *   TRUE if the flood control has triggered, FALSE otherwise.
+   */
+  public function setFloodControlTriggered($floodControlTriggered) {
+    $this->floodControlTriggered = $floodControlTriggered;
+  }
+
+}
diff --git a/core/modules/user/src/Authentication/UserAuthFlood.php b/core/modules/user/src/Authentication/UserAuthFlood.php
new file mode 100644
index 0000000..fda638c
--- /dev/null
+++ b/core/modules/user/src/Authentication/UserAuthFlood.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Drupal\user\Authentication;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\user\UserAuthInterface;
+
+/**
+ * Authenticates credentials and applies flood protection.
+ *
+ * Also checks if a user is blocked.
+ */
+class UserAuthFlood implements UserAuthFloodInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * 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 type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
+   *   The config factory.
+   * @param \Drupal\user\UserAuthInterface $userAuth
+   *   The user authentication service.
+   * @param \Drupal\Core\Flood\FloodInterface $flood
+   *   The flood service.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity manager service.
+   */
+  public function __construct(ConfigFactoryInterface $configFactory, UserAuthInterface $userAuth, FloodInterface $flood, EntityTypeManagerInterface $entityTypeManager, TranslationInterface $stringTranslation) {
+    $this->configFactory = $configFactory;
+    $this->userAuth = $userAuth;
+    $this->flood = $flood;
+    $this->entityTypeManager = $entityTypeManager;
+    $this->stringTranslation = $stringTranslation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function authenticate($username, $password, $channel, $request) {
+    $auth_result = new AuthResult();
+    $flood_config = $this->configFactory->get('user.flood');
+
+    // 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("$channel.failed_login_ip", $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
+      $accounts = $this->entityTypeManager->getStorage('user')->loadByProperties(array('name' => $username));
+      $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();
+        }
+        if ($account->isBlocked()) {
+          $auth_result->setErrorCode(AuthResult::ERROR_USER_BLOCKED);
+          $auth_result->setErrorMessage($this->t('The username @name has not been activated or is blocked.', array('@name' => $username)));
+        }
+        // Don't allow login if the limit for this user has been reached.
+        // Default is to allow 5 failed attempts every 6 hours.
+        elseif ($this->flood->isAllowed("$channel.failed_login_user", $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
+          $uid = $this->userAuth->authenticate($username, $password);
+          if ($uid) {
+            // Clear past failures for this user so as not to block a user who
+            // might log in and out more than once in an hour.
+            $this->flood->clear("$channel.failed_login_user", $identifier);
+
+            $auth_result->setSuccessful(TRUE);
+            $auth_result->setUser($this->entityTypeManager->getStorage('user')->load($uid));
+
+            return $auth_result;
+          }
+          else {
+            // Register a per-user failed login event.
+            $this->flood->register("$channel.failed_login_user", $flood_config->get('user_window'), $identifier);
+
+            $auth_result->setErrorCode(AuthResult::ERROR_INVALID_CREDENTIALS);
+            $auth_result->setErrorMessage($this->t('Sorry, unrecognized username or password.'));
+          }
+        }
+        else {
+          $auth_result->setErrorCode(AuthResult::ERROR_FLOOD_USER);
+          $auth_result->setErrorMessage($this->t('There have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', [
+            '@count' => $flood_config->get('user_limit'),
+          ]));
+          $auth_result->setFloodControlTriggered(TRUE);
+        }
+      }
+      else {
+        $auth_result->setErrorCode(AuthResult::ERROR_INVALID_CREDENTIALS);
+        $auth_result->setErrorMessage($this->t('Sorry, unrecognized username or password.'));
+      }
+    }
+    else {
+      $auth_result->setErrorCode(AuthResult::ERROR_FLOOD_IP);
+      $auth_result->setErrorMessage($this->t('Too many failed login attempts from your IP address. This IP address is temporarily blocked.'));
+      $auth_result->setFloodControlTriggered(TRUE);
+    }
+
+    // Always register an IP-based failed login event.
+    $this->flood->register("$channel.failed_login_ip", $flood_config->get('ip_window'));
+
+    return $auth_result;
+  }
+
+}
diff --git a/core/modules/user/src/Authentication/UserAuthFloodInterface.php b/core/modules/user/src/Authentication/UserAuthFloodInterface.php
new file mode 100644
index 0000000..bdfa46b
--- /dev/null
+++ b/core/modules/user/src/Authentication/UserAuthFloodInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\user\Authentication;
+
+/**
+ * A service that authenticates credentials and applies flood control.
+ *
+ * A service implementing this interface must also check if the given user
+ * account is blocked.
+ */
+interface UserAuthFloodInterface {
+
+  /**
+   * Authenticates the given credentials and applies flood protection.
+   *
+   * @param string $username
+   *   The user name to authenticate.
+   * @param string $password
+   *   A plain-text password, such as trimmed text from form values.
+   * @param string $channel
+   *   The source of the authentication request. Example: 'user', 'http_login',
+   *   'basic_auth'.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request that performs the authentication.
+   *
+   * @return \Drupal\user\Authentication\AuthResult
+   *   An authentication result object providing status information.
+   */
+  public function authenticate($username, $password, $channel, $request);
+
+}
diff --git a/core/modules/user/src/Controller/UserAuthenticationController.php b/core/modules/user/src/Controller/UserAuthenticationController.php
index 3aff75f..f1fde5c 100644
--- a/core/modules/user/src/Controller/UserAuthenticationController.php
+++ b/core/modules/user/src/Controller/UserAuthenticationController.php
@@ -5,11 +5,9 @@
 use Drupal\Core\Access\CsrfTokenGenerator;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\Core\Flood\FloodInterface;
 use Drupal\Core\Routing\RouteProviderInterface;
-use Drupal\user\UserAuthInterface;
+use Drupal\user\Authentication\UserAuthFloodInterface;
 use Drupal\user\UserInterface;
-use Drupal\user\UserStorageInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -38,18 +36,11 @@ class UserAuthenticationController extends ControllerBase implements ContainerIn
   const LOGGED_OUT = 0;
 
   /**
-   * The flood controller.
+   * The user authentication and flood control service.
    *
-   * @var \Drupal\Core\Flood\FloodInterface
+   * @var \Drupal\user\Authentication\UserAuthFloodInterface
    */
-  protected $flood;
-
-  /**
-   * The user storage.
-   *
-   * @var \Drupal\user\UserStorageInterface
-   */
-  protected $userStorage;
+  protected $userAuthFlood;
 
   /**
    * The CSRF token generator.
@@ -59,13 +50,6 @@ class UserAuthenticationController extends ControllerBase implements ContainerIn
   protected $csrfToken;
 
   /**
-   * The user authentication.
-   *
-   * @var \Drupal\user\UserAuthInterface
-   */
-  protected $userAuth;
-
-  /**
    * The route provider.
    *
    * @var \Drupal\Core\Routing\RouteProviderInterface
@@ -89,14 +73,10 @@ class UserAuthenticationController extends ControllerBase implements ContainerIn
   /**
    * Constructs a new UserAuthenticationController object.
    *
-   * @param \Drupal\Core\Flood\FloodInterface $flood
-   *   The flood controller.
-   * @param \Drupal\user\UserStorageInterface $user_storage
-   *   The user storage.
+   * @param \Drupal\user\Authentication\UserAuthFloodInterface $userAuthFlood
+   *   The user authentication and flood control service.
    * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
    *   The CSRF token generator.
-   * @param \Drupal\user\UserAuthInterface $user_auth
-   *   The user authentication.
    * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
    *   The route provider.
    * @param \Symfony\Component\Serializer\Serializer $serializer
@@ -104,11 +84,9 @@ class UserAuthenticationController extends ControllerBase implements ContainerIn
    * @param array $serializer_formats
    *   The available serialization formats.
    */
-  public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats) {
-    $this->flood = $flood;
-    $this->userStorage = $user_storage;
+  public function __construct(UserAuthFloodInterface $userAuthFlood, CsrfTokenGenerator $csrf_token, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats) {
+    $this->userAuthFlood = $userAuthFlood;
     $this->csrfToken = $csrf_token;
-    $this->userAuth = $user_auth;
     $this->serializer = $serializer;
     $this->serializerFormats = $serializer_formats;
     $this->routeProvider = $route_provider;
@@ -129,10 +107,8 @@ public static function create(ContainerInterface $container) {
     }
 
     return new static(
-      $container->get('flood'),
-      $container->get('entity_type.manager')->getStorage('user'),
+      $container->get('user.auth_flood'),
       $container->get('csrf_token'),
-      $container->get('user.auth'),
       $container->get('router.route_provider'),
       $serializer,
       $formats
@@ -164,47 +140,38 @@ public function login(Request $request) {
       throw new BadRequestHttpException('Missing credentials.pass.');
     }
 
-    $this->floodControl($request, $credentials['name']);
-
-    if ($this->userIsBlocked($credentials['name'])) {
-      throw new BadRequestHttpException('The user has not been activated or is blocked.');
+    $auth_result = $this->userAuthFlood->authenticate($credentials['name'], $credentials['pass'], 'http_login', $request);
+    if (!$auth_result->isSuccessful()) {
+      if ($auth_result->isFloodControlTriggered()) {
+        throw new AccessDeniedHttpException($auth_result->getErrorMessage());
+      }
+      throw new BadRequestHttpException($auth_result->getErrorMessage());
     }
 
-    if ($uid = $this->userAuth->authenticate($credentials['name'], $credentials['pass'])) {
-      $this->flood->clear('user.http_login', $this->getLoginFloodIdentifier($request, $credentials['name']));
-      /** @var \Drupal\user\UserInterface $user */
-      $user = $this->userStorage->load($uid);
-      $this->userLoginFinalize($user);
+    $user = $auth_result->getUser();
 
-      // Send basic metadata about the logged in user.
-      $response_data = [];
-      if ($user->get('uid')->access('view', $user)) {
-        $response_data['current_user']['uid'] = $user->id();
-      }
-      if ($user->get('roles')->access('view', $user)) {
-        $response_data['current_user']['roles'] = $user->getRoles();
-      }
-      if ($user->get('name')->access('view', $user)) {
-        $response_data['current_user']['name'] = $user->getAccountName();
-      }
-      $response_data['csrf_token'] = $this->csrfToken->get('rest');
-
-      $logout_route = $this->routeProvider->getRouteByName('user.logout.http');
-      // Trim '/' off path to match \Drupal\Core\Access\CsrfAccessCheck.
-      $logout_path = ltrim($logout_route->getPath(), '/');
-      $response_data['logout_token'] = $this->csrfToken->get($logout_path);
+    $this->userLoginFinalize($user);
 
-      $encoded_response_data = $this->serializer->encode($response_data, $format);
-      return new Response($encoded_response_data);
+    // Send basic metadata about the logged in user.
+    $response_data = [];
+    if ($user->get('uid')->access('view', $user)) {
+      $response_data['current_user']['uid'] = $user->id();
     }
-
-    $flood_config = $this->config('user.flood');
-    if ($identifier = $this->getLoginFloodIdentifier($request, $credentials['name'])) {
-      $this->flood->register('user.http_login', $flood_config->get('user_window'), $identifier);
+    if ($user->get('roles')->access('view', $user)) {
+      $response_data['current_user']['roles'] = $user->getRoles();
+    }
+    if ($user->get('name')->access('view', $user)) {
+      $response_data['current_user']['name'] = $user->getAccountName();
     }
-    // Always register an IP-based failed login event.
-    $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window'));
-    throw new BadRequestHttpException('Sorry, unrecognized username or password.');
+    $response_data['csrf_token'] = $this->csrfToken->get('rest');
+
+    $logout_route = $this->routeProvider->getRouteByName('user.logout.http');
+    // Trim '/' off path to match \Drupal\Core\Access\CsrfAccessCheck.
+    $logout_path = ltrim($logout_route->getPath(), '/');
+    $response_data['logout_token'] = $this->csrfToken->get($logout_path);
+
+    $encoded_response_data = $this->serializer->encode($response_data, $format);
+    return new Response($encoded_response_data);
   }
 
   /**
@@ -282,64 +249,4 @@ protected function getRequestFormat(Request $request) {
     return $format;
   }
 
-  /**
-   * Enforces flood control for the current login request.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The current request.
-   * @param string $username
-   *   The user name sent for login credentials.
-   */
-  protected function floodControl(Request $request, $username) {
-    $flood_config = $this->config('user.flood');
-    if (!$this->flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
-      throw new AccessDeniedHttpException('Access is blocked because of IP based flood prevention.', NULL, Response::HTTP_TOO_MANY_REQUESTS);
-    }
-
-    if ($identifier = $this->getLoginFloodIdentifier($request, $username)) {
-      // 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('user.http_login', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
-        if ($flood_config->get('uid_only')) {
-          $error_message = sprintf('There have been more than %s failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', $flood_config->get('user_limit'));
-        }
-        else {
-          $error_message = 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.';
-        }
-        throw new AccessDeniedHttpException($error_message, NULL, Response::HTTP_TOO_MANY_REQUESTS);
-      }
-    }
-  }
-
-  /**
-   * Gets the login identifier for user login flood control.
-   *
-   * @param \Symfony\Component\HttpFoundation\Request $request
-   *   The current request.
-   * @param string $username
-   *   The username supplied in login credentials.
-   *
-   * @return string
-   *   The login identifier or if the user does not exist an empty string.
-   */
-  protected function getLoginFloodIdentifier(Request $request, $username) {
-    $flood_config = $this->config('user.flood');
-    $accounts = $this->userStorage->loadByProperties(['name' => $username, 'status' => 1]);
-    if ($account = reset($accounts)) {
-      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();
-      }
-      return $identifier;
-    }
-    return '';
-  }
-
 }
diff --git a/core/modules/user/src/Form/UserLoginForm.php b/core/modules/user/src/Form/UserLoginForm.php
index 43b29e5..560d400 100644
--- a/core/modules/user/src/Form/UserLoginForm.php
+++ b/core/modules/user/src/Form/UserLoginForm.php
@@ -2,11 +2,11 @@
 
 namespace Drupal\user\Form;
 
-use Drupal\Core\Flood\FloodInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\RendererInterface;
-use Drupal\user\UserAuthInterface;
+use Drupal\user\Authentication\AuthResult;
+use Drupal\user\Authentication\UserAuthFloodInterface;
 use Drupal\user\UserStorageInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -16,11 +16,11 @@
 class UserLoginForm extends FormBase {
 
   /**
-   * The flood service.
+   * The user authentication and flood control service.
    *
-   * @var \Drupal\Core\Flood\FloodInterface
+   * @var \Drupal\user\Authentication\UserAuthFloodInterface
    */
-  protected $flood;
+  protected $userAuthFlood;
 
   /**
    * The user storage.
@@ -30,13 +30,6 @@ class UserLoginForm extends FormBase {
   protected $userStorage;
 
   /**
-   * The user authentication object.
-   *
-   * @var \Drupal\user\UserAuthInterface
-   */
-  protected $userAuth;
-
-  /**
    * The renderer.
    *
    * @var \Drupal\Core\Render\RendererInterface
@@ -46,19 +39,16 @@ class UserLoginForm extends FormBase {
   /**
    * Constructs a new UserLoginForm.
    *
-   * @param \Drupal\Core\Flood\FloodInterface $flood
-   *   The flood service.
-   * @param \Drupal\user\UserStorageInterface $user_storage
+   * @param \Drupal\user\Authentication\UserAuthFloodInterface $userAuthFlood
+   *   The use authentication and flood control service.
+   * @param \Drupal\user\UserStorageInterface $userStorage
    *   The user storage.
-   * @param \Drupal\user\UserAuthInterface $user_auth
-   *   The user authentication object.
    * @param \Drupal\Core\Render\RendererInterface $renderer
    *   The renderer.
    */
-  public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, UserAuthInterface $user_auth, RendererInterface $renderer) {
-    $this->flood = $flood;
-    $this->userStorage = $user_storage;
-    $this->userAuth = $user_auth;
+  public function __construct(UserAuthFloodInterface $userAuthFlood, UserStorageInterface $userStorage, RendererInterface $renderer) {
+    $this->userAuthFlood = $userAuthFlood;
+    $this->userStorage = $userStorage;
     $this->renderer = $renderer;
   }
 
@@ -67,9 +57,8 @@ public function __construct(FloodInterface $flood, UserStorageInterface $user_st
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('flood'),
+      $container->get('user.auth_flood'),
       $container->get('entity.manager')->getStorage('user'),
-      $container->get('user.auth'),
       $container->get('renderer')
     );
   }
@@ -114,9 +103,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form['actions'] = array('#type' => 'actions');
     $form['actions']['submit'] = array('#type' => 'submit', '#value' => $this->t('Log in'));
 
-    $form['#validate'][] = '::validateName';
     $form['#validate'][] = '::validateAuthentication';
-    $form['#validate'][] = '::validateFinal';
 
     $this->renderer->addCacheableDependency($form, $config);
 
@@ -144,111 +131,63 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
   }
 
   /**
-   * Sets an error if supplied username has been blocked.
-   */
-  public function validateName(array &$form, FormStateInterface $form_state) {
-    if (!$form_state->isValueEmpty('name') && user_is_blocked($form_state->getValue('name'))) {
-      // Blocked in user administration.
-      $form_state->setErrorByName('name', $this->t('The username %name has not been activated or is blocked.', array('%name' => $form_state->getValue('name'))));
-    }
-  }
-
-  /**
-   * Checks supplied username/password against local users table.
+   * Checks supplied username/password against the authentication service.
    *
    * If successful, $form_state->get('uid') is set to the matching user ID.
    */
   public function validateAuthentication(array &$form, FormStateInterface $form_state) {
     $password = trim($form_state->getValue('pass'));
-    $flood_config = $this->config('user.flood');
     if (!$form_state->isValueEmpty('name') && strlen($password) > 0) {
-      // 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('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
-        $form_state->set('flood_control_triggered', 'ip');
-        return;
-      }
-      $accounts = $this->userStorage->loadByProperties(array('name' => $form_state->getValue('name'), '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() . '-' . $this->getRequest()->getClientIP();
-        }
-        $form_state->set('flood_control_user_identifier', $identifier);
-
-        // 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('user.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
-          $form_state->set('flood_control_triggered', 'user');
-          return;
-        }
-      }
-      // We are not limited by flood control, so try to authenticate.
-      // Store $uid in form state as a flag for self::validateFinal().
-      $uid = $this->userAuth->authenticate($form_state->getValue('name'), $password);
-      $form_state->set('uid', $uid);
-    }
-  }
 
-  /**
-   * Checks if user was not authenticated, or if too many logins were attempted.
-   *
-   * This validation function should always be the last one.
-   */
-  public function validateFinal(array &$form, FormStateInterface $form_state) {
-    $flood_config = $this->config('user.flood');
-    if (!$form_state->get('uid')) {
-      // Always register an IP-based failed login event.
-      $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window'));
-      // Register a per-user failed login event.
-      if ($flood_control_user_identifier = $form_state->get('flood_control_user_identifier')) {
-        $this->flood->register('user.failed_login_user', $flood_config->get('user_window'), $flood_control_user_identifier);
-      }
-
-      if ($flood_control_triggered = $form_state->get('flood_control_triggered')) {
-        if ($flood_control_triggered == 'user') {
-          $form_state->setErrorByName('name', $this->formatPlural($flood_config->get('user_limit'), 'There has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', 'There have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', array(':url' => $this->url('user.pass'))));
-        }
-        else {
-          // We did not find a uid, so the limit is IP-based.
-          $form_state->setErrorByName('name', $this->t('Too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', array(':url' => $this->url('user.pass'))));
-        }
+      $result = $this->userAuthFlood->authenticate($form_state->getValue('name'), $password, 'user', $this->getRequest());
+      if ($result->isSuccessful()) {
+        $form_state->set('uid', $result->getUser()->id());
       }
       else {
-        // Use $form_state->getUserInput() in the error message to guarantee
-        // that we send exactly what the user typed in. The value from
-        // $form_state->getValue() may have been modified by validation
-        // handlers that ran earlier than this one.
-        $user_input = $form_state->getUserInput();
-        $query = isset($user_input['name']) ? array('name' => $user_input['name']) : array();
-        $form_state->setErrorByName('name', $this->t('Unrecognized username or password. <a href=":password">Forgot your password?</a>', array(':password' => $this->url('user.pass', [], array('query' => $query)))));
-        $accounts = $this->userStorage->loadByProperties(array('name' => $form_state->getValue('name')));
-        if (!empty($accounts)) {
-          $this->logger('user')->notice('Login attempt failed for %user.', array('%user' => $form_state->getValue('name')));
+        // The error messages from the authentication service are plain text,
+        // but we want proper password reset links and HTML markup for them.
+        switch ($result->getErrorCode()) {
+          case AuthResult::ERROR_USER_BLOCKED:
+            $form_state->setErrorByName('name', $this->t('The username %name has not been activated or is blocked.', array('%name' => $form_state->getValue('name'))));
+            break;
+
+          case AuthResult::ERROR_INVALID_CREDENTIALS:
+            // Use $form_state->getUserInput() in the error message to guarantee
+            // that we send exactly what the user typed in. The value from
+            // $form_state->getValue() may have been modified by validation
+            // handlers that ran earlier than this one.
+            $user_input = $form_state->getUserInput();
+            $query = isset($user_input['name']) ? array('name' => $user_input['name']) : array();
+            $form_state->setErrorByName('name', $this->t('Unrecognized username or password. <a href=":password">Forgot your password?</a>', array(':password' => $this->url('user.pass', [], array('query' => $query)))));
+            break;
+
+          case AuthResult::ERROR_FLOOD_USER:
+            $flood_config = $this->config('user.flood');
+            $form_state->setErrorByName('name', $this->formatPlural($flood_config->get('user_limit'), 'There has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', 'There have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', array(':url' => $this->url('user.pass'))));
+            break;
+
+          case AuthResult::ERROR_FLOOD_IP:
+            $form_state->setErrorByName('name', $this->t('Too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href=":url">request a new password</a>.', array(':url' => $this->url('user.pass'))));
+            break;
+
+          default:
+            $form_state->setErrorByName('name', $result->getErrorMessage());
+            break;
         }
-        else {
-          // If the username entered is not a valid user,
-          // only store the IP address.
-          $this->logger('user')->notice('Login attempt failed from %ip.', array('%ip' => $this->getRequest()->getClientIp()));
+
+        if (!$result->isFloodControlTriggered()) {
+          $accounts = $this->userStorage->loadByProperties(array('name' => $form_state->getValue('name')));
+          if (!empty($accounts)) {
+            $this->logger('user')->notice('Login attempt failed for %user.', array('%user' => $form_state->getValue('name')));
+          }
+          else {
+            // If the username entered is not a valid user,
+            // only store the IP address.
+            $this->logger('user')->notice('Login attempt failed from %ip.', array('%ip' => $this->getRequest()->getClientIp()));
+          }
         }
       }
     }
-    elseif ($flood_control_user_identifier = $form_state->get('flood_control_user_identifier')) {
-      // Clear past failures for this user so as not to block a user who might
-      // log in and out more than once in an hour.
-      $this->flood->clear('user.failed_login_user', $flood_control_user_identifier);
-    }
   }
 
 }
diff --git a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
index 29b9727..15e444f 100644
--- a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
+++ b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
@@ -131,7 +131,7 @@ public function testLogin() {
         $this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or password.', $format);
 
         $response = $this->loginRequest($name, 'wrong-pass', $format);
-        $this->assertHttpResponseWithMessage($response, 403, 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.', $format);
+        $this->assertHttpResponseWithMessage($response, 403, 'There have been more than 3 failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.', $format);
 
         // After testing the flood control we can increase the limit.
         $this->config('user.flood')
@@ -153,7 +153,7 @@ public function testLogin() {
           ->save();
 
         $response = $this->loginRequest($name, $pass, $format);
-        $this->assertHttpResponseWithMessage($response, 400, 'The user has not been activated or is blocked.', $format);
+        $this->assertHttpResponseWithMessage($response, 400, "The username $name has not been activated or is blocked.", $format);
 
         $account
           ->activate()
@@ -242,7 +242,7 @@ public function testGlobalLoginFloodControl() {
 
     // IP limit has reached to its limit. Even valid user credentials will fail.
     $response = $this->loginRequest($user->getUsername(), $user->passRaw);
-    $this->assertHttpResponseWithMessage($response, '403', 'Access is blocked because of IP based flood prevention.');
+    $this->assertHttpResponseWithMessage($response, '403', 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.');
   }
 
   /**
@@ -323,13 +323,7 @@ public function testPerUserLoginFloodControl() {
       // Try one more attempt for user 1, it should be rejected, even if the
       // correct password has been used.
       $response = $this->loginRequest($user1->getUsername(), $user1->passRaw);
-      // Depending on the uid_only setting the error message will be different.
-      if ($uid_only_setting) {
-        $excepted_message = 'There have been more than 3 failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.';
-      }
-      else {
-        $excepted_message = 'Too many failed login attempts from your IP address. This IP address is temporarily blocked.';
-      }
+      $excepted_message = 'There have been more than 3 failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.';
       $this->assertHttpResponseWithMessage($response, 403, $excepted_message);
     }
 
diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml
index c273cb3..65dd19a 100644
--- a/core/modules/user/user.services.yml
+++ b/core/modules/user/user.services.yml
@@ -48,6 +48,9 @@ services:
   user.auth:
     class: Drupal\user\UserAuth
     arguments: ['@entity.manager', '@password']
+  user.auth_flood:
+    class: Drupal\user\Authentication\UserAuthFlood
+    arguments: ['@config.factory', '@user.auth', '@flood', '@entity_type.manager', '@string_translation']
   user.private_tempstore:
     class: Drupal\user\PrivateTempStoreFactory
     arguments: ['@keyvalue.expirable', '@lock', '@current_user', '@request_stack', '%user.tempstore.expire%']
