Commit f792bb74 authored by agata's avatar agata

added plugin two factor 2fa

parent 7d7c303b
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">
<g fill="none" fill-rule="evenodd">
<path fill="#CCC" d="M98 150a60 60 0 1 1 60 0v60a8 8 0 0 1-8 8h-44a8 8 0 0 1-8-8v-60z"/>
<path fill="#0073AA" d="M116 132a36 36 0 1 1 24 0v64.7a4 4 0 0 1-4 4h-16a4 4 0 0 1-4-4v-64-.7z"/>
</g>
</svg>
<?php
/**
* Class for creating two factor authorization.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Two_Factor_Core {
/**
* The user meta provider key.
*
* @type string
*/
const PROVIDER_USER_META_KEY = '_two_factor_provider';
/**
* The user meta enabled providers key.
*
* @type string
*/
const ENABLED_PROVIDERS_USER_META_KEY = '_two_factor_enabled_providers';
/**
* The user meta nonce key.
*
* @type string
*/
const USER_META_NONCE_KEY = '_two_factor_nonce';
/**
* Set up filters and actions.
*
* @since 0.1-dev
*/
public static function add_hooks() {
add_action( 'plugins_loaded', array( __CLASS__, 'load_textdomain' ) );
add_action( 'init', array( __CLASS__, 'get_providers' ) );
add_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10, 2 );
add_action( 'login_form_validate_2fa', array( __CLASS__, 'login_form_validate_2fa' ) );
add_action( 'login_form_backup_2fa', array( __CLASS__, 'backup_2fa' ) );
add_action( 'show_user_profile', array( __CLASS__, 'user_two_factor_options' ) );
add_action( 'edit_user_profile', array( __CLASS__, 'user_two_factor_options' ) );
add_action( 'personal_options_update', array( __CLASS__, 'user_two_factor_options_update' ) );
add_action( 'edit_user_profile_update', array( __CLASS__, 'user_two_factor_options_update' ) );
add_filter( 'manage_users_columns', array( __CLASS__, 'filter_manage_users_columns' ) );
add_filter( 'wpmu_users_columns', array( __CLASS__, 'filter_manage_users_columns' ) );
add_filter( 'manage_users_custom_column', array( __CLASS__, 'manage_users_custom_column' ), 10, 3 );
// Run only after the core wp_authenticate_username_password() check.
add_filter( 'authenticate', array( __CLASS__, 'filter_authenticate' ), 50 );
}
/**
* Loads the plugin's text domain.
*
* Sites on WordPress 4.6+ benefit from just-in-time loading of translations.
*/
public static function load_textdomain() {
load_plugin_textdomain( 'two-factor' );
}
/**
* For each provider, include it and then instantiate it.
*
* @since 0.1-dev
*
* @return array
*/
public static function get_providers() {
$providers = array(
'Two_Factor_Email' => TWO_FACTOR_DIR . 'providers/class.two-factor-email.php',
'Two_Factor_Totp' => TWO_FACTOR_DIR . 'providers/class.two-factor-totp.php',
'Two_Factor_FIDO_U2F' => TWO_FACTOR_DIR . 'providers/class.two-factor-fido-u2f.php',
'Two_Factor_Backup_Codes' => TWO_FACTOR_DIR . 'providers/class.two-factor-backup-codes.php',
'Two_Factor_Dummy' => TWO_FACTOR_DIR . 'providers/class.two-factor-dummy.php',
);
/**
* Filter the supplied providers.
*
* This lets third-parties either remove providers (such as Email), or
* add their own providers (such as text message or Clef).
*
* @param array $providers A key-value array where the key is the class name, and
* the value is the path to the file containing the class.
*/
$providers = apply_filters( 'two_factor_providers', $providers );
// FIDO U2F is PHP 5.3+ only.
if ( isset( $providers['Two_Factor_FIDO_U2F'] ) && version_compare( PHP_VERSION, '5.3.0', '<' ) ) {
unset( $providers['Two_Factor_FIDO_U2F'] );
trigger_error( sprintf( // WPCS: XSS OK.
/* translators: %s: version number */
__( 'FIDO U2F is not available because you are using PHP %s. (Requires 5.3 or greater)', 'two-factor' ),
PHP_VERSION
) );
}
/**
* For each filtered provider,
*/
foreach ( $providers as $class => $path ) {
include_once( $path );
/**
* Confirm that it's been successfully included before instantiating.
*/
if ( class_exists( $class ) ) {
try {
$providers[ $class ] = call_user_func( array( $class, 'get_instance' ) );
} catch ( Exception $e ) {
unset( $providers[ $class ] );
}
}
}
return $providers;
}
/**
* Get all Two-Factor Auth providers that are enabled for the specified|current user.
*
* @param WP_User $user WP_User object of the logged-in user.
* @return array
*/
public static function get_enabled_providers_for_user( $user = null ) {
if ( empty( $user ) || ! is_a( $user, 'WP_User' ) ) {
$user = wp_get_current_user();
}
$providers = self::get_providers();
$enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true );
if ( empty( $enabled_providers ) ) {
$enabled_providers = array();
}
$enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) );
return $enabled_providers;
}
/**
* Get all Two-Factor Auth providers that are both enabled and configured for the specified|current user.
*
* @param WP_User $user WP_User object of the logged-in user.
* @return array
*/
public static function get_available_providers_for_user( $user = null ) {
if ( empty( $user ) || ! is_a( $user, 'WP_User' ) ) {
$user = wp_get_current_user();
}
$providers = self::get_providers();
$enabled_providers = self::get_enabled_providers_for_user( $user );
$configured_providers = array();
foreach ( $providers as $classname => $provider ) {
if ( in_array( $classname, $enabled_providers ) && $provider->is_available_for_user( $user ) ) {
$configured_providers[ $classname ] = $provider;
}
}
return $configured_providers;
}
/**
* Gets the Two-Factor Auth provider for the specified|current user.
*
* @since 0.1-dev
*
* @param int $user_id Optional. User ID. Default is 'null'.
* @return object|null
*/
public static function get_primary_provider_for_user( $user_id = null ) {
if ( empty( $user_id ) || ! is_numeric( $user_id ) ) {
$user_id = get_current_user_id();
}
$providers = self::get_providers();
$available_providers = self::get_available_providers_for_user( get_userdata( $user_id ) );
// If there's only one available provider, force that to be the primary.
if ( empty( $available_providers ) ) {
return null;
} elseif ( 1 === count( $available_providers ) ) {
$provider = key( $available_providers );
} else {
$provider = get_user_meta( $user_id, self::PROVIDER_USER_META_KEY, true );
// If the provider specified isn't enabled, just grab the first one that is.
if ( ! isset( $available_providers[ $provider ] ) ) {
$provider = key( $available_providers );
}
}
/**
* Filter the two-factor authentication provider used for this user.
*
* @param string $provider The provider currently being used.
* @param int $user_id The user ID.
*/
$provider = apply_filters( 'two_factor_primary_provider_for_user', $provider, $user_id );
if ( isset( $providers[ $provider ] ) ) {
return $providers[ $provider ];
}
return null;
}
/**
* Quick boolean check for whether a given user is using two-step.
*
* @since 0.1-dev
*
* @param int $user_id Optional. User ID. Default is 'null'.
* @return bool
*/
public static function is_user_using_two_factor( $user_id = null ) {
$provider = self::get_primary_provider_for_user( $user_id );
return ! empty( $provider );
}
/**
* Handle the browser-based login.
*
* @since 0.1-dev
*
* @param string $user_login Username.
* @param WP_User $user WP_User object of the logged-in user.
*/
public static function wp_login( $user_login, $user ) {
if ( ! self::is_user_using_two_factor( $user->ID ) ) {
return;
}
wp_clear_auth_cookie();
self::show_two_factor_login( $user );
exit;
}
/**
* Prevent login through XML-RPC and REST API for users with at least one
* two-factor method enabled.
*
* @param WP_User|WP_Error $user Valid WP_User only if the previous filters
* have verified and confirmed the
* authentication credentials.
*
* @return WP_User|WP_Error
*/
public static function filter_authenticate( $user ) {
if ( $user instanceof WP_User && self::is_api_request() && self::is_user_using_two_factor( $user->ID ) && ! self::is_user_api_login_enabled( $user->ID ) ) {
return new WP_Error(
'invalid_application_credentials',
__( 'Error: API login for user disabled.', 'two-factor' )
);
}
return $user;
}
/**
* If the current user can login via API requests such as XML-RPC and REST.
*
* @param integer $user_id User ID.
*
* @return boolean
*/
public static function is_user_api_login_enabled( $user_id ) {
return (bool) apply_filters( 'two_factor_user_api_login_enable', false, $user_id );
}
/**
* Is the current request an XML-RPC or REST request.
*
* @return boolean
*/
public static function is_api_request() {
if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
return true;
}
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
return true;
}
return false;
}
/**
* Display the login form.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
*/
public static function show_two_factor_login( $user ) {
if ( ! $user ) {
$user = wp_get_current_user();
}
$login_nonce = self::create_login_nonce( $user->ID );
if ( ! $login_nonce ) {
wp_die( esc_html__( 'Failed to create a login nonce.', 'two-factor' ) );
}
$redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : admin_url();
self::login_html( $user, $login_nonce['key'], $redirect_to );
}
/**
* Display the Backup code 2fa screen.
*
* @since 0.1-dev
*/
public static function backup_2fa() {
if ( ! isset( $_GET['wp-auth-id'], $_GET['wp-auth-nonce'], $_GET['provider'] ) ) {
return;
}
$user = get_userdata( $_GET['wp-auth-id'] );
if ( ! $user ) {
return;
}
$nonce = $_GET['wp-auth-nonce'];
if ( true !== self::verify_login_nonce( $user->ID, $nonce ) ) {
wp_safe_redirect( get_bloginfo( 'url' ) );
exit;
}
$providers = self::get_available_providers_for_user( $user );
if ( isset( $providers[ $_GET['provider'] ] ) ) {
$provider = $providers[ $_GET['provider'] ];
} else {
wp_die( esc_html__( 'Cheatin&#8217; uh?', 'two-factor' ), 403 );
}
self::login_html( $user, $_GET['wp-auth-nonce'], $_GET['redirect_to'], '', $provider );
exit;
}
/**
* Generates the html form for the second step of the authentication process.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @param string $login_nonce A string nonce stored in usermeta.
* @param string $redirect_to The URL to which the user would like to be redirected.
* @param string $error_msg Optional. Login error message.
* @param string|object $provider An override to the provider.
*/
public static function login_html( $user, $login_nonce, $redirect_to, $error_msg = '', $provider = null ) {
if ( empty( $provider ) ) {
$provider = self::get_primary_provider_for_user( $user->ID );
} elseif ( is_string( $provider ) && method_exists( $provider, 'get_instance' ) ) {
$provider = call_user_func( array( $provider, 'get_instance' ) );
}
$provider_class = get_class( $provider );
$available_providers = self::get_available_providers_for_user( $user );
$backup_providers = array_diff_key( $available_providers, array( $provider_class => null ) );
$interim_login = isset( $_REQUEST['interim-login'] ); // WPCS: CSRF ok.
$rememberme = 0;
if ( isset( $_REQUEST['rememberme'] ) && $_REQUEST['rememberme'] ) {
$rememberme = 1;
}
if ( ! function_exists( 'login_header' ) ) {
// We really should migrate login_header() out of `wp-login.php` so it can be called from an includes file.
include_once( TWO_FACTOR_DIR . 'includes/function.login-header.php' );
}
login_header();
if ( ! empty( $error_msg ) ) {
echo '<div id="login_error"><strong>' . esc_html( $error_msg ) . '</strong><br /></div>';
}
?>
<form name="validate_2fa_form" id="loginform" action="<?php echo esc_url( self::login_url( array( 'action' => 'validate_2fa' ), 'login_post' ) ); ?>" method="post" autocomplete="off">
<input type="hidden" name="provider" id="provider" value="<?php echo esc_attr( $provider_class ); ?>" />
<input type="hidden" name="wp-auth-id" id="wp-auth-id" value="<?php echo esc_attr( $user->ID ); ?>" />
<input type="hidden" name="wp-auth-nonce" id="wp-auth-nonce" value="<?php echo esc_attr( $login_nonce ); ?>" />
<?php if ( $interim_login ) { ?>
<input type="hidden" name="interim-login" value="1" />
<?php } else { ?>
<input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>" />
<?php } ?>
<input type="hidden" name="rememberme" id="rememberme" value="<?php echo esc_attr( $rememberme ); ?>" />
<?php $provider->authentication_page( $user ); ?>
</form>
<?php
if ( 1 === count( $backup_providers ) ) :
$backup_classname = key( $backup_providers );
$backup_provider = $backup_providers[ $backup_classname ];
$login_url = self::login_url(
array(
'action' => 'backup_2fa',
'provider' => $backup_classname,
'wp-auth-id' => $user->ID,
'wp-auth-nonce' => $login_nonce,
'redirect_to' => $redirect_to,
'rememberme' => $rememberme,
)
);
?>
<div class="backup-methods-wrap">
<p class="backup-methods">
<a href="<?php echo esc_url( $login_url ); ?>">
<?php
echo esc_html(
sprintf(
// translators: %s: Two-factor method name.
__( 'Or, use your backup method: %s &rarr;', 'two-factor' ),
$backup_provider->get_label()
)
);
?>
</a>
</p>
</div>
<?php elseif ( 1 < count( $backup_providers ) ) : ?>
<div class="backup-methods-wrap">
<p class="backup-methods">
<a href="javascript:;" onclick="document.querySelector('ul.backup-methods').style.display = 'block';">
<?php esc_html_e( 'Or, use a backup method…', 'two-factor' ); ?>
</a>
</p>
<ul class="backup-methods">
<?php
foreach ( $backup_providers as $backup_classname => $backup_provider ) :
$login_url = self::login_url(
array(
'action' => 'backup_2fa',
'provider' => $backup_classname,
'wp-auth-id' => $user->ID,
'wp-auth-nonce' => $login_nonce,
'redirect_to' => $redirect_to,
'rememberme' => $rememberme,
)
);
?>
<li>
<a href="<?php echo esc_url( $login_url ); ?>">
<?php $backup_provider->print_label(); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<p id="backtoblog">
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" title="<?php esc_attr_e( 'Are you lost?', 'two-factor' ); ?>">
<?php
echo esc_html(
sprintf(
// translators: %s: site name.
__( '&larr; Back to %s', 'two-factor' ),
get_bloginfo( 'title', 'display' )
)
);
?>
</a>
</p>
</div>
<style>
/* @todo: migrate to an external stylesheet. */
.backup-methods-wrap {
margin-top: 16px;
padding: 0 24px;
}
.backup-methods-wrap a {
color: #999;
text-decoration: none;
}
ul.backup-methods {
display: none;
padding-left: 1.5em;
}
/* Prevent Jetpack from hiding our controls, see https://github.com/Automattic/jetpack/issues/3747 */
.jetpack-sso-form-display #loginform > p,
.jetpack-sso-form-display #loginform > div {
display: block;
}
</style>
<?php
/** This action is documented in wp-login.php */
do_action( 'login_footer' ); ?>
<div class="clear"></div>
</body>
</html>
<?php
}
/**
* Generate the two-factor login form URL.
*
* @param array $params List of query argument pairs to add to the URL.
* @param string $scheme URL scheme context.
*
* @return string
*/
public static function login_url( $params = array(), $scheme = 'login' ) {
if ( ! is_array( $params ) ) {
$params = array();
}
$params = urlencode_deep( $params );
return add_query_arg( $params, site_url( 'wp-login.php', $scheme ) );
}
/**
* Create the login nonce.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @return array
*/
public static function create_login_nonce( $user_id ) {
$login_nonce = array();
try {
$login_nonce['key'] = bin2hex( random_bytes( 32 ) );
} catch (Exception $ex) {
$login_nonce['key'] = wp_hash( $user_id . mt_rand() . microtime(), 'nonce' );
}
$login_nonce['expiration'] = time() + HOUR_IN_SECONDS;
if ( ! update_user_meta( $user_id, self::USER_META_NONCE_KEY, $login_nonce ) ) {
return false;
}
return $login_nonce;
}
/**
* Delete the login nonce.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @return bool
*/
public static function delete_login_nonce( $user_id ) {
return delete_user_meta( $user_id, self::USER_META_NONCE_KEY );
}
/**
* Verify the login nonce.
*
* @since 0.1-dev
*
* @param int $user_id User ID.
* @param string $nonce Login nonce.
* @return bool
*/
public static function verify_login_nonce( $user_id, $nonce ) {
$login_nonce = get_user_meta( $user_id, self::USER_META_NONCE_KEY, true );
if ( ! $login_nonce ) {
return false;
}
if ( $nonce !== $login_nonce['key'] || time() > $login_nonce['expiration'] ) {
self::delete_login_nonce( $user_id );
return false;
}
return true;
}
/**
* Login form validation.
*
* @since 0.1-dev
*/
public static function login_form_validate_2fa() {
if ( ! isset( $_POST['wp-auth-id'], $_POST['wp-auth-nonce'] ) ) {
return;
}
$user = get_userdata( $_POST['wp-auth-id'] );
if ( ! $user ) {
return;
}