diff --git a/modules/user/tests/user_flood_test.info b/modules/user/tests/user_flood_test.info
new file mode 100644
index 0000000000..909997a771
--- /dev/null
+++ b/modules/user/tests/user_flood_test.info
@@ -0,0 +1,6 @@
+name = "User module flood control tests"
+description = "Support module for user flood control testing."
+package = Testing
+version = VERSION
+core = 7.x
+hidden = TRUE
diff --git a/modules/user/tests/user_flood_test.module b/modules/user/tests/user_flood_test.module
new file mode 100644
index 0000000000..f7388690f5
--- /dev/null
+++ b/modules/user/tests/user_flood_test.module
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @file
+ * Dummy module implementing hook_user_flood_control.
+ */
+
+/**
+ * Implements hook_user_flood_control().
+ */
+function user_flood_test_user_flood_control($ip, $username = FALSE) {
+  if (!empty($username)) {
+    watchdog('user_flood_test', 'hook_user_flood_control was passed username %username and IP %ip.', array('%username' => $username, '%ip' => $ip));
+  }
+  else {
+    watchdog('user_flood_test', 'hook_user_flood_control was passed IP %ip.', array('%ip' => $ip));
+  }
+}
diff --git a/modules/user/tests/user_form_test.module b/modules/user/tests/user_form_test.module
index 382bc57b82..2af15cb83b 100644
--- a/modules/user/tests/user_form_test.module
+++ b/modules/user/tests/user_form_test.module
@@ -35,7 +35,7 @@ function user_form_test_current_password($form, &$form_state, $account) {
     '#description' => t('A field that would require a correct password to change.'),
     '#required' => TRUE,
   );
-  
+
   $form['current_pass'] = array(
     '#type' => 'password',
     '#title' => t('Current password'),
diff --git a/modules/user/user.api.php b/modules/user/user.api.php
index f205a85b58..eab0f694e1 100644
--- a/modules/user/user.api.php
+++ b/modules/user/user.api.php
@@ -472,6 +472,37 @@ function hook_user_role_delete($role) {
     ->execute();
 }
 
+/**
+ * Respond to user flood control events.
+ *
+ * This hook allows you act when an unsuccessful user login has triggered
+ * flood control. This means that either an IP address has been temporarily
+ * blocked from logging in, or a specific user account has been temporarily
+ * blocked from logging in.
+ *
+ * @param $ip
+ *   The IP address that triggered flood control.
+ * @param $username
+ *   The username that has been temporarily blocked.
+ *
+ * @see user_login_final_validate()
+ */
+function hook_user_flood_control($ip, $username = FALSE) {
+  if (!empty($username)) {
+    // Do something with the blocked $username and $ip. For example, send an
+    // e-mail to the user and/or site administrator.
+    
+    // Drupal core uses this hook to log the event:
+    watchdog('user', 'Flood control blocked login attempt for %user from %ip.', array('%user' => $username, '%ip' => $ip));
+  }
+  else {
+    // Do something with the blocked $ip. For example, add it to a block-list.
+
+    // Drupal core uses this hook to log the event:
+    watchdog('user', 'Flood control blocked login attempt from %ip.', array('%ip' => $ip));
+  }
+}
+
 /**
  * @} End of "addtogroup hooks".
  */
diff --git a/modules/user/user.module b/modules/user/user.module
index 2309aa9296..95ba732704 100644
--- a/modules/user/user.module
+++ b/modules/user/user.module
@@ -2225,11 +2225,17 @@ function user_login_final_validate($form, &$form_state) {
     if (isset($form_state['flood_control_triggered'])) {
       if ($form_state['flood_control_triggered'] == 'user') {
         form_set_error('name', format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, 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>.', 'Sorry, 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' => url('user/password'))));
+        module_invoke_all('user_flood_control', ip_address(), $form_state['values']['name']);
       }
       else {
         // We did not find a uid, so the limit is IP-based.
         form_set_error('name', t('Sorry, 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' => url('user/password'))));
+        module_invoke_all('user_flood_control', ip_address());
       }
+      // We cannot call drupal_access_denied() here as that can result in an
+      // infinite loop if the login form is rendered on the 403 page (e.g. in a
+      // block). So add the 403 header and allow form processing to finish.
+      drupal_add_http_header('Status', '403 Forbidden');
     }
     else {
       // Use $form_state['input']['name'] here to guarantee that we send
@@ -2247,6 +2253,18 @@ function user_login_final_validate($form, &$form_state) {
   }
 }
 
+/**
+ * Implements hook_user_flood_control().
+ */
+function user_user_flood_control($ip, $username = FALSE) {
+  if (!empty($username)) {
+    watchdog('user', 'Flood control blocked login attempt for %user from %ip.', array('%user' => $username, '%ip' => $ip));
+  }
+  else {
+    watchdog('user', 'Flood control blocked login attempt from %ip.', array('%ip' => $ip));
+  }
+}
+
 /**
  * Try to validate the user's login credentials locally.
  *
diff --git a/modules/user/user.test b/modules/user/user.test
index 835154b25f..d6fda48817 100644
--- a/modules/user/user.test
+++ b/modules/user/user.test
@@ -322,7 +322,7 @@ class UserLoginTestCase extends DrupalWebTestCase {
   }
 
   function setUp() {
-    parent::setUp('user_session_test');
+    parent::setUp('user_session_test', 'user_flood_test');
   }
 
   /**
@@ -453,12 +453,19 @@ class UserLoginTestCase extends DrupalWebTestCase {
     $this->drupalPost('user', $edit, t('Log in'));
     $this->assertNoFieldByXPath("//input[@name='pass' and @value!='']", NULL, 'Password value attribute is blank.');
     if (isset($flood_trigger)) {
+      $this->assertResponse(403);
+      $user_log = db_query_range('SELECT message FROM {watchdog} WHERE type = :type ORDER BY wid DESC', 0, 1, array(':type' => 'user'))->fetchField();
+      $user_flood_test_log = db_query_range('SELECT message FROM {watchdog} WHERE type = :type ORDER BY wid DESC', 0, 1, array(':type' => 'user_flood_test'))->fetchField();
       if ($flood_trigger == 'user') {
-        $this->assertRaw(format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, 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>.', 'Sorry, 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' => url('user/password'))));
+        $this->assertRaw(t('Sorry, 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' => url('user/password'), '@count' => variable_get('user_failed_login_user_limit', 5))));
+        $this->assertEqual('Flood control blocked login attempt for %user from %ip.', $user_log, 'A watchdog message was logged for the login attempt blocked by flood control per user');
+        $this->assertEqual('hook_user_flood_control was passed username %username and IP %ip.', $user_flood_test_log, 'hook_user_flood_control was invoked by flood control per user');
       }
       else {
         // No uid, so the limit is IP-based.
         $this->assertRaw(t('Sorry, 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' => url('user/password'))));
+        $this->assertEqual('Flood control blocked login attempt from %ip.', $user_log, 'A watchdog message was logged for the login attempt blocked by flood control per IP');
+        $this->assertEqual('hook_user_flood_control was passed IP %ip.', $user_flood_test_log, 'hook_user_flood_control was invoked by flood control per IP');
       }
     }
     else {
