Initial geladen: WP App Portal
This commit is contained in:
+486
@@ -0,0 +1,486 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for creating a backup codes provider.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for creating a backup codes provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
class Two_Factor_Backup_Codes extends Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* The user meta backup codes key.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
const BACKUP_CODES_META_KEY = '_two_factor_backup_codes';
|
||||
|
||||
/**
|
||||
* The number backup codes.
|
||||
*
|
||||
* @type int
|
||||
*/
|
||||
const NUMBER_OF_CODES = 10;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
|
||||
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
|
||||
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts for backup codes.
|
||||
*
|
||||
* @since 0.10.0
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $hook_suffix Optional. The current admin page hook suffix.
|
||||
*/
|
||||
public function enqueue_assets( $hook_suffix = '' ) {
|
||||
wp_register_script(
|
||||
'two-factor-backup-codes-admin',
|
||||
plugins_url( 'js/backup-codes-admin.js', __FILE__ ),
|
||||
array( 'jquery', 'wp-api-request' ),
|
||||
TWO_FACTOR_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the rest-api endpoints required for this provider.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function register_rest_routes() {
|
||||
register_rest_route(
|
||||
Two_Factor_Core::REST_NAMESPACE,
|
||||
'/generate-backup-codes',
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'rest_generate_codes' ),
|
||||
'permission_callback' => function ( $request ) {
|
||||
return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] );
|
||||
},
|
||||
'args' => array(
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
),
|
||||
'enable_provider' => array(
|
||||
'required' => false,
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an admin notice when backup codes have run out.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function admin_notices() {
|
||||
$user = wp_get_current_user();
|
||||
|
||||
// Return if the provider is not enabled.
|
||||
if ( ! in_array( __CLASS__, Two_Factor_Core::get_enabled_providers_for_user( $user->ID ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return if we are not out of codes.
|
||||
if ( $this->is_available_for_user( $user ) ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="error">
|
||||
<p>
|
||||
<span>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
sprintf(
|
||||
/* translators: %s: URL for code regeneration */
|
||||
__( 'Two-Factor: You are out of recovery codes and need to <a href="%s">regenerate!</a>', 'two-factor' ),
|
||||
esc_url( get_edit_user_link( $user->ID ) . '#two-factor-backup-codes' )
|
||||
),
|
||||
array( 'a' => array( 'href' => true ) )
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function get_label() {
|
||||
return _x( 'Recovery Codes', 'Provider Label', 'two-factor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "continue with" text provider for the login screen.
|
||||
*
|
||||
* @since 0.9.0
|
||||
*/
|
||||
public function get_alternative_provider_label() {
|
||||
return __( 'Use a recovery code', 'two-factor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and codes are available for the user specified.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_available_for_user( $user ) {
|
||||
// Does this user have available codes?
|
||||
if ( 0 < self::codes_remaining_for_user( $user ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts markup at the end of the user profile field for this provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
public function user_options( $user ) {
|
||||
wp_localize_script(
|
||||
'two-factor-backup-codes-admin',
|
||||
'twoFactorBackupCodes',
|
||||
array(
|
||||
'restPath' => Two_Factor_Core::REST_NAMESPACE . '/generate-backup-codes',
|
||||
'userId' => $user->ID,
|
||||
)
|
||||
);
|
||||
wp_enqueue_script( 'two-factor-backup-codes-admin' );
|
||||
|
||||
$count = self::codes_remaining_for_user( $user );
|
||||
?>
|
||||
<div id="two-factor-backup-codes">
|
||||
<p class="two-factor-backup-codes-count">
|
||||
<?php
|
||||
echo esc_html(
|
||||
sprintf(
|
||||
/* translators: %s: count */
|
||||
_n( '%s unused code remaining, each recovery code can only be used once.', '%s unused codes remaining, each recovery code can only be used once.', $count, 'two-factor' ),
|
||||
$count
|
||||
)
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<p>
|
||||
<button type="button" class="button button-two-factor-backup-codes-generate button-secondary hide-if-no-js">
|
||||
<?php esc_html_e( 'Generate new recovery codes', 'two-factor' ); ?>
|
||||
</button>
|
||||
|
||||
<em><?php esc_html_e( 'This invalidates all currently stored codes.', 'two-factor' ); ?></em>
|
||||
</p>
|
||||
</div>
|
||||
<div class="two-factor-backup-codes-wrapper" style="display:none;">
|
||||
<div class="two-factor-backup-codes-list-wrap">
|
||||
<ol class="two-factor-backup-codes-unused-codes"></ol>
|
||||
</div>
|
||||
<p class="description"><?php esc_html_e( 'Write these down! Once you navigate away from this page, you will not be able to view these codes again.', 'two-factor' ); ?></p>
|
||||
<p>
|
||||
<a class="button button-two-factor-backup-codes-copy button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-copy-link"><?php esc_html_e( 'Copy Codes', 'two-factor' ); ?></a>
|
||||
<a class="button button-two-factor-backup-codes-download button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-download-link" download="two-factor-backup-codes.txt"><?php esc_html_e( 'Download Codes', 'two-factor' ); ?></a>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the backup code length for a user.
|
||||
*
|
||||
* @since 0.11.0
|
||||
*
|
||||
* @param WP_User $user User object.
|
||||
*
|
||||
* @return int Number of characters.
|
||||
*/
|
||||
private function get_backup_code_length( $user ) {
|
||||
/**
|
||||
* Filters the character count of the backup codes.
|
||||
*
|
||||
* @since 0.11.0
|
||||
*
|
||||
* @param int $code_length Length of the backup code. Default 8.
|
||||
* @param 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.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @param array $args Optional arguments for assigning new codes.
|
||||
* @return array
|
||||
*/
|
||||
public function generate_codes( $user, $args = array() ) {
|
||||
$codes = array();
|
||||
$codes_hashed = array();
|
||||
|
||||
// Check for arguments.
|
||||
if ( isset( $args['number'] ) ) {
|
||||
$num_codes = (int) $args['number'];
|
||||
} else {
|
||||
$num_codes = self::NUMBER_OF_CODES;
|
||||
}
|
||||
|
||||
// Append or replace (default).
|
||||
if ( isset( $args['method'] ) && 'append' === $args['method'] ) {
|
||||
$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_length );
|
||||
$codes_hashed[] = wp_hash_password( $code );
|
||||
$codes[] = $code;
|
||||
unset( $code );
|
||||
}
|
||||
|
||||
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $codes_hashed );
|
||||
|
||||
// Unhashed.
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates Backup Codes for returning through the WordPress Rest API.
|
||||
*
|
||||
* @since 0.8.0
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function rest_generate_codes( $request ) {
|
||||
$user_id = $request['user_id'];
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
|
||||
// Hardcode these, the user shouldn't be able to choose them.
|
||||
$args = array(
|
||||
'number' => self::NUMBER_OF_CODES,
|
||||
'method' => 'replace',
|
||||
);
|
||||
|
||||
// Setup the return data.
|
||||
$codes = $this->generate_codes( $user, $args );
|
||||
$count = self::codes_remaining_for_user( $user );
|
||||
$title = sprintf(
|
||||
/* translators: %s: the site's domain */
|
||||
__( 'Two-Factor Recovery Codes for %s', 'two-factor' ),
|
||||
home_url( '/' )
|
||||
);
|
||||
|
||||
// Generate download content.
|
||||
$download_link = 'data:application/text;charset=utf-8,';
|
||||
$download_link .= rawurlencode( "{$title}\r\n\r\n" );
|
||||
|
||||
$i = 1;
|
||||
foreach ( $codes as $code ) {
|
||||
$download_link .= rawurlencode( "{$i}. {$code}\r\n" );
|
||||
++$i;
|
||||
}
|
||||
|
||||
$i18n = array(
|
||||
/* translators: %s: count */
|
||||
'count' => esc_html( sprintf( _n( '%s unused code remaining, each recovery code can only be used once.', '%s unused codes remaining, each recovery code can only be used once.', $count, 'two-factor' ), $count ) ),
|
||||
);
|
||||
|
||||
if ( $request->get_param( 'enable_provider' ) && ! Two_Factor_Core::enable_provider_for_user( $user_id, 'Two_Factor_Backup_Codes' ) ) {
|
||||
return new WP_Error( 'db_error', __( 'Unable to enable recovery codes for this user.', 'two-factor' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
return array(
|
||||
'codes' => $codes,
|
||||
'download_link' => $download_link,
|
||||
'remaining' => $count,
|
||||
'i18n' => $i18n,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of unused codes for the specified user
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return int $int The number of unused codes remaining
|
||||
*/
|
||||
public static function codes_remaining_for_user( $user ) {
|
||||
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
|
||||
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
|
||||
return count( $backup_codes );
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in 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 );
|
||||
|
||||
?>
|
||||
<?php
|
||||
/**
|
||||
* Fires before the two-factor authentication prompt text.
|
||||
*
|
||||
* @since 0.15.0
|
||||
*
|
||||
* @param Two_Factor_Provider $provider The two-factor provider instance.
|
||||
*/
|
||||
do_action( 'two_factor_before_authentication_prompt', $this );
|
||||
?>
|
||||
<p class="two-factor-prompt"><?php esc_html_e( 'Enter a recovery code.', 'two-factor' ); ?></p>
|
||||
<?php
|
||||
/**
|
||||
* Fires after the two-factor authentication prompt text.
|
||||
*
|
||||
* @since 0.15.0
|
||||
*
|
||||
* @param Two_Factor_Provider $provider The two-factor provider instance.
|
||||
*/
|
||||
do_action( 'two_factor_after_authentication_prompt', $this );
|
||||
?>
|
||||
<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="<?php echo esc_attr( $code_placeholder ); ?>" data-digits="<?php echo esc_attr( $code_length ); ?>" />
|
||||
</p>
|
||||
<?php
|
||||
/**
|
||||
* Fires after the two-factor authentication input field.
|
||||
*
|
||||
* @since 0.15.0
|
||||
*
|
||||
* @param Two_Factor_Provider $provider The two-factor provider instance.
|
||||
*/
|
||||
do_action( 'two_factor_after_authentication_input', $this );
|
||||
?>
|
||||
<?php
|
||||
submit_button( __( 'Verify', 'two-factor' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the users input token.
|
||||
*
|
||||
* In this class we just return true.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_authentication( $user ) {
|
||||
$backup_code = $this->sanitize_code_from_request( 'two-factor-backup-code' );
|
||||
if ( ! $backup_code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->validate_code( $user, $backup_code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a backup code.
|
||||
*
|
||||
* Backup Codes are single use and are deleted upon a successful validation.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @param int $code The backup code.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_code( $user, $code ) {
|
||||
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
|
||||
|
||||
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
|
||||
foreach ( $backup_codes as $code_index => $code_hashed ) {
|
||||
if ( wp_check_password( $code, $code_hashed, $user->ID ) ) {
|
||||
$this->delete_code( $user, $code_hashed );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a backup code.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @param string $code_hashed The hashed the backup code.
|
||||
*/
|
||||
public function delete_code( $user, $code_hashed ) {
|
||||
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
|
||||
|
||||
// Delete the current code from the list since it's been used.
|
||||
$backup_codes = array_flip( $backup_codes );
|
||||
unset( $backup_codes[ $code_hashed ] );
|
||||
$backup_codes = array_values( array_flip( $backup_codes ) );
|
||||
|
||||
// Update the backup code master list.
|
||||
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $backup_codes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return user meta keys to delete during plugin uninstall.
|
||||
*
|
||||
* @since 0.10.0
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function uninstall_user_meta_keys() {
|
||||
return array(
|
||||
self::BACKUP_CODES_META_KEY,
|
||||
);
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for creating a dummy provider.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for creating a dummy provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
class Two_Factor_Dummy extends Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
protected function __construct() {
|
||||
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function get_label() {
|
||||
return _x( 'Dummy Method', 'Provider Label', 'two-factor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
public function authentication_page( $user ) {
|
||||
require_once ABSPATH . '/wp-admin/includes/template.php';
|
||||
?>
|
||||
<?php
|
||||
/** This action is documented in providers/class-two-factor-backup-codes.php */
|
||||
do_action( 'two_factor_before_authentication_prompt', $this );
|
||||
?>
|
||||
<p><?php esc_html_e( 'Are you really you?', 'two-factor' ); ?></p>
|
||||
<?php
|
||||
/** This action is documented in providers/class-two-factor-backup-codes.php */
|
||||
do_action( 'two_factor_after_authentication_prompt', $this );
|
||||
?>
|
||||
<?php
|
||||
/** This action is documented in providers/class-two-factor-backup-codes.php */
|
||||
do_action( 'two_factor_after_authentication_input', $this );
|
||||
?>
|
||||
<?php
|
||||
submit_button( __( 'Yup.', 'two-factor' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the users input token.
|
||||
*
|
||||
* In this class we just return true.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_authentication( $user ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and available for the user specified.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_available_for_user( $user ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts markup at the end of the user profile field for this provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
public function user_options( $user ) {}
|
||||
}
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for creating an email provider.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for creating an email provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
class Two_Factor_Email extends Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* The user meta token key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TOKEN_META_KEY = '_two_factor_email_token';
|
||||
|
||||
/**
|
||||
* Store the timestamp when the token was generated.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TOKEN_META_KEY_TIMESTAMP = '_two_factor_email_token_timestamp';
|
||||
|
||||
/**
|
||||
* Name of the input field used for code resend.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const INPUT_NAME_RESEND_CODE = 'two-factor-email-code-resend';
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function __construct() {
|
||||
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function get_label() {
|
||||
return _x( 'Email', 'Provider Label', 'two-factor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "continue with" text provider for the login screen.
|
||||
*
|
||||
* @since 0.9.0
|
||||
*/
|
||||
public function get_alternative_provider_label() {
|
||||
return __( 'Send a code to your email', 'two-factor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email token length.
|
||||
*
|
||||
* @since 0.11.0
|
||||
*
|
||||
* @return int Email token string length.
|
||||
*/
|
||||
private function get_token_length() {
|
||||
/**
|
||||
* Filters the number of characters in the email token.
|
||||
*
|
||||
* @since 0.11.0
|
||||
*
|
||||
* @param int $token_length Number of characters in the email token. Default 8.
|
||||
*/
|
||||
$token_length = (int) apply_filters( 'two_factor_email_token_length', 8 );
|
||||
|
||||
return $token_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the user token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return string
|
||||
*/
|
||||
public function generate_token( $user_id ) {
|
||||
$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 ) );
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a valid token already.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return boolean If user has a valid email token.
|
||||
*/
|
||||
public function user_has_token( $user_id ) {
|
||||
$hashed_token = $this->get_user_token( $user_id );
|
||||
|
||||
if ( ! empty( $hashed_token ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the user token validity timestamp expired.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function user_token_has_expired( $user_id ) {
|
||||
$token_lifetime = $this->user_token_lifetime( $user_id );
|
||||
$token_ttl = $this->user_token_ttl( $user_id );
|
||||
|
||||
// Invalid token lifetime is considered an expired token.
|
||||
if ( is_int( $token_lifetime ) && $token_lifetime <= $token_ttl ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lifetime of a user token in seconds.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*
|
||||
* @return integer|null Return `null` if the lifetime can't be measured.
|
||||
*/
|
||||
public function user_token_lifetime( $user_id ) {
|
||||
$timestamp = intval( get_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, true ) );
|
||||
|
||||
if ( ! empty( $timestamp ) ) {
|
||||
return time() - $timestamp;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the token time-to-live for a user.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function user_token_ttl( $user_id ) {
|
||||
$token_ttl = 15 * MINUTE_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Filters the number of seconds the email token is considered valid after generation.
|
||||
*
|
||||
* @since 0.6.0
|
||||
* @deprecated 0.11.0 Use {@see 'two_factor_email_token_ttl'} instead.
|
||||
*
|
||||
* @param int $token_ttl Token time-to-live in seconds.
|
||||
* @param int $user_id 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' );
|
||||
|
||||
/**
|
||||
* Filters the number of seconds the email token is considered valid after generation.
|
||||
*
|
||||
* @since 0.11.0
|
||||
*
|
||||
* @param int $token_ttl Token time-to-live in seconds.
|
||||
* @param int $user_id User ID.
|
||||
*/
|
||||
return (int) apply_filters( 'two_factor_email_token_ttl', $token_ttl, $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication token for the user.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return string|boolean User token or `false` if no token found.
|
||||
*/
|
||||
public function get_user_token( $user_id ) {
|
||||
$hashed_token = get_user_meta( $user_id, self::TOKEN_META_KEY, true );
|
||||
|
||||
if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
|
||||
return $hashed_token;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the user token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $token User token.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_token( $user_id, $token ) {
|
||||
$hashed_token = $this->get_user_token( $user_id );
|
||||
|
||||
// Bail if token is empty or it doesn't match.
|
||||
if ( empty( $hashed_token ) || ! hash_equals( wp_hash( $token ), $hashed_token ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->user_token_has_expired( $user_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the token can be used only once.
|
||||
$this->delete_token( $user_id );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*/
|
||||
public function delete_token( $user_id ) {
|
||||
delete_user_meta( $user_id, self::TOKEN_META_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the client IP address for the current request.
|
||||
*
|
||||
* @since 0.15.0
|
||||
*
|
||||
* Note that the IP address is used only for information purposes
|
||||
* and is expected to be configured correctly, if behind proxy.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function get_client_ip() {
|
||||
if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { // phpcs:ignore WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders -- don't have more reliable option for now.
|
||||
return preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__REMOTE_ADDR__ -- we're limit the allowed characters.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and email the user token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return bool Whether the email contents were sent successfully.
|
||||
*/
|
||||
public function generate_and_email_token( $user ) {
|
||||
$token = $this->generate_token( $user->ID );
|
||||
$remote_ip = $this->get_client_ip();
|
||||
$ttl_minutes = (int) ceil( $this->user_token_ttl( $user->ID ) / MINUTE_IN_SECONDS );
|
||||
|
||||
$subject = wp_strip_all_tags(
|
||||
sprintf(
|
||||
/* translators: %s: site name */
|
||||
__( '[%s] Login confirmation code', 'two-factor' ),
|
||||
wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES )
|
||||
)
|
||||
);
|
||||
|
||||
$message_parts = array(
|
||||
__( 'Please complete the login by entering the verification code below:', 'two-factor' ),
|
||||
$token,
|
||||
sprintf(
|
||||
/* translators: %d: number of minutes */
|
||||
__( 'This code will expire in %d minutes.', 'two-factor' ),
|
||||
$ttl_minutes
|
||||
),
|
||||
sprintf(
|
||||
/* translators: %1$s: IP address of user, %2$s: user login */
|
||||
__( 'A user from IP address %1$s has successfully authenticated as %2$s. If this wasn\'t you, please change your password.', 'two-factor' ),
|
||||
$remote_ip,
|
||||
$user->user_login
|
||||
),
|
||||
);
|
||||
|
||||
$message = wp_strip_all_tags( implode( "\n\n", $message_parts ) );
|
||||
|
||||
/**
|
||||
* Filters the token email subject.
|
||||
*
|
||||
* @since 0.5.2
|
||||
*
|
||||
* @param string $subject The email subject line.
|
||||
* @param int $user_id The ID of the user.
|
||||
*/
|
||||
$subject = apply_filters( 'two_factor_token_email_subject', $subject, $user->ID );
|
||||
|
||||
/**
|
||||
* Filters the token email message.
|
||||
*
|
||||
* @since 0.5.2
|
||||
*
|
||||
* @param string $message The email message.
|
||||
* @param string $token The token.
|
||||
* @param int $user_id The ID of the user.
|
||||
*/
|
||||
$message = apply_filters( 'two_factor_token_email_message', $message, $token, $user->ID );
|
||||
|
||||
return wp_mail( $user->user_email, $subject, $message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
public function authentication_page( $user ) {
|
||||
if ( ! $user ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->user_has_token( $user->ID ) || $this->user_token_has_expired( $user->ID ) ) {
|
||||
$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';
|
||||
?>
|
||||
<?php
|
||||
/** This action is documented in providers/class-two-factor-backup-codes.php */
|
||||
do_action( 'two_factor_before_authentication_prompt', $this );
|
||||
?>
|
||||
<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>
|
||||
<?php
|
||||
/** This action is documented in providers/class-two-factor-backup-codes.php */
|
||||
do_action( 'two_factor_after_authentication_prompt', $this );
|
||||
?>
|
||||
<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 ]*" autocomplete="one-time-code" placeholder="<?php echo esc_attr( $token_placeholder ); ?>" data-digits="<?php echo esc_attr( $token_length ); ?>" />
|
||||
</p>
|
||||
<?php
|
||||
/** This action is documented in providers/class-two-factor-backup-codes.php */
|
||||
do_action( 'two_factor_after_authentication_input', $this );
|
||||
?>
|
||||
<?php submit_button( __( 'Verify', 'two-factor' ) ); ?>
|
||||
<p class="two-factor-email-resend">
|
||||
<input type="submit" class="button" name="<?php echo esc_attr( self::INPUT_NAME_RESEND_CODE ); ?>" value="<?php esc_attr_e( 'Resend Code', 'two-factor' ); ?>" />
|
||||
</p>
|
||||
<?php wp_enqueue_script( 'two-factor-login' ); ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the email code if missing or requested. Stop the authentication
|
||||
* validation if a new token has been generated and sent.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function pre_process_authentication( $user ) {
|
||||
if ( isset( $user->ID ) && isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- non-distructive option that relies on user state.
|
||||
$this->generate_and_email_token( $user );
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the users input token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_authentication( $user ) {
|
||||
$code = $this->sanitize_code_from_request( 'two-factor-email-code' );
|
||||
if ( ! isset( $user->ID ) || ! $code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->validate_token( $user->ID, $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and available for the user specified.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_available_for_user( $user ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts markup at the end of the user profile field for this provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
public function user_options( $user ) {
|
||||
$email = $user->user_email;
|
||||
?>
|
||||
<p>
|
||||
<?php
|
||||
echo esc_html(
|
||||
sprintf(
|
||||
/* translators: %s: email address */
|
||||
__( 'Authentication codes will be sent to %s.', 'two-factor' ),
|
||||
$email
|
||||
)
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Return user meta keys to delete during plugin uninstall.
|
||||
*
|
||||
* @since 0.10.0
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function uninstall_user_meta_keys() {
|
||||
return array(
|
||||
self::TOKEN_META_KEY,
|
||||
self::TOKEN_META_KEY_TIMESTAMP,
|
||||
);
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract class for creating two factor authentication providers.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstract class for creating two factor authentication providers.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
protected function __construct() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function get_label();
|
||||
|
||||
/**
|
||||
* Returns the "continue with" text provider for the login screen.
|
||||
*
|
||||
* @since 0.9.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_alternative_provider_label() {
|
||||
return sprintf(
|
||||
/* translators: the two factor provider name */
|
||||
__( 'Use %s', 'two-factor' ),
|
||||
$this->get_label()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
abstract public function authentication_page( $user );
|
||||
|
||||
/**
|
||||
* Allow providers to do extra processing before the authentication.
|
||||
* Return `true` to prevent the authentication and render the
|
||||
* authentication page.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function pre_process_authentication( $user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the users input token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
abstract public function validate_authentication( $user );
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and available for the user specified.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
abstract public function is_available_for_user( $user );
|
||||
|
||||
/**
|
||||
* If this provider should be available for the user.
|
||||
*
|
||||
* @since 0.13.0
|
||||
*
|
||||
* @param WP_User|int $user WP_User object, user ID or null to resolve the current user.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_supported_for_user( $user = null ) {
|
||||
$providers = Two_Factor_Core::get_supported_providers_for_user( $user );
|
||||
|
||||
return isset( $providers[ static::class ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random eight-digit string to send out as an auth code.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param int $length The code length.
|
||||
* @param string|array $chars Valid auth code characters.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_code( $length = 8, $chars = '1234567890' ) {
|
||||
$code = '';
|
||||
if ( is_array( $chars ) ) {
|
||||
$chars = implode( '', $chars );
|
||||
}
|
||||
for ( $i = 0; $i < $length; $i++ ) {
|
||||
$code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a numeric code to be used as an auth code.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @param string $field The _REQUEST field to check for the code.
|
||||
* @param int $length The valid expected length of the field.
|
||||
* @return false|string Auth code on success, false if the field is not set or not expected length.
|
||||
*/
|
||||
public static function sanitize_code_from_request( $field, $length = 0 ) {
|
||||
if ( empty( $_REQUEST[ $field ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$code = wp_unslash( $_REQUEST[ $field ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, handled by the core method already.
|
||||
$code = preg_replace( '/\s+/', '', $code );
|
||||
|
||||
// Maybe validate the length.
|
||||
if ( $length && strlen( $code ) !== $length ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string) $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user meta keys that need to be deletated on plugin uninstall.
|
||||
*
|
||||
* @since 0.10.0
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function uninstall_user_meta_keys() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the option keys that need to be deleted on plugin uninstall.
|
||||
*
|
||||
* @since 0.10.0
|
||||
*
|
||||
* Note: this method doesn't have access to the instantiated provider object.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function uninstall_options() {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
+905
@@ -0,0 +1,905 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for creating a Time Based One-Time Password provider.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class Two_Factor_Totp
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* The user meta key for the TOTP Secret key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SECRET_META_KEY = '_two_factor_totp_key';
|
||||
|
||||
/**
|
||||
* The user meta key for the last successful TOTP token timestamp logged in with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const LAST_SUCCESSFUL_LOGIN_META_KEY = '_two_factor_totp_last_successful_login';
|
||||
|
||||
const DEFAULT_KEY_BIT_SIZE = 160;
|
||||
const DEFAULT_CRYPTO = 'sha1';
|
||||
const DEFAULT_DIGIT_COUNT = 6;
|
||||
const DEFAULT_TIME_STEP_SEC = 30;
|
||||
const DEFAULT_TIME_STEP_ALLOWANCE = 4;
|
||||
|
||||
/**
|
||||
* Characters used in base32 encoding.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
|
||||
/**
|
||||
* Class constructor. Sets up hooks, etc.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
|
||||
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_two_factor_options' ) );
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp returned by time()
|
||||
*
|
||||
* @var int $now
|
||||
*/
|
||||
private static $now;
|
||||
|
||||
/**
|
||||
* Override time() in the current object for testing.
|
||||
*
|
||||
* @since 0.15.0
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private static function time() {
|
||||
return self::$now ? self::$now : time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the internal state of time() invocations for deterministic generation.
|
||||
*
|
||||
* @since 0.15.0
|
||||
*
|
||||
* @param int $now Timestamp to use when overriding time().
|
||||
*/
|
||||
public static function set_time( $now ) {
|
||||
self::$now = $now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the rest-api endpoints required for this provider.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function register_rest_routes() {
|
||||
register_rest_route(
|
||||
Two_Factor_Core::REST_NAMESPACE,
|
||||
'/totp',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => array( $this, 'rest_delete_totp' ),
|
||||
'permission_callback' => function ( $request ) {
|
||||
return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] );
|
||||
},
|
||||
'args' => array(
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'rest_setup_totp' ),
|
||||
'permission_callback' => function ( $request ) {
|
||||
return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] );
|
||||
},
|
||||
'args' => array(
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
),
|
||||
'key' => array(
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'validate_callback' => null, // Note: validation handled in ::rest_setup_totp().
|
||||
),
|
||||
'code' => array(
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'validate_callback' => null, // Note: validation handled in ::rest_setup_totp().
|
||||
),
|
||||
'enable_provider' => array(
|
||||
'required' => false,
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
public function get_label() {
|
||||
return _x( 'Authenticator App', 'Provider Label', 'two-factor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "continue with" text provider for the login screen.
|
||||
*
|
||||
* @since 0.9.0
|
||||
*/
|
||||
public function get_alternative_provider_label() {
|
||||
return __( 'Use your authenticator app for time-based one-time passwords (TOTP)', 'two-factor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @param string $hook_suffix Hook suffix.
|
||||
*/
|
||||
public function enqueue_assets( $hook_suffix ) {
|
||||
$environment_prefix = file_exists( TWO_FACTOR_DIR . '/dist' ) ? '/dist' : '';
|
||||
|
||||
wp_register_script(
|
||||
'two-factor-qr-code-generator',
|
||||
plugins_url( $environment_prefix . '/includes/qrcode-generator/qrcode.js', __DIR__ ),
|
||||
array(),
|
||||
TWO_FACTOR_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_register_script(
|
||||
'two-factor-totp-qrcode',
|
||||
plugins_url( 'js/totp-admin-qrcode.js', __FILE__ ),
|
||||
array( 'two-factor-qr-code-generator' ),
|
||||
TWO_FACTOR_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_register_script(
|
||||
'two-factor-totp-admin',
|
||||
plugins_url( 'js/totp-admin.js', __FILE__ ),
|
||||
array( 'jquery', 'wp-api-request', 'two-factor-qr-code-generator' ),
|
||||
TWO_FACTOR_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rest API endpoint for handling deactivation of TOTP.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @param WP_REST_Request $request The Rest Request object.
|
||||
* @return WP_Error|array Array of data on success, WP_Error on error.
|
||||
*/
|
||||
public function rest_delete_totp( $request ) {
|
||||
$user_id = $request['user_id'];
|
||||
$user = get_user_by( 'id', $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 ) );
|
||||
}
|
||||
|
||||
$this->delete_user_totp_key( $user_id );
|
||||
|
||||
ob_start();
|
||||
$this->user_two_factor_options( $user );
|
||||
$html = ob_get_clean();
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API endpoint for setting up TOTP.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @param WP_REST_Request $request The Rest Request object.
|
||||
* @return WP_Error|array Array of data on success, WP_Error on error.
|
||||
*/
|
||||
public function rest_setup_totp( $request ) {
|
||||
$user_id = $request['user_id'];
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
|
||||
$key = $request['key'];
|
||||
$code = preg_replace( '/\s+/', '', $request['code'] );
|
||||
|
||||
if ( ! $this->is_valid_key( $key ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid Two Factor Authentication secret key.', 'two-factor' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
if ( ! $this->is_valid_authcode( $key, $code ) ) {
|
||||
return new WP_Error( 'invalid_key_code', __( 'Invalid Two Factor Authentication code.', 'two-factor' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
if ( ! $this->set_user_totp_key( $user_id, $key ) ) {
|
||||
return new WP_Error( 'db_error', __( 'Unable to save Two Factor Authentication code. Please re-scan the QR code and enter the code provided by your application.', 'two-factor' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
if ( $request->get_param( 'enable_provider' ) && ! Two_Factor_Core::enable_provider_for_user( $user_id, 'Two_Factor_Totp' ) ) {
|
||||
return new WP_Error( 'db_error', __( 'Unable to enable TOTP provider for this user.', 'two-factor' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
ob_start();
|
||||
$this->user_two_factor_options( $user );
|
||||
$html = ob_get_clean();
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a URL that can be used to create a QR code.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @param WP_User $user The user to generate a URL for.
|
||||
* @param string $secret_key The secret key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function generate_qr_code_url( $user, $secret_key ) {
|
||||
$issuer = get_bloginfo( 'name', 'display' );
|
||||
|
||||
/**
|
||||
* Filters the Issuer for the TOTP.
|
||||
*
|
||||
* Must follow the TOTP format for a "issuer". Do not URL Encode.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format#issuer
|
||||
*
|
||||
* @param string $issuer The issuer for TOTP.
|
||||
*/
|
||||
$issuer = apply_filters( 'two_factor_totp_issuer', $issuer );
|
||||
|
||||
/**
|
||||
* Filters the Label for the TOTP.
|
||||
*
|
||||
* Must follow the TOTP format for a "label". Do not URL Encode.
|
||||
*
|
||||
* @since 0.4.7
|
||||
*
|
||||
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format#label
|
||||
*
|
||||
* @param string $totp_title The label for the TOTP.
|
||||
* @param WP_User $user The User object.
|
||||
* @param string $issuer The issuer of the TOTP. This should be the prefix of the result.
|
||||
*/
|
||||
$totp_title = apply_filters( 'two_factor_totp_title', $issuer . ':' . $user->user_login, $user, $issuer );
|
||||
|
||||
$totp_url = add_query_arg(
|
||||
array(
|
||||
'secret' => rawurlencode( $secret_key ),
|
||||
'issuer' => rawurlencode( $issuer ),
|
||||
),
|
||||
'otpauth://totp/' . rawurlencode( $totp_title )
|
||||
);
|
||||
|
||||
/**
|
||||
* Filters the TOTP generated URL.
|
||||
*
|
||||
* Must follow the TOTP format. Do not URL Encode.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
*
|
||||
* @param string $totp_url The TOTP URL.
|
||||
* @param WP_User $user The user object.
|
||||
*/
|
||||
$totp_url = apply_filters( 'two_factor_totp_url', $totp_url, $user );
|
||||
$totp_url = esc_url_raw( $totp_url, array( 'otpauth' ) );
|
||||
|
||||
return $totp_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display TOTP options on the user settings page.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param WP_User $user The current user being edited.
|
||||
* @return void
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function user_two_factor_options( $user ) {
|
||||
if ( ! isset( $user->ID ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$key = $this->get_user_totp_key( $user->ID );
|
||||
|
||||
wp_localize_script(
|
||||
'two-factor-totp-admin',
|
||||
'twoFactorTotpAdmin',
|
||||
array(
|
||||
'restPath' => Two_Factor_Core::REST_NAMESPACE . '/totp',
|
||||
'userId' => $user->ID,
|
||||
'qrCodeAriaLabel' => __( 'Authenticator App QR Code', 'two-factor' ),
|
||||
)
|
||||
);
|
||||
wp_enqueue_script( 'two-factor-totp-admin' );
|
||||
|
||||
?>
|
||||
<div id="two-factor-totp-options">
|
||||
<?php
|
||||
if ( empty( $key ) ) :
|
||||
$key = $this->generate_key();
|
||||
$totp_url = $this->generate_qr_code_url( $user, $key );
|
||||
?>
|
||||
<p>
|
||||
<?php esc_html_e( 'Please follow these steps in order to complete setup:', 'two-factor' ); ?>
|
||||
</p>
|
||||
<ol class="totp-steps">
|
||||
<li>
|
||||
<?php esc_html_e( 'Install an authenticator app on your desktop/laptop and/or phone. Popular examples are Microsoft Authenticator, Google Authenticator and Authy.', 'two-factor' ); ?>
|
||||
</li>
|
||||
<li>
|
||||
<?php esc_html_e( 'Scan this QR code using the app you installed:', 'two-factor' ); ?>
|
||||
<p id="two-factor-qr-code">
|
||||
<a href="<?php echo esc_url( $totp_url, array( 'otpauth' ) ); ?>">
|
||||
<?php esc_html_e( 'Loading…', 'two-factor' ); ?>
|
||||
<img src="<?php echo esc_url( admin_url( 'images/spinner.gif' ) ); ?>" alt="" />
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<?php
|
||||
esc_html_e(
|
||||
'If scanning isn\'t possible or doesn\'t work, click on the QR code or use the secret key shown below to add the account to your chosen app:',
|
||||
'two-factor'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<p>
|
||||
<code><?php echo esc_html( $key ); ?></code>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><?php esc_html_e( 'Enter the code generated by the Authenticator app to complete the setup:', 'two-factor' ); ?></p>
|
||||
<p>
|
||||
<input type="hidden" id="two-factor-totp-key" name="two-factor-totp-key" value="<?php echo esc_attr( $key ); ?>" />
|
||||
<label for="two-factor-totp-authcode">
|
||||
<?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?>
|
||||
<?php
|
||||
/* translators: Example auth code. */
|
||||
$placeholder = sprintf( __( 'eg. %s', 'two-factor' ), '123456' );
|
||||
?>
|
||||
<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( 'Verify', 'two-factor' ); ?>" />
|
||||
</p>
|
||||
<p class="description">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: 1: server date and time */
|
||||
esc_html__( 'If the code is rejected, check that your web server time is accurate: %1$s. Your device and server times must match.', 'two-factor' ),
|
||||
sprintf(
|
||||
'<time class="two-factor-server-datetime-epoch" datetime="%1$s">%2$s (%3$s)</time>',
|
||||
esc_attr( wp_date( 'c' ) ),
|
||||
esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ),
|
||||
esc_html( wp_timezone_string() )
|
||||
)
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
<?php
|
||||
wp_localize_script(
|
||||
'two-factor-totp-qrcode',
|
||||
'twoFactorTotpQrcode',
|
||||
array(
|
||||
'totpUrl' => $totp_url,
|
||||
'qrCodeLabel' => __( 'Authenticator App QR Code', 'two-factor' ),
|
||||
)
|
||||
);
|
||||
wp_enqueue_script( 'two-factor-totp-qrcode' );
|
||||
?>
|
||||
<?php else : ?>
|
||||
<p class="success">
|
||||
<?php esc_html_e( 'An authenticator app is currently configured. You will need to re-scan the QR code on all devices if reset.', 'two-factor' ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<button type="button" class="button button-secondary reset-totp-key hide-if-no-js">
|
||||
<?php esc_html_e( 'Reset authenticator app', 'two-factor' ); ?>
|
||||
</button>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the TOTP secret key for a user.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_user_totp_key( $user_id ) {
|
||||
return (string) get_user_meta( $user_id, self::SECRET_META_KEY, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the TOTP secret key for a user.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $key TOTP secret key.
|
||||
*
|
||||
* @return boolean If the key was stored successfully.
|
||||
*/
|
||||
public function set_user_totp_key( $user_id, $key ) {
|
||||
return update_user_meta( $user_id, self::SECRET_META_KEY, $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the TOTP secret key for a user.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return boolean If the key was deleted successfully.
|
||||
*/
|
||||
public function delete_user_totp_key( $user_id ) {
|
||||
delete_user_meta( $user_id, self::LAST_SUCCESSFUL_LOGIN_META_KEY );
|
||||
return delete_user_meta( $user_id, self::SECRET_META_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the TOTP secret key has a proper format.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $key TOTP secret key.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_valid_key( $key ) {
|
||||
$check = sprintf( '/^[%s]+$/', self::$base_32_chars );
|
||||
|
||||
if ( 1 === preg_match( $check, $key ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates authentication.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @return bool Whether the user gave a valid code
|
||||
*/
|
||||
public function validate_authentication( $user ) {
|
||||
$code = $this->sanitize_code_from_request( 'authcode', self::DEFAULT_DIGIT_COUNT );
|
||||
if ( ! $code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->validate_code_for_user( $user, $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an authentication code for a given user, preventing re-use and older TOTP keys.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @param int $code The TOTP token to validate.
|
||||
*
|
||||
* @return bool Whether the code is valid for the user and a newer code has not been used.
|
||||
*/
|
||||
public function validate_code_for_user( $user, $code ) {
|
||||
$valid_timestamp = $this->get_authcode_valid_ticktime(
|
||||
$this->get_user_totp_key( $user->ID ),
|
||||
$code
|
||||
);
|
||||
|
||||
if ( ! $valid_timestamp ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$last_totp_login = (int) get_user_meta( $user->ID, self::LAST_SUCCESSFUL_LOGIN_META_KEY, true );
|
||||
|
||||
// The TOTP authentication is not valid, if we've seen the same or newer code.
|
||||
if ( $last_totp_login && $last_totp_login >= $valid_timestamp ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update_user_meta( $user->ID, self::LAST_SUCCESSFUL_LOGIN_META_KEY, $valid_timestamp );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a given code is valid for a given key, allowing for a certain amount of time drift.
|
||||
*
|
||||
* @since 0.15.0
|
||||
*
|
||||
* @param string $key The share secret key to use.
|
||||
* @param string $authcode The code to test.
|
||||
* @param string $hash The hash used to calculate the code.
|
||||
* @param int $time_step The size of the time step.
|
||||
*
|
||||
* @return bool Whether the code is valid within the time frame.
|
||||
*/
|
||||
public static function is_valid_authcode( $key, $authcode, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
|
||||
return (bool) self::get_authcode_valid_ticktime( $key, $authcode, $hash, $time_step );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given code is valid for a given key, allowing for a certain amount of time drift.
|
||||
*
|
||||
* @since 0.15.0
|
||||
*
|
||||
* @param string $key The share secret key to use.
|
||||
* @param string $authcode The code to test.
|
||||
* @param string $hash The hash used to calculate the code.
|
||||
* @param int $time_step The size of the time step.
|
||||
*
|
||||
* @return false|int Returns the timestamp of the authcode on success, False otherwise.
|
||||
*/
|
||||
public static function get_authcode_valid_ticktime( $key, $authcode, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
|
||||
/**
|
||||
* Filter the maximum ticks to allow when checking valid codes.
|
||||
*
|
||||
* Ticks are the allowed offset from the correct time in 30 second increments,
|
||||
* so the default of 4 allows codes that are two minutes to either side of server time.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @deprecated 0.7.0 Use {@see 'two_factor_totp_time_step_allowance'} instead.
|
||||
*
|
||||
* @param int $max_ticks Max ticks of time correction to allow. Default 4.
|
||||
*/
|
||||
$max_ticks = apply_filters_deprecated( 'two-factor-totp-time-step-allowance', array( self::DEFAULT_TIME_STEP_ALLOWANCE ), '0.7.0', 'two_factor_totp_time_step_allowance' );
|
||||
|
||||
/**
|
||||
* Filters the maximum ticks to allow when checking valid codes.
|
||||
*
|
||||
* Ticks are the allowed offset from the correct time in 30 second increments,
|
||||
* so the default of 4 allows codes that are two minutes to either side of server time.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param int $max_ticks Max ticks of time correction to allow. Default 4.
|
||||
*/
|
||||
$max_ticks = apply_filters( 'two_factor_totp_time_step_allowance', self::DEFAULT_TIME_STEP_ALLOWANCE );
|
||||
|
||||
// Array of all ticks to allow, sorted using absolute value to test closest match first.
|
||||
$ticks = range( - $max_ticks, $max_ticks );
|
||||
usort( $ticks, array( __CLASS__, 'abssort' ) );
|
||||
|
||||
$time = (int) floor( self::time() / $time_step );
|
||||
|
||||
$digits = strlen( $authcode );
|
||||
|
||||
foreach ( $ticks as $offset ) {
|
||||
$log_time = (int) ( $time + $offset );
|
||||
if ( hash_equals( self::calc_totp( $key, $log_time, $digits, $hash, $time_step ), $authcode ) ) {
|
||||
// Return the tick timestamp.
|
||||
return (int) ( $log_time * self::DEFAULT_TIME_STEP_SEC );
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates key
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param int $bitsize Nume of bits to use for key.
|
||||
*
|
||||
* @return string $bitsize long string composed of available base32 chars.
|
||||
*/
|
||||
public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) {
|
||||
$bytes = ceil( $bitsize / 8 );
|
||||
$secret = wp_generate_password( $bytes, true, true );
|
||||
|
||||
return self::base32_encode( $secret );
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack stuff. We're currently only using this to pack integers, however the generic `pack` method can handle mixed.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param int $value The value to be packed.
|
||||
*
|
||||
* @return string Binary packed string.
|
||||
*/
|
||||
public static function pack64( int $value ): string {
|
||||
// Native 64-bit support (modern PHP on 64-bit builds).
|
||||
if ( 8 === PHP_INT_SIZE ) {
|
||||
return pack( 'J', $value );
|
||||
}
|
||||
|
||||
// 32-bit PHP fallback
|
||||
$higher = ( $value >> 32 ) & 0xFFFFFFFF;
|
||||
$lower = $value & 0xFFFFFFFF;
|
||||
|
||||
return pack( 'NN', $higher, $lower );
|
||||
}
|
||||
|
||||
/**
|
||||
* Pad a short secret with bytes from the same until it's the correct length
|
||||
* for hashing.
|
||||
*
|
||||
* @since 0.15.0
|
||||
*
|
||||
* @param string $secret Secret key to pad.
|
||||
* @param int $length Byte length of the desired padded secret.
|
||||
*
|
||||
* @throws InvalidArgumentException If the secret or length are invalid.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function pad_secret( $secret, $length ) {
|
||||
if ( empty( $secret ) ) {
|
||||
throw new InvalidArgumentException( 'Secret must be non-empty!' );
|
||||
}
|
||||
|
||||
$length = intval( $length );
|
||||
if ( $length <= 0 ) {
|
||||
throw new InvalidArgumentException( 'Padding length must be non-zero' );
|
||||
}
|
||||
|
||||
return str_pad( $secret, $length, $secret, STR_PAD_RIGHT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a valid code given the shared secret key
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $key The shared secret key to use for calculating code.
|
||||
* @param mixed $step_count The time step used to calculate the code, which is the floor of time() divided by step size.
|
||||
* @param int $digits The number of digits in the returned code.
|
||||
* @param string $hash The hash used to calculate the code.
|
||||
* @param int $time_step The size of the time step.
|
||||
*
|
||||
* @throws InvalidArgumentException If the hash type is invalid.
|
||||
*
|
||||
* @return string The totp code
|
||||
*/
|
||||
public static function calc_totp( $key, $step_count = false, $digits = self::DEFAULT_DIGIT_COUNT, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
|
||||
$secret = self::base32_decode( $key );
|
||||
|
||||
switch ( $hash ) {
|
||||
case 'sha1':
|
||||
$secret = self::pad_secret( $secret, 20 );
|
||||
break;
|
||||
case 'sha256':
|
||||
$secret = self::pad_secret( $secret, 32 );
|
||||
break;
|
||||
case 'sha512':
|
||||
$secret = self::pad_secret( $secret, 64 );
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException( 'Invalid hash type specified!' );
|
||||
}
|
||||
|
||||
if ( false === $step_count ) {
|
||||
$step_count = floor( self::time() / $time_step );
|
||||
}
|
||||
|
||||
$timestamp = self::pack64( $step_count );
|
||||
|
||||
$hash = hash_hmac( $hash, $timestamp, $secret, true );
|
||||
|
||||
$offset = ord( $hash[ strlen( $hash ) - 1 ] ) & 0xf;
|
||||
|
||||
$code = (
|
||||
( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) |
|
||||
( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) |
|
||||
( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) |
|
||||
( ord( $hash[ $offset + 3 ] ) & 0xff )
|
||||
) % pow( 10, $digits );
|
||||
|
||||
return str_pad( $code, $digits, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and available for the user specified.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_available_for_user( $user ) {
|
||||
// Only available if the secret key has been saved for the user.
|
||||
$key = $this->get_user_totp_key( $user->ID );
|
||||
|
||||
return ! empty( $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function authentication_page( $user ) {
|
||||
require_once ABSPATH . '/wp-admin/includes/template.php';
|
||||
?>
|
||||
<?php
|
||||
/** This action is documented in providers/class-two-factor-backup-codes.php */
|
||||
do_action( 'two_factor_before_authentication_prompt', $this );
|
||||
?>
|
||||
<p class="two-factor-prompt">
|
||||
<?php esc_html_e( 'Enter the code generated by your authenticator app.', 'two-factor' ); ?>
|
||||
</p>
|
||||
<?php
|
||||
/** This action is documented in providers/class-two-factor-backup-codes.php */
|
||||
do_action( 'two_factor_after_authentication_prompt', $this );
|
||||
?>
|
||||
<p>
|
||||
<label for="authcode"><?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?></label>
|
||||
<input type="text" inputmode="numeric" 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>
|
||||
<?php
|
||||
/** This action is documented in providers/class-two-factor-backup-codes.php */
|
||||
do_action( 'two_factor_after_authentication_input', $this );
|
||||
?>
|
||||
<?php
|
||||
wp_enqueue_script( 'two-factor-login' );
|
||||
|
||||
submit_button( __( 'Verify', 'two-factor' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a base32 encoded string.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $input String to be encoded using base32.
|
||||
*
|
||||
* @return string base32 encoded string without padding.
|
||||
*/
|
||||
public static function base32_encode( $input ) {
|
||||
if ( empty( $input ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$binary_string = '';
|
||||
|
||||
foreach ( str_split( $input ) as $character ) {
|
||||
$binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
$five_bit_sections = str_split( $binary_string, 5 );
|
||||
$base32_string = '';
|
||||
|
||||
foreach ( $five_bit_sections as $five_bit_section ) {
|
||||
$base32_string .= self::$base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ];
|
||||
}
|
||||
|
||||
return $base32_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a base32 string and return a binary representation
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $base32_string The base 32 string to decode.
|
||||
*
|
||||
* @throws Exception If string contains non-base32 characters.
|
||||
*
|
||||
* @return string Binary representation of decoded string
|
||||
*/
|
||||
public static function base32_decode( $base32_string ) {
|
||||
|
||||
$base32_string = strtoupper( $base32_string );
|
||||
|
||||
if ( ! preg_match( '/^[' . self::$base_32_chars . ']+$/', $base32_string, $match ) ) {
|
||||
throw new Exception( 'Invalid characters in the base32 string.' );
|
||||
}
|
||||
|
||||
$l = strlen( $base32_string );
|
||||
$n = 0;
|
||||
$j = 0;
|
||||
$binary = '';
|
||||
|
||||
for ( $i = 0; $i < $l; $i++ ) {
|
||||
|
||||
$n = $n << 5; // Move buffer left by 5 to make room.
|
||||
$n = $n + strpos( self::$base_32_chars, $base32_string[ $i ] ); // Add value into buffer.
|
||||
$j += 5; // Keep track of number of bits in buffer.
|
||||
|
||||
if ( $j >= 8 ) {
|
||||
$j -= 8;
|
||||
$binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
|
||||
}
|
||||
}
|
||||
|
||||
return $binary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used with usort to sort an array by distance from 0
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param int $a First array element.
|
||||
* @param int $b Second array element.
|
||||
*
|
||||
* @return int -1, 0, or 1 as needed by usort
|
||||
*/
|
||||
private static function abssort( $a, $b ) {
|
||||
$a = abs( $a );
|
||||
$b = abs( $b );
|
||||
if ( $a === $b ) {
|
||||
return 0;
|
||||
}
|
||||
return ( $a < $b ) ? -1 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return user meta keys to delete during plugin uninstall.
|
||||
*
|
||||
* @since 0.10.0
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function uninstall_user_meta_keys() {
|
||||
return array(
|
||||
self::SECRET_META_KEY,
|
||||
self::LAST_SUCCESSFUL_LOGIN_META_KEY,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* global twoFactorBackupCodes, wp, navigator, document, jQuery */
|
||||
( function( $ ) {
|
||||
$( '.button-two-factor-backup-codes-copy' ).click( function() {
|
||||
var csvCodes = $( '.two-factor-backup-codes-wrapper' ).data( 'codesCsv' ),
|
||||
$temp;
|
||||
|
||||
if ( ! csvCodes ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( navigator.clipboard && navigator.clipboard.writeText ) {
|
||||
navigator.clipboard.writeText( csvCodes );
|
||||
return;
|
||||
}
|
||||
|
||||
$temp = $( '<textarea>' ).val( csvCodes ).css( { position: 'absolute', left: '-9999px' } );
|
||||
$( 'body' ).append( $temp );
|
||||
$temp[0].select();
|
||||
document.execCommand( 'copy' );
|
||||
$temp.remove();
|
||||
} );
|
||||
|
||||
$( '.button-two-factor-backup-codes-generate' ).click( function() {
|
||||
wp.apiRequest( {
|
||||
method: 'POST',
|
||||
path: twoFactorBackupCodes.restPath,
|
||||
data: {
|
||||
user_id: parseInt( twoFactorBackupCodes.userId, 10 )
|
||||
}
|
||||
} ).then( function( response ) {
|
||||
var $codesList = $( '.two-factor-backup-codes-unused-codes' ),
|
||||
i;
|
||||
|
||||
$( '.two-factor-backup-codes-wrapper' ).show();
|
||||
$codesList.html( '' );
|
||||
$codesList.css( { 'column-count': 2, 'column-gap': '80px', 'max-width': '420px' } );
|
||||
$( '.two-factor-backup-codes-wrapper' ).data( 'codesCsv', response.codes.join( ',' ) );
|
||||
|
||||
// Append the codes.
|
||||
for ( i = 0; i < response.codes.length; i++ ) {
|
||||
$codesList.append( '<li class="two-factor-backup-codes-token">' + response.codes[ i ] + '</li>' );
|
||||
}
|
||||
|
||||
// Update counter.
|
||||
$( '.two-factor-backup-codes-count' ).html( response.i18n.count );
|
||||
$( '#two-factor-backup-codes-download-link' ).attr( 'href', response.download_link );
|
||||
} );
|
||||
} );
|
||||
}( jQuery ) );
|
||||
@@ -0,0 +1,35 @@
|
||||
/* global twoFactorTotpQrcode, qrcode, document, window */
|
||||
( function() {
|
||||
var qrGenerator = function() {
|
||||
/*
|
||||
* 0 = Automatically select the version, to avoid going over the limit of URL
|
||||
* length.
|
||||
* L = Least amount of error correction, because it's not needed when scanning
|
||||
* on a monitor, and it lowers the image size.
|
||||
*/
|
||||
var qr = qrcode( 0, 'L' ),
|
||||
svg,
|
||||
title;
|
||||
|
||||
qr.addData( twoFactorTotpQrcode.totpUrl );
|
||||
qr.make();
|
||||
|
||||
document.querySelector( '#two-factor-qr-code a' ).innerHTML = qr.createSvgTag( 5 );
|
||||
|
||||
// For accessibility, markup the SVG with a title and role.
|
||||
svg = document.querySelector( '#two-factor-qr-code a svg' );
|
||||
title = document.createElement( 'title' );
|
||||
|
||||
svg.setAttribute( 'role', 'img' );
|
||||
svg.setAttribute( 'aria-label', twoFactorTotpQrcode.qrCodeLabel );
|
||||
title.innerText = twoFactorTotpQrcode.qrCodeLabel;
|
||||
svg.appendChild( title );
|
||||
};
|
||||
|
||||
// Run now if the document is loaded, otherwise on DOMContentLoaded.
|
||||
if ( document.readyState === 'complete' ) {
|
||||
qrGenerator();
|
||||
} else {
|
||||
window.addEventListener( 'DOMContentLoaded', qrGenerator );
|
||||
}
|
||||
}() );
|
||||
@@ -0,0 +1,95 @@
|
||||
/* global twoFactorTotpAdmin, qrcode, wp, document, jQuery */
|
||||
( function( $ ) {
|
||||
var generateQrCode = function( totpUrl ) {
|
||||
var $qrLink = $( '#two-factor-qr-code a' ),
|
||||
qr,
|
||||
svg,
|
||||
title;
|
||||
|
||||
if ( ! $qrLink.length || typeof qrcode === 'undefined' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
qr = qrcode( 0, 'L' );
|
||||
|
||||
qr.addData( totpUrl );
|
||||
qr.make();
|
||||
$qrLink.html( qr.createSvgTag( 5 ) );
|
||||
|
||||
svg = $qrLink.find( 'svg' )[ 0 ];
|
||||
if ( svg ) {
|
||||
var ariaLabel = ( typeof twoFactorTotpAdmin !== 'undefined' && twoFactorTotpAdmin && twoFactorTotpAdmin.qrCodeAriaLabel ) ? twoFactorTotpAdmin.qrCodeAriaLabel : 'Authenticator App QR Code';
|
||||
title = document.createElement( 'title' );
|
||||
svg.setAttribute( 'role', 'img' );
|
||||
svg.setAttribute( 'aria-label', ariaLabel );
|
||||
title.innerText = ariaLabel;
|
||||
svg.appendChild( title );
|
||||
}
|
||||
};
|
||||
|
||||
var checkbox = document.getElementById( 'enabled-Two_Factor_Totp' );
|
||||
|
||||
// Focus the auth code input when the checkbox is clicked.
|
||||
if ( checkbox ) {
|
||||
checkbox.addEventListener( 'click', function( e ) {
|
||||
if ( e.target.checked ) {
|
||||
document.getElementById( 'two-factor-totp-authcode' ).focus();
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
$( '.totp-submit' ).click( function( e ) {
|
||||
var key = $( '#two-factor-totp-key' ).val(),
|
||||
code = $( '#two-factor-totp-authcode' ).val();
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
wp.apiRequest( {
|
||||
method: 'POST',
|
||||
path: twoFactorTotpAdmin.restPath,
|
||||
data: {
|
||||
user_id: parseInt( twoFactorTotpAdmin.userId, 10 ),
|
||||
key: key,
|
||||
code: code,
|
||||
enable_provider: true
|
||||
}
|
||||
} ).fail( function( response, status ) {
|
||||
var errorMessage = ( response && response.responseJSON && response.responseJSON.message ) || ( response && response.statusText ) || status || '',
|
||||
$error = $( '#totp-setup-error' );
|
||||
|
||||
if ( ! $error.length ) {
|
||||
$error = $( '<div class="error" id="totp-setup-error"><p></p></div>' ).insertAfter( $( '.totp-submit' ) );
|
||||
}
|
||||
|
||||
$error.find( 'p' ).text( errorMessage );
|
||||
|
||||
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', false ).trigger( 'change' );
|
||||
$( '#two-factor-totp-authcode' ).val( '' );
|
||||
} ).then( function( response ) {
|
||||
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', true ).trigger( 'change' );
|
||||
$( '#two-factor-totp-options' ).html( response.html );
|
||||
} );
|
||||
} );
|
||||
|
||||
$( '.button.reset-totp-key' ).click( function( e ) {
|
||||
e.preventDefault();
|
||||
|
||||
wp.apiRequest( {
|
||||
method: 'DELETE',
|
||||
path: twoFactorTotpAdmin.restPath,
|
||||
data: {
|
||||
user_id: parseInt( twoFactorTotpAdmin.userId, 10 )
|
||||
}
|
||||
} ).then( function( response ) {
|
||||
var totpUrl;
|
||||
|
||||
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', false );
|
||||
$( '#two-factor-totp-options' ).html( response.html );
|
||||
|
||||
totpUrl = $( '#two-factor-qr-code a' ).attr( 'href' );
|
||||
if ( totpUrl ) {
|
||||
generateQrCode( totpUrl );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
}( jQuery ) );
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/* global document */
|
||||
( function() {
|
||||
// Enforce numeric-only input for numeric inputmode elements.
|
||||
var form = document.querySelector( '#loginform' ),
|
||||
inputEl = document.querySelector( 'input.authcode[inputmode="numeric"]' ),
|
||||
expectedLength = ( inputEl && inputEl.dataset ) ? inputEl.dataset.digits : 0,
|
||||
spaceInserted = false;
|
||||
|
||||
if ( inputEl ) {
|
||||
inputEl.addEventListener(
|
||||
'input',
|
||||
function() {
|
||||
var value = this.value.replace( /[^0-9 ]/g, '' ).replace( /^\s+/, '' ),
|
||||
submitControl;
|
||||
|
||||
if ( ! spaceInserted && expectedLength && value.length === Math.floor( expectedLength / 2 ) ) {
|
||||
value += ' ';
|
||||
spaceInserted = true;
|
||||
} else if ( spaceInserted && ! this.value ) {
|
||||
spaceInserted = false;
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
|
||||
// Auto-submit if it's the expected length.
|
||||
if ( expectedLength && value.replace( / /g, '' ).length === parseInt( expectedLength, 10 ) ) {
|
||||
if ( form && typeof form.requestSubmit === 'function' ) {
|
||||
form.requestSubmit();
|
||||
submitControl = form.querySelector( '[type="submit"]' );
|
||||
if ( submitControl ) {
|
||||
submitControl.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}() );
|
||||
@@ -0,0 +1,11 @@
|
||||
/* global document, setTimeout */
|
||||
( function() {
|
||||
setTimeout( function() {
|
||||
var d;
|
||||
try {
|
||||
d = document.getElementById( 'authcode' );
|
||||
d.value = '';
|
||||
d.focus();
|
||||
} catch ( e ) {}
|
||||
}, 200 );
|
||||
}() );
|
||||
Reference in New Issue
Block a user