Authy two-factor authentication to WordPress. * Author: Authy Inc * Version: 1.0 * Author URI: https://www.authy.com * License: GPL2+ * Text Domain: authy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ class Authy { /** * Class variables */ // Oh look, a singleton private static $__instance = null; // Some plugin info protected $name = 'Authy Two-Factor Authentication'; // Parsed settings private $settings = null; // Is API ready, should plugin act? protected $ready = false; // Authy API protected $api = null; protected $api_key = null; protected $api_endpoint = null; // Interface keys protected $settings_page = 'authy'; protected $users_page = 'authy-user'; // Data storage keys protected $settings_key = 'authy'; protected $users_key = 'authy_user'; // Settings field placeholders protected $settings_fields = array(); protected $settings_field_defaults = array( 'label' => null, 'type' => 'text', 'sanitizer' => 'sanitize_text_field', 'section' => 'default', 'class' => null ); // Default Authy data protected $user_defaults = array( 'email' => null, 'phone' => null, 'country_code' => '+1', 'authy_id' => null, 'force_by_admin' => 'false' ); /** * Singleton implementation * * @uses this::setup * @return object */ public static function instance() { if ( ! is_a( self::$__instance, 'Authy' ) ) { self::$__instance = new Authy; self::$__instance->setup(); } return self::$__instance; } /** * Silence is golden. */ private function __construct() {} /** * Plugin setup * * @uses this::register_settings_fields, this::prepare_api, add_action, add_filter * @return null */ private function setup() { require( 'authy-api.php' ); $this->register_settings_fields(); $this->prepare_api(); // Plugin settings add_action( 'admin_init', array( $this, 'action_admin_init' ) ); add_action( 'admin_menu', array( $this, 'action_admin_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'action_admin_enqueue_scripts' ) ); add_filter( 'plugin_action_links', array( $this, 'filter_plugin_action_links' ), 10, 2 ); // Anything other than plugin configuration belongs in here. if ( $this->ready ) { // User settings add_action( 'show_user_profile', array( $this, 'action_show_user_profile' ) ); add_action( 'edit_user_profile', array( $this, 'action_edit_user_profile' ) ); add_action( 'wp_ajax_' . $this->users_page, array( $this, 'ajax_get_id' ) ); add_action( 'personal_options_update', array( $this, 'action_personal_options_update' ) ); add_action( 'edit_user_profile_update', array( $this, 'action_edit_user_profile_update' ) ); add_filter( 'user_profile_update_errors', array( $this, 'check_user_fields' ), 10, 3); // Authentication add_filter( 'authenticate', array( $this, 'authenticate_user'), 10, 3); // Disable XML-RPC if ( $this->get_setting('disable_xmlrpc') ) add_filter( 'xmlrpc_enabled', '__return_false' ); } } /** * Add settings fields for main plugin page * * @uses __ * @return null */ protected function register_settings_fields() { $this->settings_fields = array( array( 'name' => 'api_key_production', 'label' => __( 'Authy Production API Key', 'authy' ), 'type' => 'text', 'sanitizer' => 'alphanumeric' ), array( 'name' => 'disable_xmlrpc', 'label' => __( "Disable external apps that don't support Two-factor Authentication", 'authy_wp' ), 'type' => 'checkbox', 'sanitizer' => null ) ); } /** * Set class variables regarding API * Instantiates the Authy API class into $this->api * * @uses this::get_setting, Authy_WP_API::instance */ protected function prepare_api() { $endpoints = array( 'production' => 'https://api.authy.com' ); $api_key = $this->get_setting( 'api_key_production'); // Only prepare the API endpoint if we have all information needed. if ( $api_key && isset( $endpoints['production'] ) ) { $this->api_key = $api_key; $this->api_endpoint = $endpoints[ 'production' ]; $this->ready = true; } // Instantiate the API class $this->api = Authy_API::instance( $this->api_key, $this->api_endpoint ); } /** * COMMON PLUGIN ELEMENTS */ /** * Register plugin's setting and validation callback * * @param action admin_init * @uses register_setting * @return null */ public function action_admin_init() { register_setting( $this->settings_page, $this->settings_key, array( $this, 'validate_plugin_settings' ) ); register_setting( $this->settings_page, 'authy_roles', array($this, 'roles_validate')); } /** * Register plugin settings page and page's sections * * @uses add_options_page, add_settings_section * @action admin_menu * @return null */ public function action_admin_menu() { add_options_page( $this->name, 'Authy', 'manage_options', $this->settings_page, array( $this, 'plugin_settings_page' ) ); add_settings_section( 'default', '', array( $this, 'register_settings_page_sections' ), $this->settings_page ); } /** * Enqueue admin script for connection modal * * @uses get_current_screen, wp_enqueue_script, plugins_url, wp_localize_script, this::get_ajax_url, wp_enqueue_style * @action admin_enqueue_scripts * @return null */ public function action_admin_enqueue_scripts() { if ( ! $this->ready ) return; global $current_screen; if ( $current_screen->base == 'profile') { wp_enqueue_script( 'authy-profile', plugins_url( 'assets/authy-profile.js', __FILE__ ), array( 'jquery', 'thickbox' ), 1.01, true ); wp_enqueue_script( 'form-authy-js', 'https://www.authy.com/form.authy.min.js', array(), false, true); wp_localize_script( 'authy-profile', 'Authy', array( 'ajax' => $this->get_ajax_url(), 'th_text' => __( 'Two-Factor Authentication', 'authy' ), 'button_text' => __( 'Enable/Disable Authy', 'authy' ) ) ); wp_enqueue_style( 'thickbox' ); wp_enqueue_style( 'form-authy-css', 'https://www.authy.com/form.authy.min.css', array(), false, 'screen' ); }elseif ( $current_screen->base == 'user-edit' ) { wp_enqueue_script( 'form-authy-js', 'https://www.authy.com/form.authy.min.js', array(), false, true); wp_enqueue_style( 'form-authy-css', 'https://www.authy.com/form.authy.min.css', array(), false, 'screen' ); } } /** * Add settings link to plugin row actions * * @param array $links * @param string $plugin_file * @uses menu_page_url, __ * @filter plugin_action_links * @return array */ public function filter_plugin_action_links( $links, $plugin_file ) { if ( strpos( $plugin_file, pathinfo( __FILE__, PATHINFO_FILENAME ) ) !== false ) $links['settings'] = '' . __( 'Settings', 'authy' ) . ''; return $links; } /** * Retrieve a plugin setting * * @param string $key * @uses get_option, wp_parse_args, apply_filters * @return array or false */ public function get_setting( $key ) { $value = false; if ( is_null( $this->settings ) || ! is_array( $this->settings ) ) { $this->settings = get_option( $this->settings_key ); $this->settings = wp_parse_args( $this->settings, array( 'api_key_production' => '', 'environment' => apply_filters( 'authy_environment', 'production' ), 'disable_xmlrpc' => false ) ); } if ( isset( $this->settings[ $key ] ) ) $value = $this->settings[ $key ]; return $value; } /** * Build Ajax URL for users' connection management * * @uses add_query_arg, wp_create_nonce, admin_url * @return string */ protected function get_ajax_url() { return add_query_arg( array( 'action' => $this->users_page, 'nonce' => wp_create_nonce( $this->users_key . '_ajax' ), ), admin_url( 'admin-ajax.php' ) ); } /** * Check if Two factor authentication is available for role * @param object $user * @uses wp_roles, get_option * @return boolean * */ public function available_authy_for_role($user) { global $wp_roles; $available_authy = false; $listRoles = $wp_roles->get_names(); $authy_roles = get_option('authy_roles', $listRoles); foreach ($user->roles as $role) { if (array_key_exists($role, $authy_roles)) $available_authy = true; } return $available_authy; } /** * GENERAL OPTIONS PAGE */ /** * Populate settings page's sections * * @uses add_settings_field * @return null */ public function register_settings_page_sections() { add_settings_field('api_key_production', __('Authy Production API Key', 'authy'), array( $this, 'add_settings_api_key' ), $this->settings_page, 'default'); add_settings_field('authy_roles', __('Allow Authy for the following roles', 'authy'), array( $this, 'add_settings_roles' ), $this->settings_page, 'default'); add_settings_field('disable_xmlrpc', __("Disable external apps that don't support Two-factor Authentication", 'authy'), array( $this, 'add_settings_disbale_xmlrpc' ), $this->settings_page, 'default'); } /** * Render settings api key * * @uses this::get_setting, esc_attr * @return string */ public function add_settings_api_key() { $value = $this->get_setting( 'api_key_production' ); ?>get_names(); $listRoles = array(); foreach($roles as $key=>$role) { $listRoles[before_last_bar($key)] = before_last_bar($role); } $selected = get_option('authy_roles', $listRoles); foreach ($wp_roles->get_names() as $role) { ?> />
get_setting( 'disable_xmlrpc' ); ?>

ready ) : ?> api->application_details(); ?>

%1$s and create an application for access to the Authy API.', 'authy' ), 'https://www.authy.com/' ); ?>

settings_page ); ?> settings_page ); ?>

Application Details

name ?>
plan) ?>
plan == 'sandbox'){ ?>
settings_page . '-options' ); $settings_validated = array(); foreach ( $this->settings_fields as $field ) { $field = wp_parse_args( $field, $this->settings_field_defaults ); if ( ! isset( $settings[ $field['name'] ] ) ) continue; switch ( $field['type'] ) { case 'text' : switch ( $field['sanitizer'] ) { case 'alphanumeric' : $value = preg_replace( '#[^a-z0-9]#i', '', $settings[ $field['name' ] ] ); break; default: case 'sanitize_text_field' : $value = sanitize_text_field( $settings[ $field['name'] ] ); break; } break; default: $value = sanitize_text_field( $settings[ $field['name'] ] ); break; } if ( isset( $value ) && ! empty( $value ) ) $settings_validated[ $field['name'] ] = $value; } return $settings_validated; } /** * Validate roles * @param array $roles * @uses $wp_roles * @return array */ public function roles_validate ($roles){ if(!is_array($roles) || empty($roles)){ return array(); } global $wp_roles; $listRoles = $wp_roles->get_names(); foreach ($roles as $role) { if (!in_array($roles, $listRoles)) { unset($roles[$role]); } } return $roles; } /** * USER INFORMATION FUNCTIONS */ /** * Add Authy data to a given user account * * @param int $user_id * @param string $email * @param string $phone * @param string $country_code * @param string $force_by_admin * @uses this::user_has_authy_id, this::api::get_id, wp_parse_args, this::clear_authy_data, get_user_meta, update_user_meta * @return null */ public function set_authy_data( $user_id, $email, $phone, $country_code, $force_by_admin = 'false', $authy_id = '') { // Retrieve user's existing Authy ID, or get one from Authy if ( $this->user_has_authy_id( $user_id ) ) { $authy_id = $this->get_user_authy_id( $user_id ); } elseif ($authy_id == '') { // Request an Authy ID with given user information $response = $this->api->register_user( $email, $phone, $country_code ); if ( $response->user && $response->user->id ) { $authy_id = $response->user->id; } else { unset( $authy_id ); } } // Build array of Authy data $data_sanitized = array( 'email' => $email, 'phone' => $phone, 'country_code' => $country_code, 'force_by_admin' => $force_by_admin ); if ( isset( $authy_id ) ) $data_sanitized['authy_id'] = $authy_id; $data_sanitized = wp_parse_args( $data_sanitized, $this->user_defaults ); // Update Authy data if sufficient information is provided, otherwise clear the option out. if ( empty( $data_sanitized['phone'] ) ) { $this->clear_authy_data( $user_id ); } else { $data = get_user_meta( $user_id, $this->users_key, true ); if ( ! is_array( $data ) ) $data = array(); $data[ $this->api_key ] = $data_sanitized; update_user_meta( $user_id, $this->users_key, $data ); } } /** * Retrieve a user's Authy data for a given API key * * @param int $user_id * @param string $api_key * @uses get_user_meta, wp_parse_args * @return array */ protected function get_authy_data( $user_id, $api_key = null ) { // Bail without a valid user ID if ( ! $user_id ) return $this->user_defaults; // Validate API key if ( is_null( $api_key ) ) $api_key = $this->api_key; else $api_key = preg_replace( '#[a-z0-9]#i', '', $api_key ); // Get meta, which holds all Authy data by API key $data = get_user_meta( $user_id, $this->users_key, true ); if ( ! is_array( $data ) ) $data = array(); // Return data for this API, if present, otherwise return default data if ( array_key_exists( $api_key, $data ) ) return wp_parse_args( $data[ $api_key ], $this->user_defaults ); return $this->user_defaults; } /** * Delete any stored Authy connections for the given user. * Expected usage is somewhere where clearing is the known action. * * @param int $user_id * @uses delete_user_meta * @return null */ protected function clear_authy_data( $user_id ) { delete_user_meta( $user_id, $this->users_key ); } /** * Check if a given user has an Authy ID set * * @param int $user_id * @uses this::get_user_authy_id * @return bool */ protected function user_has_authy_id( $user_id ) { return (bool) $this->get_user_authy_id( $user_id ); } /** * Retrieve a given user's Authy ID * * @param int $user_id * @uses this::get_authy_data * @return int|bool */ protected function get_user_authy_id( $user_id ) { $data = $this->get_authy_data( $user_id ); if ( is_array( $data ) && is_numeric( $data['authy_id'] ) ) return (int) $data['authy_id']; return false; } /** * Check if a given user has Two factor authentication forced by admin * @param int $user_id * @uses this::get_authy_data * @return bool * */ protected function with_force_by_admin( $user_id ) { $data = $this->get_authy_data( $user_id); if ($data['force_by_admin'] == 'true') return true; return false; } /** * USER SETTINGS PAGES */ /** * Non-JS connection interface * * @param object $user * @uses this::get_authy_data, esc_attr, */ public function action_show_user_profile( $user ) { $meta = $this->get_authy_data( $user->ID ); if ( $this->user_has_authy_id( $user->ID ) ) { if (!$this->with_force_by_admin( $user->ID)){ ?>

name ); ?>

users_key . 'disable_own', $this->users_key . '[nonce]' ); ?>
available_authy_for_role($user)) {?>

name ); ?>

users_key . 'edit_own', $this->users_key . '[nonce]' ); ?>
users_key ] ) ? $_POST[ $this->users_key ] : false; // Parse for nonce and API existence if ( is_array( $authy_data ) && array_key_exists( 'nonce', $authy_data ) ) { if ( wp_verify_nonce( $authy_data['nonce'], $this->users_key . 'edit_own' ) ) { // Email address $userdata = get_userdata( $user_id ); if ( is_object( $userdata ) && ! is_wp_error( $userdata ) ) $email = $userdata->data->user_email; else $email = null; // Phone number $phone = preg_replace( '#[^\d]#', '', $authy_data['phone'] ); $country_code = preg_replace( '#[^\d\+]#', '', $authy_data['country_code'] ); // Process information with Authy $this->set_authy_data( $user_id, $email, $phone, $country_code ); } elseif ( wp_verify_nonce( $authy_data['nonce'], $this->users_key . 'disable_own' ) ) { // Delete Authy usermeta if requested if ( isset( $authy_data['disable_own'] ) ) $this->clear_authy_data( $user_id ); } } } /** * Allow sufficiently-priviledged users to disable another user's Authy service. * * @param object $user * @uses current_user_can, this::user_has_authy_id, get_user_meta, wp_parse_args, esc_attr, wp_nonce_field * @action edit_user_profile * @return string */ public function action_edit_user_profile( $user ) { if ( current_user_can( 'create_users' ) ) { ?>

Authy Two-factor Authentication

user_has_authy_id( $user->ID ) ) : $meta = get_user_meta( get_current_user_id(), $this->users_key, true ); $meta = wp_parse_args( $meta, $this->user_defaults ); $name = esc_attr( $this->users_key ); ?> users_key . '_disable', "_{$this->users_key}_wpnonce" ); else : $authy_data = $this->get_authy_data( $user->ID ); ?>

users_key . '_edit', "_{$this->users_key}_wpnonce" ); ?>
api->register_user( $_POST['email'], $_POST['authy_user']['phone'], $_POST['authy_user']['country_code'] ); if ($response->errors) { foreach ($response->errors as $attr => $message) { if ($attr == 'country_code') $errors->add('authy_error', 'Error: ' . 'Authy country code is invalid'); else $errors->add('authy_error', 'Error: ' . 'Authy ' . $attr . ' ' . $message); } } } /** * Print head element * * @uses wp_print_scripts, wp_print_styles * @return @string */ public function ajax_head() { ?> users_page} * @return string */ public function ajax_get_id() { // If nonce isn't set, bail if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( $_REQUEST['nonce'], $this->users_key . '_ajax' ) ) { ?>get_authy_data( $user_id ); $errors = array(); // Step $step = isset( $_REQUEST['authy_step'] ) ? preg_replace( '#[^a-z0-9\-_]#i', '', $_REQUEST['authy_step'] ) : false; //iframe head $this->ajax_head(); // iframe body ?>>

Authy Two-Factor Authentication

user_has_authy_id( $user_id ) ) { ?>

%s', 'authy' ), $user_data->user_login ); ?>

users_key . '_ajax_disable' ); } else { if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], $this->users_key . '_ajax_check' ) ) { $email = sanitize_email( $user_data->user_email ); $phone = isset( $_POST['authy_phone'] ) ? preg_replace( '#[^\d]#', '', $_POST['authy_phone'] ) : false; $country_code = isset( $_POST['authy_country_code'] ) ? preg_replace( '#[^\d]#', '', $_POST['authy_country_code'] ) : false; $response = $this->api->register_user( $email, $phone, $country_code ); if ( $response->success == 'true' ) { $this->set_authy_data( $user_id, $email, $phone, $country_code, $response->user->id ); if ( $this->user_has_authy_id( $user_id ) ) { ?>

%s user account.', 'authy' ), $user_data->user_login ); ?>

%s user account.', 'authy' ), $user_data->user_login ); ?>

errors); } } ?>

%s account.', 'authy' ), $user_data->user_login ); ?>

Continue.', 'authy' ); ?>

$value) { if ($key == 'country_code') { ?>

Country code is not valid.

users_key . '_ajax_check' ); ?>

users_key . '_ajax_disable' ) ) $this->clear_authy_data( $user_id );?>

get_ajax_url() ); exit; break; } ?>
get_user_authy_id( $user->ID ); $api_rsms = $this->api->request_sms( $authy_id); } /** * Clear a user's Authy configuration if an allowed user requests it. * * @param int $user_id * @uses wp_verify_nonce, this::clear_authy_data * @action edit_user_profile_update * @return null */ public function action_edit_user_profile_update( $user_id ) { if ( isset( $_POST["_{$this->users_key}_wpnonce"] ) && wp_verify_nonce( $_POST["_{$this->users_key}_wpnonce"], $this->users_key . '_disable' ) ) { if ( !isset( $_POST[ $this->users_key ] ) ){ $this->clear_authy_data( $user_id ); } }else{ $email = $_POST['email']; $phone = $_POST['authy_user']['phone']; $country_code = $_POST['authy_user']['country_code']; $this->set_authy_data( $user_id, $email, $phone, $country_code, 'true' ); } } /** * AUTHENTICATION CHANGES */ /** * Add Two factor authentication page * * @param mixed $user * @param string $redirect * @uses _e * @return string */ public function authy_token_form($user, $redirect) { $username = $user->user_login; $user_data = $this->get_authy_data( $user->ID ); ?>

Authy Two-Factor Authentication

get_user_authy_id( $user->ID ); $authy_token = preg_replace( '#[^\d]#', '', $_POST['authy_token'] ); $api_check = $this->api->check_token( $authy_id, $authy_token ); // Act on API response if ( $api_check === false ) return null; elseif ( is_string( $api_check ) ) return new WP_Error( 'authentication_failed', __('ERROR: ' . $api_check ) ); wp_set_auth_cookie($user->ID); wp_safe_redirect($_POST['redirect_to']); exit(); } // If have a username if (! empty( $username )) { $userWP = get_user_by('login', $username); // Don't bother if WP can't provide a user object. if ( ! is_object( $userWP ) || ! property_exists( $userWP, 'ID' ) ) return $userWP; // User must opt in. if ( ! $this->user_has_authy_id( $userWP->ID )) return $user; remove_action('authenticate', 'wp_authenticate_username_password', 20); $user = wp_authenticate_username_password($user, $username, $password); if (!is_wp_error($user)) { $this->action_request_sms($username); $this->authy_token_form($user, $_POST['redirect_to']); exit(); }else{ return $user; } } } } Authy::instance();