Initial commit: Atomaste website

This commit is contained in:
2025-12-10 12:17:30 -05:00
commit 0b9e5d1605
19260 changed files with 5206382 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
<?php defined( 'ABSPATH' ) or die();
/**
* @param $notices
* @return mixed
* Notice function
*/
function rsssl_code_execution_errors_notice( $notices ) {
$notices['code-execution-uploads'] = array(
'callback' => 'rsssl_code_execution_allowed',
'score' => 5,
'output' => array(
'file-not-found' => array(
'msg' => __("Could not find code execution test file.", "really-simple-ssl"),
'icon' => 'open',
'dismissible' => true,
),
'uploads-folder-not-writable' => array(
'msg' => __("Uploads folder not writable.", "really-simple-ssl"),
'icon' => 'open',
'dismissible' => true,
),
'could-not-create-test-file' => array(
'msg' => __("Could not copy code execution test file.", "really-simple-ssl"),
'icon' => 'open',
'dismissible' => true,
),
),
);
if ( rsssl_get_server() === 'nginx') {
$notices['code-execution-uploads-nginx'] = array(
'callback' => 'rsssl_code_execution_allowed',
'score' => 5,
'output' => array(
'true' => array(
'msg' => __("The code to block code execution in the uploads folder cannot be added automatically on nginx. Add the following code to your nginx.conf file:", "really-simple-ssl")
. "<br>" . rsssl_get_nginx_code_code_execution_uploads(),
'icon' => 'open',
'dismissible' => true,
),
),
);
}
return $notices;
}
add_filter('rsssl_notices', 'rsssl_code_execution_errors_notice');
/**
* Block code execution
* @param array $rules
*
* @return []
*
*/
function rsssl_disable_code_execution_rules($rules)
{
if ( !rsssl_get_option('block_code_execution_uploads')) {
return $rules;
}
if ( RSSSL()->server->apache_version_min_24() ) {
$rule = "\n" ."<Files *.php>";
$rule .= "\n" . "Require all denied";
$rule .= "\n" . "</Files>";
} else {
$rule = "\n" ."<Files *.php>";
$rule .= "\n" . "deny from all";
$rule .= "\n" . "</Files>";
}
$rules[] = ['rules' => $rule, 'identifier' => 'deny from all'];
return $rules;
}
add_filter('rsssl_htaccess_security_rules_uploads', 'rsssl_disable_code_execution_rules');
function rsssl_get_nginx_code_code_execution_uploads() {
$code = '<code>location ~* /uploads/.*\.php$ {' . "<br>";
$code .= '&nbsp;&nbsp;&nbsp;&nbsp;return 503;' . "<br>";
$code .= '}</code>' . "<br>";
return $code;
}

View File

@@ -0,0 +1,23 @@
<?php
defined( 'ABSPATH' ) or die( "you do not have access to this page!" );
/**
* Disable XMLRPC when this integration is activated
*/
add_filter('xmlrpc_enabled', '__return_false');
/**
* Remove html link
*/
remove_action( 'wp_head', 'rsd_link' );
/**
* stop all requests to xmlrpc.php for RSD per XML-RPC:
*/
if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
exit;

View File

@@ -0,0 +1,43 @@
<?php
defined( 'ABSPATH' ) or die();
/**
* Add javascript to make first and last name fields required
*/
function rsssl_disable_registration_js() {
if ( !isset($_SERVER['REQUEST_URI']) || (strpos($_SERVER['REQUEST_URI'], 'user-new.php')===false && strpos($_SERVER['REQUEST_URI'], 'profile.php')===false) ) {
return;
}
?>
<script>
window.addEventListener('load', () => {
document.getElementById('first_name').closest('tr').classList.add("form-required");
document.getElementById('last_name').closest('tr').classList.add("form-required");
});
</script>
<?php
}
add_action( 'admin_print_footer_scripts', 'rsssl_disable_registration_js' );
/**
* Add javascript to make first and last name fields required
*/
function rsssl_strip_userlogin() {
if ( !isset($_SERVER['REQUEST_URI']) || strpos($_SERVER['REQUEST_URI'], 'profile.php')===false ) {
return;
}
?>
<script>
let rsssl_user_login = document.querySelector('input[name=user_login]');
let rsssl_display_name = document.querySelector('select[name=display_name]');
if ( rsssl_display_name.options.length>1) {
for (let i = rsssl_display_name.options.length-1; i >= 0; i--) {
if ( rsssl_user_login.value.toLowerCase() === rsssl_display_name.options[i].value.toLowerCase() ) {
rsssl_display_name.removeChild(rsssl_display_name.options[i])
}
}
}
</script>
<?php
}
add_action( 'admin_print_footer_scripts', 'rsssl_strip_userlogin' );

View File

@@ -0,0 +1,45 @@
<?php
defined( 'ABSPATH' ) or die();
/**
* @return void
*
* Disable file editing
*/
function rsssl_disable_file_editing() {
if ( ! defined('DISALLOW_FILE_EDIT' ) ) {
define('DISALLOW_FILE_EDIT', true );
}
}
add_action("init", "rsssl_disable_file_editing");
/**
* Username 'admin' changed notice
* @return array
*/
function rsssl_disable_file_editing_notice( $notices ) {
$notices['disallow_file_edit_false'] = array(
'condition' => ['rsssl_file_editing_defined_but_disabled'],
'callback' => '_true_',
'score' => 5,
'output' => array(
'true' => array(
'msg' => __("The DISALLOW_FILE_EDIT constant is defined and set to false. You can remove it from your wp-config.php.", "really-simple-ssl"),
'icon' => 'open',
'dismissible' => true,
'url' => 'disallow_file_edit-defined-set-to-false'
),
),
);
return $notices;
}
add_filter('rsssl_notices', 'rsssl_disable_file_editing_notice');
/**
* Check if the constant is defined, AND set to false. In that case the plugin cannot override it anymore
* @return bool
*/
function rsssl_file_editing_defined_but_disabled(){
return defined( 'DISALLOW_FILE_EDIT' ) && ! DISALLOW_FILE_EDIT;
}

View File

@@ -0,0 +1,93 @@
<?php
defined( 'ABSPATH' ) or die();
if ( ! class_exists( 'rsssl_hide_wp_version' ) ) {
class rsssl_hide_wp_version {
private static $_this;
public $new_version = false;
function __construct() {
if ( isset( self::$_this ) ) {
wp_die( "you cannot create a second instance of a singleton class" );
}
self::$_this = $this;
add_action( 'init', array($this, 'remove_wp_version') );
add_filter( 'rsssl_fixer_output', array( $this, 'replace_wp_version') );
}
static function this() {
return self::$_this;
}
/**
* Remove WordPress version info from page source
*
* @return void
*/
public function remove_wp_version() {
// remove <meta name="generator" content="WordPress VERSION" />
add_filter( 'the_generator', function () {
return '';
} );
// remove WP ?ver=5.X.X from css/js
add_filter( 'style_loader_src', array( $this, 'remove_css_js_version' ), 9999 );
add_filter( 'script_loader_src', array ($this, 'remove_css_js_version'), 9999 );
remove_action( 'wp_head', 'wp_generator' ); // remove wordpress version
remove_action( 'wp_head', 'index_rel_link' ); // remove link to index page
remove_action( 'wp_head', 'wlwmanifest_link' ); // remove wlwmanifest.xml (needed to support windows live writer)
remove_action( 'wp_head', 'wp_shortlink_wp_head', 10 ); // Remove shortlink
}
/**
* Generate a random version number
*
* @return string
*/
public function generate_rand_version() {
if ( !$this->new_version) {
$wp_version = get_bloginfo( 'version' );
$token = get_option( 'rsssl_wp_version_token' );
if ( ! $token ) {
$token = str_shuffle( time() );
update_option( 'rsssl_wp_version_token', $token );
}
$this->new_version = hash( 'md5', $token );
}
return $this->new_version;
}
/**
* @param string $html
*
* @return string
*
*/
public function replace_wp_version( $html ) {
$wp_version = get_bloginfo( 'version' );
$new_version = $this->generate_rand_version();
return str_replace( '?ver=' . $wp_version, '?ver=' . $new_version, $html );
}
/**
* @param $src
*
* @return mixed|string
* Remove WordPress version from css and js strings
*/
public function remove_css_js_version( $src ) {
if ( empty($src) ) {
return $src;
}
if ( strpos( $src, '?ver=' ) && strpos( $src, 'wp-includes' ) ) {
$wp_version = get_bloginfo( 'version' );
$new_version = $this->generate_rand_version();
$src = str_replace( '?ver=' . $wp_version, '?ver=' . $new_version, $src );
}
return $src;
}
}
}
RSSSL_SECURITY()->components['hide-wp-version'] = new rsssl_hide_wp_version();

View File

@@ -0,0 +1 @@
<?php // You don't belong here. ?>

View File

@@ -0,0 +1,45 @@
<?php
defined('ABSPATH') or die();
/**
* Override default login error message
* @return string|void
**/
function rsssl_no_wp_login_errors()
{
return __("Invalid login details.", "really-simple-ssl");
}
add_filter( 'login_errors', 'rsssl_no_wp_login_errors' );
/**
* Hide feedback entirely on password reset (no filter available).
*
* @return void
*
*/
function rsssl_hide_pw_reset_error() {
?>
<style>
.login-action-lostpassword #login_error{
display: none;
}
</style>
<?php
}
add_action( 'login_enqueue_scripts', 'rsssl_hide_pw_reset_error' );
/**
*
* Clear username when username is valid but password is incorrect
*
* @return void
*/
function rsssl_clear_username_on_correct_username() {
?>
<script>
if ( document.getElementById('login_error') ) {
document.getElementById('user_login').value = '';
}
</script>
<?php
}
add_action( 'login_footer', 'rsssl_clear_username_on_correct_username' );

View File

@@ -0,0 +1,174 @@
<?php
defined('ABSPATH') or die();
/**
* Username 'admin' changed notice
* @return array
*/
function rsssl_admin_username_changed( $notices ) {
$notices['username_admin_changed'] = array(
'condition' => ['rsssl_username_admin_changed'],
'callback' => '_true_',
'score' => 5,
'output' => array(
'true' => array(
'msg' => sprintf(__("Username 'admin' has been changed to %s", "really-simple-ssl"),esc_html(get_site_transient('rsssl_username_admin_changed')) ),
'icon' => 'open',
'dismissible' => true,
),
),
);
return $notices;
}
add_filter('rsssl_notices', 'rsssl_admin_username_changed');
/**
* Add admin as not allowed username
* @param array $illegal_user_logins
*
* @return array
*/
function rsssl_prevent_admin_user_add(array $illegal_user_logins){
$illegal_user_logins[] = 'admin';
$illegal_user_logins[] = 'administrator';
return $illegal_user_logins;
}
add_filter( 'illegal_user_logins', 'rsssl_prevent_admin_user_add' );
/**
* Rename admin user
* @return bool
*/
function rsssl_rename_admin_user() {
if ( !rsssl_user_can_manage() ) {
return false;
}
//to be able to update the admin user email, we need to disable this filter temporarily
remove_filter( 'illegal_user_logins', 'rsssl_prevent_admin_user_add' );
// Get user data for login admin
$admin_user = get_user_by('login','admin');
if ( $admin_user ) {
// Get the new user login
$new_user_login = trim(sanitize_user(rsssl_get_option('new_admin_user_login')));
if ( rsssl_new_username_valid() ) {
$admin_user_id = $admin_user->data->ID;
$admin_userdata = get_userdata( $admin_user_id );
$admin_email = $admin_userdata->data->user_email;
global $wpdb;
//get current user hash
$user_hash = $wpdb->get_var($wpdb->prepare("select user_pass from {$wpdb->base_prefix}users where ID = %s", $admin_user_id) );
//create temp email address
$domain = site_url();
$parse = parse_url( $domain );
$host = $parse['host'] ?? 'example.com';
$email = "$new_user_login@$host";
// Do not send an e-mail with this temporary e-mail address
add_filter('send_email_change_email', '__return_false');
// update e-mail for existing user. Cannot have two accounts connected to the same e-mail address
$success = wp_update_user( array(
'ID' => $admin_user_id,
'user_email' => $email,
) );
if ( ! $success ) {
return false;
}
// Populate the new user data. Use current 'admin' userdata wherever available
$new_userdata = array(
'user_pass' => wp_generate_password( 12 ), //temp, overwrite with actual hash later.
//(string) The plain-text user password.
'user_login' => $new_user_login,
//(string) The user's login username.
'user_nicename' => isset( $admin_user->data->user_nicename ) ? $admin_user->data->user_nicename : '',
//(string) The URL-friendly user name.
'user_url' => isset( $admin_user->data->user_url ) ? $admin_user->data->user_url : '',
//(string) The user URL.
'user_email' => isset( $admin_email ) ? $admin_email : '',
//(string) The user email address.
'display_name' => isset( $admin_user->data->display_name ) ? $admin_user->data->display_name : '',
//(string) The user's display name. Default is the user's username.
'nickname' => isset( $admin_user->data->nickname ) ? $admin_user->data->nickname : '',
//(string) The user's nickname. Default is the user's username.
'first_name' => isset( $admin_user->data->user_firstname ) ? $admin_user->data->user_firstname : '',
//(string) The user's first name. For new users, will be used to build the first part of the user's display name if $display_name is not specified.
'last_name' => isset( $admin_user->data->user_lastname ) ? $admin_user->data->user_lastname : '',
//(string) The user's last name. For new users, will be used to build the second part of the user's display name if $display_name is not specified.
'description' => isset( $admin_user->data->description ) ? $admin_user->data->description : '',
//(string) The user's biographical description.
'rich_editing' => isset( $admin_user->data->rich_editing ) ? $admin_user->data->rich_editing : '',
//(string|bool) Whether to enable the rich-editor for the user. False if not empty.
'syntax_highlighting' => isset( $admin_user->data->syntax_highlighting ) ? $admin_user->data->syntax_highlighting : '',
//(string|bool) Whether to enable the rich code editor for the user. False if not empty.
'comment_shortcuts' => isset( $admin_user->data->comment_shortcuts ) ? $admin_user->data->comment_shortcuts : '',
//(string|bool) Whether to enable comment moderation keyboard shortcuts for the user. Default false.
'admin_color' => isset( $admin_user->data->admin_color ) ? $admin_user->data->admin_color : '',
//(string) Admin color scheme for the user. Default 'fresh'.
'use_ssl' => isset( $admin_user->data->use_ssl ) ? $admin_user->data->use_ssl : '',
//(bool) Whether the user should always access the admin over https. Default false.
'user_registered' => isset( $admin_user->data->user_registered ) ? $admin_user->data->user_registered : '',
//(string) Date the user registered. Format is 'Y-m-d H:i:s'.
'show_admin_bar_front' => isset( $admin_user->data->show_admin_bar_front ) ? $admin_user->data->show_admin_bar_front : '',
//(string|bool) Whether to display the Admin Bar for the user on the site's front end. Default true.
'role' => isset( $admin_user->roles[0] ) ? $admin_user->roles[0] : '',
//(string) User's role.
'locale' => isset( $admin_user->data->locale ) ? $admin_user->data->locale : '',
//(string) User's locale. Default empty.
);
// Create new admin user
$new_user_id = wp_insert_user( $new_userdata );
if ( ! $new_user_id || is_wp_error($new_user_id) ) {
return false;
}
//store original user hash in this user.
$wpdb->update(
$wpdb->base_prefix.'users',
['user_pass' => $user_hash ],
['ID' => $new_user_id]
);
require_once( ABSPATH . 'wp-admin/includes/user.php' );
wp_delete_user( $admin_user_id, $new_user_id );
// On multisite we have to update the $wpdb->prefix . sitemeta -> meta_key -> site_admins -> meta_value to the new username
if ( is_multisite() ) {
global $wpdb;
$site_admins = $wpdb->get_var( "SELECT meta_value FROM {$wpdb->base_prefix}sitemeta WHERE meta_key = 'site_admins'" );
if ( is_serialized( $site_admins ) ) {
$unserialized = unserialize( $site_admins );
foreach ( $unserialized as $index => $site_admin ) {
if ( $site_admin === 'admin' ) {
$unserialized[ $index ] = $new_user_login;
}
}
$site_admins = serialize( $unserialized );
}
$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->base_prefix}sitemeta SET meta_value = %s WHERE meta_key = 'site_admins'", $site_admins ) );
}
set_site_transient( 'rsssl_username_admin_changed', $new_user_login, DAY_IN_SECONDS );
}
return true;
}
return true;
}
add_action('rsssl_after_saved_fields','rsssl_rename_admin_user', 30);
/**
* @return bool
*
* Notice condition
*/
function rsssl_username_admin_changed() {
if ( get_site_transient('rsssl_username_admin_changed') ) {
return true;
}
return false;
}

View File

@@ -0,0 +1,54 @@
<?php
defined('ABSPATH') or die();
/**
* @param $response
* @param $handler
* @param WP_REST_Request $request
* @return mixed|WP_Error
*
* Hook into REST API requests
*/
function authorize_rest_api_requests( $response, $handler, WP_REST_Request $request ) {
// allowed routes, whitelist option?
// $routes = array(
// '/wp/v2/csp etc',
// );
// Check if authorization header is set
if ( ! $request->get_header( 'authorization' ) ) {
return new WP_Error( 'authorization', 'Unauthorized access.', array( 'status' => 401 ) );
}
// if ( rsssl_get_networkwide_option('rsssl_restrict_rest_api') === 'restrict-roles' ) {
// Check for certain role and allowed route
if ( ! in_array( 'administrator', wp_get_current_user()->roles ) ) {
return new WP_Error( 'forbidden', 'Access forbidden.', array( 'status' => 403 ) );
}
// }
// if ( rsssl_get_networkwide_option('rsssl_restrict_rest_api') === 'logged-in-users' ) {
if ( ! is_user_logged_in() ) {
return new WP_Error( 'forbidden', 'Access forbidden to non-logged in users.', array( 'status' => 403 ) );
}
// }
// if ( rsssl_get_networkwide_option('rsssl_restrict_rest_api') === 'application-passwords' ) {
if ( ! is_user_logged_in() ) {
return new WP_Error( 'forbidden', 'Access forbidden to non-logged in users.', array( 'status' => 403 ) );
}
// }
return $response;
}
/**
* @return void
* Disable REST API
*/
function rsssl_disable_rest_api() {
add_filter('json_enabled', '__return_false');
add_filter('json_jsonp_enabled', '__return_false');
}
add_filter( 'rest_request_before_callbacks', 'authorize_rest_api_requests', 10, 3 );

View File

@@ -0,0 +1,59 @@
#two-factor-qr-code {
display: flex; /* Enables Flexbox */
justify-content: left; /* Centers horizontally */
align-items: center; /* Centers vertically */
width: 100%;
min-height: 100%;
}
#qr-code-container {
margin-bottom: 20px;
position: relative;
text-align: center;
//right: 0;
}
#two-factor-totp-authcode {
width: 100%;
}
tr.rsssl_verify_email {
display: none;
}
.error {
color: red;
margin-top: -5px;
}
span.rsssl-backup-codes {
padding: 5px;
background: #fbebed;
border-radius: 8px;
box-shadow: rgba(0,0,0,0.1) 0 4px 6px -1px;
}
.input {
margin-bottom: 5px !important;
}
#totp-key {
cursor: pointer;
display: flex; /* Enables Flexbox */
justify-content: center; /* Centers horizontally */
align-items: center; /* Centers vertically */
}
table.rsssl-table-two-fa {
padding-bottom: 20px;
}
.rsssl-methods-tag {
padding: 2px 5px;
border: 1px solid #000;
color: #000;
margin-left: 5px;
background: dimgrey;
&.active {
background: darkgreen;
color: #fff;
}
}

View File

@@ -0,0 +1,98 @@
/* Style radio inputs */
.radio-input {
position: absolute;
right: 0;
margin-left: 10px; /* Adjust this value to your preferred spacing */
vertical-align: middle;
top: 5px;
}
/* Style radio labels */
.radio-label {
display: inline-block;
vertical-align: middle;
width: 100%;
position: relative;
margin: 20px 0;
}
.badge {
margin-left: 10px;
padding: 2px 4px;
}
.badge-default {
background-color: #e5e5e5;
color: black;
}
.badge-enabled {
background-color: #fbc43e;
color: black;
}
/**
* The following styles are for the onboarding form
*/
#two_fa_onboarding_form {
margin-top: 20px;
}
#two_fa_onboarding_form div {
transition: height 0.5s;
}
#skip_onboarding {
margin-right: 20px;
}
.skip_container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
a {
text-decoration: none;
}
}
.totp-submit {
margin-top: 10px;
}
div.rsssl_step_one_onboarding {
display: block;
}
div.rsssl_step_two_onboarding {
display: none;
}
div.rsssl_step_three_onboarding {
margin-top: 10px;
display: none;
}
#two-factor-qr-code {
display: flex; /* Enables Flexbox */
justify-content: center; /* Centers horizontally */
align-items: center; /* Centers vertically */
min-width: 205px;
min-height: 205px;
}
.error {
color: red;
margin-top: -5px;
}
.input {
margin-bottom: 5px !important;
}
#totp-key {
cursor: pointer;
display: flex; /* Enables Flexbox */
justify-content: center; /* Centers horizontally */
align-items: center; /* Centers vertically */
}

View File

@@ -0,0 +1,2 @@
@import "profile-settings.scss";
@import "two-fa-onboarding.scss";

View File

@@ -0,0 +1,119 @@
class BaseAuth {
constructor(root, settings) {
this.root = root;
this.settings = settings;
this.translatableStrings = {
keyCopied: this.settings.translatables.keyCopied,
// ... add more strings as needed
};
}
getElement = (id) => document.getElementById(id);
getCheckedInputValue = (name) => document.querySelector(`input[name="${name}"]:checked`).value;
/**
* Performs a fetch operation.
*
* @param {string} urlExtension - The URL extension to perform the fetch operation on.
* @param {Object} data - The data to be sent in the fetch operation.
* @param {string} [method='POST'] - The HTTP method to be used in the fetch operation. Defaults to 'POST'.
* @returns {Promise} - A Promise that resolves with the response of the fetch operation.
*/
performFetchOp = (urlExtension, data, method = 'POST') => {
let url = this.root + urlExtension;
let fetchParams = {
method: method,
headers: {'Content-Type': 'application/json',},
};
if (method === 'POST') {
fetchParams.body = JSON.stringify(data);
}
return fetch(url, fetchParams);
};
assignClickListener = (id, callback) => {
const element = this.getElement(id);
if (element) {
element.addEventListener('click', function (e) {
e.preventDefault();
callback();
});
}
}
logFetchError = (error) => console.error('There has been a problem with your fetch operation:', error);
/**
* Generates a QR code for Two-Factor Authentication using the TOTP URL.
* If the TOTP URL is not available, nothing will be generated.
*
* @function qr_generator
* @returns {void} Nothing is returned.
*/
qr_generator = () => {
const totp_url = this.settings.totp_data.totp_url;
if (!totp_url) {
return;
}
let qr = qrcode(0, 'L');
qr.addData(totp_url);
qr.make();
let qrElem = document.querySelector('#two-factor-qr-code a');
if (qrElem != null) {
qrElem.innerHTML = qr.createSvgTag(5);
}
};
/**
* Downloads backup codes as a text file.
*
* @function download_codes
*/
download_codes = () => {
let TextToCode = this.settings.totp_data.backup_codes;
let TextToCodeString = '';
TextToCode.forEach(function (item) {
TextToCodeString += item + '\n';
});
let downloadLink = document.createElement('a');
downloadLink.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(TextToCodeString));
downloadLink.setAttribute('download', 'backup_codes.txt');
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
};
/**
* This function copies the text from the `totp_data.key` property of the `settings` object
* using the Clipboard API. It then shows a success message and reverts back to the original display
* after a specified timeout.
*
* @function copyTextAndShowMessage
* @memberof BaseAuth
*/
copyTextAndShowMessage = () => {
let text = this.settings.totp_data.key; // Get the text to be copied
// Use Clipboard API to copy the text
navigator.clipboard.writeText(text).then(() => {
// Change the display of the key
let originalText = this.getElement('totp-key').innerText;
this.getElement('totp-key').innerText = this.translatableStrings.keyCopied;
this.getElement('totp-key').style.color = 'green';
// Revert back to original text after a timeout
setTimeout(() => {
this.getElement('totp-key').innerText = originalText;
this.getElement('totp-key').style.color = ''; // Reset the color
}, 2000); // Adjust timeout as needed
}, function (err) {
console.error(this.settings.translatables.keyCopiedFailed, err);
});
}
}

View File

@@ -0,0 +1,51 @@
/**
* The Global rsssl_onboard object is defined in the PHP file that enqueues this script.
* @global rsssl_onboard
* It contains the following properties:
* @typedef {Object} rsssl_onboard
* @property {string} root - The root URL of the site.
* @property {string} redirect_to - The URL to redirect to after the onboarding process is complete.
* @property {string} user_id - The ID of the user.
* @property {string} login_nonce - The nonce for the login.
* @property {string} totp_data - The data for the TOTP.
* @property {string} totp_data.totp_url - The URL for the TOTP.
* @property {string} totp_data.backup_codes - The backup codes for the TOTP.
* @property {string} totp_data.key - The key for the TOTP.
* @property {string} totp_data.authcode - The authcode for the TOTP.
* @property {string} totp_data.provider - The provider for the TOTP.
* @property {string} totp_data.redirect_to - The URL to redirect to after the TOTP process is complete.
*/
/**
* The Global rsssl_profile object is defined in the PHP file that enqueues this script.
* @global rsssl_profile
* It contains the following properties:
* @typedef {Object} rsssl_profile
* @property {string} root - The root URL of the site.
* @property {string} redirect_to - The URL to redirect to after the profile process is complete.
* @property {string} user_id - The ID of the user.
* @property {string} login_nonce - The nonce for the login.
* @property {string} totp_data - The data for the TOTP.
* @property {string} totp_data.totp_url - The URL for the TOTP.
* @property {string} totp_data.backup_codes - The backup codes for the TOTP.
* @property {string} totp_data.key - The key for the TOTP.
* @property {string} totp_data.authcode - The authcode for the TOTP.
* @property {string} totp_data.provider - The provider for the TOTP.
* @property {string} totp_data.redirect_to - The URL to redirect to after the TOTP process is complete.
* @property {string} totp_data.email - The email for the TOTP.
* @property {array} translatables - The translatable strings for the profile.
* @property {string} translatables.keyCopied - The message to display when the key is copied.
* @property {string} translatables.keyCopiedFailed - The error message to display.
*/
window.onload = function() {
if(typeof rsssl_onboard !== 'undefined') {
let onboarding = new Onboarding(rsssl_onboard.root, rsssl_onboard);
onboarding.init();
}
if (typeof rsssl_profile !== 'undefined') {
let profile = new Profile(rsssl_profile.root, rsssl_profile);
profile.init();
}
}

View File

@@ -0,0 +1,183 @@
class Onboarding extends BaseAuth {
init() {
const translatableStrings = {
keyCopied: 'Key copied',
};
let endpoints = ['do_not_ask_again', 'skip_onboarding'];
let that = this;
endpoints.forEach(endpoint => {
let endpointsElement = this.getElement(endpoint);
if (endpointsElement !== null) {
endpointsElement.addEventListener('click', (event) => { // Use arrow function here
event.preventDefault();
// we call the performFetchOp method and then log the response
this.performFetchOp(`/${endpoint}`, this.settings)
.then(response => response.json())
// We log the data and redirect to the redirect_to URL
.then(data => window
.location
.href = data.redirect_to)
// We catch any errors and log them
.catch(this.logFetchError);
});
}
});
let endpointElem = this.getElement('rsssl_continue_onboarding');
const handleClick = (event) => {
event.preventDefault();
let urlExtension = '';
let selectedProvider = this.getCheckedInputValue('preferred_method');
if (selectedProvider === 'email') {
let data = {
provider: selectedProvider,
redirect_to: this.settings.redirect_to,
user_id: this.settings.user_id,
login_nonce: this.settings.login_nonce
};
urlExtension = '/save_default_method_email';
this.performFetchOp(urlExtension, data)
.then(response => response.json())
.then(data => {
this.getElement('rsssl_step_one_onboarding').style.display = 'none';
const validation_check = document.getElementById("rsssl_step_three_onboarding");
validation_check.style.display = "block";
// Removing the 'click' event listener from the rsssl_continue_onboarding id button
endpointElem.addEventListener('click', (event) => handleValidation(event, data));
endpointElem.removeEventListener('click', handleClick);
})
.catch(that.logFetchError);
} else if (selectedProvider === 'totp') {
// Hiding step one and showing step two
this.getElement('rsssl_step_one_onboarding').style.display = 'none';
// We hide this element
endpointElem.style.display = 'none';
this.getElement('rsssl_step_two_onboarding').style.display = 'block';
}
}
const handleValidation = async (event, data) => {
event.preventDefault();
let selectedProvider = this.getCheckedInputValue('preferred_method');
let urlExtension = '/' + data.validation_action;
let sendData = {
user_id: this.settings.user_id,
login_nonce: this.settings.login_nonce,
redirect_to: this.settings.redirect_to,
token: document.getElementById('rsssl-authcode').value,
provider: selectedProvider
};
let response;
try {
response = await this.performFetchOp(urlExtension, sendData);
} catch (err) {
console.log('Fetch Error: ', err);
}
if (response && !response.ok) {
let error = await response.json();
this.displayTwoFaOnboardingError(error.error);
}
if (response && response.ok) {
let data = await response.json();
window.location.href = data.redirect_to;
}
};
if (endpointElem !== null) {
endpointElem.addEventListener('click', handleClick);
}
let totpSubmit = this.getElement('two-factor-totp-submit');
if (totpSubmit !== null) {
totpSubmit.addEventListener('click', async (event) => {
event.preventDefault();
let authCode = document.getElementById('two-factor-totp-authcode').value;
let key = this.settings.totp_data.key;
let selectedProvider = this.getCheckedInputValue('preferred_method');
let sendData = {
'two-factor-totp-authcode': authCode,
provider: selectedProvider,
key: key,
redirect_to: this.settings.redirect_to,
user_id: this.settings.user_id,
login_nonce: this.settings.login_nonce
};
try {
let response = await this.performFetchOp('/save_default_method_totp', sendData);
if (!response.ok) {
let error = await response.json();
this.displayTwoFaOnboardingError(error.error);
} else {
let data = await response.json();
window.location.href = data.redirect_to;
}
} catch (error) {
this.logFetchError(error);
}
});
}
let resendButton = this.getElement('rsssl-two-factor-email-code-resend');
if(resendButton !== null) {
resendButton.addEventListener('click', (event) => {
event.preventDefault();
let data = {
user_id: this.settings.user_id,
login_nonce: this.settings.login_nonce,
provider: 'email'
};
this.performFetchOp('/resend_email_code', data)
.then(response => response.json())
.then(data => {
this.displayTwoFaOnboardingError(data.message);
})
.catch(this.logFetchError);
});
}
let downloadButton = this.getElement('download_codes');
downloadButton.addEventListener('click', (e) => {
e.preventDefault();
this.download_codes();
});
this.getElement('two-factor-qr-code').addEventListener('click', function (e) {
e.preventDefault();
that.copyTextAndShowMessage();
});
this.getElement('totp-key').addEventListener('click', function (e) {
e.preventDefault();
that.copyTextAndShowMessage();
});
if (document.readyState === 'complete') {
this.qr_generator();
} else {
this.qr_generator();
}
}
displayTwoFaOnboardingError(error) {
let loginForm = document.getElementById('two_fa_onboarding_form');
if (loginForm) {
let errorDiv = document.getElementById('login-message');
if(!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.id = 'login-message';
errorDiv.className = 'notice notice-error message';
loginForm.insertAdjacentElement('beforebegin', errorDiv);
}
errorDiv.innerHTML = `<p>${error}</p>`;
setTimeout(() => {
// removing the error box from the loginForm
errorDiv.remove();
}, 5000);
}
}
}

View File

@@ -0,0 +1,165 @@
class Profile extends BaseAuth {
init() {
this.assignClickListener('download_codes', this.download_codes);
this.assignClickListener('two-factor-qr-code', this.copyTextAndShowMessage);
this.assignClickListener('totp-key', this.copyTextAndShowMessage);
const qrCodeContainer = this.getElement('qr-code-container');
const enableCheckbox = this.getElement('two-factor-authentication');
const tableRowSelection = this.getElement('selection_two_fa');
const methodSelection = document.querySelectorAll('input[name="preferred_method"]');
const validationEmail = document.getElementById('rsssl_verify_email');
const change2faConfig = this.getElement('change_2fa_config');
let that = this;
if (qrCodeContainer) {
qrCodeContainer.style.display = "none";
if (!enableCheckbox.checked) {
tableRowSelection.style.display = "none";
qrCodeContainer.style.display = "none";
}
}
if(enableCheckbox) {
let parent = this;
enableCheckbox.addEventListener("change", function () {
if (this.checked) {
tableRowSelection.style.display = "table-row";
let selectedMethod = document.querySelector('input[name="preferred_method"]:checked');
if (selectedMethod && selectedMethod.value === "totp") {
qrCodeContainer.style.display = "block";
parent.qr_generator();
} else {
qrCodeContainer.style.display = "none";
}
} else {
tableRowSelection.style.display = "none";
qrCodeContainer.style.display = "none";
let selectedMethod = document.querySelector('input[name="preferred_method"]:checked');
selectedMethod.value = "none";
}
});
}
if(methodSelection.length > 0 ) {
let parent = this;
methodSelection.forEach(function (element) {
element.addEventListener("change", function () {
let selectedMethod = document.querySelector('input[name="preferred_method"]:checked').value;
if (selectedMethod === "totp") {
if(validationEmail) {
validationEmail.style.display = "none";
}
qrCodeContainer.style.display = "block";
parent.qr_generator();
} else if(selectedMethod === "email") {
qrCodeContainer.style.display = "none";
if(validationEmail) {
validationEmail.style.display = "table-row";
}
let data = {
action: 'change_method_to_email',
provider: selectedMethod,
user_id: rsssl_profile.user_id,
login_nonce: document.getElementById('rsssl_two_fa_nonce').value,
redirect_to: rsssl_profile.redirect_to,
profile: true
};
fetch(rsssl_profile.ajax_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: new URLSearchParams(data)
})
.then(response => response.json())
.then(responseData => {
// Expected structure: { success: true, data: { message: "Verification code sent", token: ... } }
let errorDiv = document.getElementById('login-message');
let inPutField = document.getElementById('rsssl-two-factor-email-code');
if (inPutField) {
if (!errorDiv) {
errorDiv = document.createElement('p');
errorDiv.classList.add('notice', 'notice-success');
inPutField.insertAdjacentElement('afterend', errorDiv);
}
// Use the message returned from your PHP callback
if (responseData.data.message) {
errorDiv.innerHTML = `<p>${responseData.data.message}</p>`;
} else {
console.error('No message returned from the server.');
}
// Optionally, do something with responseData.data.token if needed.
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
})
.catch(that.logFetchError);
} else {
qrCodeContainer.style.display = "none";
}
});
});
}
let resendButton = this.getElement('rsssl_resend_code_action');
if(resendButton !== null) {
resendButton.addEventListener('click', (event) => {
event.preventDefault();
let data = {
action: 'resend_email_code_profile',
user_id: this.settings.user_id,
login_nonce: document.getElementById('rsssl_two_fa_nonce').value,
provider: 'email',
profile: true
};
let ajaxUrl = rsssl_profile.ajax_url;
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: new URLSearchParams(data)
})
.then(response => response.json())
.then(responseData => {
// responseData will have the structure: { success: true, data: { message: "..." } }
let errorDiv = document.getElementById('login-message');
let inPutField = document.getElementById('rsssl-two-factor-email-code');
if (inPutField) {
if (!errorDiv) {
errorDiv = document.createElement('p');
errorDiv.classList.add('notice', 'notice-success');
inPutField.insertAdjacentElement('afterend', errorDiv);
}
errorDiv.innerHTML = `<p>${responseData.data.message}</p>`;
// Fade out the message after 5 seconds.
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
})
.catch(this.logFetchError);
});
}
if (change2faConfig) {
change2faConfig.addEventListener('click', function (e) {
e.preventDefault();
let inputField = document.createElement('input');
inputField.setAttribute('type', 'hidden');
inputField.setAttribute('name', 'change_2fa_config_field');
inputField.setAttribute('value', 'true');
document.getElementById('change_2fa_config').insertAdjacentElement('afterend', inputField);
// we uncheck Enable Two-Factor Authentication
let enableCheckbox = document.getElementById("two-factor-authentication");
enableCheckbox.checked = false;
let profileForm = document.getElementById('your-profile');
if (profileForm) {
profileForm.requestSubmit();
}
});
}
}
}

View File

@@ -0,0 +1,178 @@
<?php
/**
* Holds the request parameters for a specific action.
* This class holds the request parameters for a specific action.
* It is used to store the parameters and pass them to the functions.
*
* @package REALLY_SIMPLE_SSL
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use WP_User;
/**
* Holds the request parameters for a specific action.
* This class holds the request parameters for a specific action.
* It is used to store the parameters and pass them to the functions.
*
* @package REALLY_SIMPLE_SSL
*/
class Rsssl_Parameter_Validation
{
/**
* Validates a user ID.
*
* @param int $user_id The user ID to be validated.
*
* @return void
*/
public static function validate_user_id(int $user_id): void
{
if (!is_numeric($user_id)) {
// Create an error message for the profile page.
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('The user ID is not valid.', 'really-simple-ssl')
);
}
}
/**
* Validates post data.
*
* @param array $post_data The post data to validate.
*
* @return void
*/
public static function validate_post_data(array $post_data): void
{
if (!isset($post_data['preferred_method'])) {
// Create an error message for the profile page.
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('The preferred method is not set.', 'really-simple-ssl')
);
}
}
/**
* Validate user object.
*
* @param mixed $user The user object to validate.
*
* @return void
*/
public static function validate_user($user): void
{
if (!$user instanceof WP_User) {
// Create an error message for the profile page.
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('The user object is not valid.', 'really-simple-ssl')
);
}
}
/**
* Validates the selected provider.
*
* @param string $selected_provider The selected provider to validate.
*
* @return void
*/
public static function validate_selected_provider(string $selected_provider): void
{
if (!in_array($selected_provider, array('totp', 'email', 'none'), true)) {
// Create an error message for the profile page.
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('The selected provider is not valid.', 'really-simple-ssl')
);
}
}
/**
* Validates an authentication code.
*
* @param mixed $auth_code The authentication code to validate.
*
* @return void
*/
public static function validate_auth_code($auth_code): void
{
if (!is_numeric($auth_code)) {
// Create an error message for the profile page.
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('The authentication code is not valid.', 'really-simple-ssl')
);
}
}
/**
* Validates a given key.
*
* @param mixed $key The key to validate.
*
* @return void
*/
public static function validate_key($key): void
{
if (!is_string($key)) {
// Create an error message for the profile page.
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('The key is not valid.', 'really-simple-ssl')
);
}
}
/**
* Cache the current errors for a user in a transient.
*
* @param int $user_id The ID of the user.
*
* @return void
*/
public static function cache_errors(int $user_id): void
{
// Put the current errors in a transient.
set_transient('rsssl_two_factor_auth_error_' . $user_id, get_settings_errors(), 60);
}
/**
* Retrieves cached errors for a specific user.
*
* @param int $user_id The ID of the user to retrieve the errors for.
*
* @return mixed|null An array of errors if found, null otherwise.
*/
public static function get_cached_errors(int $user_id)
{
// Get the errors from the transient.
$errors = get_transient('rsssl_two_factor_auth_error_' . $user_id);
// Delete the transient.
delete_transient('rsssl_two_factor_auth_error_' . $user_id);
return $errors;
}
/**
* Deletes cached errors for a specific user.
*
* @param int $user_id The ID of the user to delete the errors for.
*
* @return void
*/
public static function delete_cached_errors(int $user_id): void
{
delete_transient('rsssl_two_factor_auth_error_' . $user_id);
}
}

View File

@@ -0,0 +1,162 @@
<?php
/**
* This file contains the Rsssl_Provider_Loader class.
* This class is responsible for loading and managing Two-Factor authentication providers.
*
* @package RSSSL\Pro\Security\WordPress\Two_Fa
* @subpackage Providers
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use Exception;
use WP_User;
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Totp;
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Email;
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Status;
/**
* Class Rsssl_Provider_Loader
*
* This class is responsible for loading and managing Two-Factor authentication providers.
*
* @package RSSSL\Pro\Security\WordPress\Two_Fa
* @subpackage Providers
*/
class Rsssl_Provider_Loader {
public const METHODS = array( 'totp', 'email' ); // This is a list of all available methods.
/**
* For each provider, include it and then instantiate it.
*
* @return array
* @since 0.1-dev
*/
public static function get_providers(): array {
$providers = array(
Rsssl_Two_Factor_Email::class => __DIR__ . '/class-rsssl-two-factor-email.php',
Rsssl_Two_Factor_Totp::class => __DIR__ . '/class-rsssl-two-factor-totp.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( 'rsssl_two_factor_providers', $providers );
/**
* 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 Optional. User ID, or WP_User object of the user. Defaults to current user.
*
* @return array
*/
public static function get_enabled_providers_for_user( WP_User $user ): array {
$enabled_providers = self::get_user_enabled_providers( $user );
$statuses = Rsssl_Two_Fa_Status::get_user_two_fa_status( $user );
$forced_providers = array();
foreach ( $statuses as $method => $status ) {
/**
* Check if the provider is forced for the user.
*
* @var Rsssl_Two_Factor_Provider_Interface $provider_class
*/
$provider_class = 'RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_' . ucfirst( $method );
if ( in_array( $status, array( 'active', 'open', 'disabled' ), true ) && $provider_class::is_enabled( $user ) ) {
$forced_providers[] = $provider_class;
}
}
if ( ! empty( $forced_providers ) ) {
$enabled_providers = $forced_providers;
} else {
foreach ( $enabled_providers as $key => $enabled_provider ) {
/**
* Check if the provider is optional for the user.
*
* @var Rsssl_Two_Factor_Provider_Interface $enabled_provider
*/
if ( ! $enabled_provider::is_optional( $user ) ) {
unset( $enabled_providers[ $key ] );
}
}
}
return $enabled_providers;
}
/**
* This isn't currently set anywhere, but allows to add more providers in the future.
*
* @param WP_User $user The user to check.
*
* @return array|string[]
*/
public static function get_user_enabled_providers( WP_User $user ): array {
$enabled_providers = array();
if ( true === Rsssl_Two_Factor_Totp::is_enabled( $user ) ) {
$enabled_providers[] = Rsssl_Two_Factor_Totp::class;
}
if ( true === Rsssl_Two_Factor_Email::is_enabled( $user ) ) {
$enabled_providers[] = Rsssl_Two_Factor_Email::class;
}
return $enabled_providers;
}
/**
* Get the enabled providers for the user's roles.
*
* @param WP_User $user The user object.
*
* @return array The enabled providers.
*/
public static function get_enabled_providers_for_roles( WP_User $user ): array {
// First get all the providers that are enabled for the user's role.
$totp = Rsssl_Two_Factor_Totp::is_enabled( $user );
$email = Rsssl_Two_Factor_Email::is_enabled( $user );
// Put the enabled providers in an array.
$enabled_providers = array();
if ( $totp ) {
$enabled_providers[] = 'totp';
}
if ( $email ) {
$enabled_providers[] = 'email';
}
// Return the enabled providers.
return $enabled_providers;
}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* Holds the request parameters for a specific action.
*
* @package REALLY_SIMPLE_SSL
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use WP_REST_Request;
/**
* Holds the request parameters for a specific action.
* This class holds the request parameters for a specific action.
* It is used to store the parameters and pass them to the functions.
*
* @package REALLY_SIMPLE_SSL
*/
class Rsssl_Request_Parameters {
/**
* User ID.
*
* @var integer
*/
public $user_id;
/**
* Login nonce.
*
* @var string
*/
public $login_nonce;
/**
* User.
*
* @var WP_User
*/
public $user;
/**
* Service provider.
*
* @var object
*/
public $provider;
/**
* Redirect to URL.
*
* @var string
*/
public $redirect_to;
/**
* The code.
*
* @var string
*/
public $code;
/**
* The key.
*
* @var string
*/
public $key;
/**
* The nonce.
*
* @var mixed|null
*/
public $nonce;
/**
* @var array|string
*/
public $token;
/**
* @var bool
*/
public $profile;
/**
* Constructor for the class.
*
* @param WP_REST_Request $request The WordPress REST request object.
*
* @return void
*/
public function __construct( WP_REST_Request $request ) {
$this->user_id = $request->get_param( 'user_id' );
$this->login_nonce = $request->get_param( 'login_nonce' );
$this->nonce = $request->get_header( 'X-WP-Nonce' );
$this->user = get_user_by( 'id', $this->user_id );
$this->provider = $request->get_param( 'provider' );
$this->redirect_to = $request->get_param( 'redirect_to' )?? admin_url();
if ( 'totp' === $this->provider ) {
$this->code = wp_unslash( $request->get_param( 'two-factor-totp-authcode' ) );
$this->key = wp_unslash( $request->get_param( 'key' ) );
}
if ('email' === $this->provider) {
$this->token = wp_unslash($request->get_param('token'));
$this->profile = wp_unslash($request->get_param('profile')?? false);
}
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* Two-Factor Authentication.
*
* @package REALLY_SIMPLE_SSL
*
* @since 0.1-dev
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use Exception;
/**
* Class Rsssl_Two_Fa_Authentication
*
* Represents the two-factor authentication functionality.
*/
class Rsssl_Two_Fa_Authentication {
/**
* The user meta nonce key.
*
* @type string
*/
public const RSSSL_USER_META_NONCE_KEY = '_rsssl_two_factor_nonce';
/**
* Verify a login nonce for a user.
*
* @param int $user_id The ID of the user.
* @param string $nonce The login nonce to verify.
*
* @return bool True if the nonce is valid and has not expired, false otherwise.
*/
public static function verify_login_nonce( int $user_id, string $nonce ): bool {
$login_nonce = get_user_meta( $user_id, self::RSSSL_USER_META_NONCE_KEY, true );
if ( ! $login_nonce || empty( $login_nonce['rsssl_key'] ) || empty( $login_nonce['rsssl_expiration'] ) ) {
return false;
}
$unverified_nonce = array(
'rsssl_user_id' => $user_id,
'rsssl_expiration' => $login_nonce['rsssl_expiration'],
'rsssl_key' => $nonce,
);
$unverified_hash = self::hash_login_nonce( $unverified_nonce );
$hashes_match = $unverified_hash && hash_equals( $login_nonce['rsssl_key'], $unverified_hash );
if ( $hashes_match && time() < $login_nonce['rsssl_expiration'] ) {
return true;
}
// Require a fresh nonce if verification fails.
self::delete_login_nonce( $user_id );
return false;
}
/**
* Create a login nonce for a user.
*
* @param int $user_id The ID of the user.
*
* @return array|false The login nonce array if successfully created and stored, false otherwise.
*/
public static function create_login_nonce( int $user_id ) {
$login_nonce = array(
'rsssl_user_id' => $user_id,
'rsssl_expiration' => time() + ( 15 * MINUTE_IN_SECONDS ),
);
try {
$login_nonce['rsssl_key'] = bin2hex( random_bytes( 32 ) );
} catch ( Exception $ex ) {
$login_nonce['rsssl_key'] = wp_hash( $user_id . wp_rand() . microtime(), 'nonce' );
}
// Store the nonce hashed to avoid leaking it via database access.
$hashed_key = self::hash_login_nonce( $login_nonce );
if ( $hashed_key ) {
$login_nonce_stored = array(
'rsssl_expiration' => $login_nonce['rsssl_expiration'],
'rsssl_key' => $hashed_key,
);
if ( update_user_meta( $user_id, self::RSSSL_USER_META_NONCE_KEY, $login_nonce_stored ) ) {
return $login_nonce;
}
}
return false;
}
/**
* Delete the login nonce.
*
* @param int $user_id User ID.
*
* @return bool
* @since 0.1-dev
*/
public static function delete_login_nonce( int $user_id ): bool {
return delete_user_meta( $user_id, self::RSSSL_USER_META_NONCE_KEY );
}
/**
* Get the hash of a nonce for storage and comparison.
*
* @param array $nonce Nonce array to be hashed. ⚠️ This must contain user ID and expiration,
* to guarantee the nonce only works for the intended user during the
* intended time window.
*
* @return string|false
*/
protected static function hash_login_nonce( array $nonce ) {
$message = wp_json_encode( $nonce );
if ( ! $message ) {
return false;
}
return wp_hash( $message, 'nonce' );
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* Two-Factor Authentication Data Parameters helper.
*
* @package REALLY_SIMPLE_SSL
* @since 0.1-dev
*/
namespace RSSSL\Security\WordPress\Two_Fa;
/**
* Class Rsssl_Two_FA_Data_Parameters
*
* Represents the data parameters for the Two FA data.
*
* @package REALLY_SIMPLE_SSL
*/
class Rsssl_Two_FA_Data_Parameters {
/**
* The current page name.
*
* @var string $page The current page name.
*/
public $page;
/**
* The number of items to display per page.
*
* @var int $page_size The number of items to display per page.
*/
public $page_size;
/**
* The search term entered by the user.
*
* @var string $search_term The search term entered by the user
*/
public $search_term;
/**
* The value used for filtering.
*
* @var string|null $filter_value This variable stores the value used for filtering.
*/
public $filter_value;
/**
* The column used for filtering.
*
* @var string|null $filter_column This variable stores the column used for filtering.
*/
public $filter_column;
/**
* The column used for sorting.
*
* @var string|null $sort_column This variable stores the column used for sorting.
*/
public $sort_column;
/**
* The direction of the sorting, can be 'asc' or 'desc'.
*
* @var string $sort_direction The direction of the sorting, can be 'asc' or 'desc'
*/
public $sort_direction;
/**
* The HTTP method used for the current request, can be 'GET', 'POST', 'PUT', 'DELETE', etc.
*
* @var string $method The HTTP method used for the current request, can be 'GET', 'POST', 'PUT', 'DELETE', etc.
*/
public $method;
/**
* The allowed filters.
*
* @var array $allowed_filters The allowed filters.
*/
private const allowed_filters = array( 'all', 'open', 'disabled', 'active', 'expired' );
/**
* Constructs a new object with given data.
*
* @param array $data The data array.
*/
public function __construct( array $data ) {
$this->page = isset( $data['currentPage'] ) ? (int) $data['currentPage'] : 1;
$this->page_size = isset( $data['currentRowsPerPage'] ) ? (int) $data['currentRowsPerPage'] : 5;
$this->search_term = isset( $data['search'] ) ? sanitize_text_field( $data['search'] ) : '';
$this->filter_value = in_array( $data['filterValue'] ?? 'all', self::allowed_filters, true ) ? sanitize_text_field( $data['filterValue'] ?? 'all') : 'all';
$this->sort_direction = in_array( strtoupper( $data['sortDirection'] ?? 'DESC' ), array( 'ASC', 'DESC' ), true ) ? strtoupper( sanitize_text_field( $data['sortDirection'] ?? 'DESC')) : 'DESC';
$this->filter_column = isset( $data['filterColumn'] ) ? sanitize_text_field( $data['filterColumn'] ) : 'rsssl_two_fa_status';
$this->sort_column = isset( $data['sortColumn'] ) ? sanitize_text_field( $data['sortColumn'] ) : 'user';
$this->method = isset( $data['method'] ) ? Rsssl_Two_Factor_Settings::sanitize_method( $data['method'] ) : 'email';
}
}

View File

@@ -0,0 +1,124 @@
<?php
/**
* Two-Factor Authentication.
* Status class.
*
* @package REALLY_SIMPLE_SSL
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use RSSSL\Security\WordPress\Two_Fa\Traits\Rsssl_Two_Fa_Helper;
use WP_User;
/**
* Class Rsssl_Two_Fa_Status
*
* Represents the two-factor authentication status.
*
* @package REALLY_SIMPLE_SSL
*/
class Rsssl_Two_Fa_Status {
use Rsssl_Two_Fa_Helper;
public const STATUSES = array( 'disabled', 'open', 'active' ); // This is a list of all available statuses.
/**
* Get the status of two-factor authentication for a user.
*
* @param WP_User|null $user (optional) The user for which to retrieve the status. Defaults to current user.
*
* @return array An associative array where the method names are the keys and the status values are the values.
* The status can be one of the following: 'disabled' if the method is disabled for the user,
* 'enabled' if the method is enabled for the user, or 'unknown' if the status could not be determined.
*/
public static function get_user_two_fa_status( $user = null ): array {
$methods = Rsssl_Provider_Loader::METHODS; // Assume this function returns all available methods.
$statuses = array();
foreach ( $methods as $method ) {
$status = self::get_user_status( $method, $user->ID );
$statuses[ $method ] = $status ? $status : 'disabled';
}
return $statuses;
}
/**
* Get the user's two-factor authentication status.
*
* @param string $method The authentication method used by the user.
* @param int $user_id The ID of the user.
*
* @return string The user's two-factor authentication status (enabled or disabled).
*/
public static function get_user_status( string $method, int $user_id ): string {
$activated = $method === 'email' ? '_email' : '_' . self::sanitize_method( $method );
// Check the roles per method if they are enabled.
$enabled_roles = rsssl_get_option( 'two_fa_enabled_roles'.$activated, array());
if ( empty( $enabled_roles ) && self::is_user_role_enabled( $user_id, $enabled_roles )) {
return 'disabled';
}
$status = get_user_meta( $user_id, "rsssl_two_fa_status_$method", true );
return self::sanitize_status( $status );
}
/**
* Delete two-factor authentication metadata for a user.
*
* @param WP_User $user The user object for whom to delete the metadata.
*
* @return void
*/
public static function delete_two_fa_meta( $user ): void {
if( is_object($user) ){
$user = $user->ID;
}
delete_user_meta( $user, '_rsssl_two_factor_totp_last_successful_login' );
delete_user_meta( $user, '_rsssl_two_factor_nonce' );
delete_user_meta( $user, 'rsssl_two_fa_status' );
delete_user_meta( $user, 'rsssl_two_fa_status_email' );
delete_user_meta( $user, 'rsssl_two_fa_status_totp' );
delete_user_meta( $user, '_rsssl_two_factor_totp_key' );
delete_user_meta( $user, '_rsssl_two_factor_backup_codes' );
delete_user_meta( $user, 'rsssl_activation_date' );
delete_user_meta( $user, 'rsssl_two_fa_last_login' );
delete_user_meta( $user, 'rsssl_two_fa_skip_token' );
delete_user_meta( $user, '_rsssl_factor_email_token_timestamp' );
delete_user_meta( $user, '_rsssl_factor_email_token' );
delete_user_meta( $user, 'rsssl_two_fa_reminder_sent' );
}
/**
* Checks if a user has any of the enabled roles.
*
* @param int $user_id The user ID.
* @param array $enabled_roles The enabled roles to check against.
*
* @return bool Returns true if the user has any of the enabled roles, false otherwise.
*/
private static function is_user_role_enabled( int $user_id, array $enabled_roles ):bool {
$user = get_userdata( $user_id );
if ( ! $user ) {
return false;
}
$user_roles = $user->roles;
foreach ( $user_roles as $role ) {
if ( in_array( $role, $enabled_roles, true ) ) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,634 @@
<?php
/**
* This file contains the Rsssl_Two_Factor_Admin class.
*
* The Rsssl_Two_Factor_Admin class is responsible for handling the administrative
* aspects of the two-factor authentication feature in the Really Simple SSL plugin.
* It includes methods for displaying the two-factor authentication settings in the
* admin area, handling user input, and managing user roles and capabilities related
* to two-factor authentication.
*
* PHP version 7.2
*
* @category Security
* @package Really_Simple_SSL
* @author Really Simple SSL
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use WP_User;
/**
* The Rsssl_Two_Factor_Admin class is responsible for handling the administrative
* aspects of the two-factor authentication feature in the Really Simple SSL plugin.
* It includes methods for displaying the two-factor authentication settings in the
* admin area, handling user input, and managing user roles and capabilities related
* to two-factor authentication.
*
* @category Security
* @package Really_Simple_SSL
* @subpackage Two_Factor
*/
class Rsssl_Two_Factor_Admin
{
/**
* The Rsssl_Two_Factor_Admin instance.
*
* @var Rsssl_Two_Factor_Settings $instance The settings object.
*/
private static $instance;
/**
* The constructor.
*
* @return void
*/
public function __construct()
{
// if the user is not logged in, it don't need to do anything.
if (!rsssl_admin_logged_in()) {
return;
}
if (isset(self::$instance)) {
wp_die();
}
self::$instance = $this;
add_filter('rsssl_do_action', array($this, 'two_fa_table'), 10, 3);
add_filter('rsssl_after_save_field', array($this, 'maybe_reset_two_fa'), 20, 2);
add_filter('rsssl_after_save_field', array($this, 'change_disabled_users_when_forced'), 20, 3);
add_action('process_user_batch_event', [$this, 'process_user_batch'], 10 , 5);
}
/**
* Handles server-side processing for two-factor authentication data.
*
* @param Rsssl_Two_FA_Data_Parameters $data_parameters The data parameters for the request.
*
* @return array The response array containing the request success status, data, total records, and executed query.
*/
private function server_side_handler(Rsssl_Two_FA_Data_Parameters $data_parameters): array {
global $wpdb;
$days_threshold = rsssl_get_option('two_fa_grace_period', 30);
$filter_value = $data_parameters->filter_value;
$enabled_roles = array_unique(array_merge(
defined('rsssl_pro') ? rsssl_get_option('two_fa_enabled_roles_totp', array()) : array(),
rsssl_get_option('two_fa_enabled_roles_email', array())
));
$forced_roles = rsssl_get_option('two_fa_forced_roles', array());
$fields = ['id', 'user', 'status_for_user', 'rsssl_two_fa_providers', 'user_role']; // Example fields
$enabled_roles_placeholders = implode(',', array_map(function($role) { return "'$role'"; }, $enabled_roles));
$forced_roles_placeholder = implode(',', array_map(function($role) { return "'$role'"; }, $forced_roles));
$query = self::generate_query($fields, $enabled_roles_placeholders, $forced_roles_placeholder, $forced_roles);
if ($filter_value !== 'all') {
$query .= $wpdb->prepare(" HAVING status_for_user = %s", $filter_value);
}
$prepared_query = $wpdb->prepare($query, array_merge(
// Use array_map to generate the thresholds for each forced role
array_fill(0, count($forced_roles), $days_threshold)
));
// only execute query if there are enabled roles to show
if (empty($enabled_roles)) {
return array(
'request_success' => true,
'data' => [],
'totalRecords' => 0,
);
}
$results = $wpdb->get_results($prepared_query);
return array(
'request_success' => true,
'data' => is_array($results) ? array_values($results) : [],
'totalRecords' => is_array($results) ? count($results) : 0,
// 'executed_query' => $prepared_query,
);
}
/**
* Generates the SELECT clause for the SQL query.
*
* @param array $fields The fields to include in the SELECT clause.
* @return string The generated SELECT clause.
*/
public static function generate_select_clause(array $fields, array $forced_roles): string
{
$select_parts = [];
if ( in_array( 'id', $fields, true ) ) {
$select_parts[] = 'DISTINCT (u.ID) as id';
}
if ( in_array( 'user', $fields, true ) ) {
$select_parts[] = 'u.user_login as user';
}
// Status for User Field
if (in_array('status_for_user', $fields, true)) {
// Create placeholders for forced roles
$forced_roles_placeholders = implode(',', array_fill(0, count($forced_roles), '%s'));
// Check if forced_roles is empty or not
if (empty($forced_roles_placeholders)) {
// No forced roles, basic status handling
$select_parts[] = "
CASE
WHEN COALESCE(um_totp.meta_value, 'open') = 'open' OR COALESCE(um_email.meta_value, 'open') = 'open' THEN 'open'
WHEN COALESCE(um_totp.meta_value, 'disabled') = 'active' OR COALESCE(um_email.meta_value, 'disabled') = 'active' THEN 'active'
ELSE COALESCE(um_totp.meta_value, um_email.meta_value)
END AS status_for_user
";
} else {
// Initialize the CASE statement parts for status_for_user
$status_cases = [];
// First condition: Check if TOTP or Email is active (this is common for all roles)
$status_cases[] = "WHEN COALESCE(um_totp.meta_value, 'disabled') = 'active' OR COALESCE(um_email.meta_value, 'disabled') = 'active' THEN 'active'";
// Loop through forced roles and apply expiration logic
foreach ($forced_roles as $role) {
// Check if an expiration threshold is defined for the current role
$status_cases[] = "WHEN SUBSTRING_INDEX(SUBSTRING_INDEX(ur.meta_value, '\"', 2), '\"', -1) = '$role'
AND DATEDIFF(NOW(), um_last_login.meta_value) > %d THEN 'expired'";
}
// Fallback: If no other conditions match, default to 'open'
$status_cases[] = "ELSE COALESCE(um_totp.meta_value, um_email.meta_value, 'open')";
// Combine the conditions into a CASE clause
$select_parts[] = "CASE " . implode(' ', $status_cases) . " END AS status_for_user";
}
}
if ( in_array( 'user_role', $fields, true ) ) {
$select_parts[] = "SUBSTRING_INDEX(SUBSTRING_INDEX(ur.meta_value, '\"', 2), '\"', -1) AS user_role";
}
if ( in_array( 'rsssl_two_fa_providers', $fields, true ) ) {
$select_parts[] = "
CASE
WHEN COALESCE(um_totp.meta_value, um_email.meta_value, 'open') = 'open' THEN ''
WHEN um_totp.meta_value = 'active' THEN 'totp'
WHEN um_email.meta_value = 'active' THEN 'email'
ELSE 'none'
END AS rsssl_two_fa_providers
";
}
return implode(', ', $select_parts);
}
/**
* Generates the full SQL query.
*
* @param array $fields The fields to include in the SELECT clause.
* @param string $enabled_roles_placeholders The placeholders for enabled roles.
* @param string|null $forced_roles_placeholder The placeholders for forced roles.
* @return string The generated SQL query.
*/
public static function generate_query(array $fields, string $enabled_roles_placeholders, string $forced_roles_placeholder = '', $forced_roles = array() ): string
{
global $wpdb;
$select_clause = self::generate_select_clause($fields, $forced_roles);
$where_clause = "SUBSTRING_INDEX(SUBSTRING_INDEX(ur.meta_value, '\"', 2), '\"', -1) in ($enabled_roles_placeholders)";
// if (!empty($forced_roles_placeholder)) {
// $where_clause = "SUBSTRING_INDEX(SUBSTRING_INDEX(ur.meta_value, '\"', 2), '\"', -1) in ($forced_roles_placeholder)";
// }
$sql = "
SELECT $select_clause
FROM {$wpdb->users} u
LEFT JOIN {$wpdb->usermeta} um_totp ON u.ID = um_totp.user_id AND um_totp.meta_key = 'rsssl_two_fa_status_totp'
LEFT JOIN {$wpdb->usermeta} um_email ON u.ID = um_email.user_id AND um_email.meta_key = 'rsssl_two_fa_status_email'
";
if (is_multisite()) {
$sites = get_sites();
$conditions = [];
foreach ($sites as $site) {
$conditions[] = "ur.meta_key = '{$wpdb->get_blog_prefix($site->blog_id)}capabilities'";
}
$sql .= "LEFT JOIN {$wpdb->usermeta} ur ON u.ID = ur.user_id AND (" . implode(' OR ', $conditions) . ")";
} else {
$sql .= "LEFT JOIN {$wpdb->usermeta} ur ON u.ID = ur.user_id AND ur.meta_key = '{$wpdb->base_prefix}capabilities'";
}
$sql .="LEFT JOIN {$wpdb->usermeta} la ON u.ID = la.user_id AND la.meta_key = 'rsssl_two_fa_login_action'
LEFT JOIN {$wpdb->usermeta} um_last_login ON u.ID = um_last_login.user_id AND um_last_login.meta_key = 'rsssl_two_fa_last_login'
WHERE $where_clause
";
return $sql;
}
private static function user_count(): ?string {
global $wpdb;
return $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->users");
}
/**
* Change the disabled status of users when forced.
*
* @param string $field_id The ID of the field being changed.
* @param mixed $new_value The new value of the field.
*
* @return void
*/
public function change_disabled_users_when_forced( string $field_id, $new_value, $prev_value = [] ): void
{
if ('two_fa_forced_roles' === $field_id && !empty($new_value)) {
global $wpdb;
$forced_roles = $new_value;
if (empty($prev_value)) {
$prev_value = [];
}
$added_roles = array_diff($forced_roles, $prev_value);
$forced_roles = $added_roles;
if(empty($forced_roles)) {
return;
}
// Fetching the users that have the forced roles.
$fields = ['id', 'status_for_user'];
$enabled_roles = array_unique(array_merge(
defined('rsssl_pro') ? rsssl_get_option('two_fa_enabled_roles_totp', array()) : array(),
rsssl_get_option('two_fa_enabled_roles_email', array())
));
//This line is forcefully setting the forced roles to the enabled roles. Because we only impact enforced users with this action.
$enabled_roles_placeholders = implode(',', array_map(function($role) { return "'$role'"; }, $forced_roles));
$forced_roles_placeholder = implode(',', array_map(function($role) { return "'$role'"; }, $forced_roles));
$query = self::generate_query($fields, $enabled_roles_placeholders, $forced_roles_placeholder, $forced_roles);
$batch_size = 1000;
$offset = 0;
$this->process_user_batch($query, $enabled_roles, $forced_roles, $batch_size, $offset);
}
}
/**
* Process a batch of users.
*
* @param string $query The base query to fetch users.
* @param array $enabled_roles The enabled roles.
* @param array $forced_roles The forced roles.
* @param int $batch_size The size of each batch.
* @param int $offset The offset for the current batch.
*
* @return void
*/
public function process_user_batch(string $query, array $enabled_roles, array $forced_roles, int $batch_size, int $offset): void
{
global $wpdb;
$paged_query = $query . " LIMIT %d OFFSET %d";
$forced_roles_placeholder = implode(',', $forced_roles);
$enabled_roles_placeholders = implode(',', $enabled_roles);
$prepared_query = $wpdb->prepare($paged_query, $forced_roles_placeholder, $batch_size, $offset);
$users = $wpdb->get_results($prepared_query);
if (empty($users)) {
return;
}
foreach ($users as $user) {
// if there is an active or open method, We do nothing.
if ('active' === $user->status_for_user ) {
continue;
}
if ('open' === $user->status_for_user) {
// if the user has no meta_key rsssl_two_fa_last_login, we set it to now.
if (!get_user_meta((int)$user->id, 'rsssl_two_fa_last_login', true)) {
update_user_meta((int)$user->id, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
}
continue;
}
// now we reset the user.
Rsssl_Two_Fa_Status::delete_two_fa_meta((int)$user->id);
// Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa.
update_user_meta((int)$user->id, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
}
// Schedule the next batch
wp_schedule_single_event(time() + 60, 'process_user_batch_event', [$query, $enabled_roles, $forced_roles, $batch_size, $offset + $batch_size]);
}
/**
* Checks if the user can use two-factor authentication (2FA).
*
* @return bool Returns true if the user can use 2FA, false otherwise.
*/
public function can_i_use_2fa(): bool
{
return rsssl_get_option('login_protection_enabled');
}
/**
* Creates a captcha notice array.
*
* This method creates and returns an array representing a captcha notice.
*
* @param string $title The title of the notice.
* @param string $msg The message of the notice.
*
* @return array The captcha notice array.
*/
private function create_2fa_notice( string $title, string $msg ): array {
return array(
'callback' => '_true_',
'score' => 1,
'show_with_options' => array( 'login_protection_enabled' ),
'output' => array(
'true' => array(
'title' => $title,
'msg' => $msg,
'icon' => 'warning',
'type' => 'open',
'dismissible' => true,
'admin_notice' => false,
'plusone' => true,
'highlight_field_id' => 'two_fa_enabled_roles',
),
),
);
}
/**
* If a user role is removed, it needs to reset this role for all users
*
* @param string $field_id The field ID.
* @param mixed $new_value The new value.
*
* @return void
*/
public static function maybe_reset_two_fa( string $field_id, $new_value ): void {
if ( ! rsssl_user_can_manage() ) {
return;
}
}
/**
* Reset the two-factor authentication for a user.
*
* @param array $response The response array.
* @param string $action The action being performed.
* @param array $data The data array.
*
* @return array The updated response array.
*/
public static function reset_user_two_fa( array $response, string $action, array $data ): array {
if ( ! rsssl_user_can_manage() ) {
return $response;
}
if ( 'two_fa_table' === $action ) {
// if the user has been disabled, it needs to reset the two-factor authentication.
$user = get_user_by( 'id', $data['user_id'] );
if ( $user ) {
// Delete all 2fa related user meta.
self::delete_two_fa_meta( $user );
// Set the last login to now, so the user will be forced to use 2fa.
update_user_meta( $user->ID, 'rsssl_two_fa_last_login', gmdate( 'Y-m-d H:i:s' ) );
}
}
return $response;
}
/**
* Get users based on arguments and method.
*
* @param array $args The arguments to retrieve users.
* @param string $method The method to retrieve users.
*
* @return array The list of users matching the arguments and method.
*/
protected static function get_users( array $args, string $method ): array {
if ( ! is_multisite() ) {
return get_users( $args );
}
$users = self::get_multisite_users( $args );
if( $method !== 'two_fa_forced_roles' ) {
$users = self::filter_users_by_role( $users, $args, $method );
}
return self::slice_users_by_offset_and_number( $users, $args );
}
/**
* Get all multisite users from all sites.
*
* @param array $args {
* Optional. Arguments for filtering the users.
*
* @type int $offset Offset for pagination. Default is 0.
* @type int $number Maximum number of users to retrieve. Default is 0 (retrieve all users).
* ... Additional arguments for filtering the user query.
* }
*
* @return array Array of users.
*/
private static function get_multisite_users( array $args ): array {
$sites = get_sites();
$users = array();
unset( $args['offset'], $args['number'] );
foreach ( $sites as $site ) {
switch_to_blog( $site->blog_id );
$site_users = get_users( $args );
foreach ( $site_users as $user ) {
$user_roles = get_userdata( $user->ID )->roles;
if ( ! isset( $users[ $user->ID ] ) ) {
$users[ $user->ID ] = $user;
}
$users_roles[ $user->ID ] = array_unique( $users_roles[ $user->ID ] ?? array() + $user_roles );
}
restore_current_blog();
}
return $users;
}
/**
* Filter users by role.
*
* @param array $users The array of users.
* @param array $args The array of filter arguments.
* @param string $method The method name.
*
* @return array The filtered array of users.
*/
private static function filter_users_by_role( array $users, array $args, string $method ): array {
if ( ! isset( $args['role'] ) ) {
return $users;
}
$filter_role = $args['role'];
$filter_role_is_forced = Rsssl_Two_Factor_Settings::role_is_of_type( $method, $filter_role, 'forced' );
return array_filter(
$users,
static function ( $user_id, $user_roles ) use ( $filter_role_is_forced, $method ) {
return ! ( ! $filter_role_is_forced && Rsssl_Two_Factor_Settings::contains_role_of_type( $method, (array) $user_roles, 'forced' ) );
},
ARRAY_FILTER_USE_BOTH
);
}
/**
* Slice users by offset and number.
*
* This function takes an array of users and an array of arguments
* and applies the offset and number values to the users array.
* It returns a new array with the specified offset and number of users.
*
* @param array $users The array of users.
* @param array $args The array of arguments containing the offset and number values.
*
* @return array The new array of users with the specified offset and number.
*/
private static function slice_users_by_offset_and_number( array $users, array $args ): array {
// Apply the 'offset' to the combined result.
if ( 0 !== ( $args['offset'] ?? 0 ) ) {
$users = array_slice( $users, $args['offset'] );
}
// Ensure the final result does not exceed the specified 'number'.
if ( 0 !== ( $args['number'] ?? 0 ) ) {
$users = array_slice( $users, 0, $args['number'] );
}
// To reset array keys.
return array_values( $users );
}
/**
* Generates the two-factor authentication table data based on the action and data parameters.
*
* @param array $response The initial response data.
* @param string $action The action to perform.
* @param array $data The data needed for the action.
*
* @return array The updated response data.
*/
public function two_fa_table(array $response, string $action, array $data): array
{
$new_response = $response;
if (rsssl_user_can_manage()) {
$data_parameters = new Rsssl_Two_FA_Data_Parameters($data);
switch ($action) {
case 'two_fa_table':
return $this->server_side_handler($data_parameters);
case 'two_fa_reset_user':
// if the user has been disabled, it needs to reset the two-factor authentication.
$user = get_user_by('id', $data['id']);
if ($user) {
// Delete all 2fa related user meta.
Rsssl_Two_Fa_Status::delete_two_fa_meta($user);
// Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa.
update_user_meta($user->ID, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
}
if (!$user) {
$new_response['request_success'] = false;
}
break;
default:
// Default case if no action matches.
break;
}
}
return $new_response;
}
/**
* Reset two-factor authentication for a user if the user has been disabled.
*
* @param string $method The method to reset.
* @param int $user_id The user ID.
*
* @return string[]
*/
private function check_status_and_return(string $method, int $user_id): ?array
{
$status = Rsssl_Two_Factor_Settings::get_user_status($method, $user_id);
if (in_array($status, array('active', 'open', 'disabled'), true)) {
return array($method, $status, true);
}
return null;
}
/**
* Get the status for a given user ID, by method.
*
* @param int $user_id The user ID to get the status for.
*
* @return array The status for the given user ID, by method.
*/
public function get_status_by_method(int $user_id): array
{
$user_id = absint($user_id);
if (defined('rsssl_pro') && rsssl_pro) {
$result = $this->get_status_for_method('totp', $user_id);
}
if (!isset($result)) {
$result = $this->get_status_for_method('email', $user_id);
} else {
if ($result[0] === 'empty' || 'disabled' === $result[1]) {
$result = $this->get_status_for_method('email', $user_id);
}
}
if (empty($result) || 'disabled' === $result[1]) {
$result = array('disabled', 'disabled');
}
if (empty($result)) {
$enabled_roles = Rsssl_Two_Factor_Settings::get_enabled_roles($user_id) ?? array();
$enabled_method = Rsssl_Two_Factor_Settings::get_enabled_method($user_id);
$result = empty($enabled_roles)
? array($enabled_method, 'disabled')
: array($enabled_method, 'open');
}
return $result;
}
/**
* Get the status for a given method and user ID.
*
* @param string $method The method to get the status for.
* @param int $user_id The user ID to get the status for.
*
* @return array|null The status for the given method and user ID, or null if not found.
*/
public function get_status_for_method(string $method, int $user_id): ?array
{
$role_status = Rsssl_Two_Factor_Settings::get_role_status($method, $user_id);
$user_status = Rsssl_Two_Factor_Settings::get_user_status($method, $user_id);
if ('empty' !== $role_status && 'open' === $user_status) {
$result = $this->check_status_and_return($method, $user_id);
if ('active' === $user_status) {
return $result;
}
}
return array($role_status, $user_status);
}
}

View File

@@ -0,0 +1,442 @@
<?php
/**
* Class for creating a backup codes provider.
*
* @package Two_Factor
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use WP_User;
/**
* Class for creating a backup codes provider.
*
* @since 0.1-dev
*
* @package Two_Factor
*/
class Rsssl_Two_Factor_Backup_Codes extends Rsssl_Two_Factor_Provider {
/**
* The user meta backup codes key.
*
* @type string
*/
public const BACKUP_CODES_META_KEY = '_rsssl_two_factor_backup_codes';
/**
* The number backup codes.
*
* @type int
*/
public const NUMBER_OF_CODES = 10;
/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @since 0.1-dev
*/
public static function get_instance() {
static $instance;
$class = __CLASS__;
if ( ! is_a( $instance, $class ) ) {
$instance = new $class();
}
return $instance;
}
/**
* Class constructor.
*
* @since 0.1-dev
*
* @codeCoverageIgnore
*/
protected function __construct() {
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
parent::__construct();
}
/**
* Deletes all backup codes for a user.
*
* @param int $id
*
* @return void
*/
public static function delete_backup_codes( int $id ): void {
delete_user_meta( $id, self::BACKUP_CODES_META_KEY );
}
/**
* Register the rest-api endpoints required for this provider.
*
* @codeCoverageIgnore
*/
public function register_rest_routes(): void {
register_rest_route(
Rsssl_Two_Factor::REST_NAMESPACE,
'/generate-backup-codes',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'rest_generate_codes' ),
'permission_callback' => function ( $request ) {
return current_user_can( 'edit_user', $request['user_id'] );
},
'args' => array(
'user_id' => array(
'required' => true,
'type' => 'integer',
),
'enable_provider' => array(
'required' => false,
'type' => 'boolean',
'default' => false,
),
),
)
);
}
/**
* Displays an admin notice when backup codes have run out.
*
* @since 0.1-dev
*
* @codeCoverageIgnore
*/
public function admin_notices(): void {
$user = wp_get_current_user();
// Return if the provider is not enabled.
if ( ! in_array( __CLASS__, Rsssl_Two_Factor::get_enabled_providers_for_user( $user->ID ), true ) ) {
return;
}
// Return if not out of codes.
if ( $this->is_available_for_user( $user ) ) {
return;
}
?>
<div class="error">
<p>
<span>
<?php
echo wp_kses(
sprintf(
/* translators: %s: URL for code regeneration */
__( 'Two-Factor: You are out of backup codes and need to <a href="%s">regenerate!</a>', 'really-simple-ssl' ),
esc_url( get_edit_user_link( $user->ID ) . '#two-factor-backup-codes' )
),
array( 'a' => array( 'href' => true ) )
);
?>
<span>
</p>
</div>
<?php
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label(): ?string {
return _x( 'Backup Verification Codes (Single Use)', 'Provider Label', 'really-simple-ssl' );
}
/**
* Whether this Two Factor provider is configured and codes are available for the user specified.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function is_available_for_user( $user ): bool {
// Does this user have available codes?
if ( 0 < self::codes_remaining_for_user( $user ) ) {
return true;
}
return false;
}
/**
* Inserts markup at the end of the user profile field for this provider.
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @since 0.1-dev
*/
public function user_options( WP_User $user ): void {
wp_enqueue_script( 'wp-api-request' );
wp_enqueue_script( 'jquery' );
$count = self::codes_remaining_for_user( $user );
?>
<p id="two-factor-backup-codes">
<button type="button" class="button button-two-factor-backup-codes-generate button-secondary hide-if-no-js">
<?php esc_html_e( 'Generate Verification Codes', 'really-simple-ssl' ); ?>
</button>
<span class="two-factor-backup-codes-count">
<?php
echo esc_html(
sprintf(
/* translators: %s: count */
_n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'really-simple-ssl' ),
$count
)
);
?>
</span>
</p>
<div class="two-factor-backup-codes-wrapper" style="display:none;">
<ol class="two-factor-backup-codes-unused-codes"></ol>
<p class="description"><?php esc_html_e( 'Write these down! Once you navigate away from this page, you will not be able to view these codes again.', 'really-simple-ssl' ); ?></p>
<p>
<a class="button button-two-factor-backup-codes-download button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-download-link" download="two-factor-backup-codes.txt"><?php esc_html_e( 'Download Codes', 'really-simple-ssl' ); ?></a>
<p>
</div>
<script type="text/javascript">
( function( $ ) {
$( '.button-two-factor-backup-codes-generate' ).click( function() {
wp.apiRequest( {
method: 'POST',
path: <?php echo wp_json_encode( Rsssl_Two_Factor::REST_NAMESPACE . '/generate-backup-codes' ); ?>,
data: {
user_id: <?php echo wp_json_encode( $user->ID ); ?>
}
} ).then( function( response ) {
var $codesList = $( '.two-factor-backup-codes-unused-codes' );
$( '.two-factor-backup-codes-wrapper' ).show();
$codesList.html( '' );
// Append the codes.
for ( i = 0; i < response.codes.length; i++ ) {
$codesList.append( '<li>' + response.codes[ i ] + '</li>' );
}
// Update counter.
$( '.two-factor-backup-codes-count' ).html( response.i18n.count );
$( '#two-factor-backup-codes-download-link' ).attr( 'href', response.download_link );
} );
} );
} )( jQuery );
</script>
<?php
}
/**
* Generates backup codes & updates the user meta.
*
* @param WP_User $user WP_User object of the logged-in user.
* @param array $args Optional arguments for assigning new codes.
*
* @return array
* @since 0.1-dev
*/
public static function generate_codes( WP_User $user, array $args = array() ): array {
$codes = array();
$codes_hashed = array();
// Check for arguments.
if ( isset( $args['number'] ) ) {
$num_codes = (int) $args['number'];
} else {
$num_codes = self::NUMBER_OF_CODES;
}
// Append or replace (default).
if ( isset( $args['method'] ) && 'append' === $args['method'] ) {
$codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
}
for ( $i = 0; $i < $num_codes; $i++ ) {
$code = self::get_code();
$codes_hashed[] = wp_hash_password( $code );
$codes[] = $code;
unset( $code );
}
if ( isset( $args['cached'] ) && $args['cached'] ) {
// Place the $codes in a transient for 5 minutes.
set_transient( 'rsssl_two_factor_backup_codes_' . $user->ID, $codes, 300 );
}
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $codes_hashed );
// Unhashed.
return $codes;
}
/**
* Generates Backup Codes for returning through the WordPress Rest API.
*
* @param WP_REST_Request $request The WordPress REST request object.
*
* @since 0.8.0
*/
public function rest_generate_codes( WP_REST_Request $request ) {
$user_id = $request['user_id'];
$user = get_user_by( 'id', $user_id );
// Hardcode these, the user shouldn't be able to choose them.
$args = array(
'number' => self::NUMBER_OF_CODES,
'method' => 'replace',
);
// Setup the return data.
$codes = $this->generate_codes( $user, $args );
$count = self::codes_remaining_for_user( $user );
$title = sprintf(
/* translators: %s: the site's domain */
__( 'Two-Factor Backup Codes for %s', 'really-simple-ssl' ),
home_url( '/' )
);
// Generate download content.
$download_link = 'data:application/text;charset=utf-8,';
$download_link .= rawurlencode( "{$title}\r\n\r\n" );
$i = 1;
foreach ( $codes as $code ) {
$download_link .= rawurlencode( "{$i}. {$code}\r\n" );
++$i;
}
$i18n = array(
/* translators: %s: count */
'count' => esc_html( sprintf( _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'really-simple-ssl' ), $count ) ),
);
if ( $request->get_param( 'enable_provider' ) && ! Rsssl_Two_Factor::enable_provider_for_user( $user_id, 'Two_Factor_Backup_Codes' ) ) {
return new WP_Error( 'db_error', __( 'Unable to enable Backup Codes provider for this user.', 'really-simple-ssl' ), array( 'status' => 500 ) );
}
return array(
'codes' => $codes,
'download_link' => $download_link,
'remaining' => $count,
'i18n' => $i18n,
);
}
/**
* Returns the number of unused codes for the specified user
*
* @param WP_User $user WP_User object of the logged-in user.
* @return int $int The number of unused codes remaining
*/
public static function codes_remaining_for_user( $user ) {
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
return count( $backup_codes );
}
return 0;
}
/**
* Prints the form that prompts the user to authenticate.
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @since 0.1-dev
*/
public function authentication_page( WP_User $user ) {
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<p class="two-factor-prompt"><?php esc_html_e( 'Enter a backup verification code.', 'really-simple-ssl' ); ?></p>
<p>
<label for="authcode"><?php esc_html_e( 'Verification Code:', 'really-simple-ssl' ); ?></label>
<input type="text" inputmode="numeric" name="two-factor-backup-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="1234 5678" data-digits="8" />
</p>
<?php
submit_button( __( 'Submit', 'really-simple-ssl' ) );
}
/**
* Validates the users input token.
*
* In this class just return true.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ) {
$backup_code = self::sanitize_code_from_request( 'two-factor-backup-code' );
if ( ! $backup_code ) {
return false;
}
return self::validate_code( $user, $backup_code );
}
/**
* Validates a backup code.
*
* Backup Codes are single use and are deleted upon a successful validation.
*
* @param WP_User $user WP_User object of the logged-in user.
* @param int $code The backup code.
* @return boolean
*@since 0.1-dev
*
*/
public static function validate_code(WP_User $user, int $code, bool $delete = true): bool {
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
foreach ( $backup_codes as $code_index => $code_hashed ) {
if ( wp_check_password( $code, $code_hashed, $user->ID ) ) {
if ( $delete ) {
self::delete_code( $user, $code_hashed );
}
return true;
}
}
}
return false;
}
/**
* Deletes a backup code.
*
* @param WP_User $user WP_User object of the logged-in user.
* @param string $code_hashed The hashed the backup code.
*
* @since 0.1-dev
*/
public static function delete_code( WP_User $user, string $code_hashed ): void {
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
// Delete the current code from the list since it's been used.
$backup_codes = array_flip( $backup_codes );
unset( $backup_codes[ $code_hashed ] );
$backup_codes = array_values( array_flip( $backup_codes ) );
// Update the backup code master list.
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $backup_codes );
}
/**
* Determines if a user is selectable for a specific authentication method.
*
* @param WP_User $user The user object to check.
*
* @return bool Returns true if the user is selectable for the authentication method, false otherwise.
*/
public static function is_selectable_for_user( WP_User $user ): bool {
// TODO: Logic for when backup codes are needed (for now only totp).
if ( Rsssl_Two_Factor_Totp::is_selectable_for_user( $user ) ) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* A compatibility layer for some of the most popular plugins.
*
* @package Two_Factor
*/
namespace RSSSL\Security\WordPress\Two_Fa;
/**
* A compatibility layer for some of the most popular plugins.
*
* Should be used with care because ideally we wouldn't need
* any integration specific code for this plugin. Everything should
* be handled through clever use of hooks and best practices.
*/
class Rsssl_Two_Factor_Compat {
/**
* Initialize all the custom hooks as necessary.
*
* @return void
*/
public function init() {
/**
* Jetpack
*
* @see https://wordpress.org/plugins/jetpack/
*/
add_filter( 'rsssl_two_factor_rememberme', array( $this, 'jetpack_rememberme' ) );
}
/**
* Jetpack single sign-on wants long-lived sessions for users.
*
* @param boolean $rememberme Current state of the "remember me" toggle.
*
* @return boolean
*/
public function jetpack_rememberme( $rememberme ) {
$action = filter_input( INPUT_GET, 'action', FILTER_CALLBACK, array( 'options' => 'sanitize_key' ) );
if ( 'jetpack-sso' === $action && $this->jetpack_is_sso_active() ) {
return true;
}
return $rememberme;
}
/**
* Helper to detect the presence of the active SSO module.
*
* @return boolean
*/
public function jetpack_is_sso_active() {
return ( method_exists( '\Jetpack', 'is_module_active' ) && \Jetpack::is_module_active( 'sso' ) );
}
}

View File

@@ -0,0 +1,602 @@
<?php
/**
* Class for creating an email provider.
*
* @package Two_Factor
*/
namespace RSSSL\Security\WordPress\Two_Fa;
/**
* Class for creating an email provider.
*
* @since 7.0.6
*
* @package Two_Factor
*/
//require_once __DIR__ . '/class-rsssl-two-factor-provider.php';
//require_once __DIR__ . '/interface-rsssl-provider-interface.php';
require_once rsssl_path . 'mailer/class-mail.php';
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Settings;
use rsssl_mailer;
use Exception;
use WP_User;
/**
* Generate and email the user token.
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @return void
* @since 0.1-dev
*/
class Rsssl_Two_Factor_Email extends Rsssl_Two_Factor_Provider implements Rsssl_Two_Factor_Provider_Interface {
/**
* The user meta token key.
*
* @var string
*/
public const RSSSL_TOKEN_META_KEY = '_rsssl_factor_email_token';
/**
* Store the timestamp when the token was generated.
*
* @var string
*/
public const RSSSL_TOKEN_META_KEY_TIMESTAMP = '_rsssl_factor_email_token_timestamp';
/**
* Name of the input field used for code resend.
*
* @var string
*/
public const RSSSL_INPUT_NAME_RESEND_CODE = 'rsssl-two-factor-email-code-resend';
public const SECRET_META_KEY = 'rsssl_two_fa_email_enabled';
public const METHOD = 'email';
public const NAME = 'Email';
/**
* Ensures only one instance of this class exists in memory at any one time.
*
* @since 0.1-dev
*/
public static function get_instance() {
static $instance;
$class = __CLASS__;
if ( ! is_a( $instance, $class ) ) {
$instance = new $class();
}
return $instance;
}
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
add_action( 'rsssl_two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
parent::__construct();
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*/
public function get_label(): string {
return _x( 'Email', 'Provider Label', 'really-simple-ssl' );
}
/**
* Generate the user token.
*
* @param int $user_id User ID.
*
* @return string
* @since 0.1-dev
*/
public function generate_token( int $user_id ): string {
$token = self::get_code();
update_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY_TIMESTAMP, time() );
update_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY, wp_hash( $token ) );
return $token;
}
/**
* Check if user has a valid token already.
*
* @param int $user_id User ID.
*
* @return boolean If user has a valid email token.
*/
public function user_has_token( int $user_id ): bool {
$hashed_token = $this->get_user_token( $user_id );
if ( ! empty( $hashed_token ) ) {
return true;
}
return false;
}
/**
* Has the user token validity timestamp expired.
*
* @param integer $user_id User ID.
*
* @return boolean
*/
public function user_token_has_expired( int $user_id ): bool {
$token_lifetime = $this->user_token_lifetime( $user_id );
$token_ttl = $this->user_token_ttl( $user_id );
// Invalid token lifetime is considered an expired token.
return ! ( is_int( $token_lifetime ) && $token_lifetime <= $token_ttl );
}
/**
* Get the lifetime of a user token in seconds.
*
* @param integer $user_id User ID.
*
* @return integer|null Return `null` if the lifetime can't be measured.
*/
public function user_token_lifetime( $user_id ) {
$timestamp = (int) get_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY_TIMESTAMP, true );
if ( ! empty( $timestamp ) ) {
return time() - $timestamp;
}
return null;
}
/**
* Return the token time-to-live for a user.
*
* @param integer $user_id User ID.
*
* @return integer
*/
public function user_token_ttl( int $user_id ): int {
$token_ttl = 15 * MINUTE_IN_SECONDS;
/**
* Number of seconds the token is considered valid
* after the generation.
*
* @param integer $token_ttl Token time-to-live in seconds.
* @param integer $user_id User ID.
*/
return (int) apply_filters( 'rsssl_two_factor_token_ttl', $token_ttl, $user_id );
}
/**
* Get the authentication token for the user.
*
* @param int $user_id User ID.
*
* @return string|boolean User token or `false` if no token found.
*/
public function get_user_token( int $user_id ) {
$hashed_token = get_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY, true );
if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
return $hashed_token;
}
return false;
}
/**
* Validate the user token.
*
* @param int $user_id User ID.
* @param string $token User token.
*
* @return boolean
* @since 0.1-dev
*/
public function validate_token( int $user_id, string $token ): bool {
$hashed_token = $this->get_user_token( $user_id );
// Bail if token is empty or it doesn't match.
if ( empty( $hashed_token ) || ! hash_equals( wp_hash( $token ), $hashed_token ) ) {
return false;
}
if ( $this->user_token_has_expired( $user_id ) ) {
return false;
}
// Ensure the token can be used only once.
$this->delete_token( $user_id );
update_user_meta( $user_id, 'rsssl_two_fa_status_email', 'active' );
return true;
}
/**
* Delete the user token.
*
* @param int $user_id User ID.
*
* @since 0.1-dev
*/
public function delete_token( int $user_id ): void {
delete_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY );
}
/**
* Generate and email the user token.
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @return void
* @since 0.1-dev
*/
public function generate_and_email_token( WP_User $user, $profile = false ): void {
$token = $this->generate_token( $user->ID );
$skip_two_fa_url = Rsssl_Two_Factor_Settings::rsssl_one_time_login_url( $user->ID, false, $profile );
// Add skip button to email content.
$skip_button_html = sprintf(
'<a href="%s" class="button" style="padding: 10px 30px; background: #2A7ABF; border-color: #2A7ABF; color: #fff; text-decoration: none; text-shadow: none; display: inline-block; margin-top: 15px; font-size: 0.8125rem; font-weight: 300; transition: all .3s ease; min-height: 10px;">' . __( 'Continue', 'really-simple-ssl' ) . '</a>',
esc_url( $skip_two_fa_url )
);
/* translators: %s: site name */
$subject = wp_strip_all_tags( sprintf( __( 'Your login confirmation code for %s', 'really-simple-ssl' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) );
/* translators: %s: token */
$token_cleaned = wp_strip_all_tags( $token );
// insert whitespace after four characters in the $token, for readability.
$token_cleaned = preg_replace( '/(.{4})/', '$1 ', $token_cleaned );
$token_html = sprintf(
'
<table cellspacing="0" cellpadding="0" border="0" width="100%%" style="margin-top: 25px;background-color:white; box-shadow: 1px 3px 0 1px rgba(211, 211, 211, 0.3); height: 180px;"> <!-- Further increased height for white box -->
<tr>
<td style="padding: 45px 10px 10px 10px; vertical-align: middle; font-size: 18px; font-weight:700; text-align: center;">%s</td> <!-- Increased padding for top and bottom -->
</tr>
<tr>
<td style="padding: 10px 20px 45px 20px; vertical-align: middle; text-align: center;">%s</td> <!-- Increased padding for bottom -->
</tr>
</table>',
$token_cleaned,
$skip_button_html
);
if($profile) {
$message = sprintf(
__( "Below you'll find the email activation code for %1\$s. It's valid for 15 minutes. %2\$s", 'really-simple-ssl' ),
site_url(),
$token_html
);
} else {
$message = sprintf(
__( "Below you will find your login code for %1\$s. It's valid for 15 minutes. %2\$s", 'really-simple-ssl' ),
site_url(),
$token_html
);
}
/**
* Filter the token email subject.
*
* @param string $subject The email subject line.
* @param int $user_id The ID of the user.
*/
$subject = apply_filters( 'rsssl_two_factor_token_email_subject', $subject, $user->ID );
/**
* Filter the token email message.
*
* @param string $message The email message.
* @param string $token The token.
* @param int $user_id The ID of the user.
*/
$message = apply_filters( 'rsssl_two_factor_token_email_message', $message, $token, $user->ID );
if ( ! class_exists( 'rsssl_mailer' ) ) {
require_once rsssl_path . 'mailer/class-mail.php';
}
$mailer = new rsssl_mailer();
$mailer->subject = $subject;
$mailer->branded = false;
/* translators: %s is replaced with the site url */
$mailer->sent_by_text = "<b>" . sprintf( __( 'Notification by %s', 'really-simple-ssl' ), site_url() ) . "</b>";
$mailer->template_filename = apply_filters( 'rsssl_email_template', rsssl_path . '/mailer/templates/email-unbranded.html' );
$mailer->to = $user->user_email;
$mailer->title = __( 'Hi', 'really-simple-ssl' ) . ' ' . $user->display_name . ',';
$mailer->message = $message;
$mailer->send_mail();
}
/**
* Prints the form that prompts the user to authenticate.
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @since 0.1-dev
*/
public function authentication_page( WP_User $user ): void {
if ( ! $user ) {
return;
}
if ( ! $this->user_has_token( $user->ID ) || $this->user_token_has_expired( $user->ID ) ) {
$this->generate_and_email_token( $user );
}
require_once ABSPATH . '/wp-admin/includes/template.php';
?>
<p class="two-factor-prompt"><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'really-simple-ssl' ); ?></p>
<p>
<label for="rsssl-authcode"><?php esc_html_e( 'Verification Code:', 'really-simple-ssl' ); ?></label>
<input type="text" inputmode="numeric" name="rsssl-two-factor-email-code" id="rsssl-authcode" class="input rsssl-authcode" value="" size="20" pattern="[0-9 ]*" placeholder="1234 5678" data-digits="8" />
<?php submit_button( __( 'Log In', 'really-simple-ssl' ) ); ?>
</p>
<p class="rsssl-two-factor-email-resend">
<input type="submit" class="button" name="<?php echo esc_attr( self::RSSSL_INPUT_NAME_RESEND_CODE ); ?>" value="<?php esc_attr_e( 'Resend Code', 'really-simple-ssl' ); ?>" />
</p>
<script type="text/javascript">
setTimeout( function(){
var d;
try{
d = document.getElementById('rsssl-authcode');
d.value = '';
d.focus();
} catch(e){}
}, 200);
</script>
<?php
$provider = get_user_meta( $user->ID, 'rsssl_two_fa_status_email', true );
foreach ( $user->roles as $role ) {
// Never show the skip link if a role is a forced role.
$two_fa_forced_roles = is_array(rsssl_get_option('two_fa_forced_roles'))
? rsssl_get_option('two_fa_forced_roles')
: [];
if (in_array($role, $two_fa_forced_roles, true)) {
break;
}
// If optional and open, allow the user to skip 2FA for now.
if ( 'open' === $provider && in_array( $role, rsssl_get_option( 'two_fa_enabled_roles_email', array() ), true ) ) {
$skip_two_fa_url = Rsssl_Two_Factor_Settings::rsssl_one_time_login_url( $user->ID, true );
?>
<a class="rsssl-skip-link" href="<?php echo esc_url( $skip_two_fa_url ); ?>" style="display: flex; justify-content: center; margin: 15px 20px 0 0;">
<?php esc_html_e( "Don't use Two-Factor Authentication", 'really-simple-ssl' ); ?>
</a>
<?php
}
}
}
/**
* Send the email code if missing or requested. Stop the authentication
* validation if a new token has been generated and sent.
*
* @param WP_USer $user WP_User object of the logged-in user.
* @return boolean
*/
public function pre_process_authentication( $user ): bool {
if ( isset( $user->ID ) && isset( $_REQUEST[ self::RSSSL_INPUT_NAME_RESEND_CODE ] ) ) {
$this->generate_and_email_token( $user );
return true;
}
return false;
}
/**
* Validates the users input token.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function validate_authentication( $user ): bool {
$code = self::sanitize_code_from_request( 'rsssl-two-factor-email-code' );
if ( ! isset( $user->ID ) || ! $code ) {
return false;
}
return $this->validate_token( $user->ID, $code );
}
/**
* Whether this Two Factor provider is configured and available for the user specified.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function is_available_for_user( $user ): bool {
return true;
}
/**
* Inserts markup at the end of the user profile field for this provider.
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @since 0.1-dev
*/
public function user_options( WP_User $user ): void {
$email = $user->user_email;
?>
<div>
<?php
echo esc_html(
sprintf(
/* translators: %s: email address */
__( 'Authentication codes will be sent to %s.', 'really-simple-ssl' ),
$email
)
);
?>
</div>
<?php
}
/**
* Check if the user is forced to use two-factor authentication.
*
* @param WP_User $user The user object.
*
* @return bool Whether the user is forced to use two-factor authentication.
*/
public static function is_forced( WP_User $user ): bool {
// If there is no user logged in, it can't check if the user is forced.
if ( ! $user->exists() ) {
return false;
}
return Rsssl_Two_Factor_Settings::get_role_status( 'email', $user->ID ) === 'forced';
}
/**
* Check if a user is Optional.
*
* @param WP_User $user The user object.
*
* @return bool Whether the user is optional or not.
*/
public static function is_optional( WP_User $user ): bool {
if ( ! $user->exists() ) {
return false;
}
$user_roles = $user->roles;
$optional_roles = rsssl_get_option( 'two_fa_enabled_roles_email' );
if ( empty( $optional_roles ) ) {
$optional_roles = array();
}
if ( 'disabled' === Rsssl_Two_Factor_Settings::get_user_status( 'email', $user->ID ) ) {
return false;
}
return in_array( $user_roles[0], $optional_roles, true );
}
/**
* Set user status for two-factor authentication.
*
* @param int $user_id User ID.
* @param string $status The status to set.
*
* @return void
*/
public static function set_user_status( int $user_id, string $status ): void {
update_user_meta( $user_id, 'rsssl_two_fa_status_email', $status );
}
/**
* Returns the HTML for the selection option.
*
* @param WP_User $user The user object.
* @param bool $checked Whether the option is checked or not.
*
* @return void
* @throws Exception Throws an exception if the template file is not found.
*/
public static function get_selection_option( $user, bool $checked = false ): void {
// Get the preferred method meta, which could be a string or an array.
$preferred_method_meta = get_user_meta( $user->ID, 'rsssl_two_fa_set_provider', true );
// Normalize the preferred method to always be an array.
$preferred_methods = is_array( $preferred_method_meta ) ? $preferred_method_meta : (array) $preferred_method_meta;
// Check if 'Rsssl_Two_Factor_Email' is the preferred method.
$is_preferred = in_array( 'Rsssl_Two_Factor_Email', $preferred_methods, true );
$is_enabled = (bool) get_user_meta( $user->ID, self::SECRET_META_KEY, true );
$badge_class = $is_enabled ? 'badge-enabled' : 'badge-default';
$enabled_text = $is_enabled ? esc_html__( 'Enabled', 'really-simple-ssl' ) : esc_html__( 'Disabled', 'really-simple-ssl' );
$checked_attribute = $checked ? 'checked' : '';
$title = esc_html__( 'Email', 'really-simple-ssl' );
$description = esc_html__( 'Receive a code by email', 'really-simple-ssl' );
// Load the template.
rsssl_load_template(
'selectable-option.php',
array(
'badge_class' => $badge_class,
'enabled_text' => $enabled_text,
'checked_attribute' => $checked_attribute,
'title' => $title,
'type' => 'email', // Used this to identify the provider.
'forcible' => in_array( $user->roles[0], (array) rsssl_get_option( 'two_fa_forced_roles' ), true ),
'description' => $description,
'user' => $user,
),
rsssl_path . 'assets/templates/two_fa'
);
}
/**
* Check if a user is enabled based on their role.
*
* @param WP_User $user The user object to check.
*
* @return bool Whether the user is enabled or not.
*/
public static function is_enabled( WP_User $user ): bool {
// todo - Do we need to check for a pro version here too?
if ( ! $user->exists() ) {
return false;
}
// Get and normalize the user roles.
$user_roles = $user->roles;
if ( ! is_array( $user_roles ) ) {
$user_roles = array();
}
if ( is_multisite() ) {
$user_roles = Rsssl_Two_Factor_Settings::get_strictest_role_across_sites($user->ID, ['email']);
}
// Get and normalize enabled roles.
$enabled_roles = rsssl_get_option( 'two_fa_enabled_roles_email' );
if ( ! is_array( $enabled_roles ) ) {
$enabled_roles = array();
}
// Return true if one of the user roles is in the enabled roles.
if ( (count($user_roles) > 1) || is_multisite() ) {
return count(array_intersect($user_roles, $enabled_roles)) > 0;
}
// If the user role is in the enabled roles, return true.
$firstKey = ( empty($user_roles) ? 0 : array_key_first($user_roles) );
$firstFoundRole = $user_roles[$firstKey];
return in_array( $firstFoundRole, $enabled_roles, true );
}
public static function is_configured( WP_User $user ): bool {
$status = get_user_meta( $user->ID, 'rsssl_two_fa_status_email', true );
return 'active' === $status;
}
public static function get_status( WP_User $user ): string {
return Rsssl_Two_Factor_Settings::get_user_status( 'email', $user->ID );
}
}

View File

@@ -0,0 +1,609 @@
<?php
/**
* Handles the API routes for the two-factor authentication onboarding process.
* This class is responsible for handling the API routes for the two-factor authentication onboarding process.
* It registers the routes and handles the requests.
*
* @package REALLY_SIMPLE_SSL
* @subpackage Security\WordPress\Two_Fa
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use Exception;
use RSSSL\Pro\Security\WordPress\Limitlogin\Rsssl_IP_Fetcher;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_User;
/**
* Registers API routes for the application.
* This class is responsible for registering the API routes for the two-factor authentication onboarding process.
* It registers the routes and handles the requests.
*
* @package REALLY_SIMPLE_SSL
* @subpackage Security\WordPress\Two_Fa
*/
class Rsssl_Two_Factor_On_Board_Api
{
/**
* Initializes the object and registers API routes.
*
* @return void
*/
public function __construct() {
add_action( 'rest_api_init', array( $this, 'register_api_routes' ) );
}
/**
* Checks if the requested namespace matches our specific namespace and bypasses authentication.
*
* @param WP_REST_Request $request The REST request object.
*/
private function check_custom_validation( WP_REST_Request $request ): bool {
// first check if the $-REQUEST['rest_route'] is set.
$params = new Rsssl_Request_Parameters( $request );
if ( ! isset( $params->login_nonce ) ) {
return false;
}
return Rsssl_Two_Fa_Authentication::verify_login_nonce( $params->user_id, $params->login_nonce );
}
/**
* Verifies a login nonce, gets user by the user id, and returns an error response if any steps fail.
*
* @throws Exception
*/
private function check_login_and_get_user( int $user_id, string $login_nonce ): WP_User {
if ( ! Rsssl_Two_Fa_Authentication::verify_login_nonce( $user_id, $login_nonce ) ) {
// We throw an error
wp_die();
}
/**
* Get the user by the user ID.
*
* @var WP_User $user
*/
$user = get_user_by('id', $user_id);
if (!$user) {
throw new Exception('User not found');
}
return $user;
}
/**
* Sets the authentication cookie and returns a success response.
*
*/
private function authenticate_and_redirect( int $user_id, string $redirect_to = '' ): WP_REST_Response {
// Okay checked the provider now authenticate the user.
wp_set_auth_cookie( $user_id, true );
// Finally redirect the user to the redirect_to page or to the home page if the redirect_to is not set.
$redirect_to = $redirect_to ?: home_url();
return new WP_REST_Response( array( 'redirect_to' => $redirect_to ), 200 );
}
/**
* Starts the process of email validation for a user.
*
* @param int $user_id The ID of the user for whom the email validation process needs to be started.
* @param string $redirect_to The URL to redirect the user after the email validation process. Default is an empty string.
*
* @return WP_REST_Response The REST response object.
*/
private function start_email_validation(int $user_id, string $redirect_to = '', $profile = false): WP_REST_Response
{
$redirect_to = $redirect_to ?: home_url();
$user = get_user_by('id', $user_id);
// Sending the email with the code.
Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, $profile);
$token = get_user_meta( $user_id, Rsssl_Two_Factor_Email::RSSSL_TOKEN_META_KEY, true );
if ( $redirect_to === 'profile') {
return new WP_REST_Response( array( 'token' => $token, 'validation_action' => 'validate_email_setup' ), 200 );
}
return new WP_REST_Response( array( 'token' => $token, 'redirect_to' => $redirect_to, 'validation_action' => 'validate_email_setup' ), 200 );
}
/**
* Sets the user provider as email and redirects the user to the specified page.
*
* @param WP_REST_Request $request The REST request object.
*
* @return WP_REST_Response The REST response object if user is not logged in or provider is invalid.
*/
public function set_as_email( WP_REST_Request $request ): WP_REST_Response {
$parameters = new Rsssl_Request_Parameters($request);
try {
$this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce);
} catch (Exception $e) {
return new WP_REST_Response(['error' => $e->getMessage()], 403);
}
if ('email' !== $parameters->provider) {
return new WP_REST_Response(['error' => 'Invalid provider'], 401);
}
return $this->start_email_validation($parameters->user_id, $parameters->redirect_to, $parameters->profile);
}
/**
* Sets the profile email for a user.
*
* @param WP_REST_Request $request The REST request object.
*
* @return WP_REST_Response The REST response object.
*/
public function set_profile_email(WP_REST_Request $request ): WP_REST_Response {
$parameters = new Rsssl_Request_Parameters($request);
try {
$this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce);
} catch (Exception $e) {
return new WP_REST_Response(['error' => $e->getMessage()], 403);
}
if ('email' !== $parameters->provider) {
return new WP_REST_Response(['error' => 'Invalid provider'], 401);
}
return $this->start_email_validation($parameters->user_id, $parameters->redirect_to, $parameters->profile);
}
/**
* Validates the email setup for a user.
*
* @param WP_REST_Request $request The REST request object.
*
* @return WP_REST_Response The REST response object.
*/
public function validate_email_setup(WP_REST_Request $request ): WP_REST_Response {
$parameters = new Rsssl_Request_Parameters($request);
if ('email' !== $parameters->provider) {
return new WP_REST_Response(['error' => 'Invalid provider'], 401);
}
/*
* This will result in a wp_die otherwise it will return the user.
*/
$user = $this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce);
if (!Rsssl_Two_Factor_Email::get_instance()->validate_token($user->ID, self::sanitize_token($parameters->token))) {
Rsssl_Two_Factor_Email::set_user_status($user->ID, 'open');
Rsssl_Two_Factor_Totp::set_user_status($user->ID, 'open');
wp_logout();
return new WP_REST_Response(['error' => __('Code was invalid, try "Resend Code"', 'really-simple.ssl')], 401);
}
Rsssl_Two_Factor_Email::set_user_status($user->ID, 'active');
Rsssl_Two_Factor_Totp::set_user_status($user->ID, 'disabled');
self::set_other_providers_inactive($user->ID, 'email');
return $this->authenticate_and_redirect($user->ID, $parameters->redirect_to);
}
/**
* Resends the email code for a user.
*
* @param WP_REST_Request $request The REST request object.
*
* @return WP_REST_Response The REST response object.
*/
public function resend_email_code( WP_REST_Request $request ): WP_REST_Response {
$parameters = new Rsssl_Request_Parameters( $request );
Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($parameters->user, $parameters->profile);
return new WP_REST_Response( array( 'message' => __('Verification code re-sent', 'really-simple.ssl') ), 200 );
}
/**
* Verifies the 2FA code for TOTP.
*
* @param WP_REST_Request $request The REST request object.
*
* @return WP_REST_Response The REST response object.
*/
public function verify_2fa_code_totp( WP_REST_Request $request ): WP_REST_Response {
$parameters = new Rsssl_Request_Parameters( $request );
$user = $this->check_login_and_get_user( $parameters->user_id, $parameters->login_nonce );
// Check if the provider.
if ( 'totp' !== $parameters->provider ) {
$response = new WP_REST_Response( array( 'error' => __('Invalid provider', 'really-simple-ssl') ), 400 );
}
//This is an extra check so someone who thinks to use backup codes can't use them.
$code_backup = Rsssl_Two_Factor_Backup_Codes::sanitize_code_from_request( 'authcode', 8 );
if ( $code_backup && Rsssl_Two_Factor_Backup_Codes::validate_code( $user, $code_backup, false ) ) {
$error_message = __('Invalid Two Factor Authentication code.', 'really-simple-ssl');
return new WP_REST_Response( array( 'error' => $error_message ), 400 );
}
if ( Rsssl_Two_Factor_Totp::setup_totp( $user, $parameters->key, $parameters->code ) ) {
Rsssl_Two_Factor_Totp::set_user_status( $user->ID, 'active' );
Rsssl_Two_Factor_Email::set_user_status( $user->ID, 'disabled' );
// Mark all other statuses as inactive.
self::set_other_providers_inactive( $user->ID, 'totp' );
// Finally we redirect the user to the redirect_to page.
return $this->authenticate_and_redirect( $parameters->user_id, $parameters->redirect_to );
}
// We get the error message from the setup_totp function.
$error_message = get_transient( 'rsssl_error_message_' . $user->ID );
// We delete the transient.
delete_transient( 'rsssl_error_message_' . $user->ID );
return new WP_REST_Response( array( 'error' => $error_message ), 400 );
}
/**
* Disables two-factor authentication for the user.
*
* @param WP_REST_Request $request The REST request object.
*
* @return WP_REST_Response The REST response object.
*/
public function disable_two_fa_for_user( WP_REST_Request $request ): WP_REST_Response {
$parameters = new Rsssl_Request_Parameters($request);
try {
$user = $this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce);
} catch (Exception $e) {
return new WP_REST_Response(['error' => $e->getMessage()], 403);
}
$user_available_providers = Rsssl_Provider_Loader::get_providers();
foreach ($user_available_providers as $provider) {
$provider::set_user_status($user->ID, 'disabled');
}
return $this->authenticate_and_redirect($parameters->user_id, $parameters->redirect_to);
}
/**
* Skips the onboarding process for the user.
*
* @param WP_REST_Request $request The REST request object.
*
* @return WP_REST_Response The REST response object.
*/
public function skip_onboarding( WP_REST_Request $request ): WP_REST_Response {
$parameters = new Rsssl_Request_Parameters( $request );
// As a double we check the user_id with the login nonce.
try {
$this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce);
} catch (Exception $e) {
return new WP_REST_Response(['error' => $e->getMessage()], 403);
}
return $this->authenticate_and_redirect( $parameters->user_id, $parameters->redirect_to );
}
/**
* Registers API routes for the application.
*/
public function register_api_routes(): void {
register_rest_route(
Rsssl_Two_Factor::REST_NAMESPACE,
'/save_default_method_email',
array(
'methods' => 'POST',
'callback' => array( $this, 'set_as_email' ),
'permission_callback' => function ( WP_REST_Request $request ) {
return true; // Allow all requests; handle auth in the callback.
},
'args' => array(
'provider' => array(
'required' => true,
'type' => 'string',
),
'user_id' => array(
'required' => true,
'type' => 'integer',
),
'login_nonce' => array(
'required' => true,
'type' => 'string',
),
),
)
);
register_rest_route(
Rsssl_Two_Factor::REST_NAMESPACE,
'/save_default_method_email_profile',
array(
'methods' => 'POST',
'callback' => array( $this, 'set_profile_email' ),
'permission_callback' => function ( WP_REST_Request $request ) {
return true; // Allow all requests; handle auth in the callback.
},
'args' => array(
'provider' => array(
'required' => true,
'type' => 'string',
),
'user_id' => array(
'required' => true,
'type' => 'integer',
),
'login_nonce' => array(
'required' => true,
'type' => 'string',
),
),
)
);
register_rest_route(
Rsssl_Two_Factor::REST_NAMESPACE,
'/validate_email_setup',
array(
'methods' => 'POST',
'callback' => array( $this, 'validate_email_setup' ),
'permission_callback' => function (WP_REST_Request $request) {
$login_actions = array('onboarding', 'email'); // Define allowed login actions here
return $this->permission_callback_login_actions($request, $login_actions);
},
'args' => array(
'provider' => array(
'required' => true,
'type' => 'string',
),
'user_id' => array(
'required' => true,
'type' => 'integer',
),
'login_nonce' => array(
'required' => true,
'type' => 'string',
),
'redirect_to' => array(
'required' => false,
'type' => 'string',
),
'token' => array(
'required' => true,
'type' => 'string',
),
),
)
);
register_rest_route(
Rsssl_Two_Factor::REST_NAMESPACE,
'/resend_email_code',
array(
'methods' => 'POST',
'callback' => array( $this, 'resend_email_code' ),
'permission_callback' => function (WP_REST_Request $request) {
$login_actions = array('onboarding', 'email'); // Define allowed login actions here
return $this->permission_callback_login_actions($request, $login_actions);
},
'args' => array(
'provider' => array(
'required' => true,
'type' => 'string',
),
'user_id' => array(
'required' => true,
'type' => 'integer',
),
'login_nonce' => array(
'required' => true,
'type' => 'string',
),
),
)
);
register_rest_route(
Rsssl_Two_Factor::REST_NAMESPACE,
'/save_default_method_totp',
array(
'methods' => 'POST',
'callback' => array( $this, 'verify_2fa_code_totp' ),
'permission_callback' => function ( WP_REST_Request $request ) {
return true; // Allow all requests; handle auth in the callback.
},
'args' => array(
'two-factor-totp-authcode' => array(
'required' => true,
'type' => 'string',
),
'provider' => array(
'required' => true,
'type' => 'string',
),
'key' => array(
'required' => true,
'type' => 'string',
),
'redirect_to' => array(
'required' => false,
'type' => 'string',
),
),
)
);
register_rest_route(
Rsssl_Two_Factor::REST_NAMESPACE,
'do_not_ask_again',
array(
'methods' => 'POST',
'callback' => array( $this, 'disable_two_fa_for_user' ),
'permission_callback' => function (WP_REST_Request $request) {
$login_actions = array('onboarding'); // Define allowed login actions here
return $this->permission_callback_login_actions($request, $login_actions);
},
'args' => array(
'redirect_to' => array(
'required' => false,
'type' => 'string',
),
'user_id' => array(
'required' => true,
'type' => 'integer',
),
'login_nonce' => array(
'required' => true,
'type' => 'string',
),
),
)
);
register_rest_route(
Rsssl_Two_Factor::REST_NAMESPACE,
'skip_onboarding',
array(
'methods' => 'POST',
'callback' => array( $this, 'skip_onboarding' ),
'permission_callback' => function (WP_REST_Request $request) {
$login_actions = array('onboarding'); // Define allowed login actions here
return $this->permission_callback_login_actions($request, $login_actions);
},
'args' => array(
'redirect_to' => array(
'required' => false,
'type' => 'string',
),
'user_id' => array(
'required' => true,
'type' => 'integer',
),
'login_nonce' => array(
'required' => true,
'type' => 'string',
),
),
)
);
}
/**
* Sets all other providers to inactive.
*
* @param int $id The user ID.
* @param string $allowed_method The allowed method.
*
* @return void
*/
public static function set_other_providers_inactive( int $id, string $allowed_method ): void {
// First we get all the available providers for the user.
// We get the user from the id.
$user_available_providers = Rsssl_Provider_Loader::get_enabled_providers_for_user( get_user_by( 'id', $id ) );
foreach ( $user_available_providers as $provider ) {
$namespace_parts = explode( '\\', $provider );
$last_key = end( $namespace_parts );
// we explode the last key to get the provider name.
$provider_name = explode( '_', $last_key );
$provider_name = end( $provider_name );
if ( ucfirst( $allowed_method ) !== $provider_name ) {
$provider::set_user_status( $id, 'disabled' );
}
if ( 'email' === $allowed_method ) {
//We delete backup codes if email is the provider.
Rsssl_Two_Factor_Backup_Codes::delete_backup_codes( $id );
}
}
}
/**
* Sanitizes a token.
*
* @param string $token The token to sanitize.
* @param int $length The expected length of the token. Default is 0.
*
* @return string|false The sanitized token, or false if the length is invalid.
*/
public static function sanitize_token(string $token, int $length = 0 ) {
$code = wp_unslash( $token );
$code = preg_replace( '/\s+/', '', $code );
// Maybe validate the length.
if ( $length && strlen( $code ) !== $length ) {
return false;
}
return (string) $code;
}
/**
* Permission callback for routes that need to:
* 1) Verify a login nonce
* 2) Ensure the user exists
* 3) Check that "email" is the active login action (as an example)
*
* @param WP_REST_Request $request
*
* @return true|WP_Error
*/
public function permission_callback_login_actions( WP_REST_Request $request, array $login_actions ) {
$user_id = $request->get_param( 'user_id' );
$login_nonce = $request->get_param( 'login_nonce' );
// we check if the login nonce is a string.
if ( ! is_string( $login_nonce ) ) {
return new WP_Error(
'rest_forbidden',
esc_html__( 'Access denied.', 'really-simple-ssl' ),
array( 'status' => 403 )
);
}
//TODO: Incompatible with free vesion currently needs the following branch: get-ip-functionality
// for now we will skip if the Rsssl_IP_Fetcher does not exist.
if( class_exists( 'RSSSL\Pro\Security\WordPress\Limitlogin\Rsssl_IP_Fetcher' ) ) {
$ip_array_found = (new Rsssl_IP_Fetcher)->get_ip_address();
$ip_address = $ip_array_found[0] ?? $_SERVER['REMOTE_ADDR'];
} else {
// As a temporary solution we will get the ip from the remote header
$ip_address = $_SERVER['REMOTE_ADDR'];
}
// Check if ip is a valid ip address.
if ( ! filter_var( $ip_address, FILTER_VALIDATE_IP ) ) {
return new WP_Error(
'rest_forbidden',
esc_html__( 'Access denied.', 'really-simple-ssl' ),
array( 'status' => 403 )
);
}
// Rate limiting
$route = $request->get_route();
$transient_key = 'rsssl_rate_limit_' . md5($ip_address . $route );
$attempts = get_transient( $transient_key );
if ($attempts === false) {
$attempts = 0;
}
if ($attempts >= 5) {
return new WP_Error(
'rest_forbidden',
esc_html__('Too many attempts. Please try again later.', 'really-simple-ssl'),
array('status' => 429)
);
}
// Perform all checks silently
$current_login_action_is_allowed_for_user = in_array(
Rsssl_Two_Factor_Settings::get_login_action( $user_id ),
$login_actions,
true );
if (
! $current_login_action_is_allowed_for_user ||
! Rsssl_Two_Fa_Authentication::verify_login_nonce( $user_id, $login_nonce ) ||
! get_user_by( 'id', $user_id )
) {
// Increment the attempts count
set_transient( $transient_key, $attempts + 1, 10 * MINUTE_IN_SECONDS );
// Use a short, generic message so the user doesn't know *why* it failed
return new WP_Error(
'rest_forbidden',
esc_html__( 'Access denied.', 'really-simple-ssl' ),
array( 'status' => 403 )
);
}
// Reset the attempts count on successful validation
delete_transient($transient_key);
return true;
}
}

View File

@@ -0,0 +1,437 @@
<?php
/**
* Holds the logic for the profile page.
*
* @package REALLY_SIMPLE_SSL
*/
namespace RSSSL\Security\WordPress\Two_Fa;
//require_once __DIR__ . '/class-rsssl-provider-loader.php';
//require_once __DIR__ . '/class-rsssl-parameter-validation.php';
//require_once __DIR__ . '/class-rsssl-parameter-validation.php';
//require_once __DIR__ . '/class-rsssl-two-factor-on-board-api.php';
ob_start();
use Exception;
use WP_User;
if (!class_exists('Rsssl_Two_Factor_Profile_Settings')) {
/**
* Class Rsssl_Two_Factor_Profile_Settings
*
* This class is responsible for handling the Two-Factor Authentication settings on the user profile page.
*
* @package REALLY_SIMPLE_SSL
*/
class Rsssl_Two_Factor_Profile_Settings
{
/**
* The available providers.
*
* @var array $available_providers An array to store the available providers.
*/
private $available_providers = array();
/**
* The forced Two-Factor Authentication roles.
*
* @var array $forced_two_fa An array to store the forced Two-Factor Authentication roles.
*/
private $forced_two_fa = array();
/**
* Constructor for the class.
*
* If the user is logged in, retrieve the user object and check if two-factor authentication is turned on for the user.
* If two-factor authentication is enabled, add the necessary hooks.
*
* @return void
*/
public function __construct()
{
if (is_user_logged_in()) {
$user_id = get_current_user_id();
$user = get_user_by('ID', $user_id);
global $pagenow;
if ('profile.php' === $pagenow || ('user-edit.php' === $pagenow && isset($_GET['user_id']))) {
if ($this->validate_two_turned_on_for_user($user)) {
add_action('admin_init', array($this, 'add_hooks'));
}
}
add_action( 'wp_ajax_resend_email_code_profile', [$this, 'resend_email_code_profile_callback'] );
add_action( 'wp_ajax_change_method_to_email', [$this, 'start_email_validation_callback'] );
}
}
/**
* Add hooks for user profile page.
*
* This method adds hooks to display the Two-Factor Authentication settings on user profile pages.
*
* @return void
*/
public function add_hooks(): void
{
if (is_user_logged_in()) {
$errors = Rsssl_Parameter_Validation::get_cached_errors(get_current_user_id());
if (!empty($errors)) {
// We display the errors.
foreach ($errors as $error) {
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
$error['message'],
$error['type']
);
}
}
}
$error = Rsssl_Parameter_Validation::get_cached_errors(get_current_user_id());
add_action('show_user_profile', array($this, 'show_user_profile'));
add_action('edit_user_profile', array($this, 'show_user_profile'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_styles'));
add_action('personal_options_update', array($this, 'save_user_profile'));
add_action('edit_user_profile_update', array($this, 'save_user_profile'));
if (isset($_GET['profile'], $_GET['_wpnonce'])) {
$profile = rest_sanitize_boolean(wp_unslash($_GET['profile']));
if ($profile) {
Rsssl_Two_Factor_Email::set_user_status(get_current_user_id(), 'active');
Rsssl_Two_Factor_Totp::set_user_status(get_current_user_id(), 'disabled');
}
}
}
/**
* Resend the email code for the user.
*
* @return void
*/
public function resend_email_code_profile_callback(): void
{
// Check for nonce (make sure your nonce name and action match what you output to the page)
if ( ! isset( $_POST['login_nonce'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['login_nonce'] ) ), 'update_user_two_fa_settings' ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'really-simple-ssl' ) ), 403 );
}
// Ensure the user is logged in.
if ( ! is_user_logged_in() ) {
wp_send_json_error( array( 'message' => __( 'User not logged in.', 'really-simple-ssl' ) ), 401 );
}
// Get the user ID.
$user_id = get_current_user_id();
$user = get_user_by( 'ID', $user_id );
Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, true);
wp_send_json_success( array( 'message' => __('Verification code re-sent', 'really-simple.ssl') ), 200 );
}
/**
* Starts the process of email validation for a user.
*
*/
public function start_email_validation_callback(): void
{
if(!is_user_logged_in()) {
wp_send_json_error( array( 'message' => __( 'User not logged in.', 'really-simple-ssl' ) ), 401 );
}
$user = get_user_by('id', get_current_user_id());
// Sending the email with the code.
Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, true);
$token = get_user_meta( $user->ID, Rsssl_Two_Factor_Email::RSSSL_TOKEN_META_KEY, true );
wp_send_json_success( array( 'message' => __('Verification code sent', 'really-simple-ssl'), 'token' => $token ), 200 );
}
/**
* Save the Two-Factor Authentication settings for the user.
*
* @param int $user_id The user ID.
*
* @return void
*/
public function save_user_profile(int $user_id): void
{
// We check if the user owns the profile.
if (!current_user_can('edit_user', $user_id)) {
return;
}
if (isset($_POST['rsssl_two_fa_nonce']) && !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['rsssl_two_fa_nonce'])), 'update_user_two_fa_settings')) {
return;
}
if (isset($_POST['change_2fa_config_field'])) {
// We sanitize the input needs to be a boolean.
$reset_input = filter_var($_POST['change_2fa_config_field'], FILTER_VALIDATE_BOOLEAN);
$this->maybe_the_user_resets_config($user_id, $reset_input);
return;
}
$params = new Rsssl_Parameter_Validation();
$params::validate_user_id($user_id);
$user = get_user_by('ID', $user_id);
$params::validate_user($user);
if (!isset($_POST['two-factor-authentication'])) {
// reset the user's 2fa settings.
// Delete all 2fa related user meta.
Rsssl_Two_Fa_Status::delete_two_fa_meta($user);
// Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa.
update_user_meta($user->ID, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
// also make sure no lingering errpr messages are shown.
Rsssl_Parameter_Validation::delete_cached_errors($user_id);
return;
}
if (!isset($_POST['preferred_method'])) {
return;
}
// now we check witch provider is selected from the $_POST.
$params::validate_selected_provider($this->sanitize_method(sanitize_text_field(wp_unslash($_POST['preferred_method']))));
$selected_provider = $this->sanitize_method(sanitize_text_field(wp_unslash($_POST['preferred_method'])));
// if the selected provider is not then return.
if (!$selected_provider) {
return;
}
switch ($selected_provider) {
case 'totp':
$current_status = Rsssl_Two_Factor_Settings::get_user_status('totp', $user_id);
if ('active' === $current_status) {
return;
}
if ((empty($_POST['two-factor-totp-authcode'])) || !isset($_POST['two-factor-totp-key'])) {
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('Two-Factor Authentication for TOTP failed. No Authentication code provided, please try again.', 'really-simple-ssl'),
'error'
);
$params::cache_errors($user_id);
return;
}
$params::validate_auth_code(absint(wp_unslash($_POST['two-factor-totp-authcode'])));
$params::validate_key(sanitize_text_field(wp_unslash($_POST['two-factor-totp-key'])));
$auth_code = sanitize_text_field(wp_unslash($_POST['two-factor-totp-authcode']));
$key = sanitize_text_field(wp_unslash($_POST['two-factor-totp-key']));
if (Rsssl_Two_Factor_Totp::setup_totp($user, $key, $auth_code)) {
Rsssl_Two_Factor_Totp::set_user_status($user_id, 'active');
// We disable the email.
Rsssl_Two_Factor_Email::set_user_status($user_id, 'disabled');
// We generate the backup codes.
Rsssl_Two_Factor_Backup_Codes::generate_codes(
$user,
array(
'cached' => true,
)
);
} else {
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('The Two-Factor Authentication setup for TOTP failed. Please try again.', 'really-simple-ssl'),
'error'
);
}
// We cache the errors.
$params::cache_errors($user_id);
break;
case 'email':
$current_status = Rsssl_Two_Factor_Settings::get_user_status('email', $user_id);
if ('active' === $current_status) {
return;
}
$user = get_user_by('ID', $user_id);
// fetch current status of the user for the email method.
$status = Rsssl_Two_Factor_Settings::get_user_status('email', $user->ID);
if ('active' === $status) {
return;
}
if (Rsssl_Two_Factor_Email::get_instance()->validate_authentication($user)) {
// We set the user status to active.
Rsssl_Two_Factor_Email::set_user_status($user_id, 'active');
// We disable the TOTP.
Rsssl_Two_Factor_Totp::set_user_status($user_id, 'disabled');
} else {
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('The Two-Factor Authentication setup for email failed. Please try again.', 'really-simple-ssl'),
'error'
);
}
break;
case 'none':
// We disable the Two-Factor Authentication.
Rsssl_Two_Fa_Status::delete_two_fa_meta($user);
break;
default:
break;
}
$params::cache_errors($user_id);
}
/**
* Sanitize the input method.
*
* @param string $method The input method.
*
* @return string The sanitized input method. Defaults to 'email' if not found in the allowed methods.
*/
private function sanitize_method(string $method): string
{
$methods = array('totp', 'email', 'none');
return in_array($method, $methods, true) ? sanitize_text_field($method) : 'email';
}
/**
* Display the user profile with Two-Factor Authentication settings.
*
* @param WP_User $user The user object.
*
* @return void
* @throws Exception Throws an exception if the template file is not found.
*/
public function show_user_profile(WP_User $user): void
{
if ($user->ID !== get_current_user_id()) {
return;
}
settings_errors('two-factor-authentication');
settings_errors('rsssl-two-factor-authentication-error');
Rsssl_Two_Factor_Totp::enqueue_qrcode_script();
$available_providers = $this->available_providers;
$forced = !empty(array_intersect($user->roles, $this->forced_two_fa));
$one_enabled = 'onboarding' !== Rsssl_Two_Factor_Settings::get_login_action($user->ID);
$providers = Rsssl_Provider_Loader::get_user_enabled_providers($user);
$selected_provider = '';
if ($one_enabled) {
$selected_provider = Rsssl_Two_Factor_Settings::get_configured_provider($user->ID);
}
$backup_codes = Rsssl_Two_Factor_Settings::get_backup_codes($user->ID);
$key = Rsssl_Two_Factor_Totp::generate_key();
$totp_url = Rsssl_Two_Factor_Totp::generate_qr_code_url($user, $key);
wp_nonce_field('update_user_two_fa_settings', 'rsssl_two_fa_nonce');
$data = array(
'key' => $key,
'totp_url' => $totp_url,
'backup_codes' => $backup_codes,
'selected_provider' => $selected_provider,
'one_enabled' => $one_enabled,
'forced' => $forced,
'available_providers' => $available_providers,
'user' => $user,
);
$data_js = 'rsssl_profile.totp_data = ' . json_encode($data) . ';';
wp_add_inline_script('rsssl-profile-settings', $data_js, 'after');
// We load the needed template for the Two-Factor Authentication settings.
rsssl_load_template(
'profile-settings.php',
compact(
'user',
'available_providers',
'providers',
'forced',
'one_enabled',
'selected_provider',
'backup_codes',
'totp_url',
'key'
),
rsssl_path . 'assets/templates/two_fa/'
);
}
/**
* Validates if the Two-Factor Authentication is turned on for the user.
*
* @param WP_User $user The user object.
*
* @return bool Returns true if Two-Factor Authentication is turned on for the user, false otherwise.
*/
private function validate_two_turned_on_for_user(WP_User $user): bool
{
// Get the setting for the system to check if it is turned on.
$enabled_two_fa = rsssl_get_option('login_protection_enabled');
$providers = Rsssl_Provider_Loader::get_enabled_providers_for_user($user);
$option = rsssl_get_option('two_fa_forced_roles');
$this->forced_two_fa = $option !== false ? $option : array();
$this->available_providers = $providers;
return $enabled_two_fa && !empty($providers);
}
/**
* Enqueues the RSSSL profile settings script.
*
* @return void
*/
public function enqueue_scripts(): void
{
$uri = trailingslashit(rsssl_url) . 'assets/two-fa/rtl/two-fa-assets.min.js';
$backup_codes = Rsssl_Two_Factor_Settings::get_backup_codes(get_current_user_id());
// We check if the backup codes are available.
wp_enqueue_script('rsssl-profile-settings', $uri, array(), rsssl_version, true);
wp_localize_script('rsssl-profile-settings', 'rsssl_profile', array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'backup_codes' => $backup_codes,
'root' => esc_url_raw(rest_url(Rsssl_Two_Factor::REST_NAMESPACE)),
'user_id' => get_current_user_id(),
'redirect_to' => 'profile', //added this for comparison in the json output.
'translatables' => [
'download_codes' => esc_html__('Download Backup Codes', 'really-simple-ssl'),
'keyCopied' => __('Key copied', 'really-simple-ssl'),
'keyCopiedFailed' => __('Could not copy text: ', 'really-simple-ssl')
]
));
}
/**
* Enqueues the RSSSL profile settings stylesheet.
*
* @return void
*/
public function enqueue_styles(): void
{
$uri = trailingslashit(rsssl_url) . 'assets/two-fa/rtl/two-fa-assets.min.css';
wp_enqueue_style('rsssl-profile-settings', $uri, array(), rsssl_version);
}
/**
* Checks if the user resets the configuration and actually reset everything.
*
* @param int $user_id The ID of the user.
* @param $reset_input
*
* @return bool
*/
private function maybe_the_user_resets_config(int $user_id, $reset_input): bool
{
$user = get_user_by('ID', $user_id);
// If the reset is true, we do the reset.
if ($reset_input && $user) {
// We reset the user's Two-Factor Authentication settings.
Rsssl_Two_Fa_Status::delete_two_fa_meta($user);
}
return $reset_input;
}
}
}

View File

@@ -0,0 +1,152 @@
<?php
/**
* Abstract class for creating two factor authentication providers.
*
* @package Two_Factor
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use WP_User;
/**
* Abstract class for creating two-factor authentication providers.
*
* @since 7.0.6
*
* @package Two_Factor
*/
abstract class Rsssl_Two_Factor_Provider {
/**
* The instance of the provider.
*
* @var Rsssl_Two_Factor_Provider
*/
public $instance;
/**
* Class constructor.
*
* @since 0.1-dev
*/
protected function __construct() {
$this->instance = $this;
}
/**
* Returns the name of the provider.
*
* @since 0.1-dev
*
* @return string
*/
abstract public function get_label();
/**
* Prints the name of the provider.
*
* @since 0.1-dev
*/
public function print_label() {
echo esc_html( $this->get_label() );
}
/**
* Prints the form that prompts the user to authenticate.
*
* @param WP_User $user WP_User object of the logged-in user.
*
* @since 0.1-dev
*/
abstract public function authentication_page( WP_User $user );
/**
* Allow providers to do extra processing before the authentication.
* Return `true` to prevent the authentication and render the
* authentication page.
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
public function pre_process_authentication( $user ) {
return false;
}
/**
* Validates the users input token.
*
* @since 0.1-dev
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
abstract public function validate_authentication( $user );
/**
* Whether this Two Factor provider is configured and available for the user specified.
*
* @param WP_User $user WP_User object of the logged-in user.
* @return boolean
*/
abstract public function is_available_for_user( $user );
/**
* Generate a random eight-digit string to send out as an auth code.
*
* @since 0.1-dev
*
* @param int $length The code length.
* @param string|array $chars Valid auth code characters.
* @return string
*/
public static function get_code( $length = 8, $chars = '1234567890' ): string {
$code = '';
if ( is_array( $chars ) ) {
$chars = implode( '', $chars );
}
for ( $i = 0; $i < $length; $i++ ) {
$code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
}
return $code;
}
/**
* Sanitizes a numeric code to be used as an auth code.
*
* @param string $field The _REQUEST field to check for the code.
* @param int $length The valid expected length of the field.
*
* @return false|string Auth code on success, false if the field is not set or not expected length.
*/
public static function sanitize_code_from_request( string $field, int $length = 0 ) {
if ( empty( $_REQUEST[ $field ] ) ) {
return false;
}
$code = wp_unslash( $_REQUEST[ $field ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, handled by the core method already.
$code = preg_replace( '/\s+/', '', $code );
// Maybe validate the length.
if ( $length && strlen( $code ) !== $length ) {
return false;
}
return (string) $code;
}
/**
* Set user status.
*
* This function updates the 'rsssl_two_fa_status' user meta key with the provided status.
*
* @param int $user_id The user ID.
* @param string $status The user status.
*
* @return void
* @since 1.0.0
*/
public static function set_user_status( int $user_id, string $status ): void {
update_user_meta( $user_id, 'rsssl_two_fa_status', $status );
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace RSSSL\Security\WordPress\Two_FA;
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Admin;
use rsssl_mailer;
class RSSSL_Two_Factor_Reset_Factory {
public function __construct()
{
add_action('rsssl_process_batched_users', [self::class, 'batched_process'], 10, 3);
}
public static function reset_fix(): void
{
$self = new self();
$forced_roles = rsssl_get_option('two_fa_forced_roles', array());
if(empty($forced_roles)) {
update_option('rsssl_reset_fix', false, false);
return;
}
$user_count = (int)$self->get_count_expired_users($self->get_expired_users_query());
if ($user_count > 0 ) {
$batch_size = 1000;
wp_schedule_single_event(time()+ 20, 'rsssl_process_batched_users', [$self->get_expired_users_query(), $user_count, $batch_size]);
} else {
update_option('rsssl_reset_fix', false, false);
}
}
public function get_expired_users_query()
{
global $wpdb;
$days_threshold = rsssl_get_option('two_fa_grace_period', 30);
$filter_value = 'expired';
$enabled_roles = array_unique(array_merge(
defined('rsssl_pro') ? rsssl_get_option('two_fa_enabled_roles_totp', array()) : array(),
rsssl_get_option('two_fa_enabled_roles_email', array())
));
$forced_roles = rsssl_get_option('two_fa_forced_roles', array());
$fields = ['id', 'user', 'status_for_user', 'rsssl_two_fa_providers', 'user_role']; // Example fields
$enabled_roles_placeholders = implode(',', array_map(function($role) { return "'$role'"; }, $enabled_roles));
$forced_roles_placeholder = implode(',', array_map(function($role) { return "'$role'"; }, $forced_roles));
$query = Rsssl_Two_Factor_Admin::generate_query($fields, $enabled_roles_placeholders, $forced_roles_placeholder, $forced_roles);
if ($filter_value !== 'all') {
$query .= $wpdb->prepare(" HAVING status_for_user = %s", $filter_value);
}
$prepared_query = $wpdb->prepare($query, array_merge(
array_fill(0, count($forced_roles), $days_threshold)
));
return $prepared_query;
}
public function get_count_expired_users($query): ?string
{
global $wpdb;
$count_query = "SELECT COUNT(*) FROM ($query) AS count_table";
return $wpdb->get_var($count_query);
}
public static function batched_process($query, $user_count, $batch_size = 500): void
{
global $wpdb;
$paged_query = $query . " LIMIT %d";
while ($user_count > 0) {
$current_query = $wpdb->prepare($paged_query, $batch_size);
$users = $wpdb->get_results($current_query);
foreach ($users as $user) {
Rsssl_Two_Fa_Status::delete_two_fa_meta((int)$user->id);
// Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa.
update_user_meta((int)$user->id, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
}
$user_count -= $batch_size;
}
}
}

View File

@@ -0,0 +1,765 @@
<?php
/**
* Holds the request parameters for a specific action.
* This class holds the request parameters for a specific action.
* It is used to store the parameters and pass them to the functions.
*
* @package REALLY_SIMPLE_SSL
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use WP_User;
/**
* Class Rsssl_Two_Factor_Settings
*
* This class handles the settings for the Two-Factor Authentication plugin.
*/
class Rsssl_Two_Factor_Settings {
/**
* The class instance.
*
* @var Rsssl_Two_Factor_Settings
*/
private static $instance;
/**
* The forced roles for 2FA.
*
* @var array $forced_roles
*/
public static $forced_roles;
/**
* The enabled roles for TOTP.
*
* @var $enabled_roles_totp
*/
public static $enabled_roles_totp;
/**
* The forced roles for TOTP dynamically generated by logic.
*
* @var $forced_roles_totp
*/
public static $forced_roles_totp; // @codingStandardsIgnoreLine It is dynamically generated by logic.
/**
* The enabled roles for Email dynamically generated by logic.
*
* @var $enabled_roles_totp
*/
public static $forced_roles_email; // @codingStandardsIgnoreLine It is dynamically generated by logic.
/**
* The enabled roles for Email.
*
* @var array $enabled_roles_email
*/
public static $enabled_roles_email;
/**
* If the previous roles variables are loaded or not.
*
* @var bool $roles_loaded
*/
private static $roles_loaded = false;
/**
* The user meta enabled providers key.
*
* @type string
*/
const RSSSL_ENABLED_PROVIDERS_USER_META_KEY = 'rsssl_two_fa_providers';
/**
* Class constructor.
*
* Checks if the class instance has already been initialized. If so, returns
* immediately. Otherwise, assigns the class instance to the static variable
* "self::$instance".
*/
public function __construct() {
if ( isset( self::$instance ) ) {
return;
}
self::$instance = $this;
}
/**
* Get user roles for a user, cross multisite.
*
* @param int $user_id //the user id to get the roles for.
*
* @return array
*/
public static function get_user_roles( int $user_id ): array {
if ( is_multisite() ) {
return array_values(self::get_strictest_role_across_sites($user_id, ['totp', 'email']));
}
$user = get_userdata( $user_id );
$roles = $user->roles;
if ( ! is_array( $roles ) ) {
$roles = array();
}
return $roles;
}
/**
* Generate a one-time login URL for a user.
*
* @param int $user_id //the user ID.
* @param bool $disable_two_fa //whether to disable two-factor authentication.
*
* @return string //the generated URL.
*/
public static function rsssl_one_time_login_url( int $user_id, bool $disable_two_fa = false, $profile = false ): string {
$token = bin2hex( openssl_random_pseudo_bytes( 16 ) ); // 16 bytes * 8 bits/byte = 128 bits.
set_transient( 'skip_two_fa_token_' . $user_id, $token, 2 * MINUTE_IN_SECONDS );
$obfuscated_user_id = self::obfuscate_user_id( $user_id );
$nonce = wp_create_nonce( 'one_time_login_' . $user_id );
if(!$profile) {
$args = array(
'rsssl_one_time_login' => $obfuscated_user_id,
'token' => $token,
'_wpnonce' => $nonce,
);
} else {
$args = array(
'_wpnonce' => $nonce,
'profile' => $profile,
);
}
if ( $disable_two_fa ) {
$args['rsssl_two_fa_disable'] = true;
}
// Return the URL with the added query arguments.
return add_query_arg( $args, $profile? get_edit_profile_url( $user_id ):admin_url() );
}
/**
* Get the {method}_role_status. The role with the most weighing status will be returned. empty, optional or forded. Where forced is the most weighing.
*
* @param string $method //the method to check.
* @param int $user_id //the user id to get the roles for.
*
* @return string
*/
public static function get_role_status( string $method, int $user_id ): string {
if (is_multisite()) {
$roles = self::get_strictest_role_across_sites($user_id, [$method]);
$roles = array_values($roles); // Flatten the array to get the strictest role
} else {
$roles = self::get_user_roles($user_id);
}
$provider = 'email' === $method ? '_email' : '_' . self::sanitize_method( $method );
// Check if the method is enabled.
$enabled = rsssl_get_option( "two_fa_enabled_roles$provider" );
$forced = false;
if ( ! $enabled ) {
$return = 'empty';
}
// if the role is forced, return forced.
if ( self::contains_role_of_type( $method, $roles, 'forced' ) ) {
$return = 'forced';
$forced = true;
}
// if the role is enabled, return optional.
if ( self::contains_role_of_type( $method, $roles, 'enabled' ) && ! $forced ) {
$return = 'optional';
}
if ( empty( $return ) ) {
$return = 'empty';
}
return $return;
}
/**
* Get required 2fa action for a user.
*
* @param int|null $user_id //the user id to get the roles for.
* @return string //email, totp, onboarding or login
*/
public static function get_login_action( int $user_id = null ): string {
if ( null === $user_id ) {
$user_id = get_current_user_id();
}
$user = get_userdata( $user_id );
$totp = Rsssl_Two_Factor_Totp::get_instance();
if ( $totp::is_enabled( $user ) ) {
// first, check TOTP.
$user_status = self::get_user_status( 'totp', $user_id );
$role_status = self::get_role_status( 'totp', $user_id );
// if it's active, it's simple: the user should enter the code.
if ( 'active' === $user_status ) {
// Check the role status, in case the admin has disabled this for this role.
if ( 'forced' === $role_status || 'optional' === $role_status ) {
return 'totp';
}
}
if ( 'open' === $user_status ) {
// if the status is open, the user should get onboarding if the role status either forced or optional.
if ( 'forced' === $role_status || 'optional' === $role_status ) {
// The role is forced. So check if the grace period is over.
$grace_period = self::is_user_in_grace_period( $user );
if ( $grace_period > 0 && 'forced' === $role_status ) {
return 'onboarding';
}
if ('optional' === $role_status) {
return 'onboarding';
}
return 'expired';
}
// if empty, nothing is currently activated for this role, so check if there's an email method enabled.
return self::get_email_method_action( $user_id );
}
// Check if the role_status is not 'forced' currently. If so, show onboarding TODO: test this code below.
$role_status = self::get_role_status( 'totp', $user_id );
if ( 'forced' === $role_status && 'disabled' !== $user_status ) {
return 'onboarding';
}
}
return self::get_email_method_action( $user_id );
}
/**
* Get required action for the email 2fa method.
*
* @param int $user_id //the user id to get the roles for.
*
* @return string //email, onboarding or login
*/
public static function get_email_method_action( int $user_id ): string {
$email = Rsssl_Two_Factor_Email::get_instance();
$grace_period = self::is_user_in_grace_period( get_userdata( $user_id ) );
$return = 'login';
if ( $email::is_enabled( get_userdata( $user_id ) ) ) {
$user_status = self::get_user_status( 'email', $user_id );
$role_status = self::get_role_status( 'email', $user_id );
if ( 'active' === $user_status ) {
// Also check the role status, in case the admin has disabled this for this role.
if ( 'forced' === $role_status || 'optional' === $role_status ) {
$return = 'email';
}
}
if ( 'open' === $user_status ) {
// if the role status is forced or optional, we show onboarding.
if ( 'forced' === $role_status || 'optional' === $role_status ) {
// The role is forced. So check if the grace period is over.
if ( $grace_period > 0 && 'forced' === $role_status ) {
return 'onboarding';
}
if ('optional' === $role_status) {
return 'onboarding';
}
return 'expired';
}
}
}
// if we're here, the email method is not enabled, so we show login.
return $return;
}
/**
* Validate if the role status and user status are valid.
*
* @param string $role_status // The role status to check.
* @param string $user_status // The user status to check.
*
* @return bool // Returns true if the role status and user status are valid, otherwise false.
*/
public static function is_role_and_user_status_valid( string $role_status, string $user_status ): bool {
return ( 'forced' === $role_status || 'optional' === $role_status ) && ( 'active' === $user_status || 'open' === $user_status );
}
/**
* Get the status for a user, based on the method.
*
* @param string $method //the method to check.
* @param int $user_id //the user id to get the roles for.
*
* @return string //open, active or disabled
*/
public static function get_user_status( string $method, int $user_id ): string {
$method = 'email' === $method ? '_email' : '_' . self::sanitize_method( $method );
// first check if a user meta rsssl_two_fa_status is set.
$status = get_user_meta( $user_id, "rsssl_two_fa_status$method", true );
return self::sanitize_status( $status );
}
/**
* Get the roles for a user, based on the method and type.
*
* @param string $method //the method to check.
* @param string $type //the type to check.
*
* @return array
*/
private static function get_dynamic_roles_variable( string $method, string $type ): array {
// store these roles, as this function can be used in large loops.
if ( ! self::$roles_loaded ) {
// if the option is a boolean we convert it to an array.
self::$enabled_roles_totp = rsssl_get_option( 'two_fa_enabled_roles_totp', [] );
self::$enabled_roles_email = rsssl_get_option( 'two_fa_enabled_roles_email', [] );
self::$forced_roles = rsssl_get_option( 'two_fa_forced_roles', [] );
self::$roles_loaded = true;
}
$method = 'email' === $method ? '_email' : '_' . self::sanitize_method( $method );
$type = 'enabled' === $type ? 'enabled' : 'forced';
$name = $type . '_roles' . $method;
$roles_to_check = 'enabled_roles' . $method;
// if the type is forced, use the forced roles.
if ( 'forced' === $type ) {
// Intersect the roles with the enabled roles.
self::$$name = array_intersect( self::$forced_roles, self::$$roles_to_check );
if ( property_exists( self::class, $name ) ) {
$roles = self::$$name;
if ( ! is_array( $roles ) ) {
$roles = array();
}
return $roles;
}
}
// if the type is enabled, use the enabled roles.
if ( 'enabled' === $type ) {
self::$$name = array_merge( self::$$roles_to_check );
if ( property_exists( self::class, $name ) ) {
$roles = self::$$name;
if ( ! is_array( $roles ) ) {
$roles = array();
}
return $roles;
}
}
return array();
}
/**
* Check if the array of roles contains a role of type $type, forced or optional.
*
* @param string $method //the method to check.
* @param array $roles //the roles to check.
* @param string $type //the type to check.
*
* @return bool
*/
public static function contains_role_of_type( string $method, array $roles, string $type ): bool {
$roles_to_check = self::get_dynamic_roles_variable( $method, $type );
foreach ( $roles as $role ) {
if ( in_array( $role, $roles_to_check, true ) ) {
return true;
}
}
return false;
}
/**
* Check if a role is of a certain type, optional or forced
*
* @param string $method //the method to check.
* @param string $role //the role to check.
* @param string $type //the type to check.
*
* @return bool
*/
public static function role_is_of_type( string $method, string $role, string $type ): bool {
return self::contains_role_of_type( $method, array( $role ), $type );
}
/**
* Get the user meta enabled providers key.
*
* @param string $status //the status to filter by.
*
* @return string //the user meta key.
*/
protected static function sanitize_status( string $status ): string {
return in_array( $status, array( 'open', 'active', 'disabled' ), true ) ? $status : 'open';
}
/**
* Get the user meta enabled providers key.
*
* @param string $method //the method to sanitize.
*
* @return string
*/
public static function sanitize_method( string $method ): string {
return in_array( $method, array( 'email', 'totp' ), true ) ? $method : 'email';
}
/**
* Check if a user is forced to use 2FA based on their roles.
*
* @param int $user_id // the ID of the user to check.
*
* @return bool // true if the user is forced to use 2FA, false otherwise.
*/
public static function is_user_forced_to_use_2fa( int $user_id ): bool {
$roles = self::get_user_roles( $user_id );
$forced_roles = rsssl_get_option( 'two_fa_forced_roles', [] );
foreach ( $roles as $role ) {
if ( in_array( $role, $forced_roles, true ) ) {
return true;
}
}
return false;
}
/**
* Check if a user is in the grace period for two-factor authentication.
*
* @param WP_User $user The user to check.
*
* @return int|false The number of days remaining in the grace period, or false if the user is not in the grace period.
*/
public static function is_user_in_grace_period( WP_User $user ) {
$grace_period = rsssl_get_option( 'two_fa_grace_period');
// if the grace period is not set, return false.
if ( ! self::is_user_forced_to_use_2fa( $user->ID ) ) {
return false;
}
$last_login = get_user_meta( $user->ID, 'rsssl_two_fa_last_login', true );
if ( $last_login ) {
$last_login = strtotime( $last_login );
$now = time();
$diff = $now - $last_login;
$days = floor( $diff / ( 60 * 60 * 24 ) );
if ( $days < $grace_period ) {
$end_date = gmdate( 'Y-m-d', $last_login );
// We add the grace period to the last login date.
$end_date = date( 'Y-m-d', strtotime( $end_date . ' + ' . $grace_period . ' days' ) );
$today = gmdate('Y-m-d', $now);
// If the end date is today, return 1.
if ($end_date === $today) {
return 1;
}
return $grace_period - $days;
}
// it is now equal or greater, so return false.
return false;
}
// if the last login is not set, return the grace period. but also set the user meta.
update_user_meta( $user->ID, 'rsssl_two_fa_last_login', gmdate( 'Y-m-d H:i:s' ) );
return $grace_period;
}
/**
* Get the enabled roles for a user.
*
* @param int $user_id // The ID of the user.
*
* @return array // The array of enabled roles for the user.
*/
public static function get_enabled_roles( int $user_id ): array {
$roles = self::get_user_roles( $user_id );
if(defined('rsssl_pro') && rsssl_pro ) {
$totp = rsssl_get_option( 'two_fa_enabled_roles_totp', [] );
} else {
$totp = [];
}
$email = rsssl_get_option( 'two_fa_enabled_roles_email', [] );
$enabled_roles = array_merge( $totp, $email );
return array_intersect( $roles, $enabled_roles );
}
/**
* Get the enabled roles for a user.
* This function is used to get the roles that are enabled for a user.
*
* @param int $user_id //the user ID to obfuscate.
*
* @return string
*/
public static function obfuscate_user_id( int $user_id ): string {
// Convert the user ID to a string with some noise.
$obfuscated = 'user-' . $user_id . '-id';
// Encode the string using base64.
return base64_encode( $obfuscated );
}
/**
* Deobfuscate the user ID for use in URL.
*
* @param string $data //the data to deobfuscate.
*
* @return string|null
*/
public static function deobfuscate_user_id( string $data ): ?string {
// Decode from base64.
$decoded = base64_decode( $data );
// Remove the noise to get the user ID.
if ( preg_match( '/user-(\d+)-id/', $decoded, $matches ) ) {
return $matches[1];
}
return null;
}
/**
* Based on the roles enabled return the method for the current user.
* If both methods are enabled, return the string not set.
* If only one method is enabled, return that method as a string.
* If no method is enabled, return the string None.
*
* @param int $user_id //the user ID to get the roles for.
*
* @return string
*/
public static function get_enabled_method( int $user_id ): string {
$user_id = absint( $user_id ); // make sure an integer and not a float, negative value.
$enabled_roles = self::get_enabled_roles( $user_id ) ?? array();
$enabled_totp = rsssl_get_option( 'two_fa_enabled_roles_totp', [] );
$enabled_email = rsssl_get_option( 'two_fa_enabled_roles_email', [] );
$totp = array_intersect( $enabled_roles, $enabled_totp );
$email = array_intersect( $enabled_roles, $enabled_email );
if ( ! empty( $totp ) && ! empty( $email ) ) {
$enabled_method = __( 'not set', 'really-simple-ssl' );
}
if ( ! empty( $totp ) ) {
$enabled_method = __( 'Authenticator App', 'really-simple-ssl' );
}
if ( ! empty( $email ) ) {
$enabled_method = __( 'Email', 'really-simple-ssl' );
}
if ( ! isset( $enabled_method ) ) {
$enabled_method = __( 'None', 'really-simple-ssl' );
}
return $enabled_method;
}
/**
* Get the configured provider for a user based on their ID.
*
* @param int $user_id The ID of the user.
*
* @return string The configured provider.
*/
public static function get_configured_provider( int $user_id ): string {
// With 2 providers, TOTP and Email we check both options and get the one that is not disabled.
$totp_meta = get_user_meta( $user_id, 'rsssl_two_fa_status_totp', true );
$email_meta = get_user_meta( $user_id, 'rsssl_two_fa_status_email', true );
$provider = __( 'None', 'really-simple-ssl' );
// if the status is active, return the method.
if ( 'active' === $totp_meta ) {
$provider = Rsssl_Two_Factor_Totp::NAME;
}
if ( 'active' === $email_meta ) {
$provider = Rsssl_Two_Factor_Email::NAME;
}
return $provider;
}
/**
* Get the backup codes for a user.
*
* @param int $user_id // The user ID.
*
* @return array // An array of backup codes.
*/
public static function get_backup_codes( int $user_id ): array {
$codes = get_transient( 'rsssl_two_factor_backup_codes_' . $user_id );
if ( ! is_array( $codes ) ) {
$codes = array();
}
return $codes;
}
/**
* Check if the last login date for a user is today.
*
* @param WP_User $user //the user.
*
* @return bool //true if last login date is today, false otherwise.
*/
public static function is_today( WP_User $user ): bool {
return (1 === (int) self::is_user_in_grace_period( $user ));
}
/**
* Ensure that the default roles are first in the array
*
* @param array $roles
*
* @return array
*/
protected static function sort_roles_by_default_first( array $roles ): array {
$default_roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
$sorted_roles = array();
foreach ( $default_roles as $default_role ) {
if ( in_array( $default_role, $roles, true ) ) {
$sorted_roles[] = $default_role;
}
}
foreach ( $roles as $role ) {
if ( ! in_array( $role, $sorted_roles, true ) ) {
$sorted_roles[] = $role;
}
}
return $sorted_roles;
}
/**
* Get the strictest role across all sites for a given user
*
* @param int $user_id //the ID of the user.
*
* @return array|null //returns the strictest role or null if no roles found.
*/
public static function get_strictest_role_across_sites(int $user_id, $methods ): ?array
{
$sites = get_sites();
$all_roles = [];
foreach ($sites as $site) {
switch_to_blog($site->blog_id);
$user = get_userdata($user_id);
if ($user) {
foreach($user->roles as $role){
$all_roles[] = $role;
}
}
restore_current_blog();
}
$all_roles = array_unique($all_roles);
return self::get_strictest_role($methods, $all_roles);
}
/**
* Get the strictest role from a list of roles
*
* @param array $methods
* @param array $roles // The list of roles
*
* @return array // The strictest role
*/
protected static function get_strictest_role(array $methods, array $roles): array
{
$result = [];
if (is_multisite()) {
$roles = self::sort_roles_by_default_first($roles);
$forced_roles = rsssl_get_option('two_fa_forced_roles', []);
// if there are forced roles, prioritize them by removing all other roles
if (!empty($forced_roles) && array_intersect($roles, $forced_roles)) {
$roles = array_intersect($roles, $forced_roles);
}
}
foreach ($methods as $method) {
// First, prioritize forced roles using the default-first sorting method
if (self::contains_role_of_type($method, $roles, 'forced')) {
foreach ($roles as $role) {
if (self::role_is_of_type($method, $role, 'forced')) {
// If forced role is found, assign it to the method and continue to the next method
$result[$method] = $role;
continue 2;
}
}
}
// If no forced role, check for optional roles
if (self::contains_role_of_type($method, $roles, 'enabled')) {
foreach ($roles as $role) {
if (self::role_is_of_type($method, $role, 'enabled')) {
// If optional role is found, assign it to the method and continue to the next method
$result[$method] = $role;
continue 2;
}
}
}
// If no role was found, assign an empty string
$result[$method] = '';
}
//remove empty values
return array_values(array_unique(array_filter($result)));
}
/**
* Get the user status per method
*
* @param int $user_id // The ID of the user.
*
* @return array // The user status per method
*/
public static function get_user_status_per_method(int $user_id): array
{
$methods = self::get_available_methods();
$result = [];
foreach ($methods as $method) {
$result[$method] = self::get_user_status($method, $user_id);
}
return $result;
}
private static function get_available_methods(): array
{
if(defined('rsssl_pro') && !rsssl_pro ) {
return ['totp', 'email'];
}
return ['email'];
}
}
new Rsssl_Two_Factor_Settings();

View File

@@ -0,0 +1,89 @@
<?php
/**
* Extracted from wp-login.php since that file also loads WP core which already have.
*
* @package REALLY_SIMPLE_SSL
*/
/**
* Outputs the footer for the login page.
*
* @param string $input_id Which input to auto-focus.
*
* @global bool|string $interim_login Whether interim login modal is being displayed. String 'success'
* upon successful login.
*
* @since 3.1.0
*/
function login_footer( string $input_id = '' ) {
global $interim_login;
// Don't allow interim logins to navigate away from the page.
if ( ! $interim_login ) {
?>
<p id="backtoblog">
<?php
$html_link = sprintf(
'<a href="%s">%s</a>',
esc_url( home_url( '/' ) ),
sprintf(
/* translators: %s: Site title. */
_x( '&larr; Go to %s', 'site' ),
get_bloginfo( 'title', 'display' )
)
);
/**
* Filter the "Go to site" link displayed in the login page footer.
*
* @since 5.7.0
*
* @param string $link HTML link to the home URL of the current site.
*/
echo esc_url( apply_filters( 'login_site_html_link', $html_link ) );
?>
</p>
<?php
the_privacy_policy_link( '<div class="privacy-policy-page-link">', '</div>' );
}
?>
</div><?php // End of <div id="login">. ?>
<?php
if ( ! empty( $input_id ) ) {
?>
<script type="text/javascript">
try{document.getElementById('<?php echo esc_html( $input_id ); ?>').focus();}catch(e){}
if(typeof wpOnload==='function')wpOnload();
</script>
<?php
}
/**
* Fires in the login page footer.
*
* @since 3.1.0
*/
do_action( 'login_footer' );
?>
<div class="clear"></div>
</body>
</html>
<?php
}
/**
* Outputs the JavaScript to handle the form shaking on the login page.
*
* @since 3.0.0
*/
function wp_shake_js() {
?>
<script type="text/javascript">
document.querySelector('form').classList.add('shake');
</script>
<?php
}

View File

@@ -0,0 +1,262 @@
<?php
/**
* Extracted from wp-login.php since that file also loads WP core which already have.
*
* @package REALLY_SIMPLE_SSL
*/
/**
* Output the login page header.
*
* @param string $title Optional. WordPress login Page title to display in the `<title>` element.
* Default 'Log In'.
* @param string $message Optional. Message to display in header. Default empty.
* @param WP_Error|null $wp_error Optional. The error to pass. Default is a WP_Error instance.
*
* @global string $action The action that brought the visitor to the login page.
*
* @since 2.1.0
*
* @global string $error Login error message set by deprecated pluggable wp_login() function
* or plugins replacing it.
* @global bool|string $interim_login Whether interim login modal is being displayed. String 'success'
* upon successful login.
*/
function login_header( string $title = 'Log In', string $message = '', WP_Error $wp_error = null ) {
global $error, $interim_login, $action;
// Don't index any of these forms.
add_filter( 'wp_robots', 'wp_robots_sensitive_page' );
add_action( 'login_head', 'wp_strict_cross_origin_referrer' );
add_action( 'login_head', 'wp_login_viewport_meta' );
if ( ! is_wp_error( $wp_error ) ) {
$wp_error = new WP_Error();
}
// Shake it!
$shake_error_codes = array( 'empty_password', 'empty_email', 'invalid_email', 'invalidcombo', 'empty_username', 'invalid_username', 'incorrect_password', 'retrieve_password_email_failure' );
/**
* Filters the error codes array for shaking the login form.
*
* @since 3.0.0
*
* @param array $shake_error_codes Error codes that shake the login form.
*/
$shake_error_codes = apply_filters( 'shake_error_codes', $shake_error_codes );
if ( $shake_error_codes && $wp_error->has_errors() && in_array( $wp_error->get_error_code(), $shake_error_codes, true ) ) {
add_action( 'login_footer', 'wp_shake_js', 12 );
}
$login_title = get_bloginfo( 'name', 'display' );
/* translators: Login screen title. 1: Login screen name, 2: Network or site name. */
$login_title = sprintf( __( '%1$s &lsaquo; %2$s &#8212; WordPress' ), $title, $login_title );
if ( wp_is_recovery_mode() ) {
/* translators: %s: Login screen title. */
$login_title = sprintf( __( 'Recovery Mode &#8212; %s' ), $login_title );
}
/**
* Filters the title tag content for login page.
*
* @since 4.9.0
*
* @param string $login_title The page title, with extra context added.
* @param string $title The original page title.
*/
$login_title = apply_filters( 'login_title', $login_title, $title );
?><!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta http-equiv="Content-Type" content="<?php bloginfo( 'html_type' ); ?>; charset=<?php bloginfo( 'charset' ); ?>" />
<title><?php echo esc_html( $login_title ); ?></title>
<?php
wp_enqueue_style( 'login' );
/*
* Remove all stored post data on logging out.
* This could be added by add_action('login_head'...) like wp_shake_js(),
* but maybe better if it's not removable by plugins.
*/
if ( 'loggedout' === $wp_error->get_error_code() ) {
?>
<script>if("sessionStorage" in window){try{for(var key in sessionStorage){if(key.indexOf("wp-autosave-")!=-1){sessionStorage.removeItem(key)}}}catch(e){}};</script>
<?php
}
/**
* Enqueue scripts and styles for the login page.
*
* @since 3.1.0
*/
do_action( 'login_enqueue_scripts' );
/**
* Fires in the login page header after scripts are enqueued.
*
* @since 2.1.0
*/
do_action( 'login_head' );
$login_header_url = __( 'https://wordpress.org/' );
/**
* Filters link URL of the header logo above login form.
*
* @since 2.1.0
*
* @param string $login_header_url Login header logo URL.
*/
$login_header_url = apply_filters( 'login_headerurl', $login_header_url );
$login_header_title = '';
/**
* Filters the title attribute of the header logo above login form.
*
* @since 2.1.0
* @deprecated 5.2.0 Use {@see 'login_headertext'} instead.
*
* @param string $login_header_title Login header logo title attribute.
*/
$login_header_title = apply_filters_deprecated(
'login_headertitle',
array( $login_header_title ),
'5.2.0',
'login_headertext',
__( 'Usage of the title attribute on the login logo is not recommended for accessibility reasons. Use the link text instead.' )
);
$login_header_text = empty( $login_header_title ) ? __( 'Powered by WordPress' ) : $login_header_title;
/**
* Filters the link text of the header logo above the login form.
*
* @since 5.2.0
*
* @param string $login_header_text The login header logo link text.
*/
$login_header_text = apply_filters( 'login_headertext', $login_header_text );
$classes = array( 'login-action-' . $action, 'wp-core-ui' );
if ( is_rtl() ) {
$classes[] = 'rtl';
}
if ( $interim_login ) {
$classes[] = 'interim-login';
?>
<style type="text/css">html{background-color: transparent;}</style>
<?php
if ( 'success' === $interim_login ) {
$classes[] = 'interim-login-success';
}
}
$classes[] = ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_locale() ) ) );
/**
* Filters the login page body classes.
*
* @since 3.5.0
*
* @param array $classes An array of body classes.
* @param string $action The action that brought the visitor to the login page.
*/
$classes = apply_filters( 'login_body_class', $classes, $action );
?>
</head>
<body class="login no-js <?php echo esc_attr( implode( ' ', $classes ) ); ?>">
<script type="text/javascript">
document.body.className = document.body.className.replace('no-js','js');
</script>
<?php
/**
* Fires in the login page header after the body tag is opened.
*
* @since 4.6.0
*/
do_action( 'login_header' );
?>
<div id="login">
<h1><a href="<?php echo esc_url( $login_header_url ); ?>"><?php echo esc_html( $login_header_text ); ?></a></h1>
<?php
/**
* Filters the message to display above the login form.
*
* @since 2.1.0
*
* @param string $message Login message text.
*/
$message = apply_filters( 'login_message', $message );
if ( ! empty( $message ) ) {
echo esc_html( $message ) . "\n";
}
// In case a plugin uses $error rather than the $wp_errors object.
if ( ! empty( $error ) ) {
$wp_error->add( 'error', $error );
unset( $error );
}
if ( $wp_error->has_errors() ) {
$errors = '';
$messages = '';
foreach ( $wp_error->get_error_codes() as $code ) {
$severity = $wp_error->get_error_data( $code );
foreach ( $wp_error->get_error_messages( $code ) as $error_message ) {
if ( 'message' === $severity ) {
$messages .= ' ' . $error_message . "<br />\n";
} else {
$errors .= ' ' . $error_message . "<br />\n";
}
}
}
if ( ! empty( $errors ) ) {
/**
* Filters the error messages displayed above the login form.
*
* @since 2.1.0
*
* @param string $errors Login error message.
*/
echo '<div id="login_error">' . esc_html( apply_filters( 'login_errors', $errors ) ) . "</div>\n";
}
if ( ! empty( $messages ) ) {
/**
* Filters instructional messages displayed above the login form.
*
* @since 2.5.0
*
* @param string $messages Login messages.
*/
echo '<p class="message">' . esc_html( apply_filters( 'login_messages', $messages ) ) . "</p>\n";
}
}
} // End of login_header().
/**
* Outputs the viewport meta tag for the login page.
*
* @since 3.7.0
*/
function wp_login_viewport_meta() {
?>
<meta name="viewport" content="width=device-width" />
<?php
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* Holds the request parameters for a specific action.
*
* @package REALLY_SIMPLE_SSL
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use WP_User;
/**
* Check if a user is forced.
*
* @param WP_User $user The user to check.
*
* @return bool True if the user is forced, false otherwise.
*/
interface Rsssl_Two_Factor_Provider_Interface {
/**
* Check if a user is forced.
*
* @param WP_User $user The user to check.
*
* @return bool True if the user is forced, false otherwise.
*/
public static function is_forced( WP_User $user ): bool;
/**
* Check if a method is enabled within the roles of the user.
*
* @param WP_User $user The user to check.
*
* @return bool True if the user is enabled, false otherwise.
*/
public static function is_enabled( WP_User $user ): bool;
public static function is_optional( WP_User $user ): bool;
public static function is_configured( WP_User $user ): bool;
}

View File

@@ -0,0 +1,150 @@
<?php
/**
* Trait for sending emails related to two-factor authentication.
*
* @package RSSSL\Pro\Security\WordPress\Two_Fa\Traits
*/
namespace RSSSL\Security\WordPress\Two_Fa\Traits;
use rsssl_mailer;
use WP_User;
/**
* Trait Rsssl_Email_Trait
*
* This trait handles email notifications related to password reset and compromised passwords.
*/
trait Rsssl_Email_Trait {
/**
* Notify the user that their password has been compromised and reset.
*
* @param WP_User $user The user to notify.
*
* @return void
*/
public static function notify_user_password_reset( WP_User $user ): void {
$subject = __( 'Your password was compromised and has been reset', 'really-simple-ssl' );
$message = self::create_user_message( $user );
if ( ! class_exists( 'rsssl_mailer' ) ) {
require_once rsssl_path . 'mailer/class-mail.php';
}
$mailer = self::initialize_mailer( $subject, $message, $user );
$mailer->send_mail();
}
/**
* Create a user message for failed login attempts.
*
* @param WP_User $user The user object.
*
* @return string The user message.
*/
private static function create_user_message( WP_User $user ): string {
$message = sprintf(
/* translators: %1$s: user login, %2$s: site url, %3$s: password best practices link, %4$s: lost password url */
__(
'Hello %1$s, an unusually high number of failed login attempts have been detected on your account at %2$s.
These attempts successfully entered your password, and were only blocked because they failed to enter your second authentication factor. Despite not being able to access your account, this behavior indicates that the attackers have compromised your password. The most common reasons for this are that your password was easy to guess, or was reused on another site which has been compromised.
To protect your account, your password has been reset, and you will need to create a new one. For advice on setting a strong password, please read %3$s
To pick a new password, please visit %4$s
This is an automated notification. If you would like to speak to a site administrator, please contact them directly.',
'really-simple-ssl'
),
esc_html( $user->user_login ),
home_url(),
'https://wordpress.org/documentation/article/password-best-practices/',
esc_url( add_query_arg( 'action', 'lostpassword', rsssl_wp_login_url() ) )
);
return str_replace( "\t", '', $message );
}
/**
* Notify the admin that a user's password was compromised and reset.
*
* @param WP_User $user The user whose password was reset.
*
* @return void
*/
public static function notify_admin_user_password_reset( WP_User $user ): void {
if ( ! class_exists( 'rsssl_mailer' ) ) {
require_once rsssl_path . 'mailer/class-mail.php';
}
$subject = self::create_subject( $user );
$message = self::create_message( $user );
$mailer = self::initialize_mailer( $subject, $message, $user );
$mailer->send_mail();
}
/**
* Create subject for the compromised password reset email.
*
* @param WP_User $user The user object.
*
* @return string The subject of the email.
*/
private static function create_subject( WP_User $user ): string {
/* translators: %s: user login */
return sprintf(
__( 'Compromised password for %s has been reset', 'really-simple-ssl' ),
esc_html( $user->user_login )
);
}
/**
* Generate a message for notifying the user about a high number of failed login attempts.
*
* @param WP_User $user The user for whom the message is created.
*
* @return string The generated message.
*/
private static function create_message( WP_User $user ): string {
$documentation_url = 'https://developer.wordpress.org/plugins/hooks/';
return str_replace(
"\t",
'',
// translators: %1$s: user login, %2$d: user ID, %3$s: documentation URL.
sprintf(
__( 'Hello, this is a notice from your website to inform you that an unusually high number of failed login attempts have been detected on the %1$s account (ID %2$d). Those attempts successfully entered the user\'s password, and were only blocked because they entered invalid second authentication factors. To protect their account, the password has automatically been reset, and they have been notified that they will need to create a new one. If you do not wish to receive these notifications, you can disable them with the `two_factor_notify_admin_user_password_reset` filter. See %3$s for more information. Thank you', 'really-simple-ssl' ),
esc_html( $user->user_login ),
$user->ID,
$documentation_url
)
);
}
/**
* Initialize the mailer for sending a notification email.
*
* @param string $subject The subject of the email.
* @param string $message The message content of the email.
* @param WP_User $user The user object to send the email to.
*
* @return rsssl_mailer The initialized mailer object.
*/
private static function initialize_mailer( string $subject, string $message, WP_User $user ): rsssl_mailer {
$mailer = new rsssl_mailer();
$mailer->subject = $subject;
$mailer->branded = false;
$mailer->sent_by_text = "<b>" . sprintf( __( 'Notification by %s', 'really-simple-ssl' ), site_url() ) . "</b>";
$mailer->template_filename = apply_filters( 'rsssl_email_template', rsssl_path . '/mailer/templates/email-unbranded.html' );
$mailer->to = $user->user_email;
$mailer->title = __( 'Compromised password reset', 'really-simple-ssl' );
$mailer->message = $message;
return $mailer;
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* A helper trait for sanitizing status and method values.
*
* @package really-simple-ssl
*/
namespace RSSSL\Security\WordPress\Two_Fa\Traits;
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Provider_Loader;
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Status;
/**
* A helper trait for sanitizing status and method values.
*/
trait Rsssl_Two_Fa_Helper {
/**
* Sanitize the given status.
*
* @param string $status The status to sanitize.
*
* @return string The sanitized status.
*/
private static function sanitize_status( string $status ): string {
$statuses_available = Rsssl_Two_Fa_Status::STATUSES;
if ( empty( $status ) ) {
return 'open';
}
// Check if the $status is in the array of available statuses.
if ( ! in_array( $status, $statuses_available, true ) ) {
// if not, set it to 'disabled'.
$status = 'disabled';
}
return sanitize_text_field( $status );
}
/**
* Sanitize a given method.
*
* @param string $method The method to sanitize.
*
* @return string The sanitized method.
*/
private static function sanitize_method( string $method ): string {
$methods_available = Rsssl_Provider_Loader::METHODS;
// Check if the $method is in the array of available methods.
if ( ! in_array( $method, $methods_available, true ) ) {
// if not, set it to 'disabled'.
$method = 'disabled';
}
return sanitize_text_field( $method );
}
}

View File

@@ -0,0 +1,54 @@
<?php
defined('ABSPATH') or die();
/**
* Prevent User Enumeration
* @return void
*/
function rsssl_check_user_enumeration() {
if ( ! is_user_logged_in() && isset( $_REQUEST['author'] ) ) {
if ( preg_match( '/\\d/', $_REQUEST['author'] ) > 0 ) {
wp_die( sprintf(__( 'forbidden - number in author name not allowed = %s', 'really-simple-ssl' ), esc_html( $_REQUEST['author'] ) ) );
}
}
}
add_action('init', 'rsssl_check_user_enumeration');
/**
* @return bool
* Remove author from Yoast sitemap
*/
function rsssl_remove_author_from_yoast_sitemap( $users ) {
return false;
}
add_filter('wpseo_sitemap_exclude_author', 'rsssl_remove_author_from_yoast_sitemap', 10, 1 );
/**
* Prevent WP JSON API User Enumeration
* Do not disable in when logged in, preventing issues in the Gutenberg Editor
*/
if ( !is_user_logged_in() || !current_user_can('edit_posts') ) {
add_filter( 'rest_endpoints', function ( $endpoints ) {
if ( isset( $endpoints['/wp/v2/users'] ) ) {
unset( $endpoints['/wp/v2/users'] );
}
if ( isset( $endpoints['/wp/v2/users/(?P[\d]+)'] ) ) {
unset( $endpoints['/wp/v2/users/(?P[\d]+)'] );
}
return $endpoints;
} );
}
//prevent xml site map user enumeration
add_filter(
'wp_sitemaps_add_provider',
function( $provider, $name ) {
if ( 'users' === $name ) {
return false;
}
return $provider;
},
10,
2
);

View File

@@ -0,0 +1,11 @@
<?php
defined('ABSPATH') or die();
/**
* Action to disable user registration
*
* @return bool
*/
function rsssl_users_can_register($value, $option) {
return false;
}
add_filter( "option_users_can_register", 'rsssl_users_can_register', 999, 2 );

View File

@@ -0,0 +1,116 @@
<?php
namespace security\wordpress\vulnerabilities;
defined('ABSPATH') or die();
class FileStorage
{
private $hash;
/**
* FileStorage constructor.
*/
public function __construct()
{
//Fetching the key from the database
$this->generateHashKey();
}
public Static function StoreFile($file, $data)
{
$storage = new FileStorage();
$storage->set($data, $file);
}
public Static function GetFile($file)
{
$storage = new FileStorage();
return $storage->get($file);
}
/** Get the data from the file
* @param $file
* @return bool|mixed
*/
public function get($file)
{
if (file_exists($file)) {
$data = file_get_contents($file);
$data = $this->Decode64WithHash($data);
return json_decode($data);
}
return false;
}
/** Save the data to the file
* @param $data
* @param $file
*/
public function set($data, $file)
{
$data = $this->Encode64WithHash(json_encode($data));
file_put_contents($file, $data);
}
/** encode the data with a hash
* @param $data
* @return string
*/
private function Encode64WithHash($data): string
{
//we create a simple encoding, using the hashkey as a salt
$data = base64_encode($data);
return base64_encode($data . $this->hash);
}
/** decode the data with a hash
* @param $data
* @return string
*/
private function Decode64WithHash($data): string
{
//we create a simple decoding, using the hashkey as a salt
$data = base64_decode($data);
$data = substr($data, 0, -strlen($this->hash));
return base64_decode($data);
}
/** Generate a hashkey and store it in the database
* @return void
*/
private function generateHashKey(): void
{
if (get_option('rsssl_hashkey') && get_option('rsssl_hashkey') !== "") {
$this->hash = get_option('rsssl_hashkey');
} else {
$this->hash = md5(uniqid(rand(), true));
update_option('rsssl_hashkey', $this->hash, false);
}
}
public static function GetDate(string $file)
{
if (file_exists($file)) {
return filemtime($file);
}
return false;
}
public static function DeleteAll()
{
//we get the upload folder
$upload_dir = wp_upload_dir();
//we get the really-simple-ssl folder
$rsssl_dir = $upload_dir['basedir'] . '/really-simple-ssl';
//then we delete the following files from that folder: manifest.json, components.json and core.json
$files = array('manifest.json', 'components.json', 'core.json');
foreach ($files as $file) {
//we delete the file
$file = $rsssl_dir . '/' . $file;
if (file_exists($file)) {
unlink($file);
}
}
}
}

View File

@@ -0,0 +1,182 @@
<?php
namespace security\wordpress\vulnerabilities;
defined( 'ABSPATH' ) or die();
require_once rsssl_path . 'lib/admin/class-encryption.php';
require_once 'class-rsssl-folder-name.php';
use RSSSL\lib\admin\Encryption;
class Rsssl_File_Storage {
use Encryption;
public $folder; //for the folder name
/**
* Rsssl_File_Storage constructor.
*/
public function __construct() {
//Fetching the key from the database
$upload_dir = wp_upload_dir();
$this->folder = $upload_dir['basedir'] . '/' . Rsssl_Folder_Name::getFolderName();
}
public static function StoreFile( $file, $data ): void {
$storage = new Rsssl_File_Storage();
//first we check if the storage folder is already in the $file string
if ( strpos( $file, $storage->folder ) !== false ) {
$file = str_replace( $storage->folder . '/', '', $file );
}
$storage->set( $data, $storage->folder . '/' . $file );
}
public static function GetFile( $file ) {
$storage = new Rsssl_File_Storage();
//first we check if the storage folder is already in the $file string
if ( strpos( $file, $storage->folder ) !== false ) {
$file = str_replace( $storage->folder . '/', '', $file );
}
return $storage->get( $storage->folder . '/' . $file );
}
/** Get the data from the file
*
* @param $file
*
* @return bool|mixed
*/
public function get( $file ) {
if ( file_exists( $file ) ) {
$data = file_get_contents( $file );
$data = $this->decrypt( $data );
return json_decode( $data );
}
return false;
}
/** Save the data to the file
*
* @param $data
* @param $file
*/
public function set( $data, $file ) {
if ( ! is_dir( $this->folder ) ) {
return;
}
if ( ! is_writable( $this->folder ) ) {
return;
}
$data = $this->encrypt( json_encode( $data ) );
//first we check if the storage folder is already in the $file string
if ( strpos( $file, $this->folder ) !== false ) {
$file = str_replace( $this->folder . '/', '', $file );
}
file_put_contents( $this->folder . '/' . $file, $data );
}
public static function GetDate( string $file ) {
if ( file_exists( $file ) ) {
return filemtime( $file );
}
return false;
}
public static function get_upload_dir() {
return ( new Rsssl_File_Storage() )->folder;
}
public static function validateFile( string $file ): bool {
$storage = new Rsssl_File_Storage();
$file = $storage->folder . '/' . $file;
if ( file_exists( $file ) ) {
return true;
}
return false;
}
/**
* Delete all files in the storage folder
*
* @return void
*/
public static function DeleteAll(): void {
$storage = new Rsssl_File_Storage();
//we get the really-simple-ssl folder
$rsssl_dir = $storage->folder;
//then we delete the following files from that folder: manifest.json, components.json and core.json
$files = array( 'manifest.json', 'components.json', 'core.json' );
foreach ( $files as $file ) {
//we delete the file
$file = $rsssl_dir . '/' . $file;
if ( file_exists( $file ) ) {
unlink( $file );
}
}
//we delete the folder
if ( file_exists( $rsssl_dir ) ) {
self::DeleteFolder($rsssl_dir);
//we delete the option
delete_option( 'rsssl_folder_name' );
}
}
/**
* Recursively delete a folder and its contents.
*
* @param string $dir The path to the folder to be deleted.
*
* @return bool Returns true if the folder was successfully deleted, false otherwise.
*/
public static function DeleteFolder($dir): bool {
if (substr($dir, strlen($dir) - 1, 1) != '/')
$dir .= '/';
if ($handle = opendir($dir)) {
while ($obj = readdir($handle)) {
if ($obj != '.' && $obj != '..') {
if (is_dir($dir.$obj)) {
if (!self::DeleteFolder($dir.$obj))
return false;
}
elseif (is_file($dir.$obj)) {
if (!unlink($dir.$obj))
return false;
}
}
}
closedir($handle);
if (!rmdir($dir))
return false;
return true;
}
return false;
}
/**
* Delete all files in the storage folder
*
* @return void
*/
public static function DeleteOldFiles(): void {
$rsssl_dir = wp_upload_dir()['basedir'] . '/really-simple-ssl';
//then we delete the following files from that folder: manifest.json, components.json and core.json
$files = array( 'manifest.json', 'components.json', 'core.json' );
foreach ( $files as $file ) {
//we delete the file
$file = $rsssl_dir . '/' . $file;
if ( file_exists( $file ) ) {
unlink( $file );
}
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace security\wordpress\vulnerabilities;
require_once rsssl_path . '/lib/admin/class-helper.php';
use RSSSL\lib\admin\Helper;
class Rsssl_Folder_Name {
use Helper;
public $folderName;
private function __construct() {
$this->initializeFolderName();
$this->verifyAndCreateFolder();
}
private function initializeFolderName(): void {
$rsssl_folder = get_option( 'rsssl_folder_name' );
if ( $rsssl_folder ) {
$this->folderName = $this->folderName( $rsssl_folder );
} else {
$newFolderName = 'really-simple-ssl/' . md5( uniqid( mt_rand(), true ) );
$this->folderName = $this->folderName( $newFolderName );
require_once 'class-rsssl-file-storage.php';
Rsssl_File_Storage::DeleteOldFiles();
update_option( 'rsssl_folder_name', $this->folderName );
}
}
private function folderName( $name ): string {
return $name;
}
private function verifyAndCreateFolder(): void {
$upload_dir = wp_upload_dir();
if ( ! file_exists( $upload_dir['basedir'] . '/' . $this->folderName ) ) {
$this->createFolder();
}
}
public function createFolder(): void {
$upload_dir = wp_upload_dir();
$folder_path = $upload_dir['basedir'] . '/' . $this->folderName;
if ( ! file_exists( $folder_path ) && is_writable($upload_dir['basedir'] ) ) {
if ( ! mkdir( $folder_path, 0755, true ) && ! is_dir( $folder_path ) ) {
$this->log( sprintf( 'Really Simple Security: Directory "%s" was not created', $folder_path ) );
}
}
}
/**
* Creates a new folder name and saves it in the settings
*
* @return string
*/
public static function getFolderName(): string
{
return (new Rsssl_Folder_Name())->folderName;
}
}