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/' ); ?>
Application Details
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 ); ?>
available_authy_for_role($user)) {?>
name ); ?>
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
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
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();