installed plugin Two Factor
version 0.7.3
This commit is contained in:
@ -0,0 +1,355 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for creating a backup codes provider.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for creating a backup codes provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
class Two_Factor_Backup_Codes extends Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* The user meta backup codes key.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
const BACKUP_CODES_META_KEY = '_two_factor_backup_codes';
|
||||
|
||||
/**
|
||||
* The number backup codes.
|
||||
*
|
||||
* @type int
|
||||
*/
|
||||
const NUMBER_OF_CODES = 10;
|
||||
|
||||
/**
|
||||
* Ensures only one instance of this class exists in memory at any one time.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public static function get_instance() {
|
||||
static $instance;
|
||||
$class = __CLASS__;
|
||||
if ( ! is_a( $instance, $class ) ) {
|
||||
$instance = new $class();
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
protected function __construct() {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an admin notice when backup codes have run out.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function admin_notices() {
|
||||
$user = wp_get_current_user();
|
||||
|
||||
// Return if the provider is not enabled.
|
||||
if ( ! in_array( __CLASS__, Two_Factor_Core::get_enabled_providers_for_user( $user->ID ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return if we are not out of codes.
|
||||
if ( $this->is_available_for_user( $user ) ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="error">
|
||||
<p>
|
||||
<span>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
sprintf(
|
||||
/* translators: %s: URL for code regeneration */
|
||||
__( 'Two-Factor: You are out of backup codes and need to <a href="%s">regenerate!</a>', 'two-factor' ),
|
||||
esc_url( get_edit_user_link( $user->ID ) . '#two-factor-backup-codes' )
|
||||
),
|
||||
array( 'a' => array( 'href' => true ) )
|
||||
);
|
||||
?>
|
||||
<span>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function get_label() {
|
||||
return _x( 'Backup Verification Codes (Single Use)', 'Provider Label', 'two-factor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and codes are available for the user specified.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_available_for_user( $user ) {
|
||||
// Does this user have available codes?
|
||||
if ( 0 < self::codes_remaining_for_user( $user ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts markup at the end of the user profile field for this provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
public function user_options( $user ) {
|
||||
$ajax_nonce = wp_create_nonce( 'two-factor-backup-codes-generate-json-' . $user->ID );
|
||||
$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">
|
||||
<?php esc_html_e( 'Generate Verification Codes', 'two-factor' ); ?>
|
||||
</button>
|
||||
<span class="two-factor-backup-codes-count">
|
||||
<?php
|
||||
echo esc_html(
|
||||
sprintf(
|
||||
/* translators: %s: count */
|
||||
_n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'two-factor' ),
|
||||
$count
|
||||
)
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</p>
|
||||
<div class="two-factor-backup-codes-wrapper" style="display:none;">
|
||||
<ol class="two-factor-backup-codes-unused-codes"></ol>
|
||||
<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-download button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-download-link" download="two-factor-backup-codes.txt"><?php esc_html_e( 'Download Codes', 'two-factor' ); ?></a>
|
||||
<p>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
( function( $ ) {
|
||||
$( '.button-two-factor-backup-codes-generate' ).click( function() {
|
||||
$.ajax( {
|
||||
method: 'POST',
|
||||
url: ajaxurl,
|
||||
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 ) );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
} )( jQuery );
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates backup codes & updates the user meta.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @param array $args Optional arguments for assigning new codes.
|
||||
* @return array
|
||||
*/
|
||||
public function generate_codes( $user, $args = '' ) {
|
||||
$codes = array();
|
||||
$codes_hashed = array();
|
||||
|
||||
// Check for arguments.
|
||||
if ( isset( $args['number'] ) ) {
|
||||
$num_codes = (int) $args['number'];
|
||||
} else {
|
||||
$num_codes = self::NUMBER_OF_CODES;
|
||||
}
|
||||
|
||||
// Append or replace (default).
|
||||
if ( isset( $args['method'] ) && 'append' === $args['method'] ) {
|
||||
$codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
|
||||
}
|
||||
|
||||
for ( $i = 0; $i < $num_codes; $i++ ) {
|
||||
$code = $this->get_code();
|
||||
$codes_hashed[] = wp_hash_password( $code );
|
||||
$codes[] = $code;
|
||||
unset( $code );
|
||||
}
|
||||
|
||||
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $codes_hashed );
|
||||
|
||||
// Unhashed.
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a JSON object of backup codes.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
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' );
|
||||
|
||||
// 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' ),
|
||||
);
|
||||
|
||||
// Send the response.
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'codes' => $codes,
|
||||
'i18n' => $i18n,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of unused codes for the specified user
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return int $int The number of unused codes remaining
|
||||
*/
|
||||
public static function codes_remaining_for_user( $user ) {
|
||||
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
|
||||
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
|
||||
return count( $backup_codes );
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
public function authentication_page( $user ) {
|
||||
require_once ABSPATH . '/wp-admin/includes/template.php';
|
||||
?>
|
||||
<p><?php esc_html_e( 'Enter a backup verification code.', 'two-factor' ); ?></p><br/>
|
||||
<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]*" />
|
||||
</p>
|
||||
<?php
|
||||
submit_button( __( 'Submit', 'two-factor' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the users input token.
|
||||
*
|
||||
* In this class we just return true.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_authentication( $user ) {
|
||||
$backup_code = isset( $_POST['two-factor-backup-code'] ) ? sanitize_text_field( wp_unslash( $_POST['two-factor-backup-code'] ) ) : '';
|
||||
return $this->validate_code( $user, $backup_code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a backup code.
|
||||
*
|
||||
* Backup Codes are single use and are deleted upon a successful validation.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @param int $code The backup code.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_code( $user, $code ) {
|
||||
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
|
||||
|
||||
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
|
||||
foreach ( $backup_codes as $code_index => $code_hashed ) {
|
||||
if ( wp_check_password( $code, $code_hashed, $user->ID ) ) {
|
||||
$this->delete_code( $user, $code_hashed );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a backup code.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @param string $code_hashed The hashed the backup code.
|
||||
*/
|
||||
public function delete_code( $user, $code_hashed ) {
|
||||
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
|
||||
|
||||
// Delete the current code from the list since it's been used.
|
||||
$backup_codes = array_flip( $backup_codes );
|
||||
unset( $backup_codes[ $code_hashed ] );
|
||||
$backup_codes = array_values( array_flip( $backup_codes ) );
|
||||
|
||||
// Update the backup code master list.
|
||||
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $backup_codes );
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for creating a dummy provider.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for creating a dummy provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
class Two_Factor_Dummy extends Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* Ensures only one instance of this class exists in memory at any one time.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public static function get_instance() {
|
||||
static $instance;
|
||||
$class = __CLASS__;
|
||||
if ( ! is_a( $instance, $class ) ) {
|
||||
$instance = new $class();
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
protected function __construct() {
|
||||
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
|
||||
return parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function get_label() {
|
||||
return _x( 'Dummy Method', 'Provider Label', 'two-factor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
public function authentication_page( $user ) {
|
||||
require_once ABSPATH . '/wp-admin/includes/template.php';
|
||||
?>
|
||||
<p><?php esc_html_e( 'Are you really you?', 'two-factor' ); ?></p>
|
||||
<?php
|
||||
submit_button( __( 'Yup.', 'two-factor' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the users input token.
|
||||
*
|
||||
* In this class we just return true.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_authentication( $user ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and available for the user specified.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_available_for_user( $user ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts markup at the end of the user profile field for this provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
public function user_options( $user ) {}
|
||||
}
|
@ -0,0 +1,361 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for creating an email provider.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for creating an email provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
class Two_Factor_Email extends Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* The user meta token key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TOKEN_META_KEY = '_two_factor_email_token';
|
||||
|
||||
/**
|
||||
* Store the timestamp when the token was generated.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TOKEN_META_KEY_TIMESTAMP = '_two_factor_email_token_timestamp';
|
||||
|
||||
/**
|
||||
* Name of the input field used for code resend.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const INPUT_NAME_RESEND_CODE = 'two-factor-email-code-resend';
|
||||
|
||||
/**
|
||||
* Ensures only one instance of this class exists in memory at any one time.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public static function get_instance() {
|
||||
static $instance;
|
||||
$class = __CLASS__;
|
||||
if ( ! is_a( $instance, $class ) ) {
|
||||
$instance = new $class();
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
protected function __construct() {
|
||||
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
|
||||
return parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function get_label() {
|
||||
return _x( 'Email', 'Provider Label', 'two-factor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the user token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return string
|
||||
*/
|
||||
public function generate_token( $user_id ) {
|
||||
$token = $this->get_code();
|
||||
|
||||
update_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, time() );
|
||||
update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) );
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a valid token already.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return boolean If user has a valid email token.
|
||||
*/
|
||||
public function user_has_token( $user_id ) {
|
||||
$hashed_token = $this->get_user_token( $user_id );
|
||||
|
||||
if ( ! empty( $hashed_token ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the user token validity timestamp expired.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function user_token_has_expired( $user_id ) {
|
||||
$token_lifetime = $this->user_token_lifetime( $user_id );
|
||||
$token_ttl = $this->user_token_ttl( $user_id );
|
||||
|
||||
// Invalid token lifetime is considered an expired token.
|
||||
if ( is_int( $token_lifetime ) && $token_lifetime <= $token_ttl ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lifetime of a user token in seconds.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*
|
||||
* @return integer|null Return `null` if the lifetime can't be measured.
|
||||
*/
|
||||
public function user_token_lifetime( $user_id ) {
|
||||
$timestamp = intval( get_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, true ) );
|
||||
|
||||
if ( ! empty( $timestamp ) ) {
|
||||
return time() - $timestamp;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the token time-to-live for a user.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function user_token_ttl( $user_id ) {
|
||||
$token_ttl = 15 * MINUTE_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Number of seconds the token is considered valid
|
||||
* after the generation.
|
||||
*
|
||||
* @param integer $token_ttl Token time-to-live in seconds.
|
||||
* @param integer $user_id User ID.
|
||||
*/
|
||||
return (int) apply_filters( 'two_factor_token_ttl', $token_ttl, $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication token for the user.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return string|boolean User token or `false` if no token found.
|
||||
*/
|
||||
public function get_user_token( $user_id ) {
|
||||
$hashed_token = get_user_meta( $user_id, self::TOKEN_META_KEY, true );
|
||||
|
||||
if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
|
||||
return $hashed_token;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the user token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $token User token.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_token( $user_id, $token ) {
|
||||
$hashed_token = $this->get_user_token( $user_id );
|
||||
|
||||
// Bail if token is empty or it doesn't match.
|
||||
if ( empty( $hashed_token ) || ! hash_equals( wp_hash( $token ), $hashed_token ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->user_token_has_expired( $user_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the token can be used only once.
|
||||
$this->delete_token( $user_id );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*/
|
||||
public function delete_token( $user_id ) {
|
||||
delete_user_meta( $user_id, self::TOKEN_META_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and email the user token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return bool Whether the email contents were sent successfully.
|
||||
*/
|
||||
public function generate_and_email_token( $user ) {
|
||||
$token = $this->generate_token( $user->ID );
|
||||
|
||||
/* 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 ) );
|
||||
|
||||
/**
|
||||
* Filter the token email subject.
|
||||
*
|
||||
* @param string $subject The email subject line.
|
||||
* @param int $user_id The ID of the user.
|
||||
*/
|
||||
$subject = apply_filters( 'two_factor_token_email_subject', $subject, $user->ID );
|
||||
|
||||
/**
|
||||
* Filter the token email message.
|
||||
*
|
||||
* @param string $message The email message.
|
||||
* @param string $token The token.
|
||||
* @param int $user_id The ID of the user.
|
||||
*/
|
||||
$message = apply_filters( 'two_factor_token_email_message', $message, $token, $user->ID );
|
||||
|
||||
return wp_mail( $user->user_email, $subject, $message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
public function authentication_page( $user ) {
|
||||
if ( ! $user ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->user_has_token( $user->ID ) || $this->user_token_has_expired( $user->ID ) ) {
|
||||
$this->generate_and_email_token( $user );
|
||||
}
|
||||
|
||||
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>
|
||||
<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" />
|
||||
<?php submit_button( __( 'Log In', 'two-factor' ) ); ?>
|
||||
</p>
|
||||
<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
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the email code if missing or requested. Stop the authentication
|
||||
* validation if a new token has been generated and sent.
|
||||
*
|
||||
* @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 ] ) ) {
|
||||
$this->generate_and_email_token( $user );
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the users input token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_authentication( $user ) {
|
||||
if ( ! isset( $user->ID ) || ! isset( $_REQUEST['two-factor-email-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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and available for the user specified.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_available_for_user( $user ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts markup at the end of the user profile field for this provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
public function user_options( $user ) {
|
||||
$email = $user->user_email;
|
||||
?>
|
||||
<div>
|
||||
<?php
|
||||
echo esc_html(
|
||||
sprintf(
|
||||
/* translators: %s: email address */
|
||||
__( 'Authentication codes will be sent to %s.', 'two-factor' ),
|
||||
$email
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
<?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
|
||||
}
|
||||
}
|
@ -0,0 +1,359 @@
|
||||
<?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 ) {
|
||||
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 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 false
|
||||
*/
|
||||
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 false;
|
||||
}
|
||||
|
||||
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 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();
|
||||
}
|
||||
}
|
@ -0,0 +1,397 @@
|
||||
<?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';
|
||||
|
||||
/**
|
||||
* Ensures only one instance of this class exists in memory at any one time.
|
||||
*
|
||||
* @return \Two_Factor_FIDO_U2F
|
||||
*/
|
||||
public static function get_instance() {
|
||||
static $instance;
|
||||
|
||||
if ( ! isset( $instance ) ) {
|
||||
$instance = new self();
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @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' ) );
|
||||
|
||||
return 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' );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 null
|
||||
*/
|
||||
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 null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract class for creating two factor authentication providers.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstract class for creating two factor authentication providers.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
abstract class Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
protected function __construct() {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function get_label();
|
||||
|
||||
/**
|
||||
* Prints the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function print_label() {
|
||||
echo esc_html( $this->get_label() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*/
|
||||
abstract public function authentication_page( $user );
|
||||
|
||||
/**
|
||||
* Allow providers to do extra processing before the authentication.
|
||||
* Return `true` to prevent the authentication and render the
|
||||
* authentication page.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function pre_process_authentication( $user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the users input token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
abstract public function validate_authentication( $user );
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and available for the user specified.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
abstract public function is_available_for_user( $user );
|
||||
|
||||
/**
|
||||
* Generate a random eight-digit string to send out as an auth code.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param int $length The code length.
|
||||
* @param string|array $chars Valid auth code characters.
|
||||
* @return string
|
||||
*/
|
||||
public function get_code( $length = 8, $chars = '1234567890' ) {
|
||||
$code = '';
|
||||
if ( is_array( $chars ) ) {
|
||||
$chars = implode( '', $chars );
|
||||
}
|
||||
for ( $i = 0; $i < $length; $i++ ) {
|
||||
$code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
}
|
@ -0,0 +1,578 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for creating a Time Based One-Time Password provider.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class Two_Factor_Totp
|
||||
*/
|
||||
class Two_Factor_Totp extends Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* The user meta token key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SECRET_META_KEY = '_two_factor_totp_key';
|
||||
|
||||
/**
|
||||
* The user meta token key.
|
||||
*
|
||||
* @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 DEFAULT_KEY_BIT_SIZE = 160;
|
||||
const DEFAULT_CRYPTO = 'sha1';
|
||||
const DEFAULT_DIGIT_COUNT = 6;
|
||||
const DEFAULT_TIME_STEP_SEC = 30;
|
||||
const DEFAULT_TIME_STEP_ALLOWANCE = 4;
|
||||
|
||||
/**
|
||||
* Characters used in base32 encoding.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
|
||||
/**
|
||||
* Class constructor. Sets up hooks, etc.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function get_instance() {
|
||||
static $instance;
|
||||
if ( ! isset( $instance ) ) {
|
||||
$instance = new self();
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*/
|
||||
public function get_label() {
|
||||
return _x( 'Time Based One-Time Password (TOTP)', 'Provider Label', 'two-factor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger our custom user settings actions.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
* @param string $action Action ID.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function user_settings_action( $user_id, $action ) {
|
||||
if ( self::ACTION_SECRET_DELETE === $action ) {
|
||||
$this->delete_user_totp_key( $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for deleting the secret token.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*
|
||||
* @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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Display TOTP options on the user settings page.
|
||||
*
|
||||
* @param WP_User $user The current user being edited.
|
||||
* @return false
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function user_two_factor_options( $user ) {
|
||||
if ( ! isset( $user->ID ) ) {
|
||||
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 );
|
||||
|
||||
?>
|
||||
<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 );
|
||||
?>
|
||||
<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>
|
||||
<p>
|
||||
<code><?php echo esc_html( $key ); ?></code>
|
||||
</p>
|
||||
<p>
|
||||
<input type="hidden" 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]*" />
|
||||
</label>
|
||||
<input type="submit" class="button" name="two-factor-totp-submit" value="<?php esc_attr_e( 'Submit', 'two-factor' ); ?>" />
|
||||
</p>
|
||||
<?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>
|
||||
<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>
|
||||
</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.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_user_totp_key( $user_id ) {
|
||||
return (string) get_user_meta( $user_id, self::SECRET_META_KEY, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the TOTP secret key for a user.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $key TOTP secret key.
|
||||
*
|
||||
* @return boolean If the key was stored successfully.
|
||||
*/
|
||||
public function set_user_totp_key( $user_id, $key ) {
|
||||
return update_user_meta( $user_id, self::SECRET_META_KEY, $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the TOTP secret key for a user.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return boolean If the key was deleted successfully.
|
||||
*/
|
||||
public function delete_user_totp_key( $user_id ) {
|
||||
return delete_user_meta( $user_id, self::SECRET_META_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the TOTP secret key has a proper format.
|
||||
*
|
||||
* @param string $key TOTP secret key.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_valid_key( $key ) {
|
||||
$check = sprintf( '/^[%s]+$/', self::$base_32_chars );
|
||||
|
||||
if ( 1 === preg_match( $check, $key ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'] )
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function is_valid_authcode( $key, $authcode ) {
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @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' );
|
||||
|
||||
$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 = 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;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates key
|
||||
*
|
||||
* @param int $bitsize Nume of bits to use for key.
|
||||
*
|
||||
* @return string $bitsize long string composed of available base32 chars.
|
||||
*/
|
||||
public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) {
|
||||
$bytes = ceil( $bitsize / 8 );
|
||||
$secret = wp_generate_password( $bytes, true, true );
|
||||
|
||||
return self::base32_encode( $secret );
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack stuff
|
||||
*
|
||||
* @param string $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;
|
||||
}
|
||||
|
||||
$lowmap = 0xffffffff;
|
||||
$lower = $value & $lowmap;
|
||||
|
||||
return pack( 'NN', $higher, $lower );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a valid code given the shared secret key
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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 );
|
||||
|
||||
if ( false === $step_count ) {
|
||||
$step_count = floor( time() / $time_step );
|
||||
}
|
||||
|
||||
$timestamp = self::pack64( $step_count );
|
||||
|
||||
$hash = hash_hmac( $hash, $timestamp, $secret, true );
|
||||
|
||||
$offset = ord( $hash[19] ) & 0xf;
|
||||
|
||||
$code = (
|
||||
( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) |
|
||||
( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) |
|
||||
( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) |
|
||||
( ord( $hash[ $offset + 3 ] ) & 0xff )
|
||||
) % pow( 10, $digits );
|
||||
|
||||
return str_pad( $code, $digits, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_available_for_user( $user ) {
|
||||
// Only available if the secret key has been saved for the user.
|
||||
$key = $this->get_user_totp_key( $user->ID );
|
||||
|
||||
return ! empty( $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function authentication_page( $user ) {
|
||||
require_once ABSPATH . '/wp-admin/includes/template.php';
|
||||
?>
|
||||
<p>
|
||||
<?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]*" />
|
||||
</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' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a base32 encoded string.
|
||||
*
|
||||
* @param string $string String to be encoded using base32.
|
||||
*
|
||||
* @return string base32 encoded string without padding.
|
||||
*/
|
||||
public static function base32_encode( $string ) {
|
||||
if ( empty( $string ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$binary_string = '';
|
||||
|
||||
foreach ( str_split( $string ) as $character ) {
|
||||
$binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
$five_bit_sections = str_split( $binary_string, 5 );
|
||||
$base32_string = '';
|
||||
|
||||
foreach ( $five_bit_sections as $five_bit_section ) {
|
||||
$base32_string .= self::$base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ];
|
||||
}
|
||||
|
||||
return $base32_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a base32 string and return a binary representation
|
||||
*
|
||||
* @param string $base32_string The base 32 string to decode.
|
||||
*
|
||||
* @throws Exception If string contains non-base32 characters.
|
||||
*
|
||||
* @return string Binary representation of decoded string
|
||||
*/
|
||||
public static function base32_decode( $base32_string ) {
|
||||
|
||||
$base32_string = strtoupper( $base32_string );
|
||||
|
||||
if ( ! preg_match( '/^[' . self::$base_32_chars . ']+$/', $base32_string, $match ) ) {
|
||||
throw new Exception( 'Invalid characters in the base32 string.' );
|
||||
}
|
||||
|
||||
$l = strlen( $base32_string );
|
||||
$n = 0;
|
||||
$j = 0;
|
||||
$binary = '';
|
||||
|
||||
for ( $i = 0; $i < $l; $i++ ) {
|
||||
|
||||
$n = $n << 5; // Move buffer left by 5 to make room.
|
||||
$n = $n + strpos( self::$base_32_chars, $base32_string[ $i ] ); // Add value into buffer.
|
||||
$j += 5; // Keep track of number of bits in buffer.
|
||||
|
||||
if ( $j >= 8 ) {
|
||||
$j -= 8;
|
||||
$binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
|
||||
}
|
||||
}
|
||||
|
||||
return $binary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used with usort to sort an array by distance from 0
|
||||
*
|
||||
* @param int $a First array element.
|
||||
* @param int $b Second array element.
|
||||
*
|
||||
* @return int -1, 0, or 1 as needed by usort
|
||||
*/
|
||||
private static function abssort( $a, $b ) {
|
||||
$a = abs( $a );
|
||||
$b = abs( $b );
|
||||
if ( $a === $b ) {
|
||||
return 0;
|
||||
}
|
||||
return ( $a < $b ) ? -1 : 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
#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;
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
/* 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 ) );
|
48
wp-content/plugins/two-factor/providers/js/fido-u2f-admin.js
Normal file
48
wp-content/plugins/two-factor/providers/js/fido-u2f-admin.js
Normal file
@ -0,0 +1,48 @@
|
||||
/* 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 ) );
|
16
wp-content/plugins/two-factor/providers/js/fido-u2f-login.js
Normal file
16
wp-content/plugins/two-factor/providers/js/fido-u2f-login.js
Normal file
@ -0,0 +1,16 @@
|
||||
/* 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 ) );
|
Reference in New Issue
Block a user