updated plugin Two Factor
version 0.8.1
This commit is contained in:
@ -46,19 +46,53 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
|
||||
* 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( 'wp_ajax_two_factor_backup_codes_generate', array( $this, 'ajax_generate_json' ) );
|
||||
|
||||
return parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the rest-api endpoints required for this provider.
|
||||
*
|
||||
* @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 current_user_can( 'edit_user', $request['user_id'] );
|
||||
},
|
||||
'args' => array(
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'number',
|
||||
),
|
||||
'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();
|
||||
@ -125,8 +159,7 @@ 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 ) {
|
||||
$ajax_nonce = wp_create_nonce( 'two-factor-backup-codes-generate-json-' . $user->ID );
|
||||
$count = self::codes_remaining_for_user( $user );
|
||||
$count = self::codes_remaining_for_user( $user );
|
||||
?>
|
||||
<p id="two-factor-backup-codes">
|
||||
<button type="button" class="button button-two-factor-backup-codes-generate button-secondary hide-if-no-js">
|
||||
@ -154,39 +187,26 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
|
||||
<script type="text/javascript">
|
||||
( function( $ ) {
|
||||
$( '.button-two-factor-backup-codes-generate' ).click( function() {
|
||||
$.ajax( {
|
||||
wp.apiRequest( {
|
||||
method: 'POST',
|
||||
url: ajaxurl,
|
||||
path: <?php echo wp_json_encode( Two_Factor_Core::REST_NAMESPACE . '/generate-backup-codes' ); ?>,
|
||||
data: {
|
||||
action: 'two_factor_backup_codes_generate',
|
||||
user_id: '<?php echo esc_js( $user->ID ); ?>',
|
||||
nonce: '<?php echo esc_js( $ajax_nonce ); ?>'
|
||||
},
|
||||
dataType: 'JSON',
|
||||
success: 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.data.codes.length; i++ ) {
|
||||
$codesList.append( '<li>' + response.data.codes[ i ] + '</li>' );
|
||||
}
|
||||
|
||||
// Update counter.
|
||||
$( '.two-factor-backup-codes-count' ).html( response.data.i18n.count );
|
||||
|
||||
// Build the download link.
|
||||
var txt_data = 'data:application/text;charset=utf-8,' + '\n';
|
||||
txt_data += response.data.i18n.title.replace( /%s/g, document.domain ) + '\n\n';
|
||||
|
||||
for ( i = 0; i < response.data.codes.length; i++ ) {
|
||||
txt_data += i + 1 + '. ' + response.data.codes[ i ] + '\n';
|
||||
}
|
||||
|
||||
$( '#two-factor-backup-codes-download-link' ).attr( 'href', encodeURI( txt_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 );
|
||||
@ -233,30 +253,53 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a JSON object of backup codes.
|
||||
* Generates Backup Codes for returning through the WordPress Rest API.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
* @since 0.8.0
|
||||
*/
|
||||
public function ajax_generate_json() {
|
||||
$user = get_user_by( 'id', filter_input( INPUT_POST, 'user_id', FILTER_SANITIZE_NUMBER_INT ) );
|
||||
check_ajax_referer( 'two-factor-backup-codes-generate-json-' . $user->ID, 'nonce' );
|
||||
public function rest_generate_codes( $request ) {
|
||||
$user_id = $request['user_id'];
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
|
||||
// Setup the return data.
|
||||
$codes = $this->generate_codes( $user );
|
||||
$count = self::codes_remaining_for_user( $user );
|
||||
$i18n = array(
|
||||
/* translators: %s: count */
|
||||
'count' => esc_html( sprintf( _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'two-factor' ), $count ) ),
|
||||
/* translators: %s: the site's domain */
|
||||
'title' => esc_html__( 'Two-Factor Backup Codes for %s', 'two-factor' ),
|
||||
// Hardcode these, the user shouldn't be able to choose them.
|
||||
$args = array(
|
||||
'number' => self::NUMBER_OF_CODES,
|
||||
'method' => 'replace',
|
||||
);
|
||||
|
||||
// Send the response.
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'codes' => $codes,
|
||||
'i18n' => $i18n,
|
||||
)
|
||||
// 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 Backup 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.', '%s unused codes remaining.', $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 Backup Codes provider for this user.', 'two-factor' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
return array(
|
||||
'codes' => $codes,
|
||||
'download_link' => $download_link,
|
||||
'remaining' => $count,
|
||||
'i18n' => $i18n,
|
||||
);
|
||||
}
|
||||
|
||||
@ -284,10 +327,10 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
|
||||
public function authentication_page( $user ) {
|
||||
require_once ABSPATH . '/wp-admin/includes/template.php';
|
||||
?>
|
||||
<p><?php esc_html_e( 'Enter a backup verification code.', 'two-factor' ); ?></p><br/>
|
||||
<p class="two-factor-prompt"><?php esc_html_e( 'Enter a backup verification code.', 'two-factor' ); ?></p>
|
||||
<p>
|
||||
<label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label>
|
||||
<input type="tel" name="two-factor-backup-code" id="authcode" class="input" value="" size="20" pattern="[0-9]*" />
|
||||
<input type="text" inputmode="numeric" name="two-factor-backup-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="1234 5678" data-digits="8" />
|
||||
</p>
|
||||
<?php
|
||||
submit_button( __( 'Submit', 'two-factor' ) );
|
||||
@ -304,7 +347,11 @@ class Two_Factor_Backup_Codes extends Two_Factor_Provider {
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_authentication( $user ) {
|
||||
$backup_code = isset( $_POST['two-factor-backup-code'] ) ? sanitize_text_field( wp_unslash( $_POST['two-factor-backup-code'] ) ) : '';
|
||||
$backup_code = $this->sanitize_code_from_request( 'two-factor-backup-code' );
|
||||
if ( ! $backup_code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->validate_code( $user, $backup_code );
|
||||
}
|
||||
|
||||
|
@ -266,10 +266,10 @@ class Two_Factor_Email extends Two_Factor_Provider {
|
||||
|
||||
require_once ABSPATH . '/wp-admin/includes/template.php';
|
||||
?>
|
||||
<p><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'two-factor' ); ?></p>
|
||||
<p class="two-factor-prompt"><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'two-factor' ); ?></p>
|
||||
<p>
|
||||
<label for="authcode"><?php esc_html_e( 'Verification Code:', 'two-factor' ); ?></label>
|
||||
<input type="tel" name="two-factor-email-code" id="authcode" class="input" value="" size="20" />
|
||||
<input type="text" inputmode="numeric" name="two-factor-email-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="1234 5678" data-digits="8" />
|
||||
<?php submit_button( __( 'Log In', 'two-factor' ) ); ?>
|
||||
</p>
|
||||
<p class="two-factor-email-resend">
|
||||
@ -313,13 +313,11 @@ class Two_Factor_Email extends Two_Factor_Provider {
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_authentication( $user ) {
|
||||
if ( ! isset( $user->ID ) || ! isset( $_REQUEST['two-factor-email-code'] ) ) {
|
||||
$code = $this->sanitize_code_from_request( 'two-factor-email-code' );
|
||||
if ( ! isset( $user->ID ) || ! $code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure there are no spaces or line breaks around the code.
|
||||
$code = trim( sanitize_text_field( $_REQUEST['two-factor-email-code'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, handled by the core method already.
|
||||
|
||||
return $this->validate_token( $user->ID, $code );
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ abstract class Two_Factor_Provider {
|
||||
* @param string|array $chars Valid auth code characters.
|
||||
* @return string
|
||||
*/
|
||||
public function get_code( $length = 8, $chars = '1234567890' ) {
|
||||
public static function get_code( $length = 8, $chars = '1234567890' ) {
|
||||
$code = '';
|
||||
if ( is_array( $chars ) ) {
|
||||
$chars = implode( '', $chars );
|
||||
@ -99,4 +99,27 @@ abstract class Two_Factor_Provider {
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a numeric code to be used as an auth code.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
@ -11,25 +11,18 @@
|
||||
class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* The user meta token key.
|
||||
* The user meta key for the TOTP Secret key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SECRET_META_KEY = '_two_factor_totp_key';
|
||||
|
||||
/**
|
||||
* The user meta token key.
|
||||
* The user meta key for the last successful TOTP token timestamp logged in with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const NOTICES_META_KEY = '_two_factor_totp_notices';
|
||||
|
||||
/**
|
||||
* Action name for resetting the secret token.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const ACTION_SECRET_DELETE = 'totp-delete';
|
||||
const LAST_SUCCESSFUL_LOGIN_META_KEY = '_two_factor_totp_last_successful_login';
|
||||
|
||||
const DEFAULT_KEY_BIT_SIZE = 160;
|
||||
const DEFAULT_CRYPTO = 'sha1';
|
||||
@ -44,20 +37,6 @@ class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
*/
|
||||
private static $base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
|
||||
/**
|
||||
* Class constructor. Sets up hooks, etc.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function __construct() {
|
||||
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_two_factor_options' ) );
|
||||
add_action( 'personal_options_update', array( $this, 'user_two_factor_options_update' ) );
|
||||
add_action( 'edit_user_profile_update', array( $this, 'user_two_factor_options_update' ) );
|
||||
add_action( 'two_factor_user_settings_action', array( $this, 'user_settings_action' ), 10, 2 );
|
||||
|
||||
return parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures only one instance of this class exists in memory at any one time.
|
||||
*
|
||||
@ -71,6 +50,75 @@ class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor. Sets up hooks, etc.
|
||||
*
|
||||
* @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' ) );
|
||||
|
||||
return parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the rest-api endpoints required for this provider.
|
||||
*
|
||||
* @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 current_user_can( 'edit_user', $request['user_id'] );
|
||||
},
|
||||
'args' => array(
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'number',
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'rest_setup_totp' ),
|
||||
'permission_callback' => function( $request ) {
|
||||
return current_user_can( 'edit_user', $request['user_id'] );
|
||||
},
|
||||
'args' => array(
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'number',
|
||||
),
|
||||
'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.
|
||||
*/
|
||||
@ -79,32 +127,137 @@ class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger our custom user settings actions.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
* @param string $action Action ID.
|
||||
*
|
||||
* @return void
|
||||
* Enqueue scripts
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function user_settings_action( $user_id, $action ) {
|
||||
if ( self::ACTION_SECRET_DELETE === $action ) {
|
||||
$this->delete_user_totp_key( $user_id );
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for deleting the secret token.
|
||||
* Rest API endpoint for handling deactivation of TOTP.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
* @param WP_Rest_Request $request The Rest Request object.
|
||||
* @return array Success array.
|
||||
*/
|
||||
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 );
|
||||
|
||||
ob_start();
|
||||
$this->user_two_factor_options( $user );
|
||||
$html = ob_get_clean();
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API endpoint for setting up TOTP.
|
||||
*
|
||||
* @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 [
|
||||
'success' => true,
|
||||
'html' => $html,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a URL that can be used to create a QR code.
|
||||
*
|
||||
* @param WP_User $user The user to generate a URL for.
|
||||
* @param string $key The secret key.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function get_token_delete_url_for_user( $user_id ) {
|
||||
return Two_Factor_Core::get_user_update_action_url( $user_id, self::ACTION_SECRET_DELETE );
|
||||
public static function generate_qr_code_url( $user, $secret_key ) {
|
||||
$issuer = get_bloginfo( 'name', 'display' );
|
||||
|
||||
/**
|
||||
* Filter the Issuer for the TOTP.
|
||||
*
|
||||
* Must follow the TOTP format for a "issuer". Do not URL Encode.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* Must follow the TOTP format for a "label". Do not URL Encode.
|
||||
*
|
||||
* @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 )
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the TOTP generated URL.
|
||||
*
|
||||
* Must follow the TOTP format. Do not URL Encode.
|
||||
*
|
||||
* @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' ) );
|
||||
|
||||
return $totp_url;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -120,96 +273,145 @@ class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
return false;
|
||||
}
|
||||
|
||||
wp_nonce_field( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options', false );
|
||||
|
||||
$key = $this->get_user_totp_key( $user->ID );
|
||||
$this->admin_notices( $user->ID );
|
||||
|
||||
wp_enqueue_script( 'two-factor-qr-code-generator' );
|
||||
|
||||
?>
|
||||
<div id="two-factor-totp-options">
|
||||
<?php
|
||||
if ( empty( $key ) ) :
|
||||
$key = $this->generate_key();
|
||||
$site_name = get_bloginfo( 'name', 'display' );
|
||||
$totp_title = apply_filters( 'two_factor_totp_title', $site_name . ':' . $user->user_login, $user );
|
||||
|
||||
$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' ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<img src="<?php echo esc_url( $this->get_google_qr_code( $totp_title, $key, $site_name ) ); ?>" id="two-factor-totp-qrcode" />
|
||||
<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 );
|
||||
};
|
||||
|
||||
// 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" name="two-factor-totp-key" value="<?php echo esc_attr( $key ); ?>" />
|
||||
<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' ); ?>
|
||||
<input type="tel" name="two-factor-totp-authcode" id="two-factor-totp-authcode" class="input" value="" size="20" pattern="[0-9]*" />
|
||||
<?php
|
||||
/* translators: Example auth code. */
|
||||
$placeholder = sprintf( __( 'eg. %s', 'two-factor' ), '123456' );
|
||||
?>
|
||||
<input type="tel" name="two-factor-totp-authcode" id="two-factor-totp-authcode" class="input" value="" size="20" pattern="[0-9 ]*" placeholder="<?php echo esc_attr( $placeholder ); ?>" />
|
||||
</label>
|
||||
<input type="submit" class="button" name="two-factor-totp-submit" value="<?php esc_attr_e( 'Submit', 'two-factor' ); ?>" />
|
||||
<input type="submit" class="button totp-submit" name="two-factor-totp-submit" value="<?php esc_attr_e( 'Submit', 'two-factor' ); ?>" />
|
||||
</p>
|
||||
|
||||
<script>
|
||||
(function($){
|
||||
$('.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,
|
||||
}
|
||||
} ).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 );
|
||||
|
||||
$('#two-factor-totp-authcode').val('');
|
||||
} ).then( function( response ) {
|
||||
$( '#two-factor-totp-options' ).html( response.html );
|
||||
} );
|
||||
} );
|
||||
})(jQuery);
|
||||
</script>
|
||||
|
||||
<?php else : ?>
|
||||
<p class="success">
|
||||
<?php esc_html_e( 'Secret key is configured and registered. It is not possible to view it again for security reasons.', 'two-factor' ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<a class="button" href="<?php echo esc_url( self::get_token_delete_url_for_user( $user->ID ) ); ?>"><?php esc_html_e( 'Reset Key', 'two-factor' ); ?></a>
|
||||
<a class="button reset-totp-key" href="#"><?php esc_html_e( 'Reset Key', 'two-factor' ); ?></a>
|
||||
<em class="description">
|
||||
<?php esc_html_e( 'You will have to re-scan the QR code on all devices as the previous codes will stop working.', 'two-factor' ); ?>
|
||||
</em>
|
||||
<script>
|
||||
( function( $ ) {
|
||||
$( 'a.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 ) {
|
||||
$( '#two-factor-totp-options' ).html( response.html );
|
||||
} );
|
||||
} );
|
||||
} )( jQuery );
|
||||
</script>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the options specified in `::user_two_factor_options()`
|
||||
*
|
||||
* @param integer $user_id The user ID whose options are being updated.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function user_two_factor_options_update( $user_id ) {
|
||||
$notices = array();
|
||||
$errors = array();
|
||||
|
||||
if ( isset( $_POST['_nonce_user_two_factor_totp_options'] ) ) {
|
||||
check_admin_referer( 'user_two_factor_totp_options', '_nonce_user_two_factor_totp_options' );
|
||||
|
||||
// Validate and store a new secret key.
|
||||
if ( ! empty( $_POST['two-factor-totp-authcode'] ) && ! empty( $_POST['two-factor-totp-key'] ) ) {
|
||||
// Don't use filter_input() because we can't mock it during tests for now.
|
||||
$authcode = filter_var( sanitize_text_field( $_POST['two-factor-totp-authcode'] ), FILTER_SANITIZE_NUMBER_INT );
|
||||
$key = sanitize_text_field( $_POST['two-factor-totp-key'] );
|
||||
|
||||
if ( $this->is_valid_key( $key ) ) {
|
||||
if ( $this->is_valid_authcode( $key, $authcode ) ) {
|
||||
if ( ! $this->set_user_totp_key( $user_id, $key ) ) {
|
||||
$errors[] = __( 'Unable to save Two Factor Authentication code. Please re-scan the QR code and enter the code provided by your application.', 'two-factor' );
|
||||
}
|
||||
} else {
|
||||
$errors[] = __( 'Invalid Two Factor Authentication code.', 'two-factor' );
|
||||
}
|
||||
} else {
|
||||
$errors[] = __( 'Invalid Two Factor Authentication secret key.', 'two-factor' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
$notices['error'] = $errors;
|
||||
}
|
||||
|
||||
if ( ! empty( $notices ) ) {
|
||||
update_user_meta( $user_id, self::NOTICES_META_KEY, $notices );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the TOTP secret key for a user.
|
||||
*
|
||||
@ -241,6 +443,7 @@ class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
* @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 );
|
||||
}
|
||||
|
||||
@ -261,68 +464,74 @@ class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display any available admin notices.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function admin_notices( $user_id ) {
|
||||
$notices = get_user_meta( $user_id, self::NOTICES_META_KEY, true );
|
||||
|
||||
if ( ! empty( $notices ) ) {
|
||||
delete_user_meta( $user_id, self::NOTICES_META_KEY );
|
||||
|
||||
foreach ( $notices as $class => $messages ) {
|
||||
?>
|
||||
<div class="<?php echo esc_attr( $class ); ?>">
|
||||
<?php
|
||||
foreach ( $messages as $msg ) {
|
||||
?>
|
||||
<p>
|
||||
<span><?php echo esc_html( $msg ); ?><span>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates authentication.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @return bool Whether the user gave a valid code
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function validate_authentication( $user ) {
|
||||
if ( ! empty( $_REQUEST['authcode'] ) ) {
|
||||
return $this->is_valid_authcode(
|
||||
$this->get_user_totp_key( $user->ID ),
|
||||
sanitize_text_field( $_REQUEST['authcode'] )
|
||||
);
|
||||
$code = $this->sanitize_code_from_request( 'authcode', self::DEFAULT_DIGIT_COUNT );
|
||||
if ( ! $code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->validate_code_for_user( $user, $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given code is valid for a given key, allowing for a certain amount of time drift
|
||||
* Validates an authentication code for a given user, preventing re-use and older TOTP keys.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @param string $key The share secret key to use.
|
||||
* @param string $authcode The code to test.
|
||||
*
|
||||
* @return bool Whether the code is valid within the time frame
|
||||
* @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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given code is valid for a given key, allowing for a certain amount of time drift.
|
||||
*
|
||||
* @param string $key The share secret key to use.
|
||||
* @param string $authcode The code to test.
|
||||
*
|
||||
* @return false|int Returns the timestamp of the authcode on success, False otherwise.
|
||||
*/
|
||||
public static function get_authcode_valid_ticktime( $key, $authcode ) {
|
||||
/**
|
||||
* Filter the maximum ticks to allow when checking valid codes.
|
||||
*
|
||||
@ -340,14 +549,16 @@ class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
$ticks = range( - $max_ticks, $max_ticks );
|
||||
usort( $ticks, array( __CLASS__, 'abssort' ) );
|
||||
|
||||
$time = time() / self::DEFAULT_TIME_STEP_SEC;
|
||||
$time = floor( time() / self::DEFAULT_TIME_STEP_SEC );
|
||||
|
||||
foreach ( $ticks as $offset ) {
|
||||
$log_time = $time + $offset;
|
||||
if ( hash_equals(self::calc_totp( $key, $log_time ), $authcode ) ) {
|
||||
return true;
|
||||
if ( hash_equals( self::calc_totp( $key, $log_time ), $authcode ) ) {
|
||||
// Return the tick timestamp.
|
||||
return $log_time * self::DEFAULT_TIME_STEP_SEC;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -429,27 +640,6 @@ class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
return str_pad( $code, $digits, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the Google Charts API to build a QR Code for use with an otpauth url
|
||||
*
|
||||
* @param string $name The name to display in the Authentication app.
|
||||
* @param string $key The secret key to share with the Authentication app.
|
||||
* @param string $title The title to display in the Authentication app.
|
||||
*
|
||||
* @return string A URL to use as an img src to display the QR code
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function get_google_qr_code( $name, $key, $title = null ) {
|
||||
// Encode to support spaces, question marks and other characters.
|
||||
$name = rawurlencode( $name );
|
||||
$google_url = urlencode( 'otpauth://totp/' . $name . '?secret=' . $key );
|
||||
if ( isset( $title ) ) {
|
||||
$google_url .= urlencode( '&issuer=' . rawurlencode( $title ) );
|
||||
}
|
||||
return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . $google_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and available for the user specified.
|
||||
*
|
||||
@ -474,12 +664,12 @@ class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
public function authentication_page( $user ) {
|
||||
require_once ABSPATH . '/wp-admin/includes/template.php';
|
||||
?>
|
||||
<p>
|
||||
<p class="two-factor-prompt">
|
||||
<?php esc_html_e( 'Please enter the code generated by your authenticator app.', 'two-factor' ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<label for="authcode"><?php esc_html_e( 'Authentication Code:', 'two-factor' ); ?></label>
|
||||
<input type="tel" autocomplete="one-time-code" name="authcode" id="authcode" class="input" value="" size="20" pattern="[0-9]*" />
|
||||
<input type="text" inputmode="numeric" autocomplete="one-time-code" name="authcode" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="123 456" data-digits="<?php echo esc_attr( self::DEFAULT_DIGIT_COUNT ); ?>" />
|
||||
</p>
|
||||
<script type="text/javascript">
|
||||
setTimeout( function(){
|
||||
|
Reference in New Issue
Block a user