Commit a95dfded authored by noplanman's avatar noplanman

Merge branch '12-stealth-mode' into 'develop'

Add stealth OTP mode.

See merge request !12
parents 2e352465 b6ff32e0
Pipeline #1194 passed with stage
in 50 seconds
......@@ -34,6 +34,7 @@ class Wp_Otp {
*/
public function __construct() {
$this->load_dependencies();
$this->define_constants();
$this->set_locale();
$this->define_admin_hooks();
$this->define_public_hooks();
......@@ -79,6 +80,16 @@ class Wp_Otp {
$this->loader = new Wp_Otp_Loader();
}
/**
* Define the required constants for this plugin.
*
* @since [unreleased]
* @access private
*/
private function define_constants() {
defined( 'WP_OTP_STEALTH' ) || define( 'WP_OTP_STEALTH', false );
}
/**
* Define the locale for this plugin for internationalization.
*
......@@ -119,6 +130,11 @@ class Wp_Otp {
private function define_public_hooks() {
$plugin_public = new Wp_Otp_Public();
if ( WP_OTP_STEALTH ) {
$this->loader->add_action( 'wp_authenticate', $plugin_public, 'login_form_stealth_validate', 33, 2 );
return;
}
$this->loader->add_action( 'login_form', $plugin_public, 'login_form_render' );
$this->loader->add_action( 'authenticate', $plugin_public, 'login_form_validate', 33 );
}
......
......@@ -71,6 +71,28 @@ class Wp_Otp_Public {
return $user;
}
$user_meta_data = Wp_Otp_User_Meta::get_instance( $user->ID );
$otp = $this->get_otp_if_enabled( $user_meta_data );
if ( null === $otp ) {
return $user;
}
$otp_code = isset( $_POST['wp_otp_code'] ) ? $_POST['wp_otp_code'] : '';
// If this is a valid OTP code, all good!
if ( $this->verify_otp( $otp, $otp_code ) ) {
return $user;
}
// Check if a recovery code is being used.
$recovery_codes = $user_meta_data->get( 'recovery_codes' );
if ( array_key_exists( $otp_code, array_filter( $recovery_codes ) ) ) {
// Unset the recovery code that has just been used.
$recovery_codes[ $otp_code ] = false;
$user_meta_data->set( 'recovery_codes', $recovery_codes, true );
return $user;
}
/**
* Filter for the OTP login form error text when an invalid code is entered.
*
......@@ -83,35 +105,101 @@ class Wp_Otp_Public {
__( '<strong>Invalid code!</strong> Please try again.', 'wp-otp' )
);
return new WP_Error( 'invalid_otp', $otp_invalid_code_text );
}
/**
* Validation of the user login, to check if the stealth OTP was correct.
*
* @param string $username The username that's trying to log in.
* @param string $password The password being used to log in.
*
* @return void
*/
public function login_form_stealth_validate( &$username, &$password ) {
$user = get_user_by( 'login', $username );
if ( ! $user ) {
return;
}
$user_meta_data = Wp_Otp_User_Meta::get_instance( $user->ID );
if ( $user_meta_data->get( 'enabled' ) && null !== $user_meta_data->get( 'secret' ) ) {
$otp_code = isset( $_POST['wp_otp_code'] ) ? $_POST['wp_otp_code'] : '';
/**
* Filter for the OTP code expiration window.
*
* @since 0.1.0
*
* @param string $otp_window
*/
$otp_window = (int) apply_filters( 'wp_otp_code_expiration_window', 2 );
$otp = new TOTP( '', $user_meta_data->get( 'secret' ) );
// If this isn't a valid OTP code, check if it's a recovery code, else fail.
if ( ! $otp->verify( $otp_code, null, $otp_window ) ) {
$recovery_codes = $user_meta_data->get( 'recovery_codes' );
if ( array_key_exists( $otp_code, $recovery_codes ) && $recovery_codes[ $otp_code ] ) {
// Unset the recovery code that has just been used.
$recovery_codes[ $otp_code ] = false;
$user_meta_data->set( 'recovery_codes', $recovery_codes, true );
} else {
return new WP_Error( 'invalid_otp', $otp_invalid_code_text );
}
$otp = $this->get_otp_if_enabled( $user_meta_data );
if ( null === $otp ) {
return;
}
// Check if no OTP code has been added.
if ( wp_check_password( $password, $user->user_pass, $user->ID ) ) {
// To prevent a login without OTP, modify password.
$password .= '_nootp';
return;
}
// First let's check for a valid OTP code input.
$otp_code = substr( $password, - 6 );
$tmp_pass = substr( $password, 0, - 6 );
if ( wp_check_password( $tmp_pass, $user->user_pass, $user->ID ) && $this->verify_otp( $otp, $otp_code ) ) {
$password = $tmp_pass;
return;
}
// Then check if it's a recovery code.
$recovery_codes = $user_meta_data->get( 'recovery_codes' );
foreach ( array_keys( array_filter( $recovery_codes ) ) as $recovery_code ) {
$otp_code = substr( $password, - strlen( $recovery_code ) );
if ( $otp_code !== $recovery_code ) {
continue;
}
$tmp_pass = substr( $password, 0, - strlen( $recovery_code ) );
if ( wp_check_password( $tmp_pass, $user->user_pass, $user->ID ) ) {
// Unset the recovery code that has just been used.
$recovery_codes[ $otp_code ] = false;
$user_meta_data->set( 'recovery_codes', $recovery_codes, true );
$password = $tmp_pass;
return;
}
}
}
/**
* Get the TOTP object if applicable for this user.
*
* @since [unreleased]
*
* @param Wp_Otp_User_Meta $user_meta_data
*
* @return TOTP
*/
private function get_otp_if_enabled( $user_meta_data ) {
if ( $user_meta_data->get( 'enabled' ) && null !== $user_meta_data->get( 'secret' ) ) {
return new TOTP( '', $user_meta_data->get( 'secret' ) );
}
return null;
}
/**
* Verify the OTP code using the passed TOTP object.
*
* @since [unreleased]
*
* @param TOTP $otp
* @param string $otp_code
*
* @return bool
*/
private function verify_otp( $otp, $otp_code ) {
/**
* Filter for the OTP code expiration window.
*
* @since 0.1.0
*
* @param string $otp_window
*/
$otp_window = (int) apply_filters( 'wp_otp_code_expiration_window', 2 );
return $user;
return $otp->verify( $otp_code, null, $otp_window );
}
}
......@@ -15,6 +15,8 @@ Make your WordPress login extra secure with One Time Passwords.
With WP-OTP you can easily set up 2 Factor Authentication with One Time Passwords for your WordPress login.
This extra layer makes your WordPress site a lot more secure.
The new stealth mode allows for invisible OTP code entry, making your login screen look like any other, no extra OTP code input field.
= Getting started =
After installing and activating the plugin, every user can enable WP-OTP on their profile page.
......@@ -29,6 +31,9 @@ This plugin is completely open source and a work of passion.
If you would like to be part of it and join in, make your way over to the [project page](https://git.feneas.org/noplanman/wp-otp) now.
Also, if you have an idea you would like to see in this plugin or if you've found a bug, please [let me know](https://git.feneas.org/noplanman/wp-otp/issues/new).
= Configuration =
* `WP_OTP_STEALTH`: Set this to `true` to enable stealth OTP mode.
= Filters =
There are a multitude of filters to be adjusted.
......@@ -68,10 +73,15 @@ Be sure to regenerate them when you run out though, or better yet, reconfigure y
= Can I reset my OTP secret key? =
Yes, just click the `Reconfigure` button on the profile page.
= Why is there no OTP input field on the login form? =
Your site admin has either disabled the plugin or enabled stealth mode.
This means that you will need to add your OTP (or recovery) code at the end of your password.
== Changelog ==
= [unreleased] =
* Update list of OTP mobile apps.
* Add stealth mode (via WP_OTP_STEALTH), passing OTP code concatenated to password.
= 0.2.1 =
* Add GitLab CI for PHP Code Sniffer.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment