diff --git a/providers/class-two-factor-provider.php b/providers/class-two-factor-provider.php
index a2f9be06..8842bce2 100644
--- a/providers/class-two-factor-provider.php
+++ b/providers/class-two-factor-provider.php
@@ -72,6 +72,39 @@ public function pre_process_authentication( $user ) {
 	 */
 	abstract public function validate_authentication( $user );
 
+	/**
+	 * Logs the failed authentication.
+	 *
+	 * @param WP_User      $user WP_User object of the user trying to login.
+	 * @param string|false $code The code used to authenticate, if available.
+	 *
+	 * @return void
+	 */
+	public function log_failure( $user, $code = false ) {
+		/**
+		 * This action is triggered when a Two Factor validation fails.
+		 *
+		 * @param WP_User      $user WP_User object of the user trying to login.
+		 * @param string|false $code The code used to authenticate, if available.
+		 */
+		do_action( 'two_factor_user_login_failed', $user, $code );
+
+		/* translators: %1$d: the user's ID %2$s: the code used to authenticate */
+		$log_message = sprintf( esc_html__( 'The user with ID %1$d failed to login using the code "%2$s"', 'two-factor' ), $user->ID, esc_html( $code ) );
+
+		/**
+		 * This action is triggered when a Two Factor validation fails.
+		 *
+		 * @param boolean      $should_log  Whether or not the authentication failure should be logged.
+		 * @param WP_User      $user        WP_User object of the user trying to login.
+		 * @param string|false $code        The code used to authenticate, if available.
+		 * @param string       $log_message The generated log message.
+		 */
+		if ( apply_filters( 'two_factor_log_failure', true, $user, $code, $log_message ) ) {
+			error_log( $log_message );
+		}
+	}
+
 	/**
 	 * Whether this Two Factor provider is configured and available for the user specified.
 	 *
diff --git a/providers/class-two-factor-totp.php b/providers/class-two-factor-totp.php
index 4d5a2828..046e6a65 100644
--- a/providers/class-two-factor-totp.php
+++ b/providers/class-two-factor-totp.php
@@ -288,14 +288,19 @@ public function admin_notices( $user_id ) {
 	 * @return bool Whether the user gave a valid code
 	 */
 	public function validate_authentication( $user ) {
+		$success = false;
 		if ( ! empty( $_REQUEST['authcode'] ) ) {
-			return $this->is_valid_authcode(
+			$success = $this->is_valid_authcode(
 				$this->get_user_totp_key( $user->ID ),
 				sanitize_text_field( $_REQUEST['authcode'] )
 			);
 		}
 
-		return false;
+		if ( ! $success ) {
+			$this->log_failure( $user, ! empty( $_REQUEST['authcode'] ) ? sanitize_text_field( $_REQUEST['authcode'] ) : false );
+		}
+
+		return $success;
 	}
 
 	/**