Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into profile-ux
Browse files Browse the repository at this point in the history
# Conflicts:
#	class-two-factor-core.php
  • Loading branch information
kasparsd committed Jan 9, 2025
2 parents 26d7cb3 + 4c733e4 commit f77f2ac
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 42 deletions.
34 changes: 19 additions & 15 deletions class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ private static function get_providers_classes( $providers ) {
}

/**
* Get all enabled two-factor providers.
* Get all enabled two-factor providers with keys as the original
* provider class names and the values as the provider class instances.
*
* @since 0.1-dev
*
Expand Down Expand Up @@ -1913,15 +1914,14 @@ public static function user_two_factor_options( $user ) {
* @return bool True if the provider was enabled, false otherwise.
*/
public static function enable_provider_for_user( $user_id, $new_provider ) {
$available_providers = self::get_providers();

if ( ! array_key_exists( $new_provider, $available_providers ) ) {
// Ensure the provider is even available.
if ( ! array_key_exists( $new_provider, self::get_providers() ) ) {
return false;
}

$user = get_userdata( $user_id );
$enabled_providers = self::get_enabled_providers_for_user( $user );
$enabled_providers = self::get_enabled_providers_for_user( $user_id );

// Check if this is enabled already.
if ( in_array( $new_provider, $enabled_providers ) ) {
return true;
}
Expand All @@ -1945,23 +1945,27 @@ public static function enable_provider_for_user( $user_id, $new_provider ) {
* @return bool True if the provider was disabled, false otherwise.
*/
public static function disable_provider_for_user( $user_id, $provider_to_delete ) {
$is_registered = array_key_exists( $provider_to_delete, self::get_providers() );

if ( ! $is_registered ) {
// Check if the provider is even enabled.
if ( ! array_key_exists( $provider_to_delete, self::get_providers() ) ) {
return false;
}

$old_enabled_providers = self::get_enabled_providers_for_user( $user_id );
$is_enabled = in_array( $provider_to_delete, $old_enabled_providers );
$enabled_providers = self::get_enabled_providers_for_user( $user_id );

if ( ! $is_enabled ) {
// Check if this is disabled already.
if ( ! in_array( $provider_to_delete, $enabled_providers ) ) {
return true;
}

$new_enabled_providers = array_diff( $old_enabled_providers, array( $provider_to_delete ) );
$was_disabled = update_user_meta( $user_id, self::ENABLED_PROVIDERS_USER_META_KEY, $new_enabled_providers );
$enabled_providers = array_diff( $enabled_providers, array( $provider_to_delete ) );

return (bool) $was_disabled;
// Remove this from being a primary provider, if set.
$primary_provider = self::get_primary_provider_for_user( $user_id );
if ( $primary_provider && $primary_provider->get_key() === $provider_to_delete ) {
delete_user_meta( $user_id, self::PROVIDER_USER_META_KEY );
}

return (bool) update_user_meta( $user_id, self::ENABLED_PROVIDERS_USER_META_KEY, $enabled_providers );
}

/**
Expand Down
31 changes: 28 additions & 3 deletions providers/class-two-factor-backup-codes.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,25 @@ public function user_options( $user ) {
<?php
}

/**
* Get the backup code length for a user.
*
* @param WP_User $user User object.
*
* @return int Number of characters.
*/
private function get_backup_code_length( $user ) {
/**
* Customize the character count of the backup codes.
*
* @var int $code_length Length of the backup code.
* @var WP_User $user User object.
*/
$code_length = (int) apply_filters( 'two_factor_backup_code_length', 8, $user );

return $code_length;
}

/**
* Generates backup codes & updates the user meta.
*
Expand All @@ -239,8 +258,10 @@ public function generate_codes( $user, $args = '' ) {
$codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
}

$code_length = $this->get_backup_code_length( $user );

for ( $i = 0; $i < $num_codes; $i++ ) {
$code = $this->get_code();
$code = $this->get_code( $code_length );
$codes_hashed[] = wp_hash_password( $code );
$codes[] = $code;
unset( $code );
Expand Down Expand Up @@ -326,11 +347,15 @@ public static function codes_remaining_for_user( $user ) {
*/
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';

$code_length = $this->get_backup_code_length( $user );
$code_placeholder = str_repeat( 'X', $code_length );

?>
<p class="two-factor-prompt"><?php esc_html_e( 'Enter a recovery code.', 'two-factor' ); ?></p><br/>
<p class="two-factor-prompt"><?php esc_html_e( 'Enter a recovery code.', 'two-factor' ); ?></p>
<p>
<label for="authcode"><?php esc_html_e( 'Recovery Code:', 'two-factor' ); ?></label>
<input type="text" inputmode="numeric" name="two-factor-backup-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="1234 5678" data-digits="8" />
<input type="text" inputmode="numeric" name="two-factor-backup-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="<?php echo esc_attr( $code_placeholder ); ?>" data-digits="<?php echo esc_attr( $code_length ); ?>" />
</p>
<?php
submit_button( __( 'Submit', 'two-factor' ) );
Expand Down
36 changes: 33 additions & 3 deletions providers/class-two-factor-email.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ public function get_alternative_provider_label() {
return __( 'Send a code to your email', 'two-factor' );
}

/**
* Get the email token length.
*
* @return int Email token string length.
*/
private function get_token_length() {
/**
* Number of characters in the email token.
*
* @param int $token_length Number of characters in the email token.
*/
$token_length = (int) apply_filters( 'two_factor_email_token_length', 8 );

return $token_length;
}

/**
* Generate the user token.
*
Expand All @@ -72,7 +88,7 @@ public function get_alternative_provider_label() {
* @return string
*/
public function generate_token( $user_id ) {
$token = $this->get_code();
$token = $this->get_code( $this->get_token_length() );

update_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, time() );
update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) );
Expand Down Expand Up @@ -146,10 +162,21 @@ public function user_token_ttl( $user_id ) {
* Number of seconds the token is considered valid
* after the generation.
*
* @deprecated 0.11.0 Use {@see 'two_factor_email_token_ttl'} instead.
*
* @param integer $token_ttl Token time-to-live in seconds.
* @param integer $user_id User ID.
*/
return (int) apply_filters( 'two_factor_token_ttl', $token_ttl, $user_id );
$token_ttl = (int) apply_filters_deprecated( 'two_factor_token_ttl', array( $token_ttl, $user_id ), '0.11.0', 'two_factor_email_token_ttl' );

/**
* Number of seconds the token is considered valid
* after the generation.
*
* @param integer $token_ttl Token time-to-live in seconds.
* @param integer $user_id User ID.
*/
return (int) apply_filters( 'two_factor_email_token_ttl', $token_ttl, $user_id );
}

/**
Expand Down Expand Up @@ -259,12 +286,15 @@ public function authentication_page( $user ) {
$this->generate_and_email_token( $user );
}

$token_length = $this->get_token_length();
$token_placeholder = str_repeat( 'X', $token_length );

require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<p class="two-factor-prompt"><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'two-factor' ); ?></p>
<p>
<label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label>
<input type="text" inputmode="numeric" name="two-factor-email-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="1234 5678" data-digits="8" />
<input type="text" inputmode="numeric" name="two-factor-email-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" autocomplete="one-time-code" placeholder="<?php echo esc_attr( $token_placeholder ); ?>" data-digits="<?php echo esc_attr( $token_length ); ?>" />
<?php submit_button( __( 'Log In', 'two-factor' ) ); ?>
</p>
<p class="two-factor-email-resend">
Expand Down
12 changes: 10 additions & 2 deletions providers/class-two-factor-totp.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ public function rest_delete_totp( $request ) {

$this->delete_user_totp_key( $user_id );

if ( ! Two_Factor_Core::disable_provider_for_user( $user_id, 'Two_Factor_Totp' ) ) {
return new WP_Error( 'db_error', __( 'Unable to disable TOTP provider for this user.', 'two-factor' ), array( 'status' => 500 ) );
}

ob_start();
$this->user_two_factor_options( $user );
$html = ob_get_clean();
Expand Down Expand Up @@ -349,7 +353,7 @@ public function user_two_factor_options( $user ) {
/* translators: Example auth code. */
$placeholder = sprintf( __( 'eg. %s', 'two-factor' ), '123456' );
?>
<input type="tel" name="two-factor-totp-authcode" id="two-factor-totp-authcode" class="input" value="" size="20" pattern="[0-9 ]*" placeholder="<?php echo esc_attr( $placeholder ); ?>" />
<input type="text" inputmode="numeric" name="two-factor-totp-authcode" id="two-factor-totp-authcode" class="input" value="" size="20" pattern="[0-9 ]*" placeholder="<?php echo esc_attr( $placeholder ); ?>" autocomplete="off" />
</label>
<input type="submit" class="button totp-submit" name="two-factor-totp-submit" value="<?php esc_attr_e( 'Submit', 'two-factor' ); ?>" />
</p>
Expand All @@ -375,6 +379,7 @@ public function user_two_factor_options( $user ) {
user_id: <?php echo wp_json_encode( $user->ID ); ?>,
key: key,
code: code,
enable_provider: true,
}
} ).fail( function( response, status ) {
var errorMessage = response.responseJSON.message || status,
Expand All @@ -386,8 +391,10 @@ public function user_two_factor_options( $user ) {

$error.find('p').text( errorMessage );

$( '#enabled-Two_Factor_Totp' ).prop( 'checked', false );
$('#two-factor-totp-authcode').val('');
} ).then( function( response ) {
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', true );
$( '#two-factor-totp-options' ).html( response.html );
} );
} );
Expand All @@ -414,6 +421,7 @@ public function user_two_factor_options( $user ) {
user_id: <?php echo wp_json_encode( $user->ID ); ?>,
}
} ).then( function( response ) {
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', false );
$( '#two-factor-totp-options' ).html( response.html );
} );
} );
Expand Down Expand Up @@ -682,7 +690,7 @@ public function authentication_page( $user ) {
</p>
<p>
<label for="authcode"><?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?></label>
<input type="text" inputmode="numeric" autocomplete="one-time-code" name="authcode" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="123 456" data-digits="<?php echo esc_attr( self::DEFAULT_DIGIT_COUNT ); ?>" />
<input type="text" inputmode="numeric" autocomplete="one-time-code" name="authcode" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="123 456" autocomplete="one-time-code" data-digits="<?php echo esc_attr( self::DEFAULT_DIGIT_COUNT ); ?>" />
</p>
<script type="text/javascript">
setTimeout( function(){
Expand Down
4 changes: 3 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ Here is a list of action and filter hooks provided by the plugin:
- `two_factor_providers` filter overrides the available two-factor providers such as email and time-based one-time passwords. Array values are PHP classnames of the two-factor providers.
- `two_factor_enabled_providers_for_user` filter overrides the list of two-factor providers enabled for a user. First argument is an array of enabled provider classnames as values, the second argument is the user ID.
- `two_factor_user_authenticated` action which receives the logged in `WP_User` object as the first argument for determining the logged in user right after the authentication workflow.
- `two_factor_token_ttl` filter overrides the time interval in seconds that an email token is considered after generation. Accepts the time in seconds as the first argument and the ID of the `WP_User` object being authenticated.
- `two_factor_email_token_ttl` filter overrides the time interval in seconds that an email token is considered after generation. Accepts the time in seconds as the first argument and the ID of the `WP_User` object being authenticated.
- `two_factor_email_token_length` filter overrides the default 8 character count for email tokens.
- `two_factor_backup_code_length` filter overrides the default 8 character count for backup codes. Providers the `WP_User` of the associated user as the second argument.

== Frequently Asked Questions ==

Expand Down
36 changes: 18 additions & 18 deletions tests/class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -818,52 +818,52 @@ public function test_show_password_reset_error() {
public function test_enable_disable_provider_for_user() {
$user = self::factory()->user->create_and_get();
$enabled_providers = Two_Factor_Core::get_enabled_providers_for_user( $user->ID );
$this->assertEmpty( $enabled_providers );
$this->assertEmpty( $enabled_providers, 'No providers are enabled by default' );

// Disabling one that's already disabled should succeed.
$totp_disabled = Two_Factor_Core::disable_provider_for_user( $user->ID, 'Two_Factor_Totp' );
$this->assertTrue( $totp_disabled );
$this->assertTrue( $totp_disabled, 'Disabling something that wasn\'t enabled should succeed' );

// Disabling one that doesn't exist should fail.
$nonexistent_enabled = Two_Factor_Core::enable_provider_for_user( $user->ID, 'Nonexistent_Provider' );
$enabled_providers = Two_Factor_Core::get_enabled_providers_for_user( $user->ID );
$this->assertFalse( $nonexistent_enabled );
$this->assertEmpty( $enabled_providers );
$this->assertNull( Two_Factor_Core::get_primary_provider_for_user( $user->ID ) );
$this->assertFalse( $nonexistent_enabled, 'Nonexistent shouldn\'t be allowed to be enabled' );
$this->assertEmpty( $enabled_providers, 'Nonexistent wasn\'t enabled' );
$this->assertNull( Two_Factor_Core::get_primary_provider_for_user( $user->ID ), 'Nonexistent wasn\'t set as primary' );

// Enabling a valid one should succeed. The first one that's enabled and configured should be the default primary.
$totp = Two_Factor_Totp::get_instance();
$totp->set_user_totp_key( $user->ID, 'foo' );
$totp_enabled = Two_Factor_Core::enable_provider_for_user( $user->ID, 'Two_Factor_Totp' );
$enabled_providers = Two_Factor_Core::get_enabled_providers_for_user( $user->ID );
$this->assertTrue( $totp_enabled );
$this->assertSame( array( 'Two_Factor_Totp' ), $enabled_providers );
$this->assertSame( 'Two_Factor_Totp', Two_Factor_Core::get_primary_provider_for_user( $user->ID )->get_key() );
$this->assertTrue( $totp_enabled, 'Can enable a valid provider' );
$this->assertSame( array( 'Two_Factor_Totp' ), $enabled_providers, 'Enabled provider is now listed as enabled' );
$this->assertSame( 'Two_Factor_Totp', Two_Factor_Core::get_primary_provider_for_user( $user->ID )->get_key(), 'Primary is now the only enabled provider' );

// Enabling one that's already enabled should succeed.
$totp_enabled = Two_Factor_Core::enable_provider_for_user( $user->ID, 'Two_Factor_Totp' );
$this->assertTrue( $totp_enabled );
$this->assertTrue( $totp_enabled, 'Can enable a provider that is already enabled' );

// Enabling another should succeed, and not change the primary.
$dummy_enabled = Two_Factor_Core::enable_provider_for_user( $user->ID, 'Two_Factor_Dummy' );
$enabled_providers = Two_Factor_Core::get_enabled_providers_for_user( $user->ID );
$this->assertTrue( $dummy_enabled );
$this->assertSame( array( 'Two_Factor_Totp', 'Two_Factor_Dummy' ), $enabled_providers );
$this->assertSame( 'Two_Factor_Totp', Two_Factor_Core::get_primary_provider_for_user( $user->ID )->get_key() );
$this->assertTrue( $dummy_enabled, 'Can enable valid provider' );
$this->assertSame( array( 'Two_Factor_Totp', 'Two_Factor_Dummy' ), $enabled_providers, 'Multiple can be enabled at the same time' );
$this->assertSame( 'Two_Factor_Totp', Two_Factor_Core::get_primary_provider_for_user( $user->ID )->get_key(), 'The primary not changed upon additional providers being enabled' );

// Disabling one that doesn't exist should fail.
$nonexistent_disabled = Two_Factor_Core::disable_provider_for_user( $user->ID, 'Nonexistent_Provider' );
$enabled_providers = Two_Factor_Core::get_enabled_providers_for_user( $user->ID );
$this->assertFalse( $nonexistent_disabled );
$this->assertSame( array( 'Two_Factor_Totp', 'Two_Factor_Dummy' ), $enabled_providers );
$this->assertSame( 'Two_Factor_Totp', Two_Factor_Core::get_primary_provider_for_user( $user->ID )->get_key() );
$this->assertFalse( $nonexistent_disabled, 'Unavailable provider can\'t be disabled' );
$this->assertSame( array( 'Two_Factor_Totp', 'Two_Factor_Dummy' ), $enabled_providers, 'Unavailable wasn\'t added to the list of enabled proviers' );
$this->assertSame( 'Two_Factor_Totp', Two_Factor_Core::get_primary_provider_for_user( $user->ID )->get_key(), 'The primary is still the same after unavailable disable attempt' );

// Disabling one that's enabled should succeed, and change the primary to the next available one.
$totp_disabled = Two_Factor_Core::disable_provider_for_user( $user->ID, 'Two_Factor_Totp' );
$enabled_providers = Two_Factor_Core::get_enabled_providers_for_user( $user->ID );
$this->assertTrue( $totp_disabled ); //todo enable and fix
$this->assertSame( array( 1 => 'Two_Factor_Dummy' ), $enabled_providers );
$this->assertSame( 'Two_Factor_Dummy', Two_Factor_Core::get_primary_provider_for_user( $user->ID )->get_key() );
$this->assertTrue( $totp_disabled, 'Can disable a provider that is enabled' );
$this->assertSame( array( 1 => 'Two_Factor_Dummy' ), $enabled_providers, 'The other providers are kept enabled' );
$this->assertSame( 'Two_Factor_Dummy', Two_Factor_Core::get_primary_provider_for_user( $user->ID )->get_key(), 'Primary is updated to the first available' );
}

/**
Expand Down
21 changes: 21 additions & 0 deletions tests/providers/class-two-factor-backup-codes.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,25 @@ public function test_delete_code() {
$this->provider->delete_code( $user, $backup_codes[0] );
$this->assertEquals( 1, $this->provider->codes_remaining_for_user( $user ) );
}

public function test_backup_code_length_filter() {
$user = new WP_User( self::factory()->user->create() );

$code_default = $this->provider->generate_codes( $user, array( 'number' => 1 ) );

add_filter(
'two_factor_backup_code_length',
function() {
return 7;
}
);

$code_custom_length = $this->provider->generate_codes( $user, array( 'number' => 1 ) );

$this->assertNotEquals( strlen( $code_custom_length[0] ), strlen( $code_default[0] ), 'Backup code length can be adjusted via filter' );

$this->assertEquals( 7, strlen( $code_custom_length[0] ), 'Backup code length matches the filtered length' );

remove_all_filters( 'two_factor_backup_code_length' );
}
}
Loading

0 comments on commit f77f2ac

Please sign in to comment.