diff --git a/.gitignore b/.gitignore
index b90689b..7b7ed3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,16 +20,6 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
-
-### Composer ###
-composer.phar
-vendor/
-
-# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
-# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
-# composer.lock
-
-
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
@@ -94,3 +84,11 @@ atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
+### Composer template
+composer.phar
+/vendor/
+
+# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
+# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
+# composer.lock
+
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
index d383e8e..4ab0a05 100644
--- a/.scrutinizer.yml
+++ b/.scrutinizer.yml
@@ -3,6 +3,7 @@ filter:
- 'vendor/*'
- '*/vendor/*'
- 'languages/*'
+ - 'asstes/js/jquery-qrcode.min.js'
tools:
external_code_coverage: false
diff --git a/README.md b/README.md
index b357064..ed5c317 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-Google Authenticator for WordPress
+Authenticator for WordPress
==================================
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/julien731/WP-Google-Authenticator/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/julien731/WP-Google-Authenticator/?branch=master)
@@ -7,8 +7,6 @@ If you are concerned about security, you should look into 2-factor authenticatio
*Quick reminder:* 2-factor authentication adds an extra layer of security by requesting a one time password in addition to standard username / password credentials.
-This plugin uses the Google Authenticator app. I bet you know Google, and you probably know they have some good products out there. Google Authenticator is one of them.
-
[Download the Google Authenticator app](https://support.google.com/accounts/answer/1066447?hl=en) on your phone (iPhone, Android or Blackberry). Install this plugin on your site. After activating it and generating a secret key, you will be able to add the site to your app by scanning a QR code. That's it!
The QR code is generated with Google Charts API using HTTPS to avoid security issues while sending your secret for generation.
@@ -29,10 +27,17 @@ What the plugin does:
- Recovery code in case the user can't use the app
### Using Authy
-You're using [Authy](https://www.authy.com/)? Google Authenticator for WordPress is fully compatible with Authy. You can add the 2-steps authentication and use Authy to generate the one time password.
+
+You're using [Authy](https://www.authy.com/)? Authenticator for WordPress is fully compatible with Authy. You can add the 2-steps authentication and use Authy to generate the one time password.
## Changelog ##
+### 1.1.1
+
+* Fix an issue with the settings page not showing up
+* Contextual help deprecated bug
+* Remove mentions of Google in the plugin name chore
+
### 1.1.0
* Add support for apps passwords
* Admins can now force 2FA by user role
diff --git a/admin/admin.class.php b/admin/admin.class.php
deleted file mode 100644
index 16537ed..0000000
--- a/admin/admin.class.php
+++ /dev/null
@@ -1,1305 +0,0 @@
-
- * @package Google Authenticator for WordPress
- *
- * TODO
- *
- * - Revoke all users at once
- * - How to block brute force? Authorized desynchronization at -1 for current IP?
- */
-class WPGA_Admin {
-
- /**
- * Instance of this class.
- *
- * @since 1.0.7
- * @var object
- */
- protected static $instance = null;
-
- /**
- * Plugin settings
- *
- * @var TAV_Settings
- */
- protected $settings;
-
- public function __construct() {
-
- $this->settings = array();
- $this->apps_settings = array();
- $this->key_length = apply_filters( 'wpga_secret_key_length', 16 );
- $this->codelength = apply_filters( 'wpga_code_length', 6 );
- $this->qr_height = 300;
- $this->qr_width = 300;
- $this->def_attempt = 3;
- $this->bkp_length = apply_filters( 'wpga_recovery_code_length', 24 );
- $this->log_max = apply_filters( 'wpga_apps_passwords_log_max', 50 );
-
- if( is_admin() ) {
-
- add_action( 'wp_ajax_wpga_get_recovery', array( $this, 'ajax_callback' ) );
- add_action( 'wp_ajax_wpga_create_app_password', array( $this, 'create_app_password' ) );
-
- if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) {
-
- if( isset( $_GET['action'] ) ) {
- add_action( 'init', array( $this, 'EditSecret' ) );
- }
-
- add_action( 'init', array( $this, 'initSettings' ) );
- add_action( 'init', array( $this, 'registerSettings' ) );
- add_action( 'admin_menu', array( $this, 'add_app_password_menu' ) );
- add_action( 'admin_notices', array( $this, 'adminNotices' ) );
- add_action( 'admin_notices', array( $this, 'ForceSetSecret' ) );
- add_action( 'show_user_profile', array( $this, 'addUserProfileFields' ) );
- add_action( 'edit_user_profile', array( $this, 'UserAdminCustomProfileFields' ) );
- add_action( 'personal_options_update', array( $this, 'SaveCustomProfileFields' ) );
- add_action( 'admin_print_scripts', array( $this, 'load_admin_scripts' ) );
-
- if ( isset( $_GET['page'] ) && $_GET['page'] == 'wpga_options' ) {
- add_filter( 'contextual_help', array( $this, 'help' ), 10, 3 );
- }
-
- if ( isset( $_GET['page'] ) && ( 'wpga_options' == $_GET['page'] ) ) {
- add_filter( 'admin_footer_text', array( $this, 'versionInFooter' ) );
- }
-
- }
- }
-
- add_action( 'init', array( $this, 'load_plugin_textdomain' ), 9 );
- add_action( 'login_enqueue_scripts', array( $this, 'loadResources' ) );
- add_action( 'login_form', array( $this, 'customizeLoginForm' ) );
- add_action( 'wp_authenticate_user', array( $this, 'authenticateUser' ), 10, 3 );
- add_filter( 'authenticate', array( $this, 'checkAppPassword' ), 50, 3 );
- add_action( 'wpas_clean_totps', array( $this, 'clean_totps' ) );
-
- }
-
- /**
- * Return an instance of this class.
- *
- * @since 1.0.7
- *
- * @return object A single instance of this class.
- */
- public static function get_instance() {
-
- // If the single instance hasn't been set, set it now.
- if ( null == self::$instance ) {
- self::$instance = new self;
- }
-
- return self::$instance;
- }
-
- /**
- * Instantiate the settings class
- */
- public function initSettings() {
-
- if ( ! class_exists( 'TAV_Settings' ) ) {
- return;
- }
-
- /* Prepare arguments */
- $args = array(
- 'name' => WPGA_PREFIX . '_options',
- 'menu_name' => esc_html__( 'Authenticator', 'wpga' ),
- 'parent' => 'options-general.php',
- 'page_title' => esc_html__( 'WP Google Authenticator Settings', 'wpga' ),
- 'slug' => WPGA_PREFIX . '_options',
- 'page' => 'wpga-settings',
- 'prefix' => WPGA_PREFIX,
- 'row_name' => WPGA_PREFIX . '_options'
- );
-
- /* Instantiate the options class */
- $this->settings = new TAV_Settings( $args );
-
- }
-
- /**
- * Add required menu items
- */
- public function add_app_password_menu() {
- add_users_page(
- esc_html__( 'Google Authenticator Applications Passwords', 'wpga' ),
- esc_html__( 'My Apps Passwords', 'wpga' ),
- 'read',
- WPGA_PREFIX . '_apps_passwords',
- 'wpga_apps_passwords_display'
- );
- }
-
- /**
- * Load the plugin text domain for translation.
- *
- * @since 1.0.3
- * @return bool Whether or not the translation was loaded
- */
- public function load_plugin_textdomain() {
-
- $domain = WPGA_PREFIX;
- $locale = apply_filters( 'plugin_locale', get_locale(), $domain );
-
- return load_textdomain( $domain, WPGA_PATH . 'languages/' . $domain . '-' . $locale . '.mo' );
-
- }
-
-
- /**
- * Load the scripts resources on the login page used for the tooltip
- */
- public function loadResources() {
-
- global $pagenow;
-
- if ( in_array( $pagenow, array( 'wp-login.php', 'users.php' ) ) ) {
- wp_enqueue_style( 'wpga-simple-hint', WPGA_URL . 'css/wpga.css', array(), null, 'all' );
- }
-
- }
-
- /**
- * Load the plugin custom JS
- *
- * @since 1.0.4
- * @return void
- */
- public function load_admin_scripts() {
-
- global $pagenow;
-
- if ( 'profile.php' === $pagenow || isset( $_GET['page'] ) && in_array( $_GET['page'], array( 'wpga_options', 'wpga_apps_passwords' ) ) ) {
- wp_enqueue_script( 'wpga-custom', WPGA_URL . 'js/custom.js', array(), WPGA_VERSION, true );
- wp_enqueue_script( 'wpga-qrcode', WPGA_URL . 'vendor/jquery.qrcode.min.js', array( 'jquery' ), null, true );
- }
- }
-
- /**
- * Register plugin settings that will be displayed
- * in the WP backend.
- */
- public function registerSettings() {
-
- $this->settings->addSection( 'general', 'general' );
- $this->settings->addSection( 'security', 'security' );
-
- $this->settings->addOption( 'general', array(
- 'id' => 'active',
- 'title' => __( 'Activate Plugin', 'wpga' ),
- 'desc' => __( 'Do you wish to enable the 2-factor authentication for this site?', 'wpga' ),
- 'field' => 'checkbox',
- 'opts' => array( 'yes' => __( 'Yes', 'wpga' ) )
- )
- );
-
- $this->settings->addOption( 'general', array(
- 'id' => 'force_2fa',
- 'title' => __( 'Force Use', 'wpga' ),
- 'desc' => __( 'Do you want to force your users to use 2-factor authentication (admins AND you included)?', 'wpga' ),
- 'field' => 'checkbox',
- 'opts' => array( 'yes' => __( 'Yes', 'wpga' ) )
- )
- );
-
- $this->settings->addOption( 'general', array(
- 'id' => 'user_roles',
- 'title' => __( 'Force Roles', 'wpga' ),
- 'desc' => __( 'You can force users to use 2-factor authentication by role. Requires «Force Use» to be enabled. If no role is checked, 2FA will be forced for ALL roles.', 'wpga' ),
- 'field' => 'user_roles',
- 'opts' => $this->get_editable_roles()
- )
- );
-
- $this->settings->addOption( 'general', array(
- 'id' => 'blog_name',
- 'title' => __( 'Site Name', 'wpga' ),
- 'desc' => __( 'Name under which this site will appear in the Google Authenticator app.', 'wpga' ),
- 'field' => 'text'
- )
- );
-
- $this->settings->addOption( 'security', array(
- 'id' => 'max_attempts',
- 'title' => __( 'Max Attempts', 'wpga' ),
- 'desc' => __( 'If you chose to force users to use 2-factor authentication, you can specify a maximum number of times a user can login WITHOUT setting up the 2-factor authentication (leave 0 for unlimited attempts).', 'wpga' ),
- 'field' => 'smalltext'
- )
- );
-
- $this->settings->addOption( 'security', array(
- 'id' => 'authorized_delay',
- 'title' => __( 'Authorized Clock Desynchronization', 'wpga' ),
- 'desc' => __( 'Must be in min (±). Avoid invalid one-time passwords issues. Please read the contextual help for more info.', 'wpga' ),
- 'field' => 'smalltext'
- )
- );
-
- }
-
- /**
- * Get roles list.
- *
- * @since 1.0.9
- * @return array List of editable roles
- */
- public function get_editable_roles() {
- global $wp_roles;
-
- $all_roles = $wp_roles->roles;
- $editable_roles = apply_filters('editable_roles', $all_roles);
- $list = array();
-
- foreach ( $editable_roles as $role_id => $role ) {
- $list[$role_id] = $role['name'];
- }
-
- return $list;
- }
-
- /**
- * Register the contextual help for the plugin admin screen
- */
- public function help() {
-
- if( ! isset( $_GET['page'] ) || $_GET['page'] != 'wpga_options' )
- return;
-
- $screen = get_current_screen();
-
- $screen->add_help_tab( array(
- 'id' => 'desynchronization',
- 'title' => esc_html__( 'Desynchronization', 'wpga' ),
- 'content' => wp_kses( __('
Authorized Clock Desynchronization
First of all, you have to understand how the 2-factor authentication works.
The Google Authenticator will generate a TOTP which stands for Time based One Time Pasword. This one time password, as you might now understand, is generated based on the current time.
If the server\'s (where your site is hosted) clock and the user\'s phone clock are not perfectly synchronized, the one time password generated won\'t work, as it will be generated on a time which is different from the server.
The authorized desynchronization will allow your users more time to use their one time password. By default, one code will be valid for 30 seconds. If you want to give them more time, you can specify a delay in minutes.
Of course, if you give users more time, the security will be lowered. It is advised to stick with the default 30 secs.
', 'wpga'), array( 'h2' => array(), 'p' => array(), 'strong' => array() ) ),
- )
- );
-
- }
-
- /**
- * Edit secret key
- *
- * This function will process various actions on the user's
- * secret key such as regenerate or revoke it. All actions
- * are checked against a nonce before doing anything.
- */
- public function EditSecret() {
-
- switch( $_GET['action'] ):
-
- case 'regenerate':
-
- if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'regenerate_key' ) ) {
- return;
- }
-
- $secret = $this->generateSecretKey();
- update_user_meta( get_current_user_id(), 'wpga_secret', $secret );
- wp_redirect( add_query_arg( array( 'update' => '10' ), admin_url( 'profile.php#wpga' ) ) );
- exit;
-
- break;
-
- case 'revoke':
-
- if ( ! isset( $_GET['user_id'] ) ) {
- return;
- }
-
- if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'revoke_key' ) ) {
- return;
- }
-
- if ( ! current_user_can( 'edit_user', $_GET['user_id'] ) ) {
- return;
- }
-
- delete_user_meta( $_GET['user_id'], 'wpga_secret' );
- wp_redirect( add_query_arg( array( 'user_id' => $_GET['user_id'], 'update' => '11' ), admin_url( 'user-edit.php' ) ) );
- exit;
-
- break;
-
- case 'reset':
-
- if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'reset_key' ) ) {
- return;
- }
-
- if ( ! current_user_can( 'edit_user', $_GET['user_id'] ) ) {
- return;
- }
-
- delete_user_meta( $_GET['user_id'], 'wpga_attempts' );
- wp_redirect( add_query_arg( array( 'user_id' => $_GET['user_id'], 'update' => '12' ), admin_url( 'user-edit.php' ) ) );
- exit;
-
- break;
-
- endswitch;
-
- }
-
- /**
- * Add admin notices
- */
- public function adminNotices() {
-
- if ( isset( $_GET['2fa_reset'] ) && 'true' == $_GET['2fa_reset'] ) { ?>
-
-
- generate_backup_key();
-
- /* Save the backup key */
- update_user_meta( $user_id, 'wpga_backup_key', sanitize_key( $key ) );
-
- /**
- * Set a session var to allow user seeing the backup key
- * without having to enter his password. This will only happen once
- */
- update_user_meta( $user_id, 'wpga_backup_key_time', time() );
-
- }
- }
-
- /**
- * Add verification code field to login form.
- */
- public function customizeLoginForm() {
-
- $options = get_option( 'wpga_options', array() );
-
- if( !isset( $options['active'] ) || !in_array( 'yes', $options['active'] ) )
- return;
-
- ?>
-
First of all, you have to understand how the 2-factor authentication works.
The Google Authenticator will generate a TOTP which stands for Time based One Time Pasword. This one time password, as you might now understand, is generated based on the current time.
If the server\'s (where your site is hosted) clock and the user\'s phone clock are not perfectly synchronized, the one time password generated won\'t work, as it will be generated on a time which is different from the server.
The authorized desynchronization will allow your users more time to use their one time password. By default, one code will be valid for 30 seconds. If you want to give them more time, you can specify a delay in minutes.
Of course, if you give users more time, the security will be lowered. It is advised to stick with the default 30 secs.
', 'wpga' ), array( 'h2' => array(),
+ 'p' => array(),
+ 'strong' => array()
+ ) ),
+ )
+ );
+
+}
+
+add_filter( 'admin_footer_text', 'wpga_version_footer' );
+/**
+ * Add version number in footer
+ */
+function wpga_version_footer() {
+
+ if ( ! isset( $_GET['page'] ) OR isset( $_GET['page'] ) && 'wpga_options' !== $_GET['page'] ) {
+ return;
+ }
+
+ printf( wp_kses( __( WPGA_NAME . ' version ' . WPGA_VERSION . ' by ' . WPGA_AUTHOR . '.', 'gtsp' ), array( 'a' => array( 'href' => array() ), ) ), esc_url( WPGA_URI ) );
+
+}
+
+/**
+ * Wrapper function for dnh_register_notice()
+ *
+ * The function dnh_register_notice() comes with the WP Dismissible Notices Handler library
+ *
+ * @since 1.2
+ *
+ * @param string $id Notice ID, used to identify it
+ * @param string $type Type of notice to display
+ * @param string $content Notice content
+ * @param array $args Additional parameters
+ *
+ * @return bool
+ */
+function wpga_register_notice( $id, $type, $content, $args = array() ) {
+
+ if ( ! function_exists( 'dnh_register_notice' ) ) {
+
+ $file = WPGA_PATH . 'vendor/julien731/wp-dismissible-notices-handler/handler.php';
+
+ if ( file_exists( $file ) ) {
+ require_once( $file );
+ }
+
+ }
+
+ if ( function_exists( 'dnh_register_notice' ) ) {
+ dnh_register_notice( $id, $type, $content, $args );
+ }
+
+}
+
+/**
+ * Get Options Page URL
+ *
+ * Return the URL to the option page whether the plugin is network activated or only activated on a single site.
+ *
+ * @since 1.2
+ * @return string
+ */
+function wpga_get_option_page_link() {
+
+ $base = is_network_admin() ? admin_url( 'network/settings.php' ) : admin_url( 'options-general.php' );
+ $url = add_query_arg( 'page', 'wpga-settings', $base );
+
+ return esc_url( $url );
+
+}
+
+add_filter( 'plugin_action_links_' . WPGA_BASENAME, 'wpga_settings_page_link' );
+add_filter( 'network_admin_plugin_action_links_' . WPGA_BASENAME, 'wpga_settings_page_link' );
+/**
+ * Add a link to the settings page
+ *
+ * @since 1.2
+ *
+ * @param array $links Plugin links
+ *
+ * @return array Links with the settings
+ */
+function wpga_settings_page_link( $links ) {
+ $link = wpga_get_option_page_link();
+ $links[] = "" . esc_html__( 'Settings', 'wpga' ) . "";
+
+ return $links;
+}
diff --git a/includes/admin/functions-secret.php b/includes/admin/functions-secret.php
new file mode 100644
index 0000000..cfa7a37
--- /dev/null
+++ b/includes/admin/functions-secret.php
@@ -0,0 +1,126 @@
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+/**
+ * Generate a secret key based on allowed chars
+ * base32 compatible.
+ *
+ * @return string Secret key
+ */
+function wpga_generate_secret_key() {
+
+ $validChars = wpga_get_valid_chars();
+ $key_length = apply_filters( 'wpga_secret_key_length', 16 );
+
+ unset( $validChars[32] );
+
+ $secret = '';
+
+ for ( $i = 0; $i < $key_length; $i ++ ) {
+
+ $secret .= $validChars[ array_rand( $validChars ) ];
+
+ }
+
+ return $secret;
+
+}
+
+/**
+ * List the base32 valid chars that can be used for
+ * secret key generation.
+ *
+ * @return array Valid chars
+ */
+function wpga_get_valid_chars() {
+
+ return array(
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
+ 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
+ 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
+ '=' // padding char
+ );
+}
+
+add_action( 'init', 'wpga_edit_secret' );
+/**
+ * Edit secret key
+ *
+ * This function will process various actions on the user's
+ * secret key such as regenerate or revoke it. All actions
+ * are checked against a nonce before doing anything.
+ */
+function wpga_edit_secret() {
+
+ if( ! isset( $_GET['action'] ) ) {
+ return;
+ }
+
+ switch( $_GET['action'] ):
+
+ case 'regenerate':
+
+ if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'regenerate_key' ) ) {
+ return;
+ }
+
+ delete_user_meta( 'get_current_user_id()', 'wpga_secret' );
+ update_user_meta( get_current_user_id(), 'wpga_secret', wpga_generate_secret_key() );
+ wp_redirect( add_query_arg( array( 'update' => '10' ), admin_url( 'profile.php#wpga' ) ) );
+ exit;
+
+ break;
+
+ case 'revoke':
+
+ if ( ! isset( $_GET['user_id'] ) ) {
+ return;
+ }
+
+ if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'revoke_key' ) ) {
+ return;
+ }
+
+ if ( ! current_user_can( 'edit_user', $_GET['user_id'] ) ) {
+ return;
+ }
+
+ delete_user_meta( $_GET['user_id'], 'wpga_secret' );
+ delete_user_meta( $_GET['user_id'], 'wpga_backup_key' );
+ wp_redirect( add_query_arg( array( 'user_id' => $_GET['user_id'], 'update' => '11' ), admin_url( 'user-edit.php' ) ) );
+ exit;
+
+ break;
+
+ case 'reset':
+
+ if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'reset_key' ) ) {
+ return;
+ }
+
+ if ( ! current_user_can( 'edit_user', $_GET['user_id'] ) ) {
+ return;
+ }
+
+ delete_user_meta( $_GET['user_id'], 'wpga_attempts' );
+ delete_user_meta( $_GET['user_id'], 'wpga_backup_key' );
+ wp_redirect( add_query_arg( array( 'user_id' => $_GET['user_id'], 'update' => '12' ), admin_url( 'user-edit.php' ) ) );
+ exit;
+
+ break;
+
+ endswitch;
+
+}
\ No newline at end of file
diff --git a/includes/admin/functions-settings.php b/includes/admin/functions-settings.php
new file mode 100644
index 0000000..47708fc
--- /dev/null
+++ b/includes/admin/functions-settings.php
@@ -0,0 +1,125 @@
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+add_filter( 'wpga_get_settings', 'wpga_get_settings' );
+/**
+ * Plugin Settings
+ *
+ * Register all the core settings for the plugin.
+ *
+ * @since 1.0
+ * @return array
+ */
+function wpga_get_settings( $options ) {
+
+ $options['general'] = array(
+ 'title' => esc_html__( 'General', 'wpga' ),
+ 'options' => array(
+ array(
+ 'id' => 'active',
+ 'title' => __( 'Activate Plugin', 'wpga' ),
+ 'desc' => __( 'Do you wish to enable the 2-factor authentication for this site?', 'wpga' ),
+ 'callback' => 'wpga_option_callback_checkbox',
+ 'opts' => array( 'yes' => __( 'Yes', 'wpga' ) ),
+ 'default' => '',
+ ),
+ array(
+ 'id' => 'force_2fa',
+ 'title' => __( 'Force Use', 'wpga' ),
+ 'desc' => __( 'Do you want to force your users to use 2-factor authentication (admins AND you included)?', 'wpga' ),
+ 'callback' => 'wpga_option_callback_checkbox',
+ 'opts' => array( 'yes' => __( 'Yes', 'wpga' ) ),
+ ),
+ array(
+ 'id' => 'user_role_status',
+ 'title' => '',
+ 'desc' => __( 'Do you want to force your users to use 2-factor authentication (admins AND you included)?', 'wpga' ),
+ 'callback' => '',
+ 'default' => 'all',
+ ),
+ array(
+ 'id' => 'user_roles',
+ 'title' => __( 'Force Roles', 'wpga' ),
+ 'desc' => __( 'You can force users to use 2-factor authentication by role. Requires «Force Use» to be enabled. If no role is checked, 2FA will be forced for ALL roles.', 'wpga' ),
+ 'callback' => 'wpga_option_callback_user_roles',
+ 'opts' => wpga_get_editable_roles(),
+ ),
+ array(
+ 'id' => 'blog_name',
+ 'title' => __( 'Site Name', 'wpga' ),
+ 'desc' => __( 'Name under which this site will appear in the Google Authenticator app.', 'wpga' ),
+ 'callback' => 'wpga_option_callback_text',
+ 'default' => get_bloginfo( 'name' ),
+ ),
+ ),
+ );
+
+ $options['security'] = array(
+ 'title' => esc_html__( 'Security', 'wpga' ),
+ 'options' => array(
+ array(
+ 'id' => 'max_attempts',
+ 'title' => __( 'Max Attempts', 'wpga' ),
+ 'desc' => __( 'If you chose to force users to use 2-factor authentication, you can specify a maximum number of times a user can login WITHOUT setting up the 2-factor authentication (leave 0 for unlimited attempts).', 'wpga' ),
+ 'callback' => 'wpga_option_callback_text_small',
+ 'default' => 3,
+ ),
+ array(
+ 'id' => 'authorized_delay',
+ 'title' => __( 'Authorized Clock Desynchronization', 'wpga' ),
+ 'desc' => __( 'Must be in min (±). Avoid invalid one-time passwords issues. Please read the contextual help for more info.', 'wpga' ),
+ 'callback' => 'wpga_option_callback_text_small',
+ 'default' => 0,
+ ),
+ ),
+ );
+
+ return apply_filters( 'wpga_settings', $options );
+
+}
+
+/**
+ * Get roles list.
+ *
+ * @since 1.0.9
+ * @return array List of editable roles
+ */
+function wpga_get_editable_roles() {
+ global $wp_roles;
+
+ $all_roles = $wp_roles->roles;
+ $editable_roles = apply_filters( 'editable_roles', $all_roles );
+ $list = array();
+
+ foreach ( $editable_roles as $role_id => $role ) {
+ $list[ $role_id ] = $role['name'];
+ }
+
+ return $list;
+
+}
+
+/**
+ * Get plugin option
+ *
+ * @since 1.2.0
+ *
+ * @param string $option ID of the option to lookup
+ * @param mixed $default Default value to return
+ *
+ * @return mixed
+ */
+function wpga_get_option( $option, $default = null ) {
+ return WPGA()->settings->get_option( $option, $default );
+}
\ No newline at end of file
diff --git a/includes/admin/functions-user-profile.php b/includes/admin/functions-user-profile.php
new file mode 100644
index 0000000..98b5f77
--- /dev/null
+++ b/includes/admin/functions-user-profile.php
@@ -0,0 +1,258 @@
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+add_action( 'show_user_profile', 'wpga_user_profile_fields' );
+/**
+ * Add profile custom fields
+ *
+ * @param WP_User $user The WP_User object of the user being edited.
+ * @return void
+ */
+function wpga_user_profile_fields( $user ) {
+
+ // Register thickbox
+ add_thickbox();
+
+ $force = wpga_get_option( 'force_2fa' );
+ $qr = true;
+ $width = apply_filters( 'wpga_user_profile_qr_width', 300 + 10 );
+ $height = apply_filters( 'wpga_user_profile_qr_height', 300 + 10 );
+ $secret = get_user_meta( $user->ID, 'wpga_secret', true );
+ $args = array( 'action' => 'regenerate' );
+ $backup = get_user_meta( $user->ID, 'wpga_backup_key', true );
+
+ if ( isset( $_GET['user_id'] ) ) {
+ $args['user_id'] = (int) $_GET['user_id'];
+ }
+
+ $regenerate = wp_nonce_url( add_query_arg( $args, admin_url( 'profile.php' ) ), 'regenerate_key' );
+
+ if ( '' == $secret ) {
+ $secret = wpga_generate_secret_key();
+ $qr = false;
+ }
+ ?>
+
+
+
+
+
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+class WPGA_Access_Log {
+
+ /**
+ * Log a new access using an app password
+ *
+ * @since 1.2
+ * @return false|int
+ */
+ public function log_access( $user_id, $key_id, $time = '', $ip, $user_agent, $method = '' ) {
+
+ $user = get_user_by( 'id', $user_id );
+
+ // Make sure the user exists
+ if ( ! is_object( $user ) || ! is_a( $user, 'WP_User' ) ) {
+ return false;
+ }
+
+ if ( empty( $data['time'] ) || '0000-00-00 00:00:00' == $data['time'] ) {
+ $data['time'] = current_time( 'mysql' );
+ }
+
+ global $wpdb;
+
+ $data = array(
+ 'ID' => false,
+ 'user_id' => (int) $user_id,
+ 'key_id' => (int) $key_id,
+ 'time' => $time,
+ 'ip' => $ip,
+ 'user_agent' => sanitize_text_field( $user_agent ),
+ 'method' => sanitize_text_field( $method ),
+ );
+
+ $insert = $wpdb->insert( wpga_apps_access_log_table, $data, array( '%s', '%s', '%d', '%s', '%s', '%s', '%s' ) );
+
+ return false === $insert ? false : $wpdb->insert_id;
+
+ }
+
+ /**
+ * Get log entries
+ *
+ * @since 1.2
+ * @return array
+ */
+ public function get_entries() {}
+
+ /**
+ * Get log entries by field
+ *
+ * @since 1.2
+ * @return array
+ */
+ public function get_entries_by() {}
+
+}
\ No newline at end of file
diff --git a/includes/class-authenticate.php b/includes/class-authenticate.php
new file mode 100644
index 0000000..b0c477f
--- /dev/null
+++ b/includes/class-authenticate.php
@@ -0,0 +1,259 @@
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+class WPGA_Authenticate {
+
+ /**
+ * Defines if the plugin is active
+ *
+ * @since 1.2.0
+ * @var bool
+ */
+ protected $is_2fa_active = null;
+
+ /**
+ * Submitted TOTP
+ *
+ * @since 1.2.0
+ * @var string
+ */
+ public $totp;
+
+ /**
+ * The user secret key
+ *
+ * @since 1.2.0
+ * @var string
+ */
+ protected $secret;
+
+ /**
+ * The user object
+ *
+ * @since 1.2
+ * @var WPGA_User
+ */
+ protected $user;
+
+ public function __construct() {
+ add_action( 'wp_authenticate_user', array( $this, 'authenticate' ), 10, 3 );
+ add_filter( 'authenticate', array( $this, 'checkAppPassword' ), 50, 3 );
+ }
+
+ /**
+ * Get the user TOTP
+ *
+ * @since 1.2.0
+ * @return null|string
+ */
+ public function get_totp() {
+
+ if ( is_null( $this->totp ) ) {
+ $this->totp = isset( $_POST['totp'] ) ? sanitize_key( $_POST['totp'] ) : null;
+ }
+
+ return $this->totp;
+
+ }
+
+ /**
+ * Add TOTP check to WordPress authentication process
+ *
+ * @param WP_User|WP_Error $user
+ *
+ * @return object User object on success or WP_Error on failure
+ */
+ public function authenticate( $user ) {
+
+ if ( true !== wpga_is_2fa_active( $user ) ) {
+ return $user;
+ }
+
+ if ( ! is_wp_error( $user ) ) {
+
+ // Instantiate our user class for easy access to user data
+ if ( is_null( $this->user ) ) {
+ $this->user = new WPGA_User( $user );
+ }
+
+ $secret = $this->user->get_secret();
+ $totp = $this->get_totp();
+
+ /* Let's make sure the user has generated a secret */
+ if ( '' !== $secret ) {
+
+ if ( is_null( $totp ) ) {
+ return new WP_Error( 'no_totp', esc_html__( 'An error is preventing the 2-factor authentication from authenticating your session.', 'wpga' ) );
+ }
+
+ if ( empty( $totp ) ) {
+ return new WP_Error( 'no_totp', esc_html__( 'Please provide your one time password.', 'wpga' ) );
+ }
+
+ $totp_valid = $this->user->is_otp_valid( $totp );
+
+ if ( is_wp_error( $totp_valid ) ) {
+ return $totp_valid;
+ }
+
+ // If TOTP is valid we revoke it and continue loggin in
+ if ( true === $totp_valid ) {
+
+ // Revoke the TOTP
+ wpga_revoke_totp( $totp );
+
+ return $user;
+
+ }
+
+ /**
+ * Check if the user is sending a recovery key.
+ *
+ * If the recovery key is valid, we deactivate
+ * 2FA for this user so that he can log-in
+ * without using the app.
+ *
+ * @since 1.0.4
+ */
+ elseif ( $this->user->is_recovery_key( $totp ) ) {
+
+ // Disable 2FA for this user
+ $this->user->deactivate_2fa();
+
+ /* Add URL var to the login redirect */
+ add_filter( 'login_redirect', 'wpga_login_redirect_notify' );
+
+ return $user;
+
+ } else {
+ return new WP_Error( 'totp_invalid', esc_html__( 'The one time password is incorrect or expired. Please try with a newly generated password.', 'wpga' ) );
+ }
+
+ } else {
+
+ /* TOTP is forced for all users */
+ if ( wpga_is_2fa_forced( $user->roles ) ) {
+
+ /* If the admin set the max attempts to unlimited we give up on security :( */
+ if ( - 1 === $this->user->remaining_attempts() ) {
+ return $user;
+ }
+
+ if ( $this->user->remaining_attempts() > 0 ) {
+
+ $this->user->add_attempt();
+
+ return $user;
+ } else {
+ return new WP_Error( '2fa_max_attempts', esc_html__( 'You have reached the maximum number of logins WITHOUT using 2-factor authentication. Please contact the admin to reset your account.', 'wpga' ) );
+ }
+
+ } else {
+ // No TOTP check? Just return the user for standard authentication
+ return $user;
+ }
+
+ }
+
+ } else {
+ return $user;
+ }
+
+ }
+
+ /**
+ * Check for app password.
+ *
+ * If the user has created one or more apps passwords,
+ * we check if the given password is a registered one.
+ *
+ * @since 1.1.0
+ *
+ * @param WP_User $user The user object
+ * @param string $username The username to authenticate
+ * @param string $password The user password
+ *
+ * @return null|WP_User|WP_Error A user object on success or an error message
+ */
+ public function checkAppPassword( $user, $username, $password ) {
+
+ // Only do our work if the authentication failed.
+ if ( ! is_wp_error( $user ) ) {
+ return $user;
+ }
+
+ $user_data = get_user_by( 'login', $username );
+
+ if ( ! is_object( $user_data ) || ! is_a( $user_data, 'WP_User' ) ) {
+ return null;
+ }
+
+ // Get the WPGA user object
+ $wpga_user = new WPGA_User( $user_data );
+
+ if ( $wpga_user->has_app_passwords() ) {
+
+ $passwords = $wpga_user->get_app_passwords_codes(); // Get all user app passwords
+ $hash = md5( $password ); // Hash the given supposed app password
+
+ if ( in_array( $hash, $passwords ) ) {
+
+ $pwd_keys = array_flip( $passwords );
+
+ WPGA()->access_log->log_access( $user_data->ID, $pwd_keys[ $hash ], current_time( 'mysql' ), $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT'] );
+
+ // INCREMENT COUNT
+
+ return new WP_User( $user_data->ID );
+
+ } else {
+ return new WP_Error( 'no_totp', esc_html__( 'Please provide your one time password.', 'wpga' ) );
+ }
+ } else {
+ return $user;
+ }
+ }
+
+ /**
+ * Check if the current user has app passwords.
+ *
+ * @since 1.1.0
+ *
+ * @param int $user_id A user ID
+ *
+ * @return boolean True if has app passwords
+ */
+ public function has_app_passwords( $user_id ) {
+
+ $passwords = wpga_get_app_passwords( $user_id );
+
+ if ( empty( $passwords ) ) {
+ return false;
+ } else {
+ return true;
+ }
+
+ }
+
+}
+
+/**
+ * Add a URL var to login redirect page
+ *
+ * @return string Redirect URL
+ * @since 1.0.4
+ */
+function wpga_login_redirect_notify() {
+ return add_query_arg( array( '2fa_reset' => 'true' ), admin_url() );
+}
\ No newline at end of file
diff --git a/includes/class-recovery-key.php b/includes/class-recovery-key.php
new file mode 100644
index 0000000..59bc940
--- /dev/null
+++ b/includes/class-recovery-key.php
@@ -0,0 +1,315 @@
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+class WPGA_Recovery_Key {
+
+ /**
+ * Generate a new, unique recovery key
+ *
+ * @since 1.2
+ * @return string
+ */
+ public function generate_key() {
+
+ $key = $this->generate_random_key();
+
+ if ( true === $this->key_exists( $key ) ) {
+ do {
+ $key = $this->generate_random_key();
+ } while ( true === $this->key_exists( $key ) );
+ }
+
+ return $key;
+
+ }
+
+ /**
+ * Generate a random recovery key
+ *
+ * @since 1.2
+ * @return string
+ */
+ private function generate_random_key() {
+
+ $length = apply_filters( 'wpga_recovery_code_length', 24 );
+ $max = ceil( $length / 40 );
+ $random = '';
+
+ for ( $i = 0; $i < $max; $i ++ ) {
+ $random .= sha1( microtime( true ) . mt_rand( 10000, 90000 ) );
+ }
+
+ return substr( $random, 0, $length );
+
+ }
+
+ /**
+ * Add a new recovery key
+ *
+ * @since 1.2
+ *
+ * @param int $user_id The ID of the user whose key it is
+ * @param string $key The key to add
+ * @param string $name An optional name for the key to be added
+ * @param string $type The type of entry
+ *
+ * @return int|false New row ID if insertion is successful, false otherwise
+ */
+ public function add_key( $user_id, $key, $name = '', $type = 'recovery_key' ) {
+
+ $user = get_user_by( 'id', $user_id );
+
+ // Make sure the user exists
+ if ( ! is_object( $user ) || ! is_a( $user, 'WP_User' ) ) {
+ return false;
+ }
+
+ // Sanitize the entry type
+ if ( ! $this->type_exists( $type ) ) {
+ $type = 'recovery_key';
+ }
+
+ global $wpdb;
+
+ $data = array(
+ 'ID' => false,
+ 'user_id' => (int) $user_id,
+ 'time' => current_time( 'mysql' ),
+ 'code' => md5( sanitize_key( $key ) ),
+ 'name' => sanitize_text_field( $name ),
+ 'type' => $type,
+ 'count' => 0,
+ );
+
+ $insert = $wpdb->insert( wpga_recovery_keys_table, $data, array( '%s', '%s', '%d', '%s', '%s', '%s', '%s' ) );
+
+ return false === $insert ? false : $wpdb->insert_id;
+
+ }
+
+ /**
+ * Check if a given key type is valid
+ *
+ * @since 1.2
+ *
+ * @param string $type Key type
+ *
+ * @return bool
+ */
+ public function type_exists( $type ) {
+ return in_array( $type, array( 'recovery_key', 'app_password' ) ) ? true : false;
+ }
+
+ /**
+ * Get a recovery key
+ *
+ * @since 1.2
+ *
+ * @param string $field What field to use to lookup the key
+ * @param mixed $value Value of the lookup field
+ * @param bool $single Should the method return a single result or not
+ * @param string $type Optional specify what type of key should be looked up
+ *
+ * @return array|WP_Error An array containing the key information on success
+ */
+ public function get_key_by( $field = 'ID', $value, $single = false, $type = '' ) {
+
+ // Sanitize field
+ if ( ! in_array( $field, array( 'ID', 'code', 'user_id', 'type' ) ) ) {
+ return new WP_Error( 'invalid_field', esc_html__( 'The field you are trying to lookup is invalid', 'wpga' ) );
+ }
+
+ // Make sure the user exists
+ if ( 'user_id' === $field ) {
+
+ $user = get_user_by( 'id', $value );
+
+ if ( ! is_object( $user ) || ! is_a( $user, 'WP_User' ) ) {
+ return new WP_Error( 'invalid_user', esc_html__( 'The user is invalid', 'wpga' ) );
+ }
+
+ }
+
+ // Make sure the id field is uppercase
+ if ( 'id' === $field ) {
+ $field = 'ID';
+ }
+
+ if ( 'code' === $field ) {
+ $value = md5( $value );
+ }
+
+ // Set the base query arguments
+ $args = array( 'where' => "$field = '$value'" );
+
+ // Possibly add the key type
+ if ( ! empty( $type ) && $this->type_exists( $type ) ) {
+ $args['where'] .= " AND type = '$type'";
+ }
+
+ $result = $this->get( $args );
+
+ if ( is_array( $result ) && count( $result ) > 1 && true === $single ) {
+ $result = $result[0];
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Check if a given key exists
+ *
+ * @since 1.2
+ *
+ * @param string $key The key to check
+ *
+ * @return bool
+ */
+ public function key_exists( $key ) {
+
+ $key = $this->get_key_by( 'code', $key, true );
+
+ if ( is_array( $key ) && ! empty( $key ) ) {
+ return true;
+ }
+
+ return false;
+
+ }
+
+ /**
+ * Get all the recovery keys
+ *
+ * @since 1.2
+ * @return array
+ */
+ public function get_keys() {
+ return $this->get();
+ }
+
+ /**
+ * Get all keys for a particular user
+ *
+ * @since 1.2
+ *
+ * @param int $user_id ID of the user whose keys we want to retrieve
+ *
+ * @return array
+ */
+ public function get_user_keys( $user_id ) {
+ return $this->get_key_by( 'user_id', $user_id );
+ }
+
+ /**
+ * Get a user's recovery keys
+ *
+ * @since 1.2
+ *
+ * @param $user_id
+ *
+ * @return bool|string
+ */
+ public function get_recovery_keys( $user_id ) {
+
+ $keys = array();
+ $recovery = $this->get_key_by( 'user_id', $user_id );
+
+ if ( is_array( $recovery ) ) {
+ foreach ( $recovery as $key ) {
+ if ( 'recovery_key' === $key['type'] ) {
+ $keys[] = $key['code'];
+ }
+ }
+ }
+
+ return $keys;
+
+ }
+
+ /**
+ * Get the actual recovery code from a key entry
+ *
+ * @since 1.2
+ *
+ * @param array $key A user's key
+ *
+ * @return false|string
+ */
+ public function get_key_code( $key ) {
+
+ if ( ! is_array( $key ) || ! isset( $key['code'] ) ) {
+ return false;
+ }
+
+ return $key['code'];
+
+ }
+
+ /**
+ * Delete a key from the database
+ *
+ * @since 1.2
+ *
+ * @param int $id ID of the key to delete
+ *
+ * @return bool
+ */
+ public function delete_key( $id ) {
+
+ $key = $this->get_key_by( 'ID', $id, true );
+
+ if ( false === $key || is_wp_error( $key ) ) {
+ return false;
+ }
+
+ global $wpdb;
+
+ $wpdb->delete( wpga_recovery_keys_table, array( 'ID' => (int) $id ), array( '%d' ) );
+
+ return true;
+
+ }
+
+ /**
+ * Run a query on the recovery keys table
+ *
+ * @since 1.2
+ *
+ * @global $wpdb
+ *
+ * @param array $args Query arguments
+ * @param string $output Desired output format
+ *
+ * @return array
+ */
+ private function get( $args = array(), $output = 'ARRAY_A' ) {
+
+ global $wpdb;
+
+ $query = 'SELECT * FROM ' . wpga_recovery_keys_table;
+
+ if ( isset( $args['where'] ) ) {
+ $query .= ' WHERE ' . $args['where'];
+ } else {
+ $query .= ' WHERE 1';
+ }
+
+ $row = $wpdb->get_results( $query, $output );
+
+ return $row;
+
+ }
+
+}
\ No newline at end of file
diff --git a/includes/class-user.php b/includes/class-user.php
new file mode 100644
index 0000000..8500cda
--- /dev/null
+++ b/includes/class-user.php
@@ -0,0 +1,343 @@
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+class WPGA_User {
+
+ /**
+ * The user ID
+ *
+ * @since 1.2
+ * @var int
+ */
+ public $user_id;
+
+ /**
+ * The standard WordPress user object
+ *
+ * @since 1.2
+ * @var WP_User
+ */
+ private $user;
+
+ /**
+ * The 2FA status for the user
+ *
+ * Whether or not the user has 2FA enabled and setup. If 2FA is enabled but the user hasn't (yet) set it up, the
+ * property remains false.
+ *
+ * @since 1.2
+ * @var bool
+ */
+ protected $has_2fa;
+
+ /**
+ * Holds the number of failed login attempts
+ *
+ * @since 1.2
+ * @var int
+ */
+ protected $login_attempts;
+
+ /**
+ * Holds the number of remaining login attempts before account lockout
+ *
+ * @since 1.2
+ * @var int
+ */
+ protected $remaining_attempts;
+
+ /**
+ * User secret key
+ *
+ * @since 1.2
+ * @var string
+ */
+ protected $secret;
+
+ /**
+ * A list of the user's app passwords
+ *
+ * @since 1.2
+ * @var array
+ */
+ protected $app_passwords;
+
+ /**
+ * WPGA_User constructor
+ *
+ * @since 1.2
+ *
+ * @param int|WP_User $user
+ */
+ public function __construct( $user ) {
+
+ if ( is_object( $user ) && is_a( $user, 'WP_User' ) ) {
+ $this->user_id = $user->ID;
+ $this->user = $user;
+ } elseif ( is_numeric( $user ) ) {
+
+ $this->user = get_user_by( 'id', $user );
+
+ if ( is_object( $this->user ) && is_a( $this->user, 'WP_User' ) ) {
+ $this->user_id = $this->user->ID;
+ }
+
+ }
+
+ }
+
+ /**
+ * Check if user has 2FA enabled
+ *
+ * @since 1.2
+ * @return bool
+ */
+ public function has_2fa() {
+
+ if ( ! is_null( $this->has_2fa ) && is_bool( $this->has_2fa ) ) {
+ return $this->has_2fa;
+ }
+
+ $this->has_2fa = false;
+
+ if ( 'yes' === get_user_meta( $this->user_id, 'wpga_active', true ) ) {
+ $this->has_2fa = true;
+ }
+
+ return $this->has_2fa;
+
+ }
+
+ /**
+ * Get user secret key
+ *
+ * @since 1.2
+ * @return string
+ */
+ public function get_secret() {
+
+ if ( ! is_null( $this->secret ) ) {
+ return $this->secret;
+ }
+
+ return $this->secret = get_user_meta( $this->user_id, 'wpga_secret', true );
+
+ }
+
+ /**
+ * Get the number of failed login attempts
+ *
+ * THis method returns the number of failed attempts or false if there is no failed attempts.
+ *
+ * @since 1.2
+ * @return int|false
+ */
+ public function login_attempts() {
+
+ if ( ! is_null( $this->login_attempts ) && is_int( $this->login_attempts ) ) {
+ return $this->login_attempts;
+ }
+
+ $this->login_attempts = (int) get_user_meta( $this->user_id, 'wpga_attempts', true );
+
+ if ( empty( $this->login_attempts ) ) {
+ $this->login_attempts = 0;
+ }
+
+ return $this->login_attempts;
+
+ }
+
+ /**
+ * Get the number of attempts remaining
+ *
+ * Based on the number of failed attempts, calculate the number of attempts the user can make before locking his
+ * account.
+ *
+ * @since 1.2
+ * @return int
+ */
+ public function remaining_attempts() {
+
+ if ( ! is_null( $this->remaining_attempts ) && is_int( $this->remaining_attempts ) ) {
+ return $this->remaining_attempts;
+ }
+
+ $options = get_option( 'wpga_options', array() );
+ $max_attempts = ( isset( $options['max_attempts'] ) && '' != $options['max_attempts'] ) ? (int) $options['max_attempts'] : 3;
+
+ if ( - 1 === $max_attempts ) {
+ return $max_attempts;
+ }
+
+ $this->remaining_attempts = 0; // Set the remaining attempts number ot 0 for security
+ $attempts = $this->login_attempts();
+
+ if ( $attempts < $max_attempts ) {
+ $this->remaining_attempts = $max_attempts - $attempts;
+ }
+
+ return $this->remaining_attempts;
+
+ }
+
+ /**
+ * Add a new attempt to the number of failed attempts
+ *
+ * @since 1.2
+ * @return int The new number of failed attempts
+ */
+ public function add_attempt() {
+
+ $attempts = $this->login_attempts();
+
+ // Increment in database
+ update_user_meta( $this->user_id, 'wpga_attempts', $attempts + 1, $attempts );
+
+ // Update the internal property
+ $this->login_attempts = $attempts + 1;
+
+ return $this->login_attempts;
+
+ }
+
+ /**
+ * Deactivate 2FA for the current user
+ *
+ * @since 1.2
+ * @return void
+ */
+ public function deactivate_2fa() {
+ /* Clean the 2FA data */
+ delete_user_meta( $this->user_id, 'wpga_active' );
+ delete_user_meta( $this->user_id, 'wpga_attempts' );
+ delete_user_meta( $this->user_id, 'wpga_secret' );
+ delete_user_meta( $this->user_id, 'wpga_backup_key' );
+ delete_user_meta( $this->user_id, 'wpga_backup_key_time' );
+ }
+
+ /**
+ * Check the validity of a one-time password for the current user
+ *
+ * @since 1.2
+ *
+ * @param string $otp The OTP to check with this user
+ *
+ * @return bool|WP_Error
+ */
+ public function is_otp_valid( $otp ) {
+
+ // Set the validity to false for the start
+ $valid = false;
+
+ // Get the authorized discrepancy delay and calculate the allowed time range
+ $options = get_option( 'wpga_options' );
+ $drift = isset( $options['authorized_delay'] ) ? (int) $options['authorized_delay'] * 2 : 1;
+ $currentTimeSlice = floor( time() / 30 );
+
+ // Generate all OTPs for the allowed time range and compare them to the OTP we got
+ for ( $i = - $drift; $i <= $drift; $i ++ ) {
+
+ // Generate a new valid OTP
+ $currently_valid_otp = wpga_get_code( $this->get_secret(), $currentTimeSlice + $i );
+
+ if ( $currently_valid_otp === $otp ) {
+ $valid = true;
+ break; // If we get a match no need to generate other OTPs
+ }
+
+ }
+
+ // If we didn't find a valid OTP no need to go any further. The login attempt is simply invalid.
+ if ( false === $valid ) {
+ return false;
+ }
+
+ // Make sure that the OTP hasn't been used yet, in which case we do not accept it
+ if ( true === wpga_was_otp_used( $otp ) ) {
+ $valid = new WP_Error( 'expired_totp', esc_html__( 'The one time password you used has already been revoked.', 'wpga' ) );
+ }
+
+ return $valid;
+
+ }
+
+ /**
+ * Check if the given key is one of the user's recovery keys
+ *
+ * @since 1.2
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function is_recovery_key( $key ) {
+
+ $is_key = false;
+ $keys = wpga_get_user_recovery_keys( $this->user_id );
+
+ if ( in_array( md5( sanitize_key( $key ) ), $keys ) ) {
+ $is_key = true;
+ }
+
+ return $is_key;
+
+ }
+
+ /**
+ * Get the user's app passwords if any
+ *
+ * @since 1.2
+ * @return array
+ */
+ public function get_app_passwords() {
+
+ if ( is_array( $this->app_passwords ) ) {
+ return $this->app_passwords;
+ }
+
+ return WPGA()->recovery->get_key_by( 'user_id', $this->user_id, false, 'app_password' );
+
+ }
+
+ /**
+ * Get the actual list of the user's app passwords
+ *
+ * @since 1.2
+ * @return array
+ */
+ public function get_app_passwords_codes() {
+
+ $passwords = $this->get_app_passwords();
+ $codes = array();
+
+ foreach ( $passwords as $key ) {
+ $codes[ $key['ID'] ] = $key['code'];
+ }
+
+ return $codes;
+
+ }
+
+ /**
+ * Check if the user has any app passwords set
+ *
+ * @since 1.2
+ * @return bool
+ */
+ public function has_app_passwords() {
+ return empty( $this->get_app_passwords() ) ? false : true;
+ }
+
+}
\ No newline at end of file
diff --git a/includes/functions-apps-passwords.php b/includes/functions-apps-passwords.php
new file mode 100644
index 0000000..5791c7a
--- /dev/null
+++ b/includes/functions-apps-passwords.php
@@ -0,0 +1,288 @@
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+/**
+ * Generate a unique key.
+ *
+ * The key is used to identify the app password.
+ * It is an unchangeable unique value.
+ *
+ * @since 1.1.0
+ * @param string $hash Hash of the newly created password
+ * @return string Unique identifying key
+ */
+function wpga_make_unique_key( $hash ) {
+
+ $passwords = wpga_get_app_passwords();
+ $key = substr( $hash, 0, 5 );
+
+ if ( !array_key_exists( $key, $passwords ) ) {
+ return $key;
+ }
+
+ $index = 0;
+ $key = $key . $index;
+
+ while ( array_key_exists( $key, $passwords ) ) {
+ ++$index;
+ $key = substr( $key, 0, 5 ) . $index;
+ }
+
+ return $key;
+
+}
+
+function wpga_get_last_access( $key ) {
+
+ global $current_user;
+
+ $log = wpga_get_app_passwords_log();
+ $last = array();
+
+ if ( empty( $log ) ) {
+ return false;
+ }
+
+ foreach ( $log as $date => $entry ) {
+ if ( $key === $entry['key'] ) {
+ array_push( $last, $entry );
+ }
+ }
+
+ if ( empty( $last ) ) {
+ return false;
+ }
+
+ $count = count( $last ) - 1;
+
+ return $last[$count];
+
+}
+
+/**
+ * Get app passwords for a user
+ *
+ * @since 1.1
+ *
+ * @param null|int $user_id
+ *
+ * @return array
+ */
+function wpga_get_app_passwords( $user_id = null ) {
+
+ if ( is_null( $user_id ) ) {
+ global $current_user;
+ $user_id = $current_user->ID;
+ }
+
+ $user = new WPGA_User( $user_id );
+
+ return $user->get_app_passwords();
+
+}
+
+function wpga_get_app_passwords_log( $user_id = null ) {
+
+ if ( is_null( $user_id ) ) {
+ global $current_user;
+ $user_id = $current_user->ID;
+ }
+
+ $log = is_array( $p = get_user_meta( $user_id, 'wpga_apps_passwords_log', true ) ) ? $p : array();
+ krsort( $log );
+
+ return $log;
+
+}
+
+/**
+ * Delete a user's app password
+ *
+ * @since 1.1
+ *
+ * @param string $id ID of the key to delete
+ *
+ * @return bool
+ */
+function wpga_delete_app_password( $id ) {
+ return WPGA()->recovery->delete_key( $id );
+}
+
+/**
+ * Delete all of a user's app passwords at one
+ *
+ * @since 1.1
+ * @return bool
+ */
+function wpga_reset_app_passwords() {
+
+ global $current_user;
+
+ $result = true;
+
+ $keys = WPGA()->recovery->get_key_by( 'user_id', $current_user->ID, false, 'app_password' );
+
+ if ( is_array( $keys ) ) {
+ foreach ( $keys as $key ) {
+ if ( ! wpga_delete_app_password( $key['ID'] ) ) {
+ $result = false;
+ }
+ }
+ }
+
+ return $result;
+
+}
+
+function wpga_clear_log() {
+ global $current_user;
+ delete_user_meta( $current_user->ID, 'wpga_apps_passwords_log' );
+}
+
+add_action( 'wp_ajax_wpga_create_app_password', 'wpga_create_app_password' );
+/**
+ * Create a new app password.
+ *
+ * @since 1.1.0
+ */
+function wpga_create_app_password() {
+
+ if ( ! isset( $_POST['description'] ) || empty( $_POST['description'] ) ) {
+ die();
+ }
+
+ global $current_user;
+
+ $pwd = WPGA()->recovery->generate_key();
+ $app_pwd = WPGA()->recovery->add_key( $current_user->ID, $pwd, sanitize_text_field( $_POST['description'] ), 'app_password' );
+
+ if ( false !== $app_pwd ) {
+ $return = json_encode( array(
+ 'desc' => sanitize_text_field( $_POST['description'] ),
+ 'pwd' => esc_attr( $pwd ),
+ ) );
+ } else {
+ $return = json_encode( array( 'error' ) );
+ }
+
+ echo esc_attr( urlencode( $return ) );
+ die();
+
+}
+
+add_action( 'admin_menu', 'wpga_add_app_password_menu' );
+/**
+ * Add required menu items
+ */
+function wpga_add_app_password_menu() {
+ add_users_page(
+ esc_html__( 'Google Authenticator Applications Passwords', 'wpga' ),
+ esc_html__( 'My Apps Passwords', 'wpga' ),
+ 'read',
+ WPGA_PREFIX . '_apps_passwords',
+ 'wpga_apps_passwords_display'
+ );
+}
+
+/**
+ * Display the applications passwords apge.
+ *
+ * @since 1.1.0
+ */
+function wpga_apps_passwords_display() {
+ require_once( WPGA_PATH . 'includes/admin/views/apps-passwords.php' );
+}
+
+add_action( 'admin_init', 'wpas_apps_passwords_actions' );
+/**
+ * Run app passwords related actions.
+ *
+ * Run the actions and redirect to the user's page
+ * in "read only" mode, without the URL vars that can cause
+ * undesired actions (like clearing the log again).
+ *
+ * @since 1.1.0
+ * @return void
+ */
+function wpas_apps_passwords_actions() {
+
+ if ( isset( $_GET['action'] ) && isset( $_GET['wpga_nonce'] ) ) {
+
+ if ( wp_verify_nonce( $_GET['wpga_nonce'], 'wpga_action' ) ) {
+
+ switch ( $_GET['action'] ) {
+ case 'delete':
+
+ if ( isset( $_GET['key'] ) ) {
+ $delete_key = sanitize_key( $_GET['key'] );
+ wpga_delete_app_password( $delete_key );
+ }
+
+ break;
+
+ case 'delete_all':
+ wpga_reset_app_passwords();
+ break;
+
+ case 'clear_log':
+ wpga_clear_log();
+ break;
+
+ }
+
+ }
+
+ wp_redirect( add_query_arg( array( 'page' => 'wpga_apps_passwords' ), admin_url( 'users.php') ) );
+ exit;
+
+ }
+
+}
+
+/**
+ * Create the custom database table to store recovery keys
+ *
+ * @since 1.2
+ * @return void
+ */
+function wpga_apps_access_log_create_table() {
+
+ global $wpdb;
+
+ $table = wpga_apps_access_log_table;
+ $charset_collate = $wpdb->get_charset_collate();
+
+ // Prepare DB structure if not already existing
+ if ( $wpdb->get_var( "show tables like '$table'" ) != $table ) {
+
+ $sql = "CREATE TABLE $table (
+ ID mediumint(9) NOT NULL AUTO_INCREMENT,
+ user_id mediumint(9) NOT NULL,
+ key_id mediumint(9) NOT NULL,
+ time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
+ ip VARCHAR(255) NOT NULL,
+ user_agent VARCHAR(255),
+ method VARCHAR(20),
+ UNIQUE KEY ID (ID)
+ ) $charset_collate;";
+
+ require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
+ dbDelta( $sql );
+
+ // Save database version. Useful for upgrades.
+ add_option( 'wpga_db_version', WPGA_DB_VERSION );
+
+ }
+
+}
\ No newline at end of file
diff --git a/includes/functions-deprecated.php b/includes/functions-deprecated.php
new file mode 100644
index 0000000..fcab24a
--- /dev/null
+++ b/includes/functions-deprecated.php
@@ -0,0 +1,140 @@
+
+ * @license GPL-2.0+
+ * @link http://themeavenue.net
+ * @copyright 2016 Julien Liabeuf
+ */
+
+/* Exit if accessed directly */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Get the number of login attempts done by a user
+ *
+ * @since 1.2.0
+ *
+ * @param int $user_id
+ *
+ * @deprecated 1.2
+ *
+ * @return int
+ */
+function wpas_get_login_attempts( $user_id ) {
+
+ /**
+ * @var WPGA_User
+ */
+ $user = new WPGA_User( $user_id );
+
+ return $user->login_attempts();
+
+}
+
+/**
+ * Get the number of remaining login attempts for a user
+ *
+ * @since 1.2.0
+ *
+ * @param $user_id
+ *
+ * @deprecated 1.2
+ *
+ * @return int
+ */
+function wpas_get_remaining_login_attempts( $user_id ) {
+
+ /**
+ * @var WPGA_User
+ */
+ $user = new WPGA_User( $user_id );
+
+ return $user->remaining_attempts();
+
+}
+
+/**
+ * Increment the number of login attempts done by a user
+ *
+ * @since 1.2.0
+ *
+ * @param $user_id
+ *
+ * @deprecated 1.2
+ *
+ * @return int
+ */
+function wpas_increment_attempts( $user_id ) {
+
+ /**
+ * @var WPGA_User
+ */
+ $user = new WPGA_User( $user_id );
+
+ return $user->add_attempt();
+
+}
+
+/**
+ * Disable 2FA for a particular user
+ *
+ * @since 1.2.0
+ *
+ * @param $user_id
+ *
+ * @deprecated 1.2
+ *
+ * @return void
+ */
+function wpga_disable_2fa( $user_id ) {
+
+ /**
+ * @var WPGA_User
+ */
+ $user = new WPGA_User( $user_id );
+ $user->deactivate_2fa();
+
+}
+
+/**
+ * Check validity of a recovery key
+ *
+ * @since 1.0.4
+ *
+ * @param object $user User object
+ * @param string $key Recovery key to check
+ *
+ * @deprecated 1.2
+ *
+ * @return boolean Whether or not the key is valid
+ */
+function wpga_check_recovery_key( $user, $key ) {
+
+ /**
+ * @var WPGA_User
+ */
+ $user = new WPGA_User( $user );
+
+ return $user->is_recovery_key( $key );
+
+}
+
+/**
+ * Generate a backup key
+ *
+ * In case the user loses his phone or cannot access the Google Authenticator app,
+ * we generate a unique backup key that the user can use to authenticate once.
+ * After one (only) authentication the key will be voided.
+ *
+ * @since 1.0.4
+ * @deprecated 1.2
+ * @return string Backup key
+ */
+function wpga_generate_backup_key() {
+ return WPGA()->recovery->generate_key();
+}
\ No newline at end of file
diff --git a/includes/functions-login.php b/includes/functions-login.php
new file mode 100644
index 0000000..e49d107
--- /dev/null
+++ b/includes/functions-login.php
@@ -0,0 +1,36 @@
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+add_action( 'login_form', 'wpga_customize_login_form' );
+/**
+ * Add verification code field to login form.
+ */
+function wpga_customize_login_form() {
+
+ if ( ! wpga_is_2fa_active() ) {
+ return;
+ }
+
+ ?>
+
+
+
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+add_action( 'wp_ajax_wpga_get_recovery', 'wpga_ajax_callback' );
+/**
+ * Get recovery code
+ *
+ * The function will check the user's password and,
+ * if the password is correct, it will return
+ * the recovery code.
+ *
+ * @return void
+ * @since 1.0.4
+ */
+function wpga_ajax_callback() {
+
+ if ( ! isset( $_POST['pwd'] ) ) {
+ return;
+ }
+
+ /* Password to check */
+ $pwd = sanitize_text_field( $_POST['pwd'] );
+
+ $user_id = get_current_user_id();
+ $user = get_user_by( 'id', $user_id );
+
+ if ( $user && wp_check_password( $pwd, $user->data->user_pass, $user->ID ) ) {
+
+ $recovery = get_user_meta( $user_id, 'wpga_backup_key', true );
+
+ if ( '' != $recovery ) {
+ echo "
" . esc_html( $recovery ) . "
" . esc_html_e( 'Write this down and keep it safe', 'wpga' ) . "
";
+ } else {
+ esc_html_e( 'No recovery code set yet.', 'wpga' );
+ }
+
+ } else {
+ ?>recovery->get_recovery_keys( $user_id );
+}
+
+/**
+ * Create the custom database table to store recovery keys
+ *
+ * @since 1.2
+ * @return void
+ */
+function wpga_recovery_keys_create_table() {
+
+ global $wpdb;
+
+ $table = wpga_recovery_keys_table;
+ $charset_collate = $wpdb->get_charset_collate();
+
+ // Prepare DB structure if not already existing
+ if ( $wpdb->get_var( "show tables like '$table'" ) != $table ) {
+
+ $sql = "CREATE TABLE $table (
+ ID mediumint(9) NOT NULL AUTO_INCREMENT,
+ user_id mediumint(9) NOT NULL,
+ time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
+ code VARCHAR(255) NOT NULL,
+ name VARCHAR(100),
+ type VARCHAR(20) NOT NULL,
+ count VARCHAR(20),
+ UNIQUE KEY ID (ID)
+ ) $charset_collate;";
+
+ require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
+ dbDelta( $sql );
+
+ // Save database version. Useful for upgrades.
+ add_option( 'wpga_db_version', WPGA_DB_VERSION );
+
+ }
+
+}
\ No newline at end of file
diff --git a/includes/functions-totp.php b/includes/functions-totp.php
new file mode 100644
index 0000000..2caf81b
--- /dev/null
+++ b/includes/functions-totp.php
@@ -0,0 +1,246 @@
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+/**
+ * Revoke a TOTP
+ *
+ * @since 1.2.0
+ *
+ * @param string $totp TOTP to invalidate
+ *
+ * @return bool
+ */
+function wpga_revoke_totp( $totp ) {
+
+ $used = get_option( 'wpga_used_totp', array() );
+
+ if ( is_array( $used ) && ! in_array( md5( $totp ), $used ) ) {
+
+ array_push( $used, md5( $totp ) );
+
+ update_option( 'wpga_used_totp', $used );
+
+ return true;
+
+ }
+
+ return false;
+
+}
+
+/**
+ * Calculate the code, with given secret and point in time
+ *
+ * @param string $secret
+ * @param integer $timeSlice
+ *
+ * @return string Generated code
+ */
+function wpga_get_code( $secret, $timeSlice = null ) {
+
+ if ( $timeSlice === null ) {
+ $timeSlice = floor( time() / 30 );
+ }
+
+ $code_length = apply_filters( 'wpga_code_length', 6 );
+ $secretkey = wpga_base32_decode( $secret );
+
+ // Pack time into binary string
+ $time = chr( 0 ) . chr( 0 ) . chr( 0 ) . chr( 0 ) . pack( 'N*', $timeSlice );
+
+ // Hash it with users secret key
+ $hm = hash_hmac( 'SHA1', $time, $secretkey, true );
+
+ // Use last nipple of result as index/offset
+ $offset = ord( substr( $hm, - 1 ) ) & 0x0F;
+
+ // grab 4 bytes of the result
+ $hashpart = substr( $hm, $offset, 4 );
+
+ // Unpak binary value
+ $value = unpack( 'N', $hashpart );
+ $value = $value[1];
+
+ // Only 32 bits
+ $value = $value & 0x7FFFFFFF;
+
+ $modulo = pow( 10, $code_length );
+
+ return str_pad( $value % $modulo, $code_length, '0', STR_PAD_LEFT );
+}
+
+/**
+ * Decode base32 string
+ *
+ * @param string $string String to decode
+ *
+ * @return string Decoded string
+ */
+function wpga_base32_decode( $string ) {
+
+ $lut = array(
+ "A" => 0,
+ "B" => 1,
+ "C" => 2,
+ "D" => 3,
+ "E" => 4,
+ "F" => 5,
+ "G" => 6,
+ "H" => 7,
+ "I" => 8,
+ "J" => 9,
+ "K" => 10,
+ "L" => 11,
+ "M" => 12,
+ "N" => 13,
+ "O" => 14,
+ "P" => 15,
+ "Q" => 16,
+ "R" => 17,
+ "S" => 18,
+ "T" => 19,
+ "U" => 20,
+ "V" => 21,
+ "W" => 22,
+ "X" => 23,
+ "Y" => 24,
+ "Z" => 25,
+ "2" => 26,
+ "3" => 27,
+ "4" => 28,
+ "5" => 29,
+ "6" => 30,
+ "7" => 31
+ );
+
+ $string = strtoupper( $string );
+ $l = strlen( $string );
+ $n = 0;
+ $j = 0;
+ $binary = "";
+
+ for ( $i = 0; $i < $l; $i ++ ) {
+
+ $n = $n << 5;
+ $n = $n + $lut[ $string[ $i ] ];
+ $j = $j + 5;
+
+ if ( $j >= 8 ) {
+ $j = $j - 8;
+ $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
+ }
+ }
+
+ return $binary;
+}
+
+add_action( 'wpas_clean_totps', 'wpga_clean_totps' );
+/**
+ * Delete all TOTPs from DB.
+ *
+ * As TOTPs expire after a defined amount of time
+ * per definition, there is no need to store them
+ * in the database forever.
+ *
+ * @since 1.0.7
+ */
+function wpga_clean_totps() {
+ delete_option( 'wpga_used_totp' );
+}
+
+/**
+ * Check if 2FA is enabled on this site
+ *
+ * If a user object is passed, we check if 2FA is enabled on the site and for this particular user.
+ *
+ * @since 1.2.0
+ *
+ * @param WP_User|bool $user Object of the user who's trying to login
+ *
+ * @return bool
+ */
+function wpga_is_2fa_active( $user = false ) {
+
+ $active = wpga_get_option( 'active', false );
+
+ if ( $active && is_array( $active ) ) {
+ $active = in_array( 'yes', $active ) ? true : false;
+ }
+
+ // If 2FA is enabled on the site, make sure the current user, if any, has it enabled for his account
+ if ( true === $active && is_object( $user ) && is_a( $user, 'WP_User' ) ) {
+ $wpga_user = new WPGA_User( $user );
+ $active = $wpga_user->has_2fa();
+ }
+
+ return $active;
+
+}
+
+/**
+ * Check if 2FA is being forced by the admin
+ *
+ * @since 1.2.0
+ *
+ * @param array $roles Roles to check for forced 2FA
+ *
+ * @return bool
+ */
+function wpga_is_2fa_forced( $roles = array() ) {
+
+ $options = get_option( 'wpga_options', array() );
+
+ /* Check if 2FA is forced by the admin */
+ if ( ! isset( $options['force_2fa'] ) || ! in_array( 'yes', (array) $options['force_2fa'] ) ) {
+ return false;
+ }
+
+ if ( 'all' === $options['user_role_status'] ) {
+ return true;
+ }
+
+ /* If the forced roles list is empty, we consider it active for all users. Hence, we add the current user role in the list. */
+ if ( ! isset( $options['user_roles'] ) || empty( $options['user_roles'] ) ) {
+ $options['user_roles'] = $roles;
+ }
+
+ /* Check if 2FA is forced for the role this user has */
+ if ( array_intersect( $roles, $options['user_roles'] ) ) {
+ return true;
+ }
+
+ return false;
+
+}
+
+/**
+ * Check if a OPT has already been used
+ *
+ * @since 1.2
+ *
+ * @param string $otp The OPT to check
+ *
+ * @return bool
+ */
+function wpga_was_otp_used( $otp ) {
+
+ $used = get_option( 'wpga_used_totp', array() );
+
+ if ( is_array( $used ) && ! in_array( md5( $totp ), $used ) ) {
+ return false;
+ } else {
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/includes/functions-users.php b/includes/functions-users.php
new file mode 100644
index 0000000..5579bac
--- /dev/null
+++ b/includes/functions-users.php
@@ -0,0 +1,67 @@
+
+ * @license GPL-2.0+
+ * @link http://themeavenue.net
+ * @copyright 2016 Julien Liabeuf
+ */
+/* Exit if accessed directly */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+add_filter( 'manage_users_columns', 'wpga_add_user_2fa_column' );
+/**
+ * Add a new column to the users list screen for displaying the 2FA status
+ *
+ * @since 1.2.0
+ *
+ * @param array $columns Existing columns
+ *
+ * @return array
+ */
+function wpga_add_user_2fa_column( $columns ) {
+
+ $new = array();
+
+ foreach ( $columns as $column => $title ) {
+
+ $new[$column] = $title;
+
+ if ( 'role' === $column ) {
+ $new['2fa'] = esc_html__( '2FA', 'wpga' );
+ }
+
+ }
+
+ return $new;
+}
+
+add_action( 'manage_users_custom_column', 'wpga_2fa_usr_column_content', 10, 3 );
+/**
+ * Display the content of the 2FA column
+ *
+ * Checks if 2FA is enabled for the current user and displays a tag if it it.
+ *
+ * @since 1.2.0
+ *
+ * @param string $value The column output
+ * @param string $column_name Column ID
+ * @param int $user_id Current user ID
+ *
+ * @return string
+ */
+function wpga_2fa_usr_column_content( $value, $column_name, $user_id ) {
+
+ $user = get_user_by( 'ID', $user_id );
+ $enabled = wpga_is_2fa_active( $user );
+
+ if ( true === $enabled ) {
+ $value = sprintf( '%2$s', 'wpga-tag', esc_html__( '2FA', 'wpga' ), esc_html__( '2 Factor Authentication is enabled for this user', 'wpga' ) );
+ }
+
+ return $value;
+}
\ No newline at end of file
diff --git a/includes/scripts-styles.php b/includes/scripts-styles.php
new file mode 100644
index 0000000..74c62e8
--- /dev/null
+++ b/includes/scripts-styles.php
@@ -0,0 +1,45 @@
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+add_action( 'admin_print_scripts', 'wpga_load_admin_scripts' );
+/**
+ * Load the plugin custom JS
+ *
+ * @since 1.0.4
+ * @return void
+ */
+function wpga_load_admin_scripts() {
+
+ global $pagenow;
+
+ if ( 'profile.php' === $pagenow || isset( $_GET['page'] ) && in_array( $_GET['page'], array( 'wpga_apps_passwords', 'wpga-settings' ) ) ) {
+ wp_enqueue_script( 'wpga-custom', WPGA_URL . 'assets/js/custom.js', array(), WPGA_VERSION, true );
+ wp_enqueue_script( 'wpga-qrcode', WPGA_URL . 'assets/js/jquery-qrcode.min.js', array( 'jquery' ), '0.14.0', true );
+ }
+}
+
+add_action( 'login_enqueue_scripts', 'wpga_load_styles' );
+add_action( 'admin_enqueue_scripts', 'wpga_load_styles' );
+/**
+ * Load the scripts resources on the login page used for the tooltip
+ */
+function wpga_load_styles() {
+
+ global $pagenow;
+
+ if ( in_array( $pagenow, array( 'wp-login.php', 'users.php' ) ) ) {
+ wp_enqueue_style( 'wpga-simple-hint', WPGA_URL . 'assets/css/wpga.css', array(), null, 'all' );
+ }
+
+}
\ No newline at end of file
diff --git a/languages/wpga-fi_FI.po b/languages/wpga-fi_FI.po
index cfc2d49..aad3f86 100644
--- a/languages/wpga-fi_FI.po
+++ b/languages/wpga-fi_FI.po
@@ -304,7 +304,7 @@ msgstr "Google Authenticator"
#: C:\wamp\www\plugins-dev\wp-content\plugins\wp-google-authenticator/admin/admin.class.php:957
msgid ""
-"If you do not have configured the 2-factor authentication, just leave "
+"If you have not configured 2-factor authentication, just leave "
"this field blank and you will be logged-in as usual.
If you can't use "
"the Google Authenticator app for whatever reason, you can use your "
"recovery code instead."
diff --git a/languages/wpga-fr_FR.po b/languages/wpga-fr_FR.po
index f9ccaec..4473b00 100644
--- a/languages/wpga-fr_FR.po
+++ b/languages/wpga-fr_FR.po
@@ -340,7 +340,7 @@ msgstr "Google Authenticator"
#: ../admin/admin.class.php:1229
msgid ""
-"If you do not have configured the 2-factor authentication, just leave this "
+"If you have not configured 2-factor authentication, just leave this "
"field blank and you will be logged-in as usual. If you can't use the Google "
"Authenticator app for whatever reason, you can use your recovery code "
"instead."
@@ -549,7 +549,7 @@ msgstr "Nettoyer"
#~ msgstr "QR Code"
#~ msgid ""
-#~ "If you do not have configured the 2-factor authentication, just leave "
+#~ "If you have not configured 2-factor authentication, just leave "
#~ "this field blank and you will be logged-in as usual.
If you can't "
#~ "use the Google Authenticator app for whatever reason, you can use your "
#~ "recovery code instead."
diff --git a/languages/wpga.pot b/languages/wpga.pot
index 921a96d..8639bd6 100644
--- a/languages/wpga.pot
+++ b/languages/wpga.pot
@@ -296,7 +296,7 @@ msgstr ""
#: ../admin/admin.class.php:1229
msgid ""
-"If you do not have configured the 2-factor authentication, just leave this "
+"If you have not configured 2-factor authentication, just leave this "
"field blank and you will be logged-in as usual. If you can't use the Google "
"Authenticator app for whatever reason, you can use your recovery code "
"instead."
diff --git a/uninstall.php b/uninstall.php
new file mode 100644
index 0000000..701a81d
--- /dev/null
+++ b/uninstall.php
@@ -0,0 +1,65 @@
+
+ * @license GPL-2.0+
+ * @link https://julienliabeuf.com
+ * @copyright 2016 Julien Liabeuf
+ */
+
+// If this file is called directly, abort.
+if ( ! defined( 'WPINC' ) ) {
+ die;
+}
+
+wpga_uninstallPlugin();
+/**
+ * Remove plugin data from database
+ */
+function wpga_uninstallPlugin() {
+
+ /* Plugin main options */
+ delete_option( WPGA_PREFIX . '_options' );
+ delete_option( WPGA_PREFIX . '_used_totp' );
+
+ $args = array( 'meta_query' => array(
+ 'relation' => 'OR',
+ array(
+ 'key' => 'wpga_attempts',
+ 'value' => '',
+ 'compare' => '!='
+ ),
+ array(
+ 'key' => 'wpga_secret',
+ 'value' => '',
+ 'compare' => '!='
+ )
+ )
+ );
+
+ $users = new WP_User_Query( $args );
+
+ /* Delete all user metas */
+ if ( ! empty( $users->results ) ) {
+
+ foreach( $users->results as $key => $user ) {
+
+ delete_user_meta( $user->ID, 'wpga_active' );
+ delete_user_meta( $user->ID, 'wpga_attempts' );
+ delete_user_meta( $user->ID, 'wpga_secret' );
+ delete_user_meta( $user->ID, 'wpga_backup_key' );
+ delete_user_meta( $user->ID, 'wpga_backup_key_time' );
+ delete_user_meta( $user->ID, 'wpga_apps_passwords' );
+ delete_user_meta( $user->ID, 'wpga_apps_passwords_log' );
+
+ }
+
+ }
+
+ /**
+ * Remove cron task
+ */
+ $timestamp = wp_next_scheduled( 'wpas_clean_totps' );
+ wp_unschedule_event( $timestamp, 'wpas_clean_totps' );
+
+}
\ No newline at end of file
diff --git a/vendor/jquery.qrcode.min.js b/vendor/jquery.qrcode.min.js
deleted file mode 100644
index fdc4642..0000000
--- a/vendor/jquery.qrcode.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/* jQuery.qrcode 0.12.0 - http://larsjung.de/jquery-qrcode/ - uses //github.com/kazuhikoarase/qrcode-generator (MIT) */
-!function(r){"use strict";function t(t,e,n,o){function i(r,t){return r-=o,t-=o,0>r||r>=u||0>t||t>=u?!1:a.isDark(r,t)}var a=r(n,e);a.addData(t),a.make(),o=o||0;var u=a.getModuleCount(),f=a.getModuleCount()+2*o,c=function(r,t,e,n){var o=this.isDark,i=1/f;this.isDark=function(a,u){var f=u*i,c=a*i,l=f+i,g=c+i;return o(a,u)&&(r>l||f>e||t>g||c>n)}};this.text=t,this.level=e,this.version=n,this.moduleCount=f,this.isDark=i,this.addBlank=c}function e(r,e,n,o,i){n=Math.max(1,n||1),o=Math.min(40,o||40);for(var a=n;o>=a;a+=1)try{return new t(r,e,a,i)}catch(u){}}function n(r,t,e){var n=e.size,o="bold "+e.mSize*n+"px "+e.fontname,i=w("")[0].getContext("2d");i.font=o;var a=i.measureText(e.label).width,u=e.mSize,f=a/n,c=(1-f)*e.mPosX,l=(1-u)*e.mPosY,g=c+f,s=l+u,h=.01;1===e.mode?r.addBlank(0,l-h,n,s+h):r.addBlank(c-h,l-h,g+h,s+h),t.fillStyle=e.fontcolor,t.font=o,t.fillText(e.label,c*n,l*n+.75*e.mSize*n)}function o(r,t,e){var n=e.size,o=e.image.naturalWidth||1,i=e.image.naturalHeight||1,a=e.mSize,u=a*o/i,f=(1-u)*e.mPosX,c=(1-a)*e.mPosY,l=f+u,g=c+a,s=.01;3===e.mode?r.addBlank(0,c-s,n,g+s):r.addBlank(f-s,c-s,l+s,g+s),t.drawImage(e.image,f*n,c*n,u*n,a*n)}function i(r,t,e){w(e.background).is("img")?t.drawImage(e.background,0,0,e.size,e.size):e.background&&(t.fillStyle=e.background,t.fillRect(e.left,e.top,e.size,e.size));var i=e.mode;1===i||2===i?n(r,t,e):(3===i||4===i)&&o(r,t,e)}function a(r,t,e,n,o,i,a,u){r.isDark(a,u)&&t.rect(n,o,i,i)}function u(r,t,e,n,o,i,a,u,f,c){a?r.moveTo(t+i,e):r.moveTo(t,e),u?(r.lineTo(n-i,e),r.arcTo(n,e,n,o,i)):r.lineTo(n,e),f?(r.lineTo(n,o-i),r.arcTo(n,o,t,o,i)):r.lineTo(n,o),c?(r.lineTo(t+i,o),r.arcTo(t,o,t,e,i)):r.lineTo(t,o),a?(r.lineTo(t,e+i),r.arcTo(t,e,n,e,i)):r.lineTo(t,e)}function f(r,t,e,n,o,i,a,u,f,c){a&&(r.moveTo(t+i,e),r.lineTo(t,e),r.lineTo(t,e+i),r.arcTo(t,e,t+i,e,i)),u&&(r.moveTo(n-i,e),r.lineTo(n,e),r.lineTo(n,e+i),r.arcTo(n,e,n-i,e,i)),f&&(r.moveTo(n-i,o),r.lineTo(n,o),r.lineTo(n,o-i),r.arcTo(n,o,n-i,o,i)),c&&(r.moveTo(t+i,o),r.lineTo(t,o),r.lineTo(t,o-i),r.arcTo(t,o,t+i,o,i))}function c(r,t,e,n,o,i,a,c){var l=r.isDark,g=n+i,s=o+i,h=e.radius*i,v=a-1,d=a+1,w=c-1,m=c+1,p=l(a,c),y=l(v,w),T=l(v,c),B=l(v,m),A=l(a,m),E=l(d,m),k=l(d,c),M=l(d,w),C=l(a,w);p?u(t,n,o,g,s,h,!T&&!C,!T&&!A,!k&&!A,!k&&!C):f(t,n,o,g,s,h,T&&C&&y,T&&A&&B,k&&A&&E,k&&C&&M)}function l(r,t,e){var n,o,i=r.moduleCount,u=e.size/i,f=a;for(p&&e.radius>0&&e.radius<=.5&&(f=c),t.beginPath(),n=0;i>n;n+=1)for(o=0;i>o;o+=1){var l=e.left+o*u,g=e.top+n*u,s=u;f(r,t,e,l,g,s,n,o)}if(w(e.fill).is("img")){t.strokeStyle="rgba(0,0,0,0.5)",t.lineWidth=2,t.stroke();var h=t.globalCompositeOperation;t.globalCompositeOperation="destination-out",t.fill(),t.globalCompositeOperation=h,t.clip(),t.drawImage(e.fill,0,0,e.size,e.size),t.restore()}else t.fillStyle=e.fill,t.fill()}function g(r,t){var n=e(t.text,t.ecLevel,t.minVersion,t.maxVersion,t.quiet);if(!n)return null;var o=w(r).data("qrcode",n),a=o[0].getContext("2d");return i(n,a,t),l(n,a,t),o}function s(r){var t=w("").attr("width",r.size).attr("height",r.size);return g(t,r)}function h(r){return w("").attr("src",s(r)[0].toDataURL("image/png"))}function v(r){var t=e(r.text,r.ecLevel,r.minVersion,r.maxVersion,r.quiet);if(!t)return null;var n,o,i=r.size,a=r.background,u=Math.floor,f=t.moduleCount,c=u(i/f),l=u(.5*(i-c*f)),g={position:"relative",left:0,top:0,padding:0,margin:0,width:i,height:i},s={position:"absolute",padding:0,margin:0,width:c,height:c,"background-color":r.fill},h=w("").data("qrcode",t).css(g);for(a&&h.css("background-color",a),n=0;f>n;n+=1)for(o=0;f>o;o+=1)t.isDark(n,o)&&w("").css(s).css({left:l+o*c,top:l+n*c}).appendTo(h);return h}function d(r){return m&&"canvas"===r.render?s(r):m&&"image"===r.render?h(r):v(r)}var w=jQuery,m=function(){var r=document.createElement("canvas");return Boolean(r.getContext&&r.getContext("2d"))}(),p="[object Opera]"!==Object.prototype.toString.call(window.opera),y={render:"canvas",minVersion:1,maxVersion:40,ecLevel:"L",left:0,top:0,size:200,fill:"#000",background:null,text:"no text",radius:0,quiet:0,mode:0,mSize:.1,mPosX:.5,mPosY:.5,label:"no label",fontname:"sans",fontcolor:"#000",image:null};w.fn.qrcode=function(r){var t=w.extend({},y,r);return this.each(function(){"canvas"===this.nodeName.toLowerCase()?g(this,t):w(this).append(d(t))})}}(function(){var r=function(){function r(t,e){if("undefined"==typeof t.length)throw new Error(t.length+"/"+e);var n=function(){for(var r=0;re;e+=1){t[e]=new Array(r);for(var n=0;r>n;n+=1)t[e][n]=null}return t}(h),y(0,0),y(h-7,0),y(0,h-7),A(),B(),k(r,t),l>=7&&E(r),null==d&&(d=D(l,g,w)),M(d,t)},y=function(r,t){for(var e=-1;7>=e;e+=1)if(!(-1>=r+e||r+e>=h))for(var n=-1;7>=n;n+=1)-1>=t+n||t+n>=h||(e>=0&&6>=e&&(0==n||6==n)||n>=0&&6>=n&&(0==e||6==e)||e>=2&&4>=e&&n>=2&&4>=n?s[r+e][t+n]=!0:s[r+e][t+n]=!1)},T=function(){for(var r=0,t=0,e=0;8>e;e+=1){p(!0,e);var n=i.getLostPoint(m);(0==e||r>n)&&(r=n,t=e)}return t},B=function(){for(var r=8;h-8>r;r+=1)null==s[r][6]&&(s[r][6]=r%2==0);for(var t=8;h-8>t;t+=1)null==s[6][t]&&(s[6][t]=t%2==0)},A=function(){for(var r=i.getPatternPosition(l),t=0;t=a;a+=1)for(var u=-2;2>=u;u+=1)-2==a||2==a||-2==u||2==u||0==a&&0==u?s[n+a][o+u]=!0:s[n+a][o+u]=!1}},E=function(r){for(var t=i.getBCHTypeNumber(l),e=0;18>e;e+=1){var n=!r&&1==(t>>e&1);s[Math.floor(e/3)][e%3+h-8-3]=n}for(var e=0;18>e;e+=1){var n=!r&&1==(t>>e&1);s[e%3+h-8-3][Math.floor(e/3)]=n}},k=function(r,t){for(var e=g<<3|t,n=i.getBCHTypeInfo(e),o=0;15>o;o+=1){var a=!r&&1==(n>>o&1);6>o?s[o][8]=a:8>o?s[o+1][8]=a:s[h-15+o][8]=a}for(var o=0;15>o;o+=1){var a=!r&&1==(n>>o&1);8>o?s[8][h-o-1]=a:9>o?s[8][15-o-1+1]=a:s[8][15-o-1]=a}s[h-8][8]=!r},M=function(r,t){for(var e=-1,n=h-1,o=7,a=0,u=i.getMaskFunction(t),f=h-1;f>0;f-=2)for(6==f&&(f-=1);;){for(var c=0;2>c;c+=1)if(null==s[n][f-c]){var l=!1;a>>o&1));var g=u(n,f-c);g&&(l=!l),s[n][f-c]=l,o-=1,-1==o&&(a+=1,o=7)}if(n+=e,0>n||n>=h){n-=e,e=-e;break}}},C=function(t,e){for(var n=0,o=0,a=0,u=new Array(e.length),f=new Array(e.length),c=0;c=0?d.getAt(w):0}}for(var m=0,s=0;ss;s+=1)for(var c=0;cs;s+=1)for(var c=0;c8*s)throw new Error("code length overflow. ("+c.getLengthInBits()+">"+8*s+")");for(c.getLengthInBits()+4<=8*s&&c.put(0,4);c.getLengthInBits()%8!=0;)c.putBit(!1);for(;;){if(c.getLengthInBits()>=8*s)break;if(c.put(o,8),c.getLengthInBits()>=8*s)break;c.put(a,8)}return C(c,n)};return m.addData=function(r){var t=c(r);w.push(t),d=null},m.isDark=function(r,t){if(0>r||r>=h||0>t||t>=h)throw new Error(r+","+t);return s[r][t]},m.getModuleCount=function(){return h},m.make=function(){p(!1,T())},m.createTableTag=function(r,t){r=r||2,t="undefined"==typeof t?4*r:t;var e="";e+='