updated plugin Two Factor version 0.16.0

This commit is contained in:
2026-06-03 21:29:19 +00:00
committed by Gitium
parent bc89bee944
commit 57bccfdbd1
33 changed files with 1920 additions and 2942 deletions

View File

@ -39,13 +39,36 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
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() {
@ -55,11 +78,11 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'rest_generate_codes' ),
'permission_callback' => function( $request ) {
'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(
'user_id' => array(
'required' => true,
'type' => 'integer',
),
@ -106,7 +129,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
array( 'a' => array( 'href' => true ) )
);
?>
<span>
</span>
</p>
</div>
<?php
@ -154,12 +177,19 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
* @param WP_User $user WP_User object of the logged-in user.
*/
public function user_options( $user ) {
wp_enqueue_script( 'wp-api-request' );
wp_enqueue_script( 'jquery' );
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 );
?>
<p id="two-factor-backup-codes">
<div id="two-factor-backup-codes">
<p class="two-factor-backup-codes-count">
<?php
echo esc_html(
@ -175,58 +205,40 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
<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>
</p>
</div>
<div class="two-factor-backup-codes-wrapper" style="display:none;">
<ol class="two-factor-backup-codes-unused-codes"></ol>
<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>
</p>
</div>
<script type="text/javascript">
( function( $ ) {
$( '.button-two-factor-backup-codes-generate' ).click( function() {
wp.apiRequest( {
method: 'POST',
path: <?php echo wp_json_encode( Two_Factor_Core::REST_NAMESPACE . '/generate-backup-codes' ); ?>,
data: {
user_id: <?php echo wp_json_encode( $user->ID ); ?>
}
} ).then( function( response ) {
var $codesList = $( '.two-factor-backup-codes-unused-codes' );
$( '.two-factor-backup-codes-wrapper' ).show();
$codesList.html( '' );
// Append the codes.
for ( i = 0; i < response.codes.length; i++ ) {
$codesList.append( '<li>' + 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 );
</script>
<?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 ) {
/**
* Customize the character count of the backup codes.
* Filters the character count of the backup codes.
*
* @var int $code_length Length of the backup code.
* @var WP_User $user User object.
* @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 );
@ -242,7 +254,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
* @param array $args Optional arguments for assigning new codes.
* @return array
*/
public function generate_codes( $user, $args = '' ) {
public function generate_codes( $user, $args = array() ) {
$codes = array();
$codes_hashed = array();
@ -277,13 +289,15 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
* 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(
$args = array(
'number' => self::NUMBER_OF_CODES,
'method' => 'replace',
);
@ -304,7 +318,7 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
$i = 1;
foreach ( $codes as $code ) {
$download_link .= rawurlencode( "{$i}. {$code}\r\n" );
$i++;
++$i;
}
$i18n = array(
@ -327,6 +341,8 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
/**
* 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
*/
@ -348,17 +364,47 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
$code_length = $this->get_backup_code_length( $user );
$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
submit_button( __( 'Submit', 'two-factor' ) );
/**
* 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' ) );
}
/**
@ -428,6 +474,8 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
/**
* Return user meta keys to delete during plugin uninstall.
*
* @since 0.10.0
*
* @return array
*/
public static function uninstall_user_meta_keys() {

View File

@ -43,8 +43,20 @@ class Two_Factor_Dummy extends Two_Factor_Provider {
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' ) );
}

View File

@ -39,6 +39,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
* Class constructor.
*
* @since 0.1-dev
*
* @codeCoverageIgnore
*/
protected function __construct() {
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
@ -66,13 +68,17 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* Get the email token length.
*
* @since 0.11.0
*
* @return int Email token string length.
*/
private function get_token_length() {
/**
* Number of characters in the email token.
* Filters the number of characters in the email token.
*
* @param int $token_length 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 );
@ -99,6 +105,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* 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.
*/
@ -115,6 +123,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* Has the user token validity timestamp expired.
*
* @since 0.6.0
*
* @param integer $user_id User ID.
*
* @return boolean
@ -134,6 +144,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* 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.
@ -151,6 +163,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* Return the token time-to-live for a user.
*
* @since 0.6.0
*
* @param integer $user_id User ID.
*
* @return integer
@ -159,22 +173,23 @@ class Two_Factor_Email extends Two_Factor_Provider {
$token_ttl = 15 * MINUTE_IN_SECONDS;
/**
* Number of seconds the token is considered valid
* after the generation.
* 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 integer $token_ttl Token time-to-live in seconds.
* @param integer $user_id User ID.
* @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' );
/**
* Number of seconds the token is considered valid
* after the generation.
* Filters the number of seconds the email token is considered valid after generation.
*
* @param integer $token_ttl Token time-to-live in seconds.
* @param integer $user_id User ID.
* @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 );
}
@ -182,6 +197,8 @@ class Two_Factor_Email extends Two_Factor_Provider {
/**
* 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.
@ -234,6 +251,24 @@ class Two_Factor_Email extends Two_Factor_Provider {
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.
*
@ -243,15 +278,40 @@ class Two_Factor_Email extends Two_Factor_Provider {
* @return bool Whether the email contents were sent successfully.
*/
public function generate_and_email_token( $user ) {
$token = $this->generate_token( $user->ID );
$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 );
/* translators: %s: site name */
$subject = wp_strip_all_tags( sprintf( __( 'Your login confirmation code for %s', 'two-factor' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) );
/* translators: %s: token */
$message = wp_strip_all_tags( sprintf( __( 'Enter %s to log in.', 'two-factor' ), $token ) );
$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 ) );
/**
* Filter the token email subject.
* 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.
@ -259,7 +319,9 @@ class Two_Factor_Email extends Two_Factor_Provider {
$subject = apply_filters( 'two_factor_token_email_subject', $subject, $user->ID );
/**
* Filter the token email message.
* Filters the token email message.
*
* @since 0.5.2
*
* @param string $message The email message.
* @param string $token The token.
@ -286,30 +348,33 @@ class Two_Factor_Email extends Two_Factor_Provider {
$this->generate_and_email_token( $user );
}
$token_length = $this->get_token_length();
$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 ); ?>" />
<?php submit_button( __( 'Log In', 'two-factor' ) ); ?>
</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>
<script type="text/javascript">
setTimeout( function(){
var d;
try{
d = document.getElementById('authcode');
d.value = '';
d.focus();
} catch(e){}
}, 200);
</script>
<?php wp_enqueue_script( 'two-factor-login' ); ?>
<?php
}
@ -317,11 +382,13 @@ class Two_Factor_Email extends Two_Factor_Provider {
* 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 ] ) ) {
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;
}
@ -368,7 +435,7 @@ class Two_Factor_Email extends Two_Factor_Provider {
public function user_options( $user ) {
$email = $user->user_email;
?>
<div>
<p>
<?php
echo esc_html(
sprintf(
@ -378,13 +445,15 @@ class Two_Factor_Email extends Two_Factor_Provider {
)
);
?>
</div>
</p>
<?php
}
/**
* Return user meta keys to delete during plugin uninstall.
*
* @since 0.10.0
*
* @return array
*/
public static function uninstall_user_meta_keys() {

View File

@ -1,160 +0,0 @@
<?php
/**
* Class for displaying the list of security key items.
*
* @package Two_Factor
*/
// Load the parent class if it doesn't exist.
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
/**
* Class for displaying the list of security key items.
*
* @since 0.1-dev
* @access private
*
* @package Two_Factor
*/
class Two_Factor_FIDO_U2F_Admin_List_Table extends WP_List_Table {
/**
* Get a list of columns.
*
* @since 0.1-dev
*
* @return array
*/
public function get_columns() {
return array(
'name' => wp_strip_all_tags( __( 'Name', 'two-factor' ) ),
'added' => wp_strip_all_tags( __( 'Added', 'two-factor' ) ),
'last_used' => wp_strip_all_tags( __( 'Last Used', 'two-factor' ) ),
);
}
/**
* Prepares the list of items for displaying.
*
* @since 0.1-dev
*/
public function prepare_items() {
$columns = $this->get_columns();
$hidden = array();
$sortable = array();
$primary = 'name';
$this->_column_headers = array( $columns, $hidden, $sortable, $primary );
}
/**
* Generates content for a single row of the table
*
* @since 0.1-dev
* @access protected
*
* @param object $item The current item.
* @param string $column_name The current column name.
* @return string
*/
protected function column_default( $item, $column_name ) {
switch ( $column_name ) {
case 'name':
$out = '<div class="hidden" id="inline_' . esc_attr( $item->keyHandle ) . '">'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$out .= '<div class="name">' . esc_html( $item->name ) . '</div>';
$out .= '</div>';
$actions = array(
'rename hide-if-no-js' => Two_Factor_FIDO_U2F_Admin::rename_link( $item ),
'delete' => Two_Factor_FIDO_U2F_Admin::delete_link( $item ),
);
return esc_html( $item->name ) . $out . self::row_actions( $actions );
case 'added':
return gmdate( get_option( 'date_format', 'r' ), $item->added );
case 'last_used':
return gmdate( get_option( 'date_format', 'r' ), $item->last_used );
default:
return 'WTF^^?';
}
}
/**
* Generates custom table navigation to prevent conflicting nonces.
*
* @since 0.1-dev
* @access protected
*
* @param string $which The location of the bulk actions: 'top' or 'bottom'.
*/
protected function display_tablenav( $which ) {
// Not used for the Security key list.
}
/**
* Generates content for a single row of the table
*
* @since 0.1-dev
* @access public
*
* @param object $item The current item.
*/
public function single_row( $item ) {
?>
<tr id="key-<?php echo esc_attr( $item->keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase ?>">
<?php $this->single_row_columns( $item ); ?>
</tr>
<?php
}
/**
* Outputs the hidden row displayed when inline editing
*
* @since 0.1-dev
*/
public function inline_edit() {
?>
<table style="display: none">
<tbody id="inlineedit">
<tr id="inline-edit" class="inline-edit-row" style="display: none">
<td colspan="<?php echo esc_attr( $this->get_column_count() ); ?>" class="colspanchange">
<fieldset>
<div class="inline-edit-col">
<label>
<span class="title"><?php esc_html_e( 'Name', 'two-factor' ); ?></span>
<span class="input-text-wrap"><input type="text" name="name" class="ptitle" value="" /></span>
</label>
</div>
</fieldset>
<?php
$core_columns = array(
'name' => true,
'added' => true,
'last_used' => true,
);
list( $columns ) = $this->get_column_info();
foreach ( $columns as $column_name => $column_display_name ) {
if ( isset( $core_columns[ $column_name ] ) ) {
continue;
}
/** This action is documented in wp-admin/includes/class-wp-posts-list-table.php */
do_action( 'quick_edit_custom_box', $column_name, 'edit-security-keys' );
}
?>
<p class="inline-edit-save submit">
<a href="#inline-edit" class="cancel button-secondary alignleft"><?php esc_html_e( 'Cancel', 'two-factor' ); ?></a>
<a href="#inline-edit" class="save button-primary alignright"><?php esc_html_e( 'Update', 'two-factor' ); ?></a>
<span class="spinner"></span>
<span class="error" style="display:none;"></span>
<?php wp_nonce_field( 'keyinlineeditnonce', '_inline_edit', false ); ?>
<br class="clear" />
</p>
</td>
</tr>
</tbody>
</table>
<?php
}
}

View File

@ -1,363 +0,0 @@
<?php
/**
* Class for registering & modifying FIDO U2F security keys.
*
* @package Two_Factor
*/
/**
* Class for registering & modifying FIDO U2F security keys.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_FIDO_U2F_Admin {
/**
* The user meta register data.
*
* @type string
*/
const REGISTER_DATA_USER_META_KEY = '_two_factor_fido_u2f_register_request';
/**
* Add various hooks.
*
* @since 0.1-dev
*
* @access public
* @static
*/
public static function add_hooks() {
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
add_action( 'show_user_security_settings', array( __CLASS__, 'show_user_profile' ) );
add_action( 'personal_options_update', array( __CLASS__, 'catch_submission' ), 0 );
add_action( 'edit_user_profile_update', array( __CLASS__, 'catch_submission' ), 0 );
add_action( 'load-profile.php', array( __CLASS__, 'catch_delete_security_key' ) );
add_action( 'load-user-edit.php', array( __CLASS__, 'catch_delete_security_key' ) );
add_action( 'wp_ajax_inline-save-key', array( __CLASS__, 'wp_ajax_inline_save' ) );
}
/**
* Enqueue assets.
*
* @since 0.1-dev
*
* @access public
* @static
*
* @param string $hook Current page.
*/
public static function enqueue_assets( $hook ) {
if ( ! in_array( $hook, array( 'user-edit.php', 'profile.php' ), true ) ) {
return;
}
$user_id = Two_Factor_Core::current_user_being_edited();
if ( ! $user_id ) {
return;
}
$security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id );
// @todo Ensure that scripts don't fail because of missing u2fL10n.
try {
$data = Two_Factor_FIDO_U2F::$u2f->getRegisterData( $security_keys );
list( $req,$sigs ) = $data;
update_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, $req );
} catch ( Exception $e ) {
return false;
}
wp_enqueue_style(
'fido-u2f-admin',
plugins_url( 'css/fido-u2f-admin.css', __FILE__ ),
null,
self::asset_version()
);
wp_enqueue_script(
'fido-u2f-admin',
plugins_url( 'js/fido-u2f-admin.js', __FILE__ ),
array( 'jquery', 'fido-u2f-api' ),
self::asset_version(),
true
);
/**
* Pass a U2F challenge and user data to our scripts
*/
$translation_array = array(
'user_id' => $user_id,
'register' => array(
'request' => $req,
'sigs' => $sigs,
),
'text' => array(
'insert' => esc_html__( 'Now insert (and tap) your Security Key.', 'two-factor' ),
'error' => esc_html__( 'U2F request failed.', 'two-factor' ),
'error_codes' => array(
// Map u2f.ErrorCodes to error messages.
0 => esc_html__( 'Request OK.', 'two-factor' ),
1 => esc_html__( 'Other U2F error.', 'two-factor' ),
2 => esc_html__( 'Bad U2F request.', 'two-factor' ),
3 => esc_html__( 'Unsupported U2F configuration.', 'two-factor' ),
4 => esc_html__( 'U2F device ineligible.', 'two-factor' ),
5 => esc_html__( 'U2F request timeout reached.', 'two-factor' ),
),
'u2f_not_supported' => esc_html__( 'FIDO U2F appears to be not supported by your web browser. Try using Google Chrome or Firefox.', 'two-factor' ),
),
);
wp_localize_script(
'fido-u2f-admin',
'u2fL10n',
$translation_array
);
/**
* Script for admin UI
*/
wp_enqueue_script(
'inline-edit-key',
plugins_url( 'js/fido-u2f-admin-inline-edit.js', __FILE__ ),
array( 'jquery' ),
self::asset_version(),
true
);
wp_localize_script(
'inline-edit-key',
'inlineEditL10n',
array(
'error' => esc_html__( 'Error while saving the changes.', 'two-factor' ),
)
);
}
/**
* Return the current asset version number.
*
* Added as own helper to allow swapping the implementation once we inject
* it as a dependency.
*
* @return string
*/
protected static function asset_version() {
return Two_Factor_FIDO_U2F::asset_version();
}
/**
* Display the security key section in a users profile.
*
* This executes during the `show_user_security_settings` action.
*
* @since 0.1-dev
*
* @access public
* @static
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public static function show_user_profile( $user ) {
if ( ! Two_Factor_FIDO_U2F::is_supported_for_user( $user ) ) {
return;
}
wp_nonce_field( "user_security_keys-{$user->ID}", '_nonce_user_security_keys' );
$new_key = false;
$security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user->ID );
if ( $security_keys ) {
foreach ( $security_keys as &$security_key ) {
if ( property_exists( $security_key, 'new' ) ) {
$new_key = true;
unset( $security_key->new );
// If we've got a new one, update the db record to not save it there any longer.
Two_Factor_FIDO_U2F::update_security_key( $user->ID, $security_key );
}
}
unset( $security_key );
}
?>
<div class="security-keys" id="security-keys-section">
<h3><?php esc_html_e( 'Security Keys', 'two-factor' ); ?></h3>
<?php if ( ! is_ssl() ) : ?>
<p class="u2f-error-https">
<em><?php esc_html_e( 'U2F requires an HTTPS connection. You won\'t be able to add new security keys over HTTP.', 'two-factor' ); ?></em>
</p>
<?php endif; ?>
<div class="register-security-key">
<input type="hidden" name="do_new_security_key" id="do_new_security_key" />
<input type="hidden" name="u2f_response" id="u2f_response" />
<button type="button" class="button button-secondary" id="register_security_key"><?php echo esc_html( _x( 'Register New Key', 'security key', 'two-factor' ) ); ?></button>
<span class="spinner"></span>
<span class="security-key-status"></span>
</div>
<?php if ( $new_key ) : ?>
<div class="notice notice-success is-dismissible">
<p class="new-security-key"><?php esc_html_e( 'Your new security key registered.', 'two-factor' ); ?></p>
</div>
<?php endif; ?>
<p><a href="https://support.google.com/accounts/answer/6103523"><?php esc_html_e( 'You can find FIDO U2F Security Key devices for sale from here.', 'two-factor' ); ?></a></p>
<?php
require_once TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin-list-table.php';
$u2f_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table();
$u2f_list_table->items = $security_keys;
$u2f_list_table->prepare_items();
$u2f_list_table->display();
$u2f_list_table->inline_edit();
?>
</div>
<?php
}
/**
* Catch the non-ajax submission from the new form.
*
* This executes during the `personal_options_update` & `edit_user_profile_update` actions.
*
* @since 0.1-dev
*
* @access public
* @static
*
* @param int $user_id User ID.
* @return void|never
*/
public static function catch_submission( $user_id ) {
if ( ! empty( $_REQUEST['do_new_security_key'] ) ) {
check_admin_referer( "user_security_keys-{$user_id}", '_nonce_user_security_keys' );
try {
$response = json_decode( stripslashes( $_POST['u2f_response'] ) );
$reg = Two_Factor_FIDO_U2F::$u2f->doRegister( get_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY, true ), $response );
$reg->new = true;
Two_Factor_FIDO_U2F::add_security_key( $user_id, $reg );
} catch ( Exception $e ) {
return;
}
delete_user_meta( $user_id, self::REGISTER_DATA_USER_META_KEY );
wp_safe_redirect(
add_query_arg(
array(
'new_app_pass' => 1,
),
wp_get_referer()
) . '#security-keys-section'
);
exit;
}
}
/**
* Catch the delete security key request.
*
* This executes during the `load-profile.php` & `load-user-edit.php` actions.
*
* @since 0.1-dev
*
* @access public
* @static
*/
public static function catch_delete_security_key() {
$user_id = Two_Factor_Core::current_user_being_edited();
if ( ! empty( $user_id ) && ! empty( $_REQUEST['delete_security_key'] ) ) {
$slug = $_REQUEST['delete_security_key'];
check_admin_referer( "delete_security_key-{$slug}", '_nonce_delete_security_key' );
Two_Factor_FIDO_U2F::delete_security_key( $user_id, $slug );
wp_safe_redirect( remove_query_arg( 'new_app_pass', wp_get_referer() ) . '#security-keys-section' );
exit;
}
}
/**
* Generate a link to rename a specified security key.
*
* @since 0.1-dev
*
* @access public
* @static
*
* @param array $item The current item.
* @return string
*/
public static function rename_link( $item ) {
return sprintf( '<a href="#" class="editinline">%s</a>', esc_html__( 'Rename', 'two-factor' ) );
}
/**
* Generate a link to delete a specified security key.
*
* @since 0.1-dev
*
* @access public
* @static
*
* @param array $item The current item.
* @return string
*/
public static function delete_link( $item ) {
$delete_link = add_query_arg( 'delete_security_key', $item->keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$delete_link = wp_nonce_url( $delete_link, "delete_security_key-{$item->keyHandle}", '_nonce_delete_security_key' );
return sprintf( '<a href="%1$s">%2$s</a>', esc_url( $delete_link ), esc_html__( 'Delete', 'two-factor' ) );
}
/**
* Ajax handler for quick edit saving for a security key.
*
* @since 0.1-dev
*
* @access public
* @static
*/
public static function wp_ajax_inline_save() {
check_ajax_referer( 'keyinlineeditnonce', '_inline_edit' );
require_once TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin-list-table.php';
$wp_list_table = new Two_Factor_FIDO_U2F_Admin_List_Table();
if ( ! isset( $_POST['keyHandle'] ) ) {
wp_die();
}
$user_id = Two_Factor_Core::current_user_being_edited();
$security_keys = Two_Factor_FIDO_U2F::get_security_keys( $user_id );
if ( ! $security_keys ) {
wp_die();
}
foreach ( $security_keys as &$key ) {
if ( $key->keyHandle === $_POST['keyHandle'] ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
break;
}
}
$key->name = $_POST['name'];
$updated = Two_Factor_FIDO_U2F::update_security_key( $user_id, $key );
if ( ! $updated ) {
wp_die( esc_html__( 'Item not updated.', 'two-factor' ) );
}
$wp_list_table->single_row( $key );
wp_die();
}
}

View File

@ -1,404 +0,0 @@
<?php
/**
* Class for creating a FIDO Universal 2nd Factor provider.
*
* @package Two_Factor
*/
/**
* Class for creating a FIDO Universal 2nd Factor provider.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_FIDO_U2F extends Two_Factor_Provider {
/**
* U2F Library
*
* @var u2flib_server\U2F
*/
public static $u2f;
/**
* The user meta registered key.
*
* @type string
*/
const REGISTERED_KEY_USER_META_KEY = '_two_factor_fido_u2f_registered_key';
/**
* The user meta authenticate data.
*
* @type string
*/
const AUTH_DATA_USER_META_KEY = '_two_factor_fido_u2f_login_request';
/**
* Version number for the bundled assets.
*
* @var string
*/
const U2F_ASSET_VERSION = '0.2.1';
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
if ( version_compare( PHP_VERSION, '5.3.0', '<' ) ) {
return;
}
require_once TWO_FACTOR_DIR . 'includes/Yubico/U2F.php';
self::$u2f = new u2flib_server\U2F( self::get_u2f_app_id() );
require_once TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f-admin.php';
Two_Factor_FIDO_U2F_Admin::add_hooks();
// Ensure the script dependencies have been registered before they're enqueued at a later priority.
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'login_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ), 5 );
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
parent::__construct();
}
/**
* Get the asset version number.
*
* TODO: There should be a plugin-level helper for getting the current plugin version.
*
* @return string
*/
public static function asset_version() {
return self::U2F_ASSET_VERSION;
}
/**
* Return the U2F AppId. U2F requires the AppID to use HTTPS
* and a top-level domain.
*
* @return string AppID URI
*/
public static function get_u2f_app_id() {
$url_parts = wp_parse_url( home_url() );
if ( ! empty( $url_parts['port'] ) ) {
return sprintf( 'https://%s:%d', $url_parts['host'], $url_parts['port'] );
} else {
return sprintf( 'https://%s', $url_parts['host'] );
}
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label() {
return _x( 'FIDO U2F Security Keys', '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 security key', 'two-factor' );
}
/**
* Register script dependencies used during login and when
* registering keys in the WP admin.
*
* @return void
*/
public static function enqueue_scripts() {
wp_register_script(
'fido-u2f-api',
plugins_url( 'includes/Google/u2f-api.js', dirname( __FILE__ ) ),
null,
self::asset_version(),
true
);
wp_register_script(
'fido-u2f-login',
plugins_url( 'js/fido-u2f-login.js', __FILE__ ),
array( 'jquery', 'fido-u2f-api' ),
self::asset_version(),
true
);
}
/**
* 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.
* @return void
*/
public function authentication_page( $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
// U2F doesn't work without HTTPS.
if ( ! is_ssl() ) {
?>
<p><?php esc_html_e( 'U2F requires an HTTPS connection. Please use an alternative 2nd factor method.', 'two-factor' ); ?></p>
<?php
return;
}
try {
$keys = self::get_security_keys( $user->ID );
$data = self::$u2f->getAuthenticateData( $keys );
update_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, $data );
} catch ( Exception $e ) {
?>
<p><?php esc_html_e( 'An error occurred while creating authentication data.', 'two-factor' ); ?></p>
<?php
return;
}
wp_localize_script(
'fido-u2f-login',
'u2fL10n',
array(
'request' => $data,
)
);
wp_enqueue_script( 'fido-u2f-login' );
?>
<p><?php esc_html_e( 'Now insert (and tap) your Security Key.', 'two-factor' ); ?></p>
<input type="hidden" name="u2f_response" id="u2f_response" />
<?php
}
/**
* 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 ) {
$requests = get_user_meta( $user->ID, self::AUTH_DATA_USER_META_KEY, true );
$response = json_decode( stripslashes( $_REQUEST['u2f_response'] ) );
$keys = self::get_security_keys( $user->ID );
try {
$reg = self::$u2f->doAuthenticate( $requests, $keys, $response );
$reg->last_used = time();
self::update_security_key( $user->ID, $reg );
return true;
} catch ( Exception $e ) {
return false;
}
}
/**
* 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 (bool) self::get_security_keys( $user->ID );
}
/**
* 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 ) {
?>
<p>
<?php esc_html_e( 'Requires an HTTPS connection. Configure your security keys in the "Security Keys" section below.', 'two-factor' ); ?>
</p>
<?php
}
/**
* Add registered security key to a user.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @param object $register The data of registered security key.
* @return int|bool Meta ID on success, false on failure.
*/
public static function add_security_key( $user_id, $register ) {
if ( ! is_numeric( $user_id ) ) {
return false;
}
if (
! is_object( $register )
|| ! property_exists( $register, 'keyHandle' ) || empty( $register->keyHandle ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! property_exists( $register, 'publicKey' ) || empty( $register->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! property_exists( $register, 'certificate' ) || empty( $register->certificate )
|| ! property_exists( $register, 'counter' ) || ( -1 > $register->counter )
) {
return false;
}
$register = array(
'keyHandle' => $register->keyHandle, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'publicKey' => $register->publicKey, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'certificate' => $register->certificate,
'counter' => $register->counter,
);
$register['name'] = __( 'New Security Key', 'two-factor' );
$register['added'] = time();
$register['last_used'] = $register['added'];
return add_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, $register );
}
/**
* Retrieve registered security keys for a user.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @return array|bool Array of keys on success, false on failure.
*/
public static function get_security_keys( $user_id ) {
if ( ! is_numeric( $user_id ) ) {
return false;
}
$keys = get_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY );
if ( $keys ) {
foreach ( $keys as &$key ) {
$key = (object) $key;
}
unset( $key );
}
return $keys;
}
/**
* Update registered security key.
*
* Use the $prev_value parameter to differentiate between meta fields with the
* same key and user ID.
*
* If the meta field for the user does not exist, it will be added.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @param object $data The data of registered security key.
* @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure.
*/
public static function update_security_key( $user_id, $data ) {
if ( ! is_numeric( $user_id ) ) {
return false;
}
if (
! is_object( $data )
|| ! property_exists( $data, 'keyHandle' ) || empty( $data->keyHandle ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! property_exists( $data, 'publicKey' ) || empty( $data->publicKey ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|| ! property_exists( $data, 'certificate' ) || empty( $data->certificate )
|| ! property_exists( $data, 'counter' ) || ( -1 > $data->counter )
) {
return false;
}
$keys = self::get_security_keys( $user_id );
if ( $keys ) {
foreach ( $keys as $key ) {
if ( $key->keyHandle === $data->keyHandle ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
return update_user_meta( $user_id, self::REGISTERED_KEY_USER_META_KEY, (array) $data, (array) $key );
}
}
}
return self::add_security_key( $user_id, $data );
}
/**
* Remove registered security key matching criteria from a user.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @param string $keyHandle Optional. Key handle.
* @return bool True on success, false on failure.
*/
public static function delete_security_key( $user_id, $keyHandle = null ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
global $wpdb;
if ( ! is_numeric( $user_id ) ) {
return false;
}
$user_id = absint( $user_id );
if ( ! $user_id ) {
return false;
}
$keyHandle = wp_unslash( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$keyHandle = maybe_serialize( $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$query = $wpdb->prepare( "SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND user_id = %d", self::REGISTERED_KEY_USER_META_KEY, $user_id );
if ( $keyHandle ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$key_handle_lookup = sprintf( ':"%s";s:', $keyHandle ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
$query .= $wpdb->prepare(
' AND meta_value LIKE %s',
'%' . $wpdb->esc_like( $key_handle_lookup ) . '%'
);
}
$meta_ids = $wpdb->get_col( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( ! count( $meta_ids ) ) {
return false;
}
foreach ( $meta_ids as $meta_id ) {
delete_metadata_by_mid( 'user', $meta_id );
}
return true;
}
/**
* Return user meta keys to delete during plugin uninstall.
*
* @return array
*/
public static function uninstall_user_meta_keys() {
return array(
self::REGISTERED_KEY_USER_META_KEY,
self::AUTH_DATA_USER_META_KEY,
'_two_factor_fido_u2f_register_request', // From Two_Factor_FIDO_U2F_Admin which is not loaded during uninstall.
);
}
}

View File

@ -25,7 +25,7 @@ abstract class Two_Factor_Provider {
$class_name = static::class;
if ( ! isset( $instances[ $class_name ] ) ) {
$instances[ $class_name ] = new $class_name;
$instances[ $class_name ] = new $class_name();
}
return $instances[ $class_name ];
@ -37,7 +37,6 @@ abstract class Two_Factor_Provider {
* @since 0.1-dev
*/
protected function __construct() {
return $this;
}
/**
@ -68,6 +67,8 @@ abstract class Two_Factor_Provider {
* Prints the name of the provider.
*
* @since 0.1-dev
*
* @codeCoverageIgnore
*/
public function print_label() {
echo esc_html( $this->get_label() );
@ -98,6 +99,8 @@ abstract class Two_Factor_Provider {
* 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
*/
@ -118,6 +121,8 @@ abstract class Two_Factor_Provider {
/**
* 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
*/
@ -126,6 +131,8 @@ abstract class Two_Factor_Provider {
/**
* 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
@ -159,6 +166,8 @@ abstract class Two_Factor_Provider {
/**
* 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.
@ -182,6 +191,8 @@ abstract class Two_Factor_Provider {
/**
* 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() {
@ -191,6 +202,8 @@ abstract class Two_Factor_Provider {
/**
* 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

View File

@ -7,6 +7,8 @@
/**
* Class Two_Factor_Totp
*
* @since 0.2.0
*/
class Two_Factor_Totp extends Two_Factor_Provider {
@ -40,6 +42,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Class constructor. Sets up hooks, etc.
*
* @since 0.2.0
*
* @codeCoverageIgnore
*/
protected function __construct() {
@ -51,9 +55,40 @@ class Two_Factor_Totp extends Two_Factor_Provider {
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() {
@ -64,7 +99,7 @@ class Two_Factor_Totp extends Two_Factor_Provider {
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'rest_delete_totp' ),
'permission_callback' => function( $request ) {
'permission_callback' => function ( $request ) {
return Two_Factor_Core::rest_api_can_edit_user_and_update_two_factor_options( $request['user_id'] );
},
'args' => array(
@ -77,20 +112,20 @@ class Two_Factor_Totp extends Two_Factor_Provider {
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'rest_setup_totp' ),
'permission_callback' => function( $request ) {
'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(
'user_id' => array(
'required' => true,
'type' => 'integer',
),
'key' => array(
'key' => array(
'type' => 'string',
'default' => '',
'validate_callback' => null, // Note: validation handled in ::rest_setup_totp().
),
'code' => array(
'code' => array(
'type' => 'string',
'default' => '',
'validate_callback' => null, // Note: validation handled in ::rest_setup_totp().
@ -108,6 +143,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Returns the name of the provider.
*
* @since 0.2.0
*/
public function get_label() {
return _x( 'Authenticator App', 'Provider Label', 'two-factor' );
@ -125,7 +162,10 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* 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' : '';
@ -137,37 +177,57 @@ class Two_Factor_Totp extends Two_Factor_Provider {
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 array Success array.
* @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 );
$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 ) );
}
$this->delete_user_totp_key( $user_id );
ob_start();
$this->user_two_factor_options( $user );
$html = ob_get_clean();
return [
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.
*/
@ -198,15 +258,17 @@ class Two_Factor_Totp extends Two_Factor_Provider {
$this->user_two_factor_options( $user );
$html = ob_get_clean();
return [
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.
*
@ -216,21 +278,27 @@ class Two_Factor_Totp extends Two_Factor_Provider {
$issuer = get_bloginfo( 'name', 'display' );
/**
* Filter the Issuer for the TOTP.
* 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 );
/**
* Filter the Label for the TOTP.
* 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.
@ -246,16 +314,19 @@ class Two_Factor_Totp extends Two_Factor_Provider {
);
/**
* Filter the TOTP generated URL.
* 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( $totp_url, array( 'otpauth' ) );
$totp_url = esc_url_raw( $totp_url, array( 'otpauth' ) );
return $totp_url;
}
@ -263,6 +334,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Display TOTP options on the user settings page.
*
* @since 0.2.0
*
* @param WP_User $user The current user being edited.
* @return void
*
@ -275,132 +348,92 @@ class Two_Factor_Totp extends Two_Factor_Provider {
$key = $this->get_user_totp_key( $user->ID );
wp_enqueue_script( 'two-factor-qr-code-generator' );
wp_enqueue_script( 'wp-api-request' );
wp_enqueue_script( 'jquery' );
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 scan the QR code or manually enter the key, then enter an authentication code from your app in order to complete setup.', 'two-factor' ); ?>
<?php esc_html_e( 'Please follow these steps in order to complete setup:', 'two-factor' ); ?>
</p>
<p id="two-factor-qr-code">
<a href="<?php echo $totp_url; ?>">
Loading...
<img src="<?php echo esc_url( admin_url( 'images/spinner.gif' ) ); ?>" alt="" />
</a>
</p>
<style>
#two-factor-qr-code {
/* The size of the image will change based on the length of the URL inside it. */
min-width: 205px;
min-height: 205px;
}
</style>
<script>
(function(){
var qr_generator = 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' );
qr.addData( <?php echo wp_json_encode( $totp_url ); ?> );
qr.make();
document.querySelector( '#two-factor-qr-code a' ).innerHTML = qr.createSvgTag( 5 );
// For accessibility, markup the SVG with a title and role.
var svg = document.querySelector( '#two-factor-qr-code a svg' ),
title = document.createElement( 'title' );
svg.role = 'image';
svg.ariaLabel = <?php echo wp_json_encode( __( 'Authenticator App QR Code', 'two-factor' ) ); ?>;
title.innerText = svg.ariaLabel;
svg.appendChild( title );
};
// Run now if the document is loaded, otherwise on DOMContentLoaded.
if ( document.readyState === 'complete' ) {
qr_generator();
} else {
window.addEventListener( 'DOMContentLoaded', qr_generator );
}
})();
</script>
<p>
<code><?php echo esc_html( $key ); ?></code>
</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( 'Submit', 'two-factor' ); ?>" />
</p>
<script>
(function($){
// Focus the auth code input when the checkbox is clicked.
document.getElementById('enabled-Two_Factor_Totp').addEventListener('click', function(e) {
if ( e.target.checked ) {
document.getElementById('two-factor-totp-authcode').focus();
}
});
$('.totp-submit').click( function( e ) {
e.preventDefault();
var key = $('#two-factor-totp-key').val(),
code = $('#two-factor-totp-authcode').val();
wp.apiRequest( {
method: 'POST',
path: <?php echo wp_json_encode( Two_Factor_Core::REST_NAMESPACE . '/totp' ); ?>,
data: {
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,
$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 );
$('#two-factor-totp-authcode').val('');
} ).then( function( response ) {
$( '#enabled-Two_Factor_Totp' ).prop( 'checked', true );
$( '#two-factor-totp-options' ).html( response.html );
} );
} );
})(jQuery);
</script>
<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' ); ?>
@ -409,24 +442,6 @@ class Two_Factor_Totp extends Two_Factor_Provider {
<button type="button" class="button button-secondary reset-totp-key hide-if-no-js">
<?php esc_html_e( 'Reset authenticator app', 'two-factor' ); ?>
</button>
<script>
( function( $ ) {
$( '.button.reset-totp-key' ).click( function( e ) {
e.preventDefault();
wp.apiRequest( {
method: 'DELETE',
path: <?php echo wp_json_encode( Two_Factor_Core::REST_NAMESPACE . '/totp' ); ?>,
data: {
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 );
} );
} );
} )( jQuery );
</script>
</p>
<?php endif; ?>
</div>
@ -436,6 +451,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Get the TOTP secret key for a user.
*
* @since 0.2.0
*
* @param int $user_id User ID.
*
* @return string
@ -447,6 +464,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Set the TOTP secret key for a user.
*
* @since 0.2.0
*
* @param int $user_id User ID.
* @param string $key TOTP secret key.
*
@ -459,6 +478,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* 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.
@ -471,6 +492,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Check if the TOTP secret key has a proper format.
*
* @since 0.2.0
*
* @param string $key TOTP secret key.
*
* @return boolean
@ -488,6 +511,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* 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
@ -504,6 +529,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* 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.
*
@ -535,48 +562,70 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* 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 ) {
return (bool) self::get_authcode_valid_ticktime( $key, $authcode );
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 ) {
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
* 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 = floor( time() / self::DEFAULT_TIME_STEP_SEC );
$time = (int) floor( self::time() / $time_step );
$digits = strlen( $authcode );
foreach ( $ticks as $offset ) {
$log_time = $time + $offset;
if ( hash_equals( self::calc_totp( $key, $log_time ), $authcode ) ) {
$log_time = (int) ( $time + $offset );
if ( hash_equals( self::calc_totp( $key, $log_time, $digits, $hash, $time_step ), $authcode ) ) {
// Return the tick timestamp.
return $log_time * self::DEFAULT_TIME_STEP_SEC;
return (int) ( $log_time * self::DEFAULT_TIME_STEP_SEC );
}
}
@ -586,6 +635,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* 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.
@ -598,58 +649,94 @@ class Two_Factor_Totp extends Two_Factor_Provider {
}
/**
* Pack stuff
* Pack stuff. We're currently only using this to pack integers, however the generic `pack` method can handle mixed.
*
* @param string $value The value to be packed.
* @since 0.2.0
*
* @param int $value The value to be packed.
*
* @return string Binary packed string.
*/
public static function pack64( $value ) {
// 64bit mode (PHP_INT_SIZE == 8).
if ( PHP_INT_SIZE >= 8 ) {
// If we're on PHP 5.6.3+ we can use the new 64bit pack functionality.
if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) {
return pack( 'J', $value ); // phpcs:ignore PHPCompatibility.ParameterValues.NewPackFormat.NewFormatFound
}
$highmap = 0xffffffff << 32;
$higher = ( $value & $highmap ) >> 32;
} else {
/*
* 32bit PHP can't shift 32 bits like that, so we have to assume 0 for the higher
* and not pack anything beyond it's limits.
*/
$higher = 0;
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 );
}
$lowmap = 0xffffffff;
$lower = $value & $lowmap;
// 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( time() / $time_step );
$step_count = floor( self::time() / $time_step );
}
$timestamp = self::pack64( $step_count );
$hash = hash_hmac( $hash, $timestamp, $secret, true );
$offset = ord( $hash[19] ) & 0xf;
$offset = ord( $hash[ strlen( $hash ) - 1 ] ) & 0xf;
$code = (
( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) |
@ -664,6 +751,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* 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
@ -678,6 +767,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* 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
@ -685,41 +776,48 @@ class Two_Factor_Totp extends Two_Factor_Provider {
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" 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 ); ?>" />
<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>
<script type="text/javascript">
setTimeout( function(){
var d;
try{
d = document.getElementById('authcode');
d.focus();
} catch(e){}
}, 200);
</script>
<?php
submit_button( __( 'Authenticate', 'two-factor' ) );
/** 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.
*
* @param string $string String to be encoded using base32.
* @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( $string ) {
if ( empty( $string ) ) {
public static function base32_encode( $input ) {
if ( empty( $input ) ) {
return '';
}
$binary_string = '';
foreach ( str_split( $string ) as $character ) {
foreach ( str_split( $input ) as $character ) {
$binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT );
}
@ -736,6 +834,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* 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.
@ -773,6 +873,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* 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.
*
@ -790,6 +892,8 @@ class Two_Factor_Totp extends Two_Factor_Provider {
/**
* Return user meta keys to delete during plugin uninstall.
*
* @since 0.10.0
*
* @return array
*/
public static function uninstall_user_meta_keys() {

View File

@ -1,12 +0,0 @@
#security-keys-section .wp-list-table {
margin-bottom: 2em;
}
#security-keys-section .register-security-key .spinner {
float: none;
}
#security-keys-section .security-key-status {
vertical-align: middle;
font-style: italic;
}

View File

@ -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 ) );

View File

@ -1,150 +0,0 @@
/* global window, document, jQuery, inlineEditL10n, ajaxurl */
var inlineEditKey;
( function( $ ) {
inlineEditKey = {
init: function() {
var t = this,
row = $( '#security-keys-section #inline-edit' );
t.what = '#key-';
$( '#security-keys-section #the-list' ).on( 'click', 'a.editinline', function() {
inlineEditKey.edit( this );
return false;
} );
// Prepare the edit row.
row.keyup( function( event ) {
if ( 27 === event.which ) {
return inlineEditKey.revert();
}
} );
$( 'a.cancel', row ).click( function() {
return inlineEditKey.revert();
} );
$( 'a.save', row ).click( function() {
return inlineEditKey.save( this );
} );
$( 'input, select', row ).keydown( function( event ) {
if ( 13 === event.which ) {
return inlineEditKey.save( this );
}
} );
},
toggle: function( el ) {
var t = this;
if ( 'none' === $( t.what + t.getId( el ) ).css( 'display' ) ) {
t.revert();
} else {
t.edit( el );
}
},
edit: function( id ) {
var editRow, rowData, val,
t = this;
t.revert();
if ( 'object' === typeof id ) {
id = t.getId( id );
}
editRow = $( '#inline-edit' ).clone( true );
rowData = $( '#inline_' + id );
$( 'td', editRow ).attr( 'colspan', $( 'th:visible, td:visible', '#security-keys-section .widefat thead' ).length );
$( t.what + id ).hide().after( editRow ).after( '<tr class="hidden"></tr>' );
val = $( '.name', rowData );
val.find( 'img' ).replaceWith( function() {
return this.alt;
} );
val = val.text();
$( ':input[name="name"]', editRow ).val( val );
$( editRow ).attr( 'id', 'edit-' + id ).addClass( 'inline-editor' ).show();
$( '.ptitle', editRow ).eq( 0 ).focus();
return false;
},
save: function( id ) {
var params, fields;
if ( 'object' === typeof id ) {
id = this.getId( id );
}
$( '#security-keys-section table.widefat .spinner' ).addClass( 'is-active' );
params = {
action: 'inline-save-key',
keyHandle: id,
user_id: window.u2fL10n.user_id
};
fields = $( '#edit-' + id ).find( ':input' ).serialize();
params = fields + '&' + $.param( params );
// Make ajax request.
$.post( ajaxurl, params,
function( r ) {
var row, newID;
$( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' );
if ( r ) {
if ( -1 !== r.indexOf( '<tr' ) ) {
$( inlineEditKey.what + id ).siblings( 'tr.hidden' ).addBack().remove();
newID = $( r ).attr( 'id' );
$( '#edit-' + id ).before( r ).remove();
if ( newID ) {
row = $( '#' + newID );
} else {
row = $( inlineEditKey.what + id );
}
row.hide().fadeIn();
} else {
$( '#edit-' + id + ' .inline-edit-save .error' ).html( r ).show();
}
} else {
$( '#edit-' + id + ' .inline-edit-save .error' ).html( inlineEditL10n.error ).show();
}
}
);
return false;
},
revert: function() {
var id = $( '#security-keys-section table.widefat tr.inline-editor' ).attr( 'id' );
if ( id ) {
$( '#security-keys-section table.widefat .spinner' ).removeClass( 'is-active' );
$( '#' + id ).siblings( 'tr.hidden' ).addBack().remove();
id = id.replace( /\w+\-/, '' );
$( this.what + id ).show();
}
return false;
},
getId: function( o ) {
var id = 'TR' === o.tagName ? o.id : $( o ).parents( 'tr' ).attr( 'id' );
return id.replace( /\w+\-/, '' );
}
};
$( document ).ready( function() {
inlineEditKey.init();
} );
}( jQuery ) );

View File

@ -1,48 +0,0 @@
/* global window, u2fL10n, jQuery */
( function( $ ) {
var $button = $( '#register_security_key' );
var $statusNotice = $( '#security-keys-section .security-key-status' );
var u2fSupported = ( window.u2f && 'register' in window.u2f );
if ( ! u2fSupported ) {
$statusNotice.text( u2fL10n.text.u2f_not_supported );
}
$button.click( function() {
var registerRequest;
if ( $( this ).prop( 'disabled' ) ) {
return false;
}
$( this ).prop( 'disabled', true );
$( '.register-security-key .spinner' ).addClass( 'is-active' );
$statusNotice.text( '' );
registerRequest = {
version: u2fL10n.register.request.version,
challenge: u2fL10n.register.request.challenge
};
window.u2f.register( u2fL10n.register.request.appId, [ registerRequest ], u2fL10n.register.sigs, function( data ) {
$( '.register-security-key .spinner' ).removeClass( 'is-active' );
$button.prop( 'disabled', false );
if ( data.errorCode ) {
if ( u2fL10n.text.error_codes[ data.errorCode ] ) {
$statusNotice.text( u2fL10n.text.error_codes[ data.errorCode ] );
} else {
$statusNotice.text( u2fL10n.text.error_codes[ u2fL10n.text.error ] );
}
return false;
}
$( '#do_new_security_key' ).val( 'true' );
$( '#u2f_response' ).val( JSON.stringify( data ) );
// See: http://stackoverflow.com/questions/833032/submit-is-not-a-function-error-in-javascript
$( '<form>' )[0].submit.call( $( '#your-profile' )[0] );
} );
} );
}( jQuery ) );

View File

@ -1,16 +0,0 @@
/* global window, u2f, u2fL10n, jQuery */
( function( $ ) {
if ( ! window.u2fL10n ) {
window.console.error( 'u2fL10n is not defined' );
return;
}
u2f.sign( u2fL10n.request[0].appId, u2fL10n.request[0].challenge, u2fL10n.request, function( data ) {
if ( data.errorCode ) {
window.console.error( 'Registration Failed', data.errorCode );
} else {
$( '#u2f_response' ).val( JSON.stringify( data ) );
$( '#loginform' ).submit();
}
} );
}( jQuery ) );

View File

@ -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 );
}
}() );

View File

@ -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 ) );

View File

@ -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;
}
}
}
}
);
}
}() );

View File

@ -0,0 +1,11 @@
/* global document, setTimeout */
( function() {
setTimeout( function() {
var d;
try {
d = document.getElementById( 'authcode' );
d.value = '';
d.focus();
} catch ( e ) {}
}, 200 );
}() );