398 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			398 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?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;
 | |
| 	}
 | |
| }
 |