356 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			356 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?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 );
 | 
						|
	}
 | 
						|
}
 |