Index: src/wp-includes/class-wp-recovery-mode-key-service.php
===================================================================
--- src/wp-includes/class-wp-recovery-mode-key-service.php	(revision 45108)
+++ src/wp-includes/class-wp-recovery-mode-key-service.php	(working copy)
@@ -20,7 +20,7 @@
 	 *
 	 * @global PasswordHash $wp_hasher
 	 *
-	 * @return string Recovery mode key.
+	 * @return array Recovery mode key and Token.
 	 */
 	public function generate_and_store_recovery_mode_key() {
 
@@ -44,34 +44,54 @@
 
 		$hashed = $wp_hasher->HashPassword( $key );
 
-		update_option(
-			'recovery_key',
-			array(
-				'hashed_key' => $hashed,
-				'created_at' => time(),
-			)
+		$records = get_option( 'recovery_key', false );
+
+		if ( false === $records ) {
+
+			$records = array();
+		}
+
+		$token = wp_generate_password( 22, false );
+
+		$records[ $token ] = array(
+			'hashed_key' => $hashed,
+			'created_at' => time(),
 		);
 
-		return $key;
+		update_option( 'recovery_key', $records );
+
+		return array( $key, $token );
 	}
 
 	/**
 	 * Verifies if the recovery mode key is correct.
+	 * Removes any old keys and the key passed to it
 	 *
 	 * @since 5.2.0
 	 *
 	 * @param string $key The unhashed key.
+	 * @param string $token The data index
 	 * @param int    $ttl Time in seconds for the key to be valid for.
+	 *
 	 * @return true|WP_Error True on success, error object on failure.
 	 */
-	public function validate_recovery_mode_key( $key, $ttl ) {
+	public function validate_and_consume_recovery_mode_key( $key, $token, $ttl ) {
 
-		$record = get_option( 'recovery_key' );
+		$records = get_option( 'recovery_key' );
 
-		if ( ! $record ) {
+		if ( ! $records ) {
 			return new WP_Error( 'no_recovery_key_set', __( 'Recovery Mode not initialized.' ) );
 		}
 
+		if ( ! isset( $records[ $token ] ) ) {
+			return new WP_Error( 'recovery_key_data_missing', __( 'Recovery Mode not initialized.' ) );
+		}
+
+		$record = $records[ $token ];
+
+		// remove so it a onetime use
+		$this->clean_recovery_key_options( $ttl, $token );
+
 		if ( ! is_array( $record ) || ! isset( $record['hashed_key'], $record['created_at'] ) ) {
 			return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) );
 		}
@@ -86,4 +106,30 @@
 
 		return true;
 	}
+
+	/**
+	 * Removes old and used recovery keys.
+	 *
+	 * @since 5.2.0
+	 *
+	 * @param int         $ttl Unix timestamp
+	 * @param null/string $token The data index
+	 *
+	 */
+	public function clean_recovery_key_options(  $ttl, $token = null  ){
+
+		$records = get_option( 'recovery_key' );
+
+		if ( null !== $token ) {
+			unset( $records[ $token ] );
+		}
+
+		foreach ( $records as $key => $record ) {
+			if ( ! isset( $record['created_at']) || time() > $record['created_at'] + $ttl ) {
+				unset( $records[ $key ] );
+			}
+		}
+
+		update_option( 'recovery_key', $records );
+	}
 }
Index: src/wp-includes/class-wp-recovery-mode-link-service.php
===================================================================
--- src/wp-includes/class-wp-recovery-mode-link-service.php	(revision 45108)
+++ src/wp-includes/class-wp-recovery-mode-link-service.php	(working copy)
@@ -53,9 +53,9 @@
 	 * @return string Generated URL.
 	 */
 	public function generate_url() {
-		$key = $this->key_service->generate_and_store_recovery_mode_key();
+		list( $key, $token ) = $this->key_service->generate_and_store_recovery_mode_key();
 
-		return $this->get_recovery_mode_begin_url( $key );
+		return $this->get_recovery_mode_begin_url( $key, $token );
 	}
 
 	/**
@@ -70,7 +70,7 @@
 			return;
 		}
 
-		if ( ! isset( $_GET['action'], $_GET['rm_key'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) {
+		if ( ! isset( $_GET['action'], $_GET['rm_key'], $_GET['rm_token'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) {
 			return;
 		}
 
@@ -78,7 +78,7 @@
 			require_once ABSPATH . WPINC . '/pluggable.php';
 		}
 
-		$validated = $this->key_service->validate_recovery_mode_key( $_GET['rm_key'], $ttl );
+		$validated = $this->key_service->validate_and_consume_recovery_mode_key( $_GET['rm_key'], $_GET['rm_token'], $ttl );
 
 		if ( is_wp_error( $validated ) ) {
 			wp_die( $validated, '' );
@@ -97,14 +97,18 @@
 	 * @since 5.2.0
 	 *
 	 * @param string $key Recovery Mode key created by {@see generate_and_store_recovery_mode_key()}
+	 *
+	 * @param string $token Recovery Mode data index created by {@see generate_and_store_recovery_mode_key()}
+	 *
 	 * @return string Recovery mode begin URL.
 	 */
-	private function get_recovery_mode_begin_url( $key ) {
+	private function get_recovery_mode_begin_url( $key, $token ) {
 
 		$url = add_query_arg(
 			array(
-				'action' => self::LOGIN_ACTION_ENTER,
-				'rm_key' => $key,
+				'action'   => self::LOGIN_ACTION_ENTER,
+				'rm_key'   => $key,
+				'rm_token' => $token,
 			),
 			wp_login_url()
 		);
@@ -116,7 +120,8 @@
 		 *
 		 * @param string $url
 		 * @param string $key
+		 * @param string $token
 		 */
-		return apply_filters( 'recovery_mode_begin_url', $url, $key );
+		return apply_filters( 'recovery_mode_begin_url', $url, $key, $token );
 	}
 }
Index: tests/phpunit/tests/error-protection/recovery-mode-key-service.php
===================================================================
--- tests/phpunit/tests/error-protection/recovery-mode-key-service.php	(revision 45108)
+++ tests/phpunit/tests/error-protection/recovery-mode-key-service.php	(working copy)
@@ -20,20 +20,48 @@
 	 */
 	public function test_validate_recovery_mode_key_returns_wp_error_if_no_key_set() {
 		$service = new WP_Recovery_Mode_Key_Service();
-		$error   = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
+		$error   = $service->validate_and_consume_recovery_mode_key( 'abcd', '', HOUR_IN_SECONDS );
 
 		$this->assertWPError( $error );
 		$this->assertEquals( 'no_recovery_key_set', $error->get_error_code() );
 	}
+	/**
+	 * @ticket 46130
+	 */
+	public function test_validate_recovery_mode_key_returns_wp_error_if_data_missing() {
+		update_option( 'recovery_key', 'gibberish' );
 
+		$service = new WP_Recovery_Mode_Key_Service();
+		$error   = $service->validate_and_consume_recovery_mode_key( 'abcd', '', HOUR_IN_SECONDS );
+
+		$this->assertWPError( $error );
+		$this->assertEquals( 'recovery_key_data_missing', $error->get_error_code() );
+	}
+
 	/**
 	 * @ticket 46130
 	 */
+	public function test_validate_recovery_mode_key_returns_wp_error_if_bad() {
+		update_option( 'recovery_key', array( 'token' => 'gibberish' ) );
+
+		$service = new WP_Recovery_Mode_Key_Service();
+		$error   = $service->validate_and_consume_recovery_mode_key( 'abcd', 'token', HOUR_IN_SECONDS );
+
+		$this->assertWPError( $error );
+		$this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() );
+	}
+
+
+	/**
+	 * @ticket 46130
+	 */
 	public function test_validate_recovery_mode_key_returns_wp_error_if_stored_format_is_invalid() {
-		update_option( 'recovery_key', 'gibberish' );
 
+		$token =  wp_generate_password( 22, false );
+		update_option( 'recovery_key', array( $token => 'gibberish' ) );
+
 		$service = new WP_Recovery_Mode_Key_Service();
-		$error   = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
+		$error   = $service->validate_and_consume_recovery_mode_key( 'abcd', $token, HOUR_IN_SECONDS );
 
 		$this->assertWPError( $error );
 		$this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() );
@@ -44,8 +72,8 @@
 	 */
 	public function test_validate_recovery_mode_key_returns_wp_error_if_empty_key() {
 		$service = new WP_Recovery_Mode_Key_Service();
-		$service->generate_and_store_recovery_mode_key();
-		$error = $service->validate_recovery_mode_key( '', HOUR_IN_SECONDS );
+		list( $key, $token ) = $service->generate_and_store_recovery_mode_key();
+		$error = $service->validate_and_consume_recovery_mode_key( '', $token, HOUR_IN_SECONDS );
 
 		$this->assertWPError( $error );
 		$this->assertEquals( 'hash_mismatch', $error->get_error_code() );
@@ -56,8 +84,8 @@
 	 */
 	public function test_validate_recovery_mode_key_returns_wp_error_if_hash_mismatch() {
 		$service = new WP_Recovery_Mode_Key_Service();
-		$service->generate_and_store_recovery_mode_key();
-		$error = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
+		list( $key, $token ) = $service->generate_and_store_recovery_mode_key();
+		$error = $service->validate_and_consume_recovery_mode_key( 'abcd', $token, HOUR_IN_SECONDS );
 
 		$this->assertWPError( $error );
 		$this->assertEquals( 'hash_mismatch', $error->get_error_code() );
@@ -68,13 +96,13 @@
 	 */
 	public function test_validate_recovery_mode_key_returns_wp_error_if_expired() {
 		$service = new WP_Recovery_Mode_Key_Service();
-		$key     = $service->generate_and_store_recovery_mode_key();
+		list( $key, $token ) = $service->generate_and_store_recovery_mode_key();
 
-		$record               = get_option( 'recovery_key' );
-		$record['created_at'] = time() - HOUR_IN_SECONDS - 30;
-		update_option( 'recovery_key', $record );
+		$records                         = get_option( 'recovery_key' );
+		$records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 30;
+		update_option( 'recovery_key', $records );
 
-		$error = $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS );
+		$error = $service->validate_and_consume_recovery_mode_key( $key, $token, HOUR_IN_SECONDS );
 
 		$this->assertWPError( $error );
 		$this->assertEquals( 'key_expired', $error->get_error_code() );
@@ -85,7 +113,45 @@
 	 */
 	public function test_validate_recovery_mode_key_returns_true_for_valid_key() {
 		$service = new WP_Recovery_Mode_Key_Service();
-		$key     = $service->generate_and_store_recovery_mode_key();
-		$this->assertTrue( $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS ) );
+		list( $key, $token ) = $service->generate_and_store_recovery_mode_key();
+		$this->assertTrue( $service->validate_and_consume_recovery_mode_key( $key, $token, HOUR_IN_SECONDS ) );
 	}
+
+
+	/**
+	 * @ticket 46130
+	 */
+	public function test_validate_recovery_mode_key_returns_error_if_token_used_more_than_once() {
+		$service = new WP_Recovery_Mode_Key_Service();
+		list( $key, $token ) = $service->generate_and_store_recovery_mode_key();
+
+		$this->assertTrue( $service->validate_and_consume_recovery_mode_key( $key, $token, HOUR_IN_SECONDS ) );
+
+		// data should be remove by first call
+		$error  = $service->validate_and_consume_recovery_mode_key( $key, $token, HOUR_IN_SECONDS );
+
+		$this->assertWPError( $error );
+		$this->assertEquals( 'no_recovery_key_set', $error->get_error_code() );
+	}
+
+
+	/**
+	 * @ticket 46130
+	 */
+	public function test_validate_recovery_mode_key_returns_error_if_token_used_more_than_once_more_than_key_stored() {
+		$service = new WP_Recovery_Mode_Key_Service();
+
+		//		create an extra key
+		$service->generate_and_store_recovery_mode_key();
+
+		list( $key, $token ) = $service->generate_and_store_recovery_mode_key();
+
+		$this->assertTrue( $service->validate_and_consume_recovery_mode_key( $key, $token, HOUR_IN_SECONDS ) );
+
+		// data should be remove by first call
+		$error  = $service->validate_and_consume_recovery_mode_key( $key, $token, HOUR_IN_SECONDS );
+
+		$this->assertWPError( $error );
+		$this->assertEquals( 'recovery_key_data_missing', $error->get_error_code() );
+	}
 }
