WordPress.org

Make WordPress Core


Ignore:
Timestamp:
04/16/2019 05:08:16 AM (5 months ago)
Author:
flixos90
Message:

Bootstrap/Load: Allow more than one recovery link to be valid at a time.

While currently a recovery link is only made available via the admin email address, this will be expanded in the future. In order to accomplish that, the mechanisms to store and validate recovery keys must support multiple keys to be valid at the same time.

This changeset adds that support, adding an additional token parameter which is part of a recovery link in addition to the key. A key itself is always associated with a token, so the two are only valid in combination. These associations are stored in a new recovery_keys option, which is regularly cleared in a new Cron hook, to prevent potential cluttering from unused recovery keys.

This changeset does not have any user-facing implications otherwise.

Props pbearne, timothyblynjacobs.
Fixes #46595. See #46130.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/error-protection/recovery-mode-key-service.php

    r44973 r45211  
    1111    public function test_generate_and_store_recovery_mode_key_returns_recovery_key() {
    1212        $service = new WP_Recovery_Mode_Key_Service();
    13         $key     = $service->generate_and_store_recovery_mode_key();
     13        $token   = $service->generate_recovery_mode_token();
     14        $key     = $service->generate_and_store_recovery_mode_key( $token );
    1415
    1516        $this->assertNotWPError( $key );
     
    2122    public function test_validate_recovery_mode_key_returns_wp_error_if_no_key_set() {
    2223        $service = new WP_Recovery_Mode_Key_Service();
    23         $error   = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
     24        $error   = $service->validate_recovery_mode_key( '', 'abcd', HOUR_IN_SECONDS );
    2425
    2526        $this->assertWPError( $error );
    26         $this->assertEquals( 'no_recovery_key_set', $error->get_error_code() );
     27        $this->assertEquals( 'token_not_found', $error->get_error_code() );
    2728    }
    2829
     
    3031     * @ticket 46130
    3132     */
    32     public function test_validate_recovery_mode_key_returns_wp_error_if_stored_format_is_invalid() {
    33         update_option( 'recovery_key', 'gibberish' );
     33    public function test_validate_recovery_mode_key_returns_wp_error_if_data_missing() {
     34        update_option( 'recovery_keys', 'gibberish' );
    3435
    3536        $service = new WP_Recovery_Mode_Key_Service();
    36         $error   = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
     37        $error   = $service->validate_recovery_mode_key( '', 'abcd', HOUR_IN_SECONDS );
     38
     39        $this->assertWPError( $error );
     40        $this->assertEquals( 'token_not_found', $error->get_error_code() );
     41    }
     42
     43    /**
     44     * @ticket 46130
     45     */
     46    public function test_validate_recovery_mode_key_returns_wp_error_if_bad() {
     47        update_option( 'recovery_keys', array( 'token' => 'gibberish' ) );
     48
     49        $service = new WP_Recovery_Mode_Key_Service();
     50        $error   = $service->validate_recovery_mode_key( 'token', 'abcd', HOUR_IN_SECONDS );
     51
     52        $this->assertWPError( $error );
     53        $this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() );
     54    }
     55
     56
     57    /**
     58     * @ticket 46130
     59     */
     60    public function test_validate_recovery_mode_key_returns_wp_error_if_stored_format_is_invalid() {
     61
     62        $token = wp_generate_password( 22, false );
     63        update_option( 'recovery_keys', array( $token => 'gibberish' ) );
     64
     65        $service = new WP_Recovery_Mode_Key_Service();
     66        $error   = $service->validate_recovery_mode_key( $token, 'abcd', HOUR_IN_SECONDS );
    3767
    3868        $this->assertWPError( $error );
     
    4575    public function test_validate_recovery_mode_key_returns_wp_error_if_empty_key() {
    4676        $service = new WP_Recovery_Mode_Key_Service();
    47         $service->generate_and_store_recovery_mode_key();
    48         $error = $service->validate_recovery_mode_key( '', HOUR_IN_SECONDS );
     77        $token   = $service->generate_recovery_mode_token();
     78        $service->generate_and_store_recovery_mode_key( $token );
     79        $error = $service->validate_recovery_mode_key( $token, '', HOUR_IN_SECONDS );
    4980
    5081        $this->assertWPError( $error );
     
    5788    public function test_validate_recovery_mode_key_returns_wp_error_if_hash_mismatch() {
    5889        $service = new WP_Recovery_Mode_Key_Service();
    59         $service->generate_and_store_recovery_mode_key();
    60         $error = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
     90        $token   = $service->generate_recovery_mode_token();
     91        $service->generate_and_store_recovery_mode_key( $token );
     92        $error = $service->validate_recovery_mode_key( $token, 'abcd', HOUR_IN_SECONDS );
    6193
    6294        $this->assertWPError( $error );
     
    69101    public function test_validate_recovery_mode_key_returns_wp_error_if_expired() {
    70102        $service = new WP_Recovery_Mode_Key_Service();
    71         $key     = $service->generate_and_store_recovery_mode_key();
     103        $token   = $service->generate_recovery_mode_token();
     104        $key     = $service->generate_and_store_recovery_mode_key( $token );
    72105
    73         $record               = get_option( 'recovery_key' );
    74         $record['created_at'] = time() - HOUR_IN_SECONDS - 30;
    75         update_option( 'recovery_key', $record );
     106        $records                         = get_option( 'recovery_keys' );
     107        $records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 30;
     108        update_option( 'recovery_keys', $records );
    76109
    77         $error = $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS );
     110        $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS );
    78111
    79112        $this->assertWPError( $error );
     
    86119    public function test_validate_recovery_mode_key_returns_true_for_valid_key() {
    87120        $service = new WP_Recovery_Mode_Key_Service();
    88         $key     = $service->generate_and_store_recovery_mode_key();
    89         $this->assertTrue( $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS ) );
     121        $token   = $service->generate_recovery_mode_token();
     122        $key     = $service->generate_and_store_recovery_mode_key( $token );
     123        $this->assertTrue( $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ) );
     124    }
     125
     126    /**
     127     * @ticket 46595
     128     */
     129    public function test_validate_recovery_mode_key_returns_error_if_token_used_more_than_once() {
     130        $service = new WP_Recovery_Mode_Key_Service();
     131        $token   = $service->generate_recovery_mode_token();
     132        $key     = $service->generate_and_store_recovery_mode_key( $token );
     133
     134        $this->assertTrue( $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ) );
     135
     136        // data should be remove by first call
     137        $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS );
     138
     139        $this->assertWPError( $error );
     140        $this->assertEquals( 'token_not_found', $error->get_error_code() );
     141    }
     142
     143    /**
     144     * @ticket 46595
     145     */
     146    public function test_validate_recovery_mode_key_returns_error_if_token_used_more_than_once_more_than_key_stored() {
     147        $service = new WP_Recovery_Mode_Key_Service();
     148
     149        // create an extra key
     150        $token = $service->generate_recovery_mode_token();
     151        $service->generate_and_store_recovery_mode_key( $token );
     152
     153        $token = $service->generate_recovery_mode_token();
     154        $key   = $service->generate_and_store_recovery_mode_key( $token );
     155
     156        $this->assertTrue( $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ) );
     157
     158        // data should be remove by first call
     159        $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS );
     160
     161        $this->assertWPError( $error );
     162        $this->assertEquals( 'token_not_found', $error->get_error_code() );
     163    }
     164
     165    /**
     166     * @ticket 46595
     167     */
     168    public function test_clean_expired_keys() {
     169        $service = new WP_Recovery_Mode_Key_Service();
     170        $token   = $service->generate_recovery_mode_token();
     171        $service->generate_and_store_recovery_mode_key( $token );
     172
     173        $records = get_option( 'recovery_keys' );
     174
     175        $records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 30;
     176
     177        update_option( 'recovery_keys', $records );
     178
     179        $service->clean_expired_keys( HOUR_IN_SECONDS );
     180
     181        $this->assertEmpty( get_option( 'recovery_keys' ) );
    90182    }
    91183}
Note: See TracChangeset for help on using the changeset viewer.