diff --git a/core/modules/user/src/Form/UserPasswordForm.php b/core/modules/user/src/Form/UserPasswordForm.php
index 8291ad5de4..627996fa72 100644
--- a/core/modules/user/src/Form/UserPasswordForm.php
+++ b/core/modules/user/src/Form/UserPasswordForm.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\user\Form;
 
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Flood\FloodInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
@@ -33,6 +35,13 @@ class UserPasswordForm extends FormBase {
    */
   protected $languageManager;
 
+  /**
+   * The flood service.
+   *
+   * @var \Drupal\Core\Flood\FloodInterface
+   */
+  protected $flood;
+
   /**
    * Constructs a UserPasswordForm object.
    *
@@ -40,10 +49,16 @@ class UserPasswordForm extends FormBase {
    *   The user storage.
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    *   The language manager.
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Flood\FloodInterface $flood
+   *   The flood service.
    */
-  public function __construct(UserStorageInterface $user_storage, LanguageManagerInterface $language_manager) {
+  public function __construct(UserStorageInterface $user_storage, LanguageManagerInterface $language_manager, ConfigFactory $config_factory, FloodInterface $flood) {
     $this->userStorage = $user_storage;
     $this->languageManager = $language_manager;
+    $this->configFactory = $config_factory;
+    $this->flood = $flood;
   }
 
   /**
@@ -52,7 +67,9 @@ public function __construct(UserStorageInterface $user_storage, LanguageManagerI
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('entity_type.manager')->getStorage('user'),
-      $container->get('language_manager')
+      $container->get('language_manager'),
+      $container->get('config.factory'),
+      $container->get('flood')
     );
   }
 
@@ -110,6 +127,12 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function validateForm(array &$form, FormStateInterface $form_state) {
+    $flood_config = $this->configFactory->get('user.flood');
+    if (!$this->flood->isAllowed('user.password_request_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
+      $form_state->setErrorByName('name', $this->t('Too many password recovery requests from your IP address. It is temporarily blocked. Try again later or contact the site administrator.'));
+      return;
+    }
+    $this->flood->register('user.password_request_ip', $flood_config->get('ip_window'));
     $name = trim($form_state->getValue('name'));
     // Try to load by email.
     $users = $this->userStorage->loadByProperties(['mail' => $name]);
@@ -124,6 +147,22 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
         $form_state->setErrorByName('name', $this->t('%name is blocked or has not been activated yet.', ['%name' => $name]));
       }
       else {
+        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();
+        }
+        if (!$this->flood->isAllowed('user.password_request_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
+          $form_state->setErrorByName('name', $this->t('Too many password recovery requests for this account. It is temporarily blocked. Try again later or contact the site administrator.'));
+          return;
+        }
+        $this->flood->register('user.password_request_user', $flood_config->get('user_window'), $identifier);
         $form_state->setValueForElement(['#parents' => ['account']], $account);
       }
     }
diff --git a/core/modules/user/tests/src/Functional/UserPasswordResetTest.php b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php
index 866b2d3242..a9e5f8eb2d 100644
--- a/core/modules/user/tests/src/Functional/UserPasswordResetTest.php
+++ b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php
@@ -72,21 +72,15 @@ public function testUserPasswordReset() {
 
     // Try to reset the password for an invalid account.
     $this->drupalGet('user/password');
-
-    $edit = ['name' => $this->randomMachineName(32)];
+    $edit = ['name' => $this->randomMachineName()];
     $this->drupalPostForm(NULL, $edit, t('Submit'));
-
-    $this->assertText(t('@name is not recognized as a username or an email address.', ['@name' => $edit['name']]), 'Validation error message shown when trying to request password for invalid account.');
-    $this->assertEqual(count($this->drupalGetMails(['id' => 'user_password_reset'])), 0, 'No email was sent when requesting a password for an invalid account.');
+    $this->assertNoValidPasswordReset($edit['name']);
 
     // Reset the password by username via the password reset page.
-    $edit['name'] = $this->account->getAccountName();
+    $this->drupalGet('user/password');
+    $edit = ['name' => $this->account->getAccountName()];
     $this->drupalPostForm(NULL, $edit, t('Submit'));
-
-    // Verify that the user was sent an email.
-    $this->assertMail('to', $this->account->getEmail(), 'Password email sent to user.');
-    $subject = t('Replacement login information for @username at @site', ['@username' => $this->account->getAccountName(), '@site' => $this->config('system.site')->get('name')]);
-    $this->assertMail('subject', $subject, 'Password reset email subject is correct.');
+    $this->assertValidPasswordReset($edit['name']);
 
     $resetURL = $this->getResetURL();
     $this->drupalGet($resetURL);
@@ -126,11 +120,12 @@ public function testUserPasswordReset() {
     $this->assertText(t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'), 'One-time link is no longer valid.');
 
     // Request a new password again, this time using the email address.
-    $this->drupalGet('user/password');
     // Count email messages before to compare with after.
     $before = count($this->drupalGetMails(['id' => 'user_password_reset']));
+    $this->drupalGet('user/password');
     $edit = ['name' => $this->account->getEmail()];
     $this->drupalPostForm(NULL, $edit, t('Submit'));
+    $this->assertValidPasswordReset($edit['name']);
     $this->assertTrue(count($this->drupalGetMails(['id' => 'user_password_reset'])) === $before + 1, 'Email sent when requesting password reset using email address.');
 
     // Visit the user edit page without pass-reset-token and make sure it does
@@ -284,6 +279,103 @@ public function testUserResetPasswordTextboxFilled() {
     $this->assertNoFieldByName('name', $edit['name'], 'User name not found.');
   }
 
+  /**
+   * Tests password reset flood control for one user.
+   */
+  public function testUserResetPasswordUserFloodControl() {
+    \Drupal::configFactory()->getEditable('user.flood')
+      ->set('user_limit', 3)
+      ->save();
+
+    $edit = ['name' => $this->account->getUsername()];
+
+    // Try 3 requests that should not trigger flood control.
+    for ($i = 0; $i < 3; $i++) {
+      $this->drupalGet('user/password');
+      $this->drupalPostForm(NULL, $edit, t('Submit'));
+      $this->assertValidPasswordReset($edit['name']);
+      $this->assertNoPasswordUserFlood();
+    }
+
+    // The next request should trigger flood control.
+    $this->drupalGet('user/password');
+    $this->drupalPostForm(NULL, $edit, t('Submit'));
+    $this->assertPasswordUserFlood();
+  }
+
+  /**
+   * Tests password reset flood control for one IP.
+   */
+  public function testUserResetPasswordIpFloodControl() {
+    \Drupal::configFactory()->getEditable('user.flood')
+        ->set('ip_limit', 3)
+        ->save();
+
+    // Try 3 requests that should not trigger flood control.
+    for ($i = 0; $i < 3; $i++) {
+      $this->drupalGet('user/password');
+      $edit = ['name' => $this->randomMachineName()];
+      $this->drupalPostForm(NULL, $edit, t('Submit'));
+      // Because we're testing with a random name, the password reset will not be valid.
+      $this->assertNoValidPasswordReset($edit['name']);
+      $this->assertNoPasswordIpFlood();
+    }
+
+    // The next request should trigger flood control.
+    $this->drupalGet('user/password');
+    $edit = ['name' => $this->randomMachineName()];
+    $this->drupalPostForm(NULL, $edit, t('Submit'));
+    $this->assertPasswordIpFlood();
+  }
+
+  /**
+   * Helper function to make assertions about a valid password reset.
+   */
+  public function assertValidPasswordReset($name) {
+    // Make sure the error text is not displayed and email sent.
+    $this->assertNoText(t('Sorry, @name is not recognized as a username or an e-mail address.', ['@name' => $name]), 'Validation error message shown when trying to request password for invalid account.');
+    $this->assertMail('to', $this->account->getEmail(), 'Password e-mail sent to user.');
+    $subject = t('Replacement login information for @username at @site', ['@username' => $this->account->getUsername(), '@site' => \Drupal::config('system.site')->get('name')]);
+    $this->assertMail('subject', $subject, 'Password reset e-mail subject is correct.');
+  }
+
+  /**
+   * Helper function to make assertions about an invalid password reset.
+   */
+  public function assertNoValidPasswordReset($name) {
+    // Make sure the error text is displayed and no email sent.
+    $this->assertText(t('@name is not recognized as a username or an email address.', ['@name' => $name]), 'Validation error message shown when trying to request password for invalid account.');
+    $this->assertEqual(count($this->drupalGetMails(['id' => 'user_password_reset'])), 0, 'No e-mail was sent when requesting a password for an invalid account.');
+  }
+
+  /**
+   * Helper function to make assertions about a password reset triggering user flood cotrol.
+   */
+  public function assertPasswordUserFlood() {
+    $this->assertText(t('Too many password recovery requests for this account. It is temporarily blocked. Try again later or contact the site administrator.'), 'User password reset flood error message shown.');
+  }
+
+  /**
+   * Helper function to make assertions about a password reset not triggering user flood control.
+   */
+  public function assertNoPasswordUserFlood() {
+    $this->assertNoText(t('Too many password recovery requests for this account. It is temporarily blocked. Try again later or contact the site administrator.'), 'User password reset flood error message not shown.');
+  }
+
+  /**
+   * Helper function to make assertions about a password reset triggering IP flood cotrol.
+   */
+  public function assertPasswordIpFlood() {
+    $this->assertText(t('Too many password recovery requests from your IP address. It is temporarily blocked. Try again later or contact the site administrator.'), 'IP password reset flood error message shown.');
+  }
+
+  /**
+   * Helper function to make assertions about a password reset not triggering IP flood control.
+   */
+  public function assertNoPasswordIpFlood() {
+    $this->assertNoText(t('Too many password recovery requests from your IP address. It is temporarily blocked. Try again later or contact the site administrator.'), 'IP password reset flood error message not shown.');
+  }
+
   /**
    * Make sure that users cannot forge password reset URLs of other users.
    */
