332 lines
12 KiB
PHP
332 lines
12 KiB
PHP
<?php
|
|
|
|
namespace RSSSL\Security\WordPress\Two_Fa\Controllers;
|
|
|
|
|
|
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Settings;
|
|
use WP_Error;
|
|
use Exception;
|
|
use RSSSL\Pro\Security\WordPress\Limitlogin\Rsssl_IP_Fetcher;
|
|
use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Email;
|
|
use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Request_Parameters;
|
|
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Authentication;
|
|
use WP_REST_Request;
|
|
use WP_REST_Response;
|
|
|
|
final class Rsssl_Email_Controller extends Rsssl_Abstract_Controller
|
|
{
|
|
protected const METHOD = 'POST';
|
|
protected const FEATURE_ROUTE = '/two-fa';
|
|
|
|
protected string $namespace;
|
|
|
|
public function __construct($namespace, $version, $featureVersion)
|
|
{
|
|
parent::__construct($namespace, $version, $featureVersion);
|
|
add_action('rest_api_init', array($this, 'register_api_routes'));
|
|
}
|
|
|
|
/**
|
|
* Registers the REST API routes for the email controller.
|
|
*
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function register_api_routes(): void
|
|
{
|
|
$this->route($this->namespace,
|
|
self::METHOD,
|
|
'save_default_method_email',
|
|
array($this, 'set_as_email'),
|
|
null,
|
|
$this->build_args(array('user_id', 'login_nonce', 'provider'), array('redirect_to'))
|
|
);
|
|
$this->route($this->namespace,
|
|
self::METHOD,
|
|
'save_default_method_email_profile',
|
|
array($this, 'set_profile_email'),
|
|
null,
|
|
$this->build_args(array('user_id', 'login_nonce', 'provider'), array('redirect_to'))
|
|
);
|
|
$this->route($this->namespace,
|
|
self::METHOD,
|
|
'validate_email_setup',
|
|
array($this, 'validate_email_setup'),
|
|
array($this, 'permission_callback_login_actions'),
|
|
$this->build_args(array('provider', 'user_id', 'login_nonce', 'token'), array('redirect_to'))
|
|
);
|
|
$this->route($this->namespace,
|
|
self::METHOD,
|
|
'resend_email_code',
|
|
array($this, 'resend_email_code'),
|
|
array($this, 'permission_callback_login_actions'),
|
|
$this->build_args(array('user_id', 'login_nonce', 'provider'), array('profile'))
|
|
);
|
|
}
|
|
|
|
###############################
|
|
# All callback functions here #
|
|
###############################
|
|
|
|
/**
|
|
* 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 {
|
|
$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);
|
|
}
|
|
// Check if the provider.
|
|
if ('email' !== $parameters->provider) {
|
|
return new WP_REST_Response(array('error' => 'Invalid provider'), 401);
|
|
}
|
|
|
|
// Finally redirect the user to the redirect_to page with a response.
|
|
return $this->start_email_validation($user->ID, $parameters->redirect_to, $parameters->profile);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
// Verify the user and login nonce.
|
|
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);
|
|
}
|
|
|
|
// Check if the provider.
|
|
if ('email' !== $parameters->provider) {
|
|
return new WP_REST_Response(array('error' => __('Invalid provider', 'really-simple-ssl')), 401);
|
|
}
|
|
|
|
// Finally redirect the user to the redirect_to page with a response.
|
|
return $this->start_email_validation($user->ID, $parameters->redirect_to, $parameters->profile);
|
|
}
|
|
|
|
|
|
/**
|
|
* Validates the email setup for a user.
|
|
*
|
|
* This function handles the validation of the email setup process. It checks the provided token
|
|
* and updates the user's two-factor authentication status accordingly. If the token is invalid,
|
|
* it resets the user's two-factor authentication settings and logs the user out.
|
|
*
|
|
* @param WP_REST_Request $request The REST request object containing the necessary parameters.
|
|
*
|
|
* @return WP_REST_Response The REST response object indicating the result of the validation process.
|
|
*/
|
|
public function validate_email_setup(WP_REST_Request $request): WP_REST_Response
|
|
{
|
|
// Extract parameters from the request.
|
|
$parameters = new Rsssl_Request_Parameters($request);
|
|
|
|
// Check if the provider is 'email'.
|
|
if ('email' !== $parameters->provider) {
|
|
return new WP_REST_Response(array('error' => 'Invalid provider'), 401);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
// Validate the provided token.
|
|
if (!Rsssl_Two_Factor_Email::get_instance()->validate_token($user->ID, self::sanitize_token($parameters->token))) {
|
|
// Reset all the settings if the token is invalid.
|
|
Rsssl_Two_Factor_Email::set_user_status($user->ID, 'open');
|
|
// Log out the user.
|
|
wp_logout();
|
|
|
|
return new WP_REST_Response(array('error' => __('Code was was invalid, try "Resend Code"', 'really-simple-ssl')), 401);
|
|
}
|
|
|
|
// Mark all other providers as inactive.
|
|
self::set_active_provider($user->ID, 'email');
|
|
|
|
// Authenticate the user and redirect them to the specified URL.
|
|
return $this->authenticate_and_redirect($user->ID, $parameters->redirect_to);
|
|
}
|
|
|
|
/**
|
|
* Resends the email verification 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);
|
|
|
|
// Verify the user and login nonce.
|
|
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);
|
|
}
|
|
|
|
// Sanitize and verify the provider.
|
|
$provider = sanitize_text_field($parameters->provider);
|
|
if ('email' !== $provider) {
|
|
return new WP_REST_Response(['error' => __('Invalid provider', 'really-simple-ssl')], 400);
|
|
}
|
|
|
|
// Determine email 2FA status for this user.
|
|
$email_status = get_user_meta($parameters->user_id, 'rsssl_two_fa_status_email', true ) ?? 'open';
|
|
$login_action = Rsssl_Two_Factor_Settings::get_login_action($parameters->user_id);
|
|
|
|
// if the status has an empty value, set it to 'open'
|
|
if (empty($email_status)) {
|
|
$email_status = 'open';
|
|
}
|
|
|
|
if ('active' !== $email_status && !('open' === $email_status && 'onboarding' === $login_action)) {
|
|
return new WP_REST_Response([
|
|
'error' => __('Email authentication is not active for this user', 'really-simple-ssl')
|
|
], 403);
|
|
}
|
|
|
|
// Generate and send a new token.
|
|
try {
|
|
Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, (bool) $parameters->profile);
|
|
} catch (WP_Error $e) {
|
|
return new WP_REST_Response(['error' => $e->get_error_message()], 500);
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
['message' => __('A verification code has been sent to the email address associated with your account.', 'really-simple-ssl')],
|
|
200
|
|
);
|
|
}
|
|
|
|
|
|
###############################
|
|
# All support functions here #
|
|
###############################
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
public function permission_callback_login_actions(WP_REST_Request $request) {
|
|
$parameters = new Rsssl_Request_Parameters($request);
|
|
$user_id = $parameters->user_id;
|
|
$login_nonce = $parameters->login_nonce;
|
|
|
|
// Ensure 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)
|
|
);
|
|
}
|
|
|
|
// Use IP fetcher if available.
|
|
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 {
|
|
// Fallback: use REMOTE_ADDR.
|
|
$ip_address = $_SERVER['REMOTE_ADDR'];
|
|
}
|
|
|
|
// Validate the 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: build a transient key based on IP and route.
|
|
$route = $request->get_route();
|
|
$transient_key = 'rsssl_rate_limit_' . md5($ip_address . $route);
|
|
$attempts = get_transient($transient_key);
|
|
if ($attempts === false) {
|
|
$attempts = 0;
|
|
}
|
|
|
|
// Limit to 5 attempts.
|
|
if ($attempts >= 5) {
|
|
return new WP_Error(
|
|
'rest_forbidden',
|
|
esc_html__('Too many attempts. Please try again later.', 'really-simple-ssl'),
|
|
array('status' => 429)
|
|
);
|
|
}
|
|
|
|
// Verify the login nonce and that the user exists.
|
|
if (!Rsssl_Two_Fa_Authentication::verify_login_nonce($user_id, $login_nonce)
|
|
|| !get_user_by('id', $user_id)
|
|
) {
|
|
// Increment the attempt count.
|
|
set_transient($transient_key, $attempts + 1, 10 * MINUTE_IN_SECONDS);
|
|
return new WP_Error(
|
|
'rest_forbidden',
|
|
esc_html__('Access denied.', 'really-simple-ssl'),
|
|
array('status' => 403)
|
|
);
|
|
}
|
|
|
|
// Reset the rate-limit on successful validation.
|
|
delete_transient($transient_key);
|
|
return true;
|
|
}
|
|
} |