2022-10-27 11:23:01 +00:00
< ? 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
2023-03-29 18:20:25 +00:00
*
* @ codeCoverageIgnore
2022-10-27 11:23:01 +00:00
*/
protected function __construct () {
2023-03-29 18:20:25 +00:00
add_action ( 'rest_api_init' , array ( $this , 'register_rest_routes' ) );
2022-10-27 11:23:01 +00:00
add_action ( 'two_factor_user_options_' . __CLASS__ , array ( $this , 'user_options' ) );
add_action ( 'admin_notices' , array ( $this , 'admin_notices' ) );
return parent :: __construct ();
}
2023-03-29 18:20:25 +00:00
/**
* 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 ,
),
),
)
);
}
2022-10-27 11:23:01 +00:00
/**
* Displays an admin notice when backup codes have run out .
*
* @ since 0.1 - dev
2023-03-29 18:20:25 +00:00
*
* @ codeCoverageIgnore
2022-10-27 11:23:01 +00:00
*/
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 ) {
2023-03-29 18:20:25 +00:00
$count = self :: codes_remaining_for_user ( $user );
2022-10-27 11:23:01 +00:00
?>
< 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 () {
2023-03-29 18:20:25 +00:00
wp . apiRequest ( {
2022-10-27 11:23:01 +00:00
method : 'POST' ,
2023-03-29 18:20:25 +00:00
path : < ? php echo wp_json_encode ( Two_Factor_Core :: REST_NAMESPACE . '/generate-backup-codes' ); ?> ,
2022-10-27 11:23:01 +00:00
data : {
2023-03-29 18:20:25 +00:00
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>' );
2022-10-27 11:23:01 +00:00
}
2023-03-29 18:20:25 +00:00
// Update counter.
$ ( '.two-factor-backup-codes-count' ) . html ( response . i18n . count );
$ ( '#two-factor-backup-codes-download-link' ) . attr ( 'href' , response . download_link );
2022-10-27 11:23:01 +00:00
} );
} );
} )( 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 ;
}
/**
2023-03-29 18:20:25 +00:00
* Generates Backup Codes for returning through the WordPress Rest API .
2022-10-27 11:23:01 +00:00
*
2023-03-29 18:20:25 +00:00
* @ since 0.8 . 0
2022-10-27 11:23:01 +00:00
*/
2023-03-29 18:20:25 +00:00
public function rest_generate_codes ( $request ) {
$user_id = $request [ 'user_id' ];
$user = get_user_by ( 'id' , $user_id );
// Hardcode these, the user shouldn't be able to choose them.
$args = array (
'number' => self :: NUMBER_OF_CODES ,
'method' => 'replace' ,
);
2022-10-27 11:23:01 +00:00
// Setup the return data.
2023-03-29 18:20:25 +00:00
$codes = $this -> generate_codes ( $user , $args );
2022-10-27 11:23:01 +00:00
$count = self :: codes_remaining_for_user ( $user );
2023-03-29 18:20:25 +00:00
$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 (
2022-10-27 11:23:01 +00:00
/* translators: %s: count */
'count' => esc_html ( sprintf ( _n ( '%s unused code remaining.' , '%s unused codes remaining.' , $count , 'two-factor' ), $count ) ),
);
2023-03-29 18:20:25 +00:00
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 ,
2022-10-27 11:23:01 +00:00
);
}
/**
* 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' ;
?>
2023-03-29 18:20:25 +00:00
< p class = " two-factor-prompt " >< ? php esc_html_e ( 'Enter a backup verification code.' , 'two-factor' ); ?> </p>
2022-10-27 11:23:01 +00:00
< p >
< label for = " authcode " >< ? php esc_html_e ( 'Verification Code:' , 'two-factor' ); ?> </label>
2023-03-29 18:20:25 +00:00
< 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 " />
2022-10-27 11:23:01 +00:00
</ 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 ) {
2023-03-29 18:20:25 +00:00
$backup_code = $this -> sanitize_code_from_request ( 'two-factor-backup-code' );
if ( ! $backup_code ) {
return false ;
}
2022-10-27 11:23:01 +00:00
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 );
}
}