Skip to content

Commit

Permalink
Allow filtering the PHP classname used for a provider (#546)
Browse files Browse the repository at this point in the history
* Add a method to providers to specify the provider slug, separately from the class name.
* Refer to it as a Provider Name rather than Provider Class.
* Move get_instance() from individual classes to the abstract base, to allow individual classes to be extended.
* Add unit tests for the filter, and that replacing a provider with another works.
* Add unit tests for Two_Factor_Provider::get_instance().
  • Loading branch information
dd32 authored Apr 12, 2023
1 parent fd9a058 commit c490519
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 94 deletions.
57 changes: 33 additions & 24 deletions class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,17 +179,27 @@ public static function get_providers() {
/**
* For each filtered provider,
*/
foreach ( $providers as $class => $path ) {
foreach ( $providers as $provider_key => $path ) {
include_once $path;

$class = $provider_key;

/**
* Filters the classname for a provider. The dynamic portion of the filter is the defined providers key.
*
* @param string $class The PHP Classname of the provider.
* @param string $path The provided provider path to be included.
*/
$class = apply_filters( "two_factor_provider_classname_{$provider_key}", $class, $path );

/**
* Confirm that it's been successfully included before instantiating.
*/
if ( class_exists( $class ) ) {
try {
$providers[ $class ] = call_user_func( array( $class, 'get_instance' ) );
$providers[ $provider_key ] = call_user_func( array( $class, 'get_instance' ) );
} catch ( Exception $e ) {
unset( $providers[ $class ] );
unset( $providers[ $provider_key ] );
}
}
}
Expand Down Expand Up @@ -411,9 +421,9 @@ public static function get_available_providers_for_user( $user = null ) {
$enabled_providers = self::get_enabled_providers_for_user( $user );
$configured_providers = array();

foreach ( $providers as $classname => $provider ) {
if ( in_array( $classname, $enabled_providers, true ) && $provider->is_available_for_user( $user ) ) {
$configured_providers[ $classname ] = $provider;
foreach ( $providers as $provider_key => $provider ) {
if ( in_array( $provider_key, $enabled_providers, true ) && $provider->is_available_for_user( $user ) ) {
$configured_providers[ $provider_key ] = $provider;
}
}

Expand Down Expand Up @@ -712,10 +722,9 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg
$provider = call_user_func( array( $provider, 'get_instance' ) );
}

$provider_class = get_class( $provider );

$provider_key = $provider->get_key();
$available_providers = self::get_available_providers_for_user( $user );
$backup_providers = array_diff_key( $available_providers, array( $provider_class => null ) );
$backup_providers = array_diff_key( $available_providers, array( $provider_key => null ) );
$interim_login = isset( $_REQUEST['interim-login'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended

$rememberme = intval( self::rememberme() );
Expand All @@ -735,7 +744,7 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg
?>

<form name="validate_2fa_form" id="loginform" action="<?php echo esc_url( self::login_url( array( 'action' => 'validate_2fa' ), 'login_post' ) ); ?>" method="post" autocomplete="off">
<input type="hidden" name="provider" id="provider" value="<?php echo esc_attr( $provider_class ); ?>" />
<input type="hidden" name="provider" id="provider" value="<?php echo esc_attr( $provider_key ); ?>" />
<input type="hidden" name="wp-auth-id" id="wp-auth-id" value="<?php echo esc_attr( $user->ID ); ?>" />
<input type="hidden" name="wp-auth-nonce" id="wp-auth-nonce" value="<?php echo esc_attr( $login_nonce ); ?>" />
<?php if ( $interim_login ) { ?>
Expand All @@ -750,12 +759,12 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg

<?php
if ( 1 === count( $backup_providers ) ) :
$backup_classname = key( $backup_providers );
$backup_provider = $backup_providers[ $backup_classname ];
$login_url = self::login_url(
$backup_provider_key = key( $backup_providers );
$backup_provider = $backup_providers[ $backup_provider_key ];
$login_url = self::login_url(
array(
'action' => 'validate_2fa',
'provider' => $backup_classname,
'provider' => $backup_provider_key,
'wp-auth-id' => $user->ID,
'wp-auth-nonce' => $login_nonce,
'redirect_to' => $redirect_to,
Expand Down Expand Up @@ -787,11 +796,11 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg
</p>
<ul class="backup-methods">
<?php
foreach ( $backup_providers as $backup_classname => $backup_provider ) :
foreach ( $backup_providers as $backup_provider_key => $backup_provider ) :
$login_url = self::login_url(
array(
'action' => 'validate_2fa',
'provider' => $backup_classname,
'provider' => $backup_provider_key,
'wp-auth-id' => $user->ID,
'wp-auth-nonce' => $login_nonce,
'redirect_to' => $redirect_to,
Expand Down Expand Up @@ -1428,7 +1437,7 @@ public static function user_two_factor_options( $user ) {
$primary_provider = self::get_primary_provider_for_user( $user->ID );

if ( ! empty( $primary_provider ) && is_object( $primary_provider ) ) {
$primary_provider_key = get_class( $primary_provider );
$primary_provider_key = $primary_provider->get_key();
} else {
$primary_provider_key = null;
}
Expand All @@ -1452,24 +1461,24 @@ public static function user_two_factor_options( $user ) {
</tr>
</thead>
<tbody>
<?php foreach ( self::get_providers() as $class => $object ) : ?>
<?php foreach ( self::get_providers() as $provider_key => $object ) : ?>
<tr>
<th scope="row"><input id="enabled-<?php echo esc_attr( $class ); ?>" type="checkbox" name="<?php echo esc_attr( self::ENABLED_PROVIDERS_USER_META_KEY ); ?>[]" value="<?php echo esc_attr( $class ); ?>" <?php checked( in_array( $class, $enabled_providers, true ) ); ?> /></th>
<th scope="row"><input type="radio" name="<?php echo esc_attr( self::PROVIDER_USER_META_KEY ); ?>" value="<?php echo esc_attr( $class ); ?>" <?php checked( $class, $primary_provider_key ); ?> /></th>
<th scope="row"><input id="enabled-<?php echo esc_attr( $provider_key ); ?>" type="checkbox" name="<?php echo esc_attr( self::ENABLED_PROVIDERS_USER_META_KEY ); ?>[]" value="<?php echo esc_attr( $provider_key ); ?>" <?php checked( in_array( $provider_key, $enabled_providers, true ) ); ?> /></th>
<th scope="row"><input type="radio" name="<?php echo esc_attr( self::PROVIDER_USER_META_KEY ); ?>" value="<?php echo esc_attr( $provider_key ); ?>" <?php checked( $provider_key, $primary_provider_key ); ?> /></th>
<td>
<label class="two-factor-method-label" for="enabled-<?php echo esc_attr( $class ); ?>"><?php echo esc_html( $object->get_label() ); ?></label>
<label class="two-factor-method-label" for="enabled-<?php echo esc_attr( $provider_key ); ?>"><?php echo esc_html( $object->get_label() ); ?></label>
<?php
/**
* Fires after user options are shown.
*
* Use the {@see 'two_factor_user_options_' . $class } hook instead.
* Use the {@see 'two_factor_user_options_' . $provider_key } hook instead.
*
* @deprecated 0.7.0
*
* @param WP_User $user The user.
*/
do_action_deprecated( 'two-factor-user-options-' . $class, array( $user ), '0.7.0', 'two_factor_user_options_' . $class );
do_action( 'two_factor_user_options_' . $class, $user );
do_action_deprecated( 'two-factor-user-options-' . $provider_key, array( $user ), '0.7.0', 'two_factor_user_options_' . $provider_key );
do_action( 'two_factor_user_options_' . $provider_key, $user );
?>
</td>
</tr>
Expand Down
14 changes: 0 additions & 14 deletions providers/class-two-factor-backup-codes.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,6 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
*/
const NUMBER_OF_CODES = 10;

/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @since 0.1-dev
*/
public static function get_instance() {
static $instance;
$class = __CLASS__;
if ( ! is_a( $instance, $class ) ) {
$instance = new $class();
}
return $instance;
}

/**
* Class constructor.
*
Expand Down
14 changes: 0 additions & 14 deletions providers/class-two-factor-dummy.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,6 @@
*/
class Two_Factor_Dummy extends Two_Factor_Provider {

/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @since 0.1-dev
*/
public static function get_instance() {
static $instance;
$class = __CLASS__;
if ( ! is_a( $instance, $class ) ) {
$instance = new $class();
}
return $instance;
}

/**
* Class constructor.
*
Expand Down
14 changes: 0 additions & 14 deletions providers/class-two-factor-email.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,6 @@ class Two_Factor_Email extends Two_Factor_Provider {
*/
const INPUT_NAME_RESEND_CODE = 'two-factor-email-code-resend';

/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @since 0.1-dev
*/
public static function get_instance() {
static $instance;
$class = __CLASS__;
if ( ! is_a( $instance, $class ) ) {
$instance = new $class();
}
return $instance;
}

/**
* Class constructor.
*
Expand Down
15 changes: 0 additions & 15 deletions providers/class-two-factor-fido-u2f.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,6 @@ class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
*/
const U2F_ASSET_VERSION = '0.2.1';

/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @return \Two_Factor_FIDO_U2F
*/
public static function get_instance() {
static $instance;

if ( ! isset( $instance ) ) {
$instance = new self();
}

return $instance;
}

/**
* Class constructor.
*
Expand Down
28 changes: 28 additions & 0 deletions providers/class-two-factor-provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@
*/
abstract class Two_Factor_Provider {

/**
* Ensures only one instance of the provider class exists in memory at any one time.
*
* @since 0.1-dev
*/
public static function get_instance() {
static $instances = array();

$class_name = static::class;

if ( ! isset( $instances[ $class_name ] ) ) {
$instances[ $class_name ] = new $class_name;
}

return $instances[ $class_name ];
}

/**
* Class constructor.
*
Expand Down Expand Up @@ -41,6 +58,17 @@ public function print_label() {
echo esc_html( $this->get_label() );
}

/**
* Retrieves the provider key / slug.
*
* @since 0.9.0
*
* @return string
*/
public function get_key() {
return get_class( $this );
}

/**
* Prints the form that prompts the user to authenticate.
*
Expand Down
13 changes: 0 additions & 13 deletions providers/class-two-factor-totp.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,6 @@ class Two_Factor_Totp extends Two_Factor_Provider {
*/
private static $base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @codeCoverageIgnore
*/
public static function get_instance() {
static $instance;
if ( ! isset( $instance ) ) {
$instance = new self();
}
return $instance;
}

/**
* Class constructor. Sets up hooks, etc.
*
Expand Down
30 changes: 30 additions & 0 deletions tests/class-secure-dummy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/**
* Class for creating a dummy provider that never passes.
*
* This is a mock for unit testing the provider class name filter, and where authentication should never pass.
*
* @package Two_Factor
*/
class Two_Factor_Dummy_Secure extends Two_Factor_Dummy {

/**
* Pretend to be the Two_Factor_Dummy provider.
*/
public function get_key() {
return 'Two_Factor_Dummy';
}

/**
* Validates the users input token.
*
* In this class we just return false.
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ) {
return false;
}

}
Loading

0 comments on commit c490519

Please sign in to comment.