diff --git a/WP_Auth0.php b/WP_Auth0.php
index c8e48665..ce64c283 100644
--- a/WP_Auth0.php
+++ b/WP_Auth0.php
@@ -126,6 +126,19 @@ public function init() {
$edit_profile = new WP_Auth0_EditProfile( $this->db_manager, $users_repo, $this->a0_options );
$edit_profile->init();
+ $api_client_creds = new WP_Auth0_Api_Client_Credentials( $this->a0_options );
+
+ $api_change_password = new WP_Auth0_Api_Change_Password( $this->a0_options, $api_client_creds );
+ $profile_change_pwd = new WP_Auth0_Profile_Change_Password( $api_change_password );
+ $profile_change_pwd->init();
+
+ $profile_delete_data = new WP_Auth0_Profile_Delete_Data( $users_repo );
+ $profile_delete_data->init();
+
+ $api_delete_mfa = new WP_Auth0_Api_Delete_User_Mfa( $this->a0_options, $api_client_creds );
+ $profile_delete_mfa = new WP_Auth0_Profile_Delete_Mfa( $this->a0_options, $api_delete_mfa );
+ $profile_delete_mfa->init();
+
WP_Auth0_Email_Verification::init();
}
@@ -420,7 +433,7 @@ public function render_form( $html ) {
// Do not show Auth0 form when ...
if (
// .. processing lost password
- ( isset( $_GET['action'] ) && $_GET['action'] == 'lostpassword' )
+ ( isset( $_GET['action'] ) && in_array( $_GET['action'], array( 'lostpassword', 'rp' ) ) )
// ... handling an Auth0 callback
|| ! empty( $_GET['auth0'] )
// ... plugin is not configured
@@ -510,6 +523,7 @@ private function autoloader( $class ) {
$source_dir . 'admin/',
$source_dir . 'api/',
$source_dir . 'exceptions/',
+ $source_dir . 'profile/',
$source_dir . 'wizard/',
$source_dir . 'initial-setup/',
);
diff --git a/assets/js/edit-user-profile.js b/assets/js/edit-user-profile.js
new file mode 100644
index 00000000..9a682d08
--- /dev/null
+++ b/assets/js/edit-user-profile.js
@@ -0,0 +1,81 @@
+/* global jQuery, wpa0UserProfile, alert */
+
+jQuery(function($) {
+ 'use strict';
+
+ var passwordFieldRow = $('#password');
+ var emailField = $('input[name=email]');
+ var deleteUserDataButton = $('#auth0_delete_data');
+ var deleteMfaDataButton = $('#auth0_delete_mfa');
+
+ /**
+ * Hide the password field if not an Auth0 strategy.
+ */
+ if ( passwordFieldRow.length && wpa0UserProfile.userStrategy && 'auth0' !== wpa0UserProfile.userStrategy ) {
+ passwordFieldRow.hide();
+ }
+
+ /**
+ * Disable email changes if not an Auth0 connection.
+ */
+ if ( emailField.length && wpa0UserProfile.userStrategy && 'auth0' !== wpa0UserProfile.userStrategy ) {
+ emailField.prop( 'disabled', true );
+ $('
' + wpa0UserProfile.i18n.cannotChangeEmail + '
')
+ .addClass('description')
+ .insertAfter(emailField);
+ }
+
+ /**
+ * Delete Auth0 data button click.
+ */
+ deleteUserDataButton.click(function (e) {
+ if ( ! window.confirm(wpa0UserProfile.i18n.confirmDeleteId) ) {
+ return;
+ }
+ e.preventDefault();
+ userProfileAjaxAction($(this), 'auth0_delete_data', wpa0UserProfile.deleteIdNonce );
+ });
+
+ /**
+ * Delete MFA data button click.
+ */
+ deleteMfaDataButton.click(function (e) {
+ if ( ! window.confirm(wpa0UserProfile.i18n.confirmDeleteMfa) ) {
+ return;
+ }
+ e.preventDefault();
+ userProfileAjaxAction($(this), 'auth0_delete_mfa', wpa0UserProfile.deleteMfaNonce);
+ });
+
+ /**
+ * Perform a generic user profile AJAX call.
+ *
+ * @param uiControl
+ * @param action
+ * @param nonce
+ */
+ function userProfileAjaxAction( uiControl, action, nonce ) {
+ var postData = {
+ 'action' : action,
+ '_ajax_nonce' : nonce,
+ 'user_id' : wpa0UserProfile.userId
+ };
+ var errorMsg = wpa0UserProfile.i18n.actionFailed;
+ uiControl.prop( 'disabled', true );
+ $.post(
+ wpa0UserProfile.ajaxUrl,
+ postData,
+ function(response) {
+ if ( response.success ) {
+ uiControl.val(wpa0UserProfile.i18n.actionComplete);
+ } else {
+ if (response.data && response.data.error) {
+ errorMsg = response.data.error;
+ }
+ alert(errorMsg);
+ uiControl.prop( 'disabled', false );
+ }
+ }
+ );
+ }
+});
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 06362b5a..5d0f8241 100644
--- a/composer.json
+++ b/composer.json
@@ -38,8 +38,7 @@
"phpcbf-tests": "./vendor/bin/phpcbf --standard=phpcs-test-ruleset.xml -s ./tests/",
"phpcbf-path": "SHELL_INTERACTIVE=1 ./vendor/bin/phpcbf --standard=phpcs-ruleset.xml",
"sniffs": "./vendor/bin/phpcs --standard=phpcs-ruleset.xml -e",
- "test": "./vendor/bin/phpunit --coverage-text",
- "test-ci": "./vendor/bin/phpunit --debug --verbose --coverage-clover=coverage.xml",
- "test-path": "SHELL_INTERACTIVE=1 ./vendor/bin/phpunit"
+ "test": "SHELL_INTERACTIVE=1 ./vendor/bin/phpunit --coverage-text --verbose",
+ "test-ci": "./vendor/bin/phpunit --debug --verbose --coverage-clover=coverage.xml"
}
}
diff --git a/lib/WP_Auth0_Api_Client.php b/lib/WP_Auth0_Api_Client.php
index a61d4d3c..b17a8f70 100755
--- a/lib/WP_Auth0_Api_Client.php
+++ b/lib/WP_Auth0_Api_Client.php
@@ -901,6 +901,9 @@ public static function delete_connection( $domain, $app_token, $id ) {
return json_decode( $response['body'] );
}
+ /**
+ * TODO: Deprecate
+ */
public static function delete_user_mfa( $domain, $app_token, $user_id, $provider ) {
$endpoint = "https://$domain/api/v2/users/$user_id/multifactor/$provider";
diff --git a/lib/WP_Auth0_EditProfile.php b/lib/WP_Auth0_EditProfile.php
index 527ca5eb..0fb0e944 100644
--- a/lib/WP_Auth0_EditProfile.php
+++ b/lib/WP_Auth0_EditProfile.php
@@ -1,38 +1,102 @@
a0_options = $a0_options;
- $this->users_repo = $users_repo;
+ /**
+ * WP_Auth0_Options instance.
+ *
+ * @var WP_Auth0_Options
+ */
+ protected $a0_options;
+
+ /**
+ * WP_Auth0_EditProfile constructor.
+ *
+ * @param WP_Auth0_DBManager $db_manager - WP_Auth0_DBManager instance.
+ * @param WP_Auth0_UsersRepo $users_repo - WP_Auth0_UsersRepo instance.
+ * @param WP_Auth0_Options $a0_options - WP_Auth0_Options instance.
+ */
+ public function __construct(
+ WP_Auth0_DBManager $db_manager,
+ WP_Auth0_UsersRepo $users_repo,
+ WP_Auth0_Options $a0_options
+ ) {
$this->db_manager = $db_manager;
+ $this->users_repo = $users_repo;
+ $this->a0_options = $a0_options;
}
+ /**
+ * Add actions and filters for the profile page.
+ */
public function init() {
- global $pagenow;
-
+ add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
add_action( 'personal_options_update', array( $this, 'override_email_update' ), 1 );
+ }
- add_action( 'edit_user_profile', array( $this, 'show_delete_identity' ) );
- add_action( 'edit_user_profile', array( $this, 'show_delete_mfa' ) );
- add_action( 'show_user_profile', array( $this, 'show_delete_mfa' ) );
-
- add_action( 'wp_ajax_auth0_delete_mfa', array( $this, 'delete_mfa' ) );
- add_action( 'wp_ajax_auth0_delete_data', array( $this, 'delete_user_data' ) );
-
- add_action( 'show_user_profile', array( $this, 'show_change_password' ) );
- add_action( 'personal_options_update', array( $this, 'update_change_password' ) );
- add_filter( 'user_profile_update_errors', array( $this, 'validate_new_password' ), 10, 3 );
+ /**
+ * Enqueue styles and scripts for the user profile edit screen.
+ * Hooked to: admin_enqueue_scripts
+ *
+ * @codeCoverageIgnore
+ */
+ public function admin_enqueue_scripts() {
+ global $user_id;
+ global $pagenow;
- if ( $pagenow == 'profile.php' || $pagenow == 'user-edit.php' ) {
- add_action( 'admin_footer', array( $this, 'disable_email_field' ) );
+ if ( ! in_array( $pagenow, array( 'profile.php', 'user-edit.php' ) ) ) {
+ return;
}
+
+ wp_enqueue_script(
+ 'wpa0_user_profile',
+ WPA0_PLUGIN_JS_URL . 'edit-user-profile.js',
+ array( 'jquery' ),
+ WPA0_VERSION
+ );
+
+ $profile = get_auth0userinfo( $user_id );
+ $strategy = isset( $profile->sub ) ? WP_Auth0_Users::get_strategy( $profile->sub ) : '';
+
+ wp_localize_script(
+ 'wpa0_user_profile',
+ 'wpa0UserProfile',
+ array(
+ 'userId' => intval( $user_id ),
+ 'userStrategy' => sanitize_text_field( $strategy ),
+ 'deleteIdNonce' => wp_create_nonce( 'delete_auth0_identity' ),
+ 'deleteMfaNonce' => wp_create_nonce( 'delete_auth0_mfa' ),
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'i18n' => array(
+ 'confirmDeleteId' => __( 'Are you sure you want to delete the Auth0 user data for this user?', 'wp-auth0' ),
+ 'confirmDeleteMfa' => __( 'Are you sure you want to delete the Auth0 MFA data for this user?', 'wp-auth0' ),
+ 'actionComplete' => __( 'Deleted', 'wp-auth0' ),
+ 'actionFailed' => __( 'Action failed, please see the Auth0 error log for details.', 'wp-auth0' ),
+ 'cannotChangeEmail' => __( 'Email cannot be changed for non-database connections.', 'wp-auth0' ),
+ ),
+ )
+ );
}
+ // TODO: Deprecate
public function validate_new_password( $errors, $update, $user ) {
$auth0_password = isset( $_POST['auth0_password'] ) ? $_POST['auth0_password'] : null;
$auth0_repeat_password = isset( $_POST['auth0_repeat_password'] ) ? $_POST['auth0_repeat_password'] : null;
@@ -42,7 +106,7 @@ public function validate_new_password( $errors, $update, $user ) {
}
}
-
+ // TODO: Deprecate
public function update_change_password() {
$current_user = get_currentauth0user();
$user_profile = $current_user->auth0_obj;
@@ -92,6 +156,7 @@ public function update_change_password() {
}
}
+ // TODO: Deprecate
public function delete_user_data() {
if ( ! is_admin() ) {
return;
@@ -102,6 +167,7 @@ public function delete_user_data() {
$this->users_repo->delete_auth0_object( $user_id );
}
+ // TODO: Deprecate
public function delete_mfa() {
if ( ! is_admin() ) {
return;
@@ -123,6 +189,7 @@ public function delete_mfa() {
WP_Auth0_Api_Client::delete_user_mfa( $domain, $app_token, $user_id, $provider );
}
+ // TODO: Deprecate
public function show_delete_identity() {
if ( ! is_admin() ) {
return;
@@ -165,6 +232,8 @@ function DeleteAuth0Data(event) {
auth0_obj;
@@ -257,6 +327,7 @@ public function show_change_password() {
auth0_obj;
@@ -294,6 +365,10 @@ public function disable_email_field() {
}
}
+ /**
+ * Process email changes and pass the update to Auth0 if it passes validation.
+ * Hooked to: personal_options_update
+ */
public function override_email_update() {
global $wpdb;
global $errors;
diff --git a/lib/WP_Auth0_Email_Verification.php b/lib/WP_Auth0_Email_Verification.php
index 83bdd1d5..5f3fc1a0 100644
--- a/lib/WP_Auth0_Email_Verification.php
+++ b/lib/WP_Auth0_Email_Verification.php
@@ -103,7 +103,7 @@ public function resend_verification_email() {
wp_send_json_error( array( 'error' => __( 'No Auth0 user ID provided.', 'wp-auth0' ) ) );
}
- if ( ! $this->api_jobs_resend->call() ) {
+ if ( ! $this->api_jobs_resend->call( $_POST['sub'] ) ) {
wp_send_json_error( array( 'error' => __( 'API call failed.', 'wp-auth0' ) ) );
}
@@ -120,8 +120,7 @@ public function resend_verification_email() {
function wp_auth0_ajax_resend_verification_email() {
$options = WP_Auth0_Options::Instance();
$api_client_creds = new WP_Auth0_Api_Client_Credentials( $options );
- $auth0_user_id = isset( $_POST['sub'] ) ? $_POST['sub'] : null;
- $api_jobs_verification = new WP_Auth0_Api_Jobs_Verification( $options, $api_client_creds, $auth0_user_id );
+ $api_jobs_verification = new WP_Auth0_Api_Jobs_Verification( $options, $api_client_creds );
$email_verification = new WP_Auth0_Email_Verification( $api_jobs_verification );
$email_verification->resend_verification_email();
diff --git a/lib/WP_Auth0_ErrorLog.php b/lib/WP_Auth0_ErrorLog.php
index 49e836f1..8efda001 100644
--- a/lib/WP_Auth0_ErrorLog.php
+++ b/lib/WP_Auth0_ErrorLog.php
@@ -125,6 +125,8 @@ private function update( array $log ) {
* Enqueue scripts and styles.
*
* @deprecated 3.6.0 - Not needed, handled in WP_Auth0_Admin::admin_enqueue()
+ *
+ * @codeCoverageIgnore
*/
public function admin_enqueue() {
// phpcs:ignore
diff --git a/lib/WP_Auth0_Users.php b/lib/WP_Auth0_Users.php
index f3de37d4..7db6fcb4 100644
--- a/lib/WP_Auth0_Users.php
+++ b/lib/WP_Auth0_Users.php
@@ -91,4 +91,19 @@ public static function create_user( $userinfo, $role = null ) {
// Return the user ID
return $user_id;
}
+
+ /**
+ * Get the strategy from an Auth0 user ID.
+ *
+ * @param string $auth0_id - Auth0 user ID.
+ *
+ * @return string
+ */
+ public static function get_strategy( $auth0_id ) {
+ if ( false === strpos( $auth0_id, '|' ) ) {
+ return '';
+ }
+ $auth0_id_parts = explode( '|', $auth0_id );
+ return $auth0_id_parts[0];
+ }
}
diff --git a/lib/api/WP_Auth0_Api_Change_Password.php b/lib/api/WP_Auth0_Api_Change_Password.php
new file mode 100644
index 00000000..8242f388
--- /dev/null
+++ b/lib/api/WP_Auth0_Api_Change_Password.php
@@ -0,0 +1,91 @@
+api_client_creds = $api_client_creds;
+ }
+
+ /**
+ * Set the user_id and password, make the API call, and handle the response.
+ *
+ * @param string|null $user_id - Auth0 user ID to change the password for.
+ * @param string|null $password - New password.
+ *
+ * @return bool|string
+ */
+ public function call( $user_id = null, $password = null ) {
+
+ if ( empty( $user_id ) || empty( $password ) ) {
+ return self::RETURN_ON_FAILURE;
+ }
+
+ if ( ! $this->set_bearer( 'update:users' ) ) {
+ return self::RETURN_ON_FAILURE;
+ }
+
+ return $this
+ ->set_path( 'api/v2/users/' . rawurlencode( $user_id ) )
+ ->add_body( 'password', $password )
+ ->patch()
+ ->handle_response( __METHOD__ );
+ }
+
+ /**
+ * Handle API response.
+ *
+ * @param string $method - Method that called the API.
+ *
+ * @return integer
+ */
+ protected function handle_response( $method ) {
+
+ if ( $this->handle_wp_error( $method ) ) {
+ return self::RETURN_ON_FAILURE;
+ }
+
+ if ( $this->handle_failed_response( $method ) ) {
+ $response_body = json_decode( $this->response_body );
+ if ( isset( $response_body->message ) && false !== strpos( $response_body->message, 'PasswordStrengthError' ) ) {
+ return __( 'Password is too weak, please choose a different one.', 'wp-auth0' );
+ }
+ return self::RETURN_ON_FAILURE;
+ }
+
+ return true;
+ }
+}
diff --git a/lib/api/WP_Auth0_Api_Client_Credentials.php b/lib/api/WP_Auth0_Api_Client_Credentials.php
index 417ca868..f1d9b9c6 100644
--- a/lib/api/WP_Auth0_Api_Client_Credentials.php
+++ b/lib/api/WP_Auth0_Api_Client_Credentials.php
@@ -32,7 +32,6 @@ class WP_Auth0_Api_Client_Credentials extends WP_Auth0_Api_Abstract {
* @param WP_Auth0_Options $options - WP_Auth0_Options instance.
*/
public function __construct( WP_Auth0_Options $options ) {
-
parent::__construct( $options );
$this->set_path( 'oauth/token' )
->send_client_id()
diff --git a/lib/api/WP_Auth0_Api_Delete_User_Mfa.php b/lib/api/WP_Auth0_Api_Delete_User_Mfa.php
new file mode 100644
index 00000000..e110ad7c
--- /dev/null
+++ b/lib/api/WP_Auth0_Api_Delete_User_Mfa.php
@@ -0,0 +1,86 @@
+api_client_creds = $api_client_creds;
+ }
+
+ /**
+ * Set the user_id and provider, set the authorization header, call the API, and handle the response.
+ *
+ * @param string|null $user_id - Auth0 user ID to delete the MFA provider.
+ * @param string $provider - MFA provider.
+ *
+ * @return int|mixed
+ */
+ public function call( $user_id = null, $provider = 'google-authenticator' ) {
+
+ if ( empty( $user_id ) || empty( $provider ) ) {
+ return self::RETURN_ON_FAILURE;
+ }
+
+ if ( ! $this->set_bearer( 'update:users' ) ) {
+ return self::RETURN_ON_FAILURE;
+ }
+
+ return $this
+ ->set_path( sprintf( 'api/v2/users/%s/multifactor/%s', rawurlencode( $user_id ), rawurlencode( $provider ) ) )
+ ->delete()
+ ->handle_response( __METHOD__ );
+ }
+
+ /**
+ * Handle API response.
+ *
+ * @param string $method - Method that called the API.
+ *
+ * @return integer
+ */
+ protected function handle_response( $method ) {
+
+ if ( $this->handle_wp_error( $method ) ) {
+ return self::RETURN_ON_FAILURE;
+ }
+
+ if ( $this->handle_failed_response( $method, 204 ) ) {
+ return self::RETURN_ON_FAILURE;
+ }
+
+ return 1;
+ }
+}
diff --git a/lib/api/WP_Auth0_Api_Jobs_Verification.php b/lib/api/WP_Auth0_Api_Jobs_Verification.php
index a4d72aea..a6a41572 100644
--- a/lib/api/WP_Auth0_Api_Jobs_Verification.php
+++ b/lib/api/WP_Auth0_Api_Jobs_Verification.php
@@ -34,32 +34,37 @@ class WP_Auth0_Api_Jobs_Verification extends WP_Auth0_Api_Abstract {
*
* @param WP_Auth0_Options $options - WP_Auth0_Options instance.
* @param WP_Auth0_Api_Client_Credentials $api_client_creds - WP_Auth0_Api_Client_Credentials instance.
- * @param string $user_id - Auth0 User ID.
*/
public function __construct(
WP_Auth0_Options $options,
- WP_Auth0_Api_Client_Credentials $api_client_creds,
- $user_id
+ WP_Auth0_Api_Client_Credentials $api_client_creds
) {
parent::__construct( $options );
$this->api_client_creds = $api_client_creds;
$this->set_path( 'api/v2/jobs/verification-email' )
- ->add_body( 'user_id', $user_id )
->send_client_id();
}
/**
* Set body data, make the API call, and handle the response.
*
- * @return boolean
+ * @param string|null $user_id - Auth0 user ID to send the verify email to.
+ *
+ * @return bool|mixed|null
*/
- public function call() {
+ public function call( $user_id = null ) {
+
+ if ( empty( $user_id ) ) {
+ return self::RETURN_ON_FAILURE;
+ }
if ( ! $this->set_bearer( self::API_SCOPE ) ) {
return self::RETURN_ON_FAILURE;
}
- return $this->post()->handle_response( __METHOD__ );
+ return $this->add_body( 'user_id', $user_id )
+ ->post()
+ ->handle_response( __METHOD__ );
}
/**
diff --git a/lib/profile/WP_Auth0_Profile_Change_Password.php b/lib/profile/WP_Auth0_Profile_Change_Password.php
new file mode 100644
index 00000000..21afbf46
--- /dev/null
+++ b/lib/profile/WP_Auth0_Profile_Change_Password.php
@@ -0,0 +1,89 @@
+api_change_password = $api_change_password;
+ }
+
+ /**
+ * Add actions and filters for the profile page.
+ *
+ * @codeCoverageIgnore - Tested in TestProfileChangePassword::testInitHooks()
+ */
+ public function init() {
+ add_action( 'user_profile_update_errors', array( $this, 'validate_new_password' ), 10, 2 );
+ add_action( 'validate_password_reset', array( $this, 'validate_new_password' ), 10, 2 );
+ }
+
+ /**
+ * Update the user's password at Auth0
+ * Hooked to: user_profile_update_errors, validate_password_reset
+ * IMPORTANT: Internal callback use only, do not call this function directly!
+ *
+ * @param WP_Error $errors - WP_Error object to use if validation fails.
+ * @param boolean|WP_User $user - Boolean update or WP_User instance, depending on action.
+ *
+ * @return boolean
+ */
+ public function validate_new_password( $errors, $user ) {
+
+ // Exit if we're not changing the password.
+ if ( empty( $_POST['pass1'] ) ) {
+ return false;
+ }
+ $new_password = $_POST['pass1'];
+
+ if ( isset( $_POST['user_id'] ) ) {
+ $wp_user_id = absint( $_POST['user_id'] );
+ } elseif ( is_object( $user ) && $user instanceof WP_User ) {
+ $wp_user_id = absint( $user->ID );
+ } else {
+ return false;
+ }
+
+ // Exit if this is not an Auth0 user.
+ $auth0_id = WP_Auth0_UsersRepo::get_meta( $wp_user_id, 'auth0_id' );
+ if ( empty( $auth0_id ) ) {
+ return false;
+ }
+ $strategy = WP_Auth0_Users::get_strategy( $auth0_id );
+
+ // Exit if this is not a database strategy user.
+ if ( 'auth0' !== $strategy ) {
+ return false;
+ }
+
+ $result = $this->api_change_password->call( $auth0_id, $new_password );
+
+ // Password change was successful, nothing else to do.
+ if ( true === $result ) {
+ return true;
+ }
+
+ // Password change was unsuccessful so don't change WP user account.
+ unset( $_POST['pass1'] );
+ unset( $_POST['pass1-text'] );
+ unset( $_POST['pass2'] );
+
+ // Add an error message to appear at the top of the page.
+ $error_msg = is_string( $result ) ? $result : __( 'Password could not be updated.', 'wp-auth0' );
+ $errors->add( 'auth0_password', $error_msg, array( 'form-field' => 'pass1' ) );
+ return false;
+ }
+}
diff --git a/lib/profile/WP_Auth0_Profile_Delete_Data.php b/lib/profile/WP_Auth0_Profile_Delete_Data.php
new file mode 100644
index 00000000..094ab4e7
--- /dev/null
+++ b/lib/profile/WP_Auth0_Profile_Delete_Data.php
@@ -0,0 +1,88 @@
+users_repo = $users_repo;
+ }
+
+ /**
+ * Add actions and filters for the profile page.
+ *
+ * @codeCoverageIgnore - Tested in TestProfileDeleteData::testInitHooks()
+ */
+ public function init() {
+ add_action( 'edit_user_profile', array( $this, 'show_delete_identity' ) );
+ add_action( 'show_user_profile', array( $this, 'show_delete_identity' ) );
+ add_action( 'wp_ajax_auth0_delete_data', array( $this, 'delete_user_data' ) );
+ }
+
+ /**
+ * Show the delete Auth0 user data button.
+ * Hooked to: edit_user_profile, show_user_profile
+ * IMPORTANT: Internal callback use only, do not call this function directly!
+ */
+ public function show_delete_identity() {
+ global $user_id;
+
+ if ( ! current_user_can( 'edit_users', $user_id ) ) {
+ return;
+ }
+
+ if ( ! get_auth0userinfo( $user_id ) ) {
+ return;
+ }
+
+ ?>
+
+ __( 'Empty user_id', 'wp-auth0' ) ) );
+ }
+
+ $user_id = $_POST['user_id'];
+
+ if ( ! current_user_can( 'edit_users' ) ) {
+ wp_send_json_error( array( 'error' => __( 'Forbidden', 'wp-auth0' ) ) );
+ }
+
+ $this->users_repo->delete_auth0_object( $user_id );
+ wp_send_json_success();
+ }
+}
diff --git a/lib/profile/WP_Auth0_Profile_Delete_Mfa.php b/lib/profile/WP_Auth0_Profile_Delete_Mfa.php
new file mode 100644
index 00000000..a1299169
--- /dev/null
+++ b/lib/profile/WP_Auth0_Profile_Delete_Mfa.php
@@ -0,0 +1,111 @@
+a0_options = $a0_options;
+ $this->api_delete_mfa = $api_delete_mfa;
+ }
+
+ /**
+ * Add actions and filters for the profile page.
+ *
+ * @codeCoverageIgnore - Tested in TestProfileDeleteMfa::testInitHooks()
+ */
+ public function init() {
+ add_action( 'edit_user_profile', array( $this, 'show_delete_mfa' ) );
+ add_action( 'show_user_profile', array( $this, 'show_delete_mfa' ) );
+ add_action( 'wp_ajax_auth0_delete_mfa', array( $this, 'delete_mfa' ) );
+ }
+
+ /**
+ * Show the delete Auth0 MFA data button.
+ * Hooked to: edit_user_profile, show_user_profile
+ * IMPORTANT: Internal callback use only, do not call this function directly!
+ */
+ public function show_delete_mfa() {
+ global $user_id;
+ if ( ! current_user_can( 'edit_users', $user_id ) ) {
+ return;
+ }
+
+ if ( ! $this->a0_options->get( 'mfa' ) ) {
+ return;
+ }
+
+ if ( ! get_auth0userinfo( $user_id ) ) {
+ return;
+ }
+ ?>
+
+ __( 'Empty user_id', 'wp-auth0' ) ) );
+ }
+
+ $user_id = $_POST['user_id'];
+
+ if ( ! current_user_can( 'edit_users', $user_id ) ) {
+ wp_send_json_error( array( 'error' => __( 'Forbidden', 'wp-auth0' ) ) );
+ }
+
+ $profile = get_auth0userinfo( $user_id );
+
+ if ( ! $profile || empty( $profile->sub ) ) {
+ wp_send_json_error( array( 'error' => __( 'Auth0 profile data not found', 'wp-auth0' ) ) );
+ }
+
+ if ( ! $this->api_delete_mfa->call( $profile->sub ) ) {
+ wp_send_json_error( array( 'error' => __( 'API call failed', 'wp-auth0' ) ) );
+ }
+
+ wp_send_json_success();
+ }
+}
diff --git a/phpcs-ruleset.xml b/phpcs-ruleset.xml
index 5d971e2e..6e43eaaf 100644
--- a/phpcs-ruleset.xml
+++ b/phpcs-ruleset.xml
@@ -34,6 +34,7 @@
+
diff --git a/phpcs-test-ruleset.xml b/phpcs-test-ruleset.xml
index c74ca821..a547f89f 100644
--- a/phpcs-test-ruleset.xml
+++ b/phpcs-test-ruleset.xml
@@ -14,6 +14,7 @@
+
diff --git a/tests/testApiChangePassword.php b/tests/testApiChangePassword.php
new file mode 100644
index 00000000..86d8dc07
--- /dev/null
+++ b/tests/testApiChangePassword.php
@@ -0,0 +1,200 @@
+startHttpHalting();
+ self::$options->set( 'domain', self::TEST_DOMAIN );
+
+ // Mock for a successful API call.
+ $change_password = $this->getStub( true );
+
+ // Should fail with a missing user_id and password.
+ $returned = $change_password->call();
+ $this->assertFalse( $returned );
+
+ // Should fail with a missing password.
+ $returned = $change_password->call( uniqid() );
+ $this->assertFalse( $returned );
+
+ // Should fail if not authorized to use the API.
+ $change_password = $this->getStub( false );
+ $returned = $change_password->call( uniqid(), uniqid() );
+ $this->assertFalse( $returned );
+
+ // Should succeed with a user_id + provider and set_bearer returning true.
+ $change_password = $this->getStub( true );
+ $decoded_res = [];
+ try {
+ $change_password->call( 'test|1234567890', 'strong-password' );
+ } catch ( Exception $e ) {
+ $decoded_res = unserialize( $e->getMessage() );
+ }
+
+ $this->assertNotEmpty( $decoded_res );
+ $this->assertEquals(
+ 'https://' . self::TEST_DOMAIN . '/api/v2/users/test%7C1234567890',
+ $decoded_res['url']
+ );
+ $this->assertEquals( 'PATCH', $decoded_res['method'] );
+ $this->assertArrayHasKey( 'password', $decoded_res['body'] );
+ $this->assertEquals( 'strong-password', $decoded_res['body']['password'] );
+ }
+
+ /**
+ * Test a basic Delete MFA call against a mock API server.
+ */
+ public function testCall() {
+ $this->startHttpMocking();
+ self::$options->set( 'domain', self::TEST_DOMAIN );
+
+ // Mock for a successful API call.
+ $delete_mfa = $this->getStub( true );
+
+ // 1. Make sure that a transport returns the default failed response and logs an error.
+ $this->http_request_type = 'wp_error';
+ $this->assertFalse( $delete_mfa->call( uniqid(), uniqid() ) );
+ $log = self::$error_log->get();
+ $this->assertCount( 1, $log );
+ $this->assertEquals( 'Caught WP_Error.', $log[0]['message'] );
+
+ // 2. Make sure that an Auth0 API error returns the default failed response and logs an error.
+ $this->http_request_type = 'auth0_api_error';
+ $this->assertFalse( $delete_mfa->call( uniqid(), uniqid() ) );
+ $log = self::$error_log->get();
+ $this->assertCount( 2, $log );
+ $this->assertEquals( 'caught_api_error', $log[0]['code'] );
+
+ // 4. Make sure that a weak password error returns the correct message.
+ $this->http_request_type = 'failed_weak_password';
+ $this->assertEquals(
+ 'Password is too weak, please choose a different one.',
+ $delete_mfa->call( uniqid(), uniqid() )
+ );
+ $log = self::$error_log->get();
+ $this->assertCount( 3, $log );
+ $this->assertEquals( '400', $log[0]['code'] );
+
+ // 4. Make sure it succeeds.
+ $this->http_request_type = 'success_empty_body';
+ $this->assertTrue( $delete_mfa->call( uniqid(), uniqid() ) );
+ $this->assertCount( 3, self::$error_log->get() );
+ }
+
+ /*
+ * PHPUnit overrides to run after tests.
+ */
+
+ /**
+ * Stop HTTP halting and mocking, reset JWKS transient.
+ */
+ public function tearDown() {
+ parent::tearDown();
+ self::$options->set( 'domain', null );
+ $this->stopHttpHalting();
+ $this->stopHttpMocking();
+ self::$error_log->clear();
+ $this->assertEmpty( self::$error_log->get() );
+ }
+
+ /*
+ * Test helper functions.
+ */
+
+ /**
+ * Specific mock API responses for this suite.
+ *
+ * @return array|null|WP_Error
+ */
+ public function httpMock() {
+ switch ( $this->getResponseType() ) {
+ case 'failed_weak_password':
+ return [
+ 'body' => json_encode(
+ [
+ 'statusCode' => 400,
+ 'error' => 'Bad Request',
+ 'message' => 'PasswordStrengthError: Password is too weak',
+ ]
+ ),
+ 'response' => [ 'code' => 400 ],
+ ];
+ }
+ return $this->httpMockDefault();
+ }
+
+ /**
+ * Get a mocked WP_Auth0_Api_Change_Password to return true or false for set_bearer.
+ *
+ * @param bool $set_bearer_returns - Should the set_bearer call succeed or fail.
+ *
+ * @return PHPUnit_Framework_MockObject_MockObject|WP_Auth0_Api_Change_Password
+ */
+ public function getStub( $set_bearer_returns ) {
+ $mock = $this
+ ->getMockBuilder( WP_Auth0_Api_Change_Password::class )
+ ->setMethods( [ 'set_bearer' ] )
+ ->setConstructorArgs( [ self::$options, self::$api_client_creds ] )
+ ->getMock();
+ $mock->method( 'set_bearer' )->willReturn( $set_bearer_returns );
+ return $mock;
+ }
+}
diff --git a/tests/testApiClientCredentials.php b/tests/testApiClientCredentials.php
index 290b00a8..86a299fc 100644
--- a/tests/testApiClientCredentials.php
+++ b/tests/testApiClientCredentials.php
@@ -152,11 +152,6 @@ public function testCall() {
* @return array|null|WP_Error
*/
public function httpMock() {
- $parent_mock = $this->httpMockDefault();
- if ( ! is_null( $parent_mock ) ) {
- return $parent_mock;
- }
-
switch ( $this->getResponseType() ) {
case 'access_token':
return [
@@ -164,6 +159,7 @@ public function httpMock() {
'response' => [ 'code' => 200 ],
];
}
+ return $this->httpMockDefault();
}
/**
@@ -173,6 +169,8 @@ public function tearDown() {
parent::tearDown();
$this->stopHttpHalting();
$this->stopHttpMocking();
+ self::$error_log->clear();
+ $this->assertEmpty( self::$error_log->get() );
delete_transient( WPA0_JWKS_CACHE_TRANSIENT_NAME );
}
}
diff --git a/tests/testApiDeleteMfa.php b/tests/testApiDeleteMfa.php
new file mode 100644
index 00000000..12a52a72
--- /dev/null
+++ b/tests/testApiDeleteMfa.php
@@ -0,0 +1,176 @@
+startHttpHalting();
+ self::$options->set( 'domain', self::TEST_DOMAIN );
+
+ // Should fail with a missing user_id.
+ $delete_mfa = $this->getStub( true );
+ $returned = $delete_mfa->call();
+ $this->assertEquals( 0, $returned );
+
+ // Should fail if not authorized to use the API.
+ $delete_mfa = $this->getStub( false );
+ $returned = $delete_mfa->call( uniqid() );
+ $this->assertEquals( 0, $returned );
+
+ // Should succeed with a user_id + provider and set_bearer returning true.
+ $delete_mfa = $this->getStub( true );
+ $decoded_res = [];
+ try {
+ $delete_mfa->call( 'test|1234567890', 'a-provider' );
+ } catch ( Exception $e ) {
+ $decoded_res = unserialize( $e->getMessage() );
+ }
+
+ $this->assertNotEmpty( $decoded_res );
+ $this->assertEquals(
+ 'https://' . self::TEST_DOMAIN . '/api/v2/users/test%7C1234567890/multifactor/a-provider',
+ $decoded_res['url']
+ );
+ $this->assertEquals( 'DELETE', $decoded_res['method'] );
+ $this->assertEmpty( $decoded_res['body'] );
+ }
+
+ /**
+ * Test a basic Delete MFA call against a mock API server.
+ */
+ public function testCall() {
+ $this->startHttpMocking();
+ self::$options->set( 'domain', self::TEST_DOMAIN );
+
+ $delete_mfa = $this->getStub( true );
+
+ // 1. Make sure that a transport returns the default failed response and logs an error.
+ $this->http_request_type = 'wp_error';
+ $this->assertEquals( 0, $delete_mfa->call( uniqid() ) );
+ $log = self::$error_log->get();
+ $this->assertCount( 1, $log );
+ $this->assertEquals( 'Caught WP_Error.', $log[0]['message'] );
+
+ // 2. Make sure that an Auth0 API error returns the default failed response and logs an error.
+ $this->http_request_type = 'auth0_api_error';
+ $this->assertEquals( 0, $delete_mfa->call( uniqid() ) );
+ $log = self::$error_log->get();
+ $this->assertCount( 2, $log );
+ $this->assertEquals( 'caught_api_error', $log[0]['code'] );
+
+ // 3. Make sure it succeeds.
+ $this->http_request_type = 'success_delete_empty_body';
+ $this->assertEquals( 1, $delete_mfa->call( uniqid() ) );
+ $this->assertCount( 2, self::$error_log->get() );
+ }
+
+ /*
+ * PHPUnit overrides to run after tests.
+ */
+
+ /**
+ * Stop HTTP halting and mocking, reset JWKS transient.
+ */
+ public function tearDown() {
+ parent::tearDown();
+ self::$options->set( 'domain', null );
+ $this->stopHttpHalting();
+ $this->stopHttpMocking();
+ self::$error_log->clear();
+ $this->assertEmpty( self::$error_log->get() );
+ }
+
+ /*
+ * Test helper functions.
+ */
+
+ /**
+ * Specific mock API responses for this suite.
+ *
+ * @return array|null|WP_Error
+ */
+ public function httpMock() {
+ switch ( $this->getResponseType() ) {
+ case 'success_delete_empty_body':
+ return [
+ 'body' => '',
+ 'response' => [ 'code' => 204 ],
+ ];
+ }
+ return $this->httpMockDefault();
+ }
+
+ /**
+ * Get a mocked WP_Auth0_Api_Delete_User_Mfa to return true or false for set_bearer.
+ *
+ * @param bool $set_bearer_returns - Should the set_bearer call succeed or fail.
+ *
+ * @return PHPUnit_Framework_MockObject_MockObject|WP_Auth0_Api_Delete_User_Mfa
+ */
+ public function getStub( $set_bearer_returns ) {
+ $mock = $this
+ ->getMockBuilder( WP_Auth0_Api_Delete_User_Mfa::class )
+ ->setMethods( [ 'set_bearer' ] )
+ ->setConstructorArgs( [ self::$options, self::$api_client_creds ] )
+ ->getMock();
+ $mock->method( 'set_bearer' )->willReturn( $set_bearer_returns );
+ return $mock;
+ }
+}
diff --git a/tests/testApiJobsVerification.php b/tests/testApiJobsVerification.php
new file mode 100644
index 00000000..67d384b1
--- /dev/null
+++ b/tests/testApiJobsVerification.php
@@ -0,0 +1,196 @@
+startHttpHalting();
+ self::$options->set( 'domain', self::TEST_DOMAIN );
+ self::$options->set( 'client_id', self::TEST_CLIENT_ID );
+
+ // Should fail without a user_id.
+ $jobs_verification = $this->getStub( true );
+ $returned = $jobs_verification->call();
+ $this->assertFalse( $returned );
+
+ // Should fail if not authorized to use the API.
+ $jobs_verification = $this->getStub( false );
+ $returned = $jobs_verification->call( self::TEST_USER_ID );
+ $this->assertFalse( $returned );
+
+ // Should succeed with a user_id + provider and set_bearer returning true.
+ $jobs_verification = $this->getStub( true );
+ $decoded_res = [];
+ try {
+ $jobs_verification->call( self::TEST_USER_ID );
+ } catch ( Exception $e ) {
+ $decoded_res = unserialize( $e->getMessage() );
+ }
+
+ $this->assertNotEmpty( $decoded_res );
+ $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/api/v2/jobs/verification-email', $decoded_res['url'] );
+ $this->assertEquals( 'POST', $decoded_res['method'] );
+ $this->assertArrayHasKey( 'user_id', $decoded_res['body'] );
+ $this->assertEquals( self::TEST_USER_ID, $decoded_res['body']['user_id'] );
+ $this->assertArrayHasKey( 'client_id', $decoded_res['body'] );
+ $this->assertEquals( self::TEST_CLIENT_ID, $decoded_res['body']['client_id'] );
+ }
+
+ /**
+ * Test a basic Delete MFA call against a mock API server.
+ */
+ public function testCall() {
+ $this->startHttpMocking();
+ self::$options->set( 'domain', self::TEST_DOMAIN );
+ self::$options->set( 'client_id', self::TEST_CLIENT_ID );
+
+ $jobs_verification = $this->getStub( true );
+
+ // 1. Make sure that a transport returns the default failed response and logs an error.
+ $this->http_request_type = 'wp_error';
+ $this->assertFalse( $jobs_verification->call( self::TEST_USER_ID ) );
+ $log = self::$error_log->get();
+ $this->assertCount( 1, $log );
+ $this->assertEquals( 'Caught WP_Error.', $log[0]['message'] );
+
+ // 2. Make sure that an Auth0 API error returns the default failed response and logs an error.
+ $this->http_request_type = 'auth0_api_error';
+ $this->assertFalse( $jobs_verification->call( self::TEST_USER_ID ) );
+ $log = self::$error_log->get();
+ $this->assertCount( 2, $log );
+ $this->assertEquals( 'caught_api_error', $log[0]['code'] );
+
+ // 3. Make sure it succeeds.
+ $this->http_request_type = 'success_job_email_verification';
+ $this->assertTrue( $jobs_verification->call( self::TEST_USER_ID ) );
+ $this->assertCount( 2, self::$error_log->get() );
+ }
+
+ /*
+ * PHPUnit overrides to run after tests.
+ */
+
+ /**
+ * Stop HTTP halting and mocking, reset JWKS transient.
+ */
+ public function tearDown() {
+ parent::tearDown();
+ self::$options->set( 'domain', null );
+ self::$options->set( 'client_id', null );
+ $this->stopHttpHalting();
+ $this->stopHttpMocking();
+ self::$error_log->clear();
+ $this->assertEmpty( self::$error_log->get() );
+ }
+
+ /*
+ * Test helper functions.
+ */
+
+ /**
+ * Specific mock API responses for this suite.
+ *
+ * @return array|null|WP_Error
+ */
+ public function httpMock() {
+ switch ( $this->getResponseType() ) {
+ case 'success_job_email_verification':
+ return [
+ 'body' => json_encode(
+ [
+ 'type' => 'verification_email',
+ 'status' => 'pending',
+ 'created_at' => date( 'c' ),
+ 'id' => 'job_' . uniqid(),
+ ]
+ ),
+ 'response' => [ 'code' => 201 ],
+ ];
+ }
+ return $this->httpMockDefault();
+ }
+
+ /**
+ * Get a mocked WP_Auth0_Api_Jobs_Verification to return true or false for set_bearer.
+ *
+ * @param bool $set_bearer_returns - Should the set_bearer call succeed or fail.
+ *
+ * @return PHPUnit_Framework_MockObject_MockObject|WP_Auth0_Api_Jobs_Verification
+ */
+ public function getStub( $set_bearer_returns ) {
+ $mock = $this
+ ->getMockBuilder( WP_Auth0_Api_Jobs_Verification::class )
+ ->setMethods( [ 'set_bearer' ] )
+ ->setConstructorArgs( [ self::$options, self::$api_client_creds, self::TEST_USER_ID ] )
+ ->getMock();
+ $mock->method( 'set_bearer' )->willReturn( $set_bearer_returns );
+ return $mock;
+ }
+}
diff --git a/tests/testEditProfile.php b/tests/testEditProfile.php
new file mode 100644
index 00000000..2bea7d01
--- /dev/null
+++ b/tests/testEditProfile.php
@@ -0,0 +1,112 @@
+ [
+ 'priority' => 1,
+ 'accepted_args' => 1,
+ ],
+ ];
+ $this->assertHooked( 'personal_options_update', 'WP_Auth0_EditProfile', $expect_hooked );
+
+ // Test page-specific JS enqueuing.
+ $expect_hooked = [
+ 'admin_enqueue_scripts' => [
+ 'priority' => 10,
+ 'accepted_args' => 1,
+ ],
+ ];
+
+ $pagenow = 'profile.php';
+ $this->clear_hooks( 'admin_enqueue_scripts' );
+ self::$editProfile->init();
+ $this->assertHooked( 'admin_enqueue_scripts', 'WP_Auth0_EditProfile', $expect_hooked );
+
+ $pagenow = 'user-edit.php';
+ $this->clear_hooks( 'admin_enqueue_scripts' );
+ self::$editProfile->init();
+ $this->assertHooked( 'admin_enqueue_scripts', 'WP_Auth0_EditProfile', $expect_hooked );
+ }
+}
diff --git a/tests/testEmailVerification.php b/tests/testEmailVerification.php
index 780c9007..d90807cd 100644
--- a/tests/testEmailVerification.php
+++ b/tests/testEmailVerification.php
@@ -63,7 +63,7 @@ public static function setUpBeforeClass() {
* Test the the AJAX handler function is hooked properly.
*/
public function testHooks() {
- $hooked = $this->getHooked( 'wp_ajax_nopriv_resend_verification_email' );
+ $hooked = $this->get_hook( 'wp_ajax_nopriv_resend_verification_email' );
$this->assertNotEmpty( $hooked[0] );
$this->assertEquals( 'wp_auth0_ajax_resend_verification_email', $hooked[0]['function'] );
@@ -121,7 +121,7 @@ public function testWpRenderDie() {
* Test AJAX email verification send.
*/
public function testResendVerificationEmail() {
- $this->start_ajax();
+ $this->startAjaxHalting();
// 1. Should fail with a bad nonce.
$caught_exception = false;
diff --git a/tests/testProfileChangePassword.php b/tests/testProfileChangePassword.php
new file mode 100644
index 00000000..55917ab9
--- /dev/null
+++ b/tests/testProfileChangePassword.php
@@ -0,0 +1,138 @@
+ [
+ 'priority' => 10,
+ 'accepted_args' => 2,
+ ],
+ ];
+ // Same method hooked to both actions.
+ $this->assertHooked( 'user_profile_update_errors', 'WP_Auth0_Profile_Change_Password', $expect_hooked );
+ $this->assertHooked( 'validate_password_reset', 'WP_Auth0_Profile_Change_Password', $expect_hooked );
+ }
+
+ /**
+ * Test that the validate_new_password method works as expected.
+ */
+ public function testValidateNewPassword() {
+ $errors = new WP_Error();
+
+ $user_data = $this->createUser();
+ $user_id = $user_data->ID;
+ $user_obj = get_user_by( 'id', $user_id );
+
+ // Create a stub for the WP_Auth0_Api_Change_Password class.
+ $mock_api_test_password = $this
+ ->getMockBuilder( WP_Auth0_Api_Change_Password::class )
+ ->setMethods( [ 'call' ] )
+ ->setConstructorArgs( [ self::$options, self::$api_client_creds ] )
+ ->getMock();
+ $mock_api_test_password->method( 'call' )->willReturn( true, false );
+
+ $change_password = new WP_Auth0_Profile_Change_Password( $mock_api_test_password );
+
+ // Call should fail because of a missing password.
+ $this->assertFalse( $change_password->validate_new_password( $errors, false ) );
+
+ $_POST['pass1'] = uniqid();
+
+ // Call should fail with a password because of a missing user ID.
+ $this->assertFalse( $change_password->validate_new_password( $errors, false ) );
+
+ // Call should fail with a user object or user_id in $_POST because of no Auth0 data stored.
+ $this->assertFalse( $change_password->validate_new_password( $errors, $user_obj ) );
+ $_POST['user_id'] = $user_id;
+ $this->assertFalse( $change_password->validate_new_password( $errors, false ) );
+
+ $this->storeAuth0Data( $user_id, 'not-auth0' );
+
+ // Call should fail with Auth0 data stored because of a wrong strategy.
+ $this->assertFalse( $change_password->validate_new_password( $errors, false ) );
+
+ $this->storeAuth0Data( $user_id );
+
+ // Call should succeed with a mocked API.
+ $this->assertTrue( $change_password->validate_new_password( $errors, false ) );
+
+ // Call should fail on the second call with a mocked API.
+ $this->assertFalse( $change_password->validate_new_password( $errors, false ) );
+ $this->assertEquals( 'Password could not be updated.', $errors->errors['auth0_password'][0] );
+ $this->assertEquals( 'pass1', $errors->error_data['auth0_password']['form-field'] );
+ $this->assertFalse( isset( $_POST['pass1'] ) );
+ }
+}
diff --git a/tests/testProfileDeleteData.php b/tests/testProfileDeleteData.php
new file mode 100644
index 00000000..510d4b9b
--- /dev/null
+++ b/tests/testProfileDeleteData.php
@@ -0,0 +1,215 @@
+ [
+ 'priority' => 10,
+ 'accepted_args' => 1,
+ ],
+ ];
+ // Same method hooked to both actions.
+ $this->assertHooked( 'edit_user_profile', 'WP_Auth0_Profile_Delete_Data', $expect_hooked );
+ $this->assertHooked( 'show_user_profile', 'WP_Auth0_Profile_Delete_Data', $expect_hooked );
+
+ $expect_hooked = [
+ 'delete_user_data' => [
+ 'priority' => 10,
+ 'accepted_args' => 1,
+ ],
+ ];
+ $this->assertHooked( 'wp_ajax_auth0_delete_data', 'WP_Auth0_Profile_Delete_Data', $expect_hooked );
+ }
+
+ /**
+ * Test that a delete_user_data AJAX call with no nonce fails.
+ */
+ public function testThatAjaxFailsWithNoNonce() {
+ $this->startAjaxHalting();
+ $caught_exception = false;
+ try {
+ self::$delete_data->delete_user_data();
+ } catch ( Exception $e ) {
+ $caught_exception = ( 'bad_nonce' === $e->getMessage() );
+ }
+ $this->assertTrue( $caught_exception );
+ }
+
+ /**
+ * Test that a delete_user_data AJAX call with no user_id fails.
+ */
+ public function testThatAjaxFailsWithNoUserId() {
+ $this->startAjaxHalting();
+
+ // Set the nonce.
+ $_REQUEST['_ajax_nonce'] = wp_create_nonce( 'delete_auth0_identity' );
+
+ $caught_exception = false;
+ ob_start();
+ try {
+ self::$delete_data->delete_user_data();
+ } catch ( Exception $e ) {
+ $caught_exception = ( 'die_ajax' === $e->getMessage() );
+ }
+ $return_json = ob_get_clean();
+
+ $this->assertTrue( $caught_exception );
+ $this->assertEquals( '{"success":false,"data":{"error":"Empty user_id"}}', $return_json );
+ }
+
+ /**
+ * Test that a delete_user_data AJAX call with a non-admin user fails.
+ */
+ public function testThatAjaxFailsWithNoAdmin() {
+ $this->startAjaxHalting();
+
+ // Set the nonce.
+ $_REQUEST['_ajax_nonce'] = wp_create_nonce( 'delete_auth0_identity' );
+
+ // Set the user ID.
+ $_POST['user_id'] = 1;
+
+ $caught_exception = false;
+ ob_start();
+ try {
+ self::$delete_data->delete_user_data();
+ } catch ( Exception $e ) {
+ $caught_exception = ( 'die_ajax' === $e->getMessage() );
+ }
+ $return_json = ob_get_clean();
+
+ $this->assertTrue( $caught_exception );
+ $this->assertEquals( '{"success":false,"data":{"error":"Forbidden"}}', $return_json );
+ }
+
+ /**
+ * Test that a delete_user_data AJAX call can succeed.
+ */
+ public function testThatAjaxCallSucceeds() {
+ $this->startAjaxReturn();
+
+ // Set the user ID.
+ $_POST['user_id'] = 1;
+
+ // Set the admin user, store Auth0 profile data to delete, and reset the nonce.
+ $this->setGlobalUser();
+ $this->storeAuth0Data( 1 );
+ $_REQUEST['_ajax_nonce'] = wp_create_nonce( 'delete_auth0_identity' );
+
+ // Make sure we have data to delete.
+ $this->assertNotEmpty( WP_Auth0_UsersRepo::get_meta( 1, 'auth0_id' ) );
+ $this->assertNotEmpty( WP_Auth0_UsersRepo::get_meta( 1, 'auth0_obj' ) );
+ $this->assertNotEmpty( WP_Auth0_UsersRepo::get_meta( 1, 'last_update' ) );
+
+ ob_start();
+ self::$delete_data->delete_user_data();
+ $this->assertEquals( '{"success":true}', ob_get_clean() );
+ $this->assertEmpty( WP_Auth0_UsersRepo::get_meta( 1, 'auth0_id' ) );
+ $this->assertEmpty( WP_Auth0_UsersRepo::get_meta( 1, 'auth0_obj' ) );
+ $this->assertEmpty( WP_Auth0_UsersRepo::get_meta( 1, 'last_update' ) );
+ }
+
+ /**
+ * Test that the ID delete control appears under certain conditions.
+ */
+ public function testShowDeleteIdentity() {
+ // Should not show this control if not an admin.
+ ob_start();
+ self::$delete_data->show_delete_identity();
+ $this->assertEmpty( ob_get_clean() );
+
+ $user_id = $this->setGlobalUser();
+
+ // Should not show this control if user is not an Auth0-connected user.
+ ob_start();
+ self::$delete_data->show_delete_identity();
+ $this->assertEmpty( ob_get_clean() );
+
+ $this->storeAuth0Data( $user_id );
+
+ ob_start();
+ self::$delete_data->show_delete_identity();
+ $delete_id_html = ob_get_clean();
+
+ // Make sure we have the id attribute that connects to the AJAX action.
+ $input = $this->getDomListFromTagName( $delete_id_html, 'input' );
+ $this->assertEquals( 1, $input->length );
+ $this->assertEquals( 'auth0_delete_data', $input->item( 0 )->getAttribute( 'id' ) );
+
+ // Make sure we have a table with the right class.
+ $table = $this->getDomListFromTagName( $delete_id_html, 'table' );
+ $this->assertEquals( 1, $table->length );
+ $this->assertEquals( 'form-table', $table->item( 0 )->getAttribute( 'class' ) );
+ }
+
+ /*
+ * PHPUnit overrides to run after tests.
+ */
+
+ /**
+ * Runs after each test completes.
+ */
+ public function tearDown() {
+ parent::tearDown();
+ $this->stopAjaxHalting();
+ $this->stopAjaxReturn();
+ }
+}
diff --git a/tests/testProfileDeleteMfa.php b/tests/testProfileDeleteMfa.php
new file mode 100644
index 00000000..b3cd2f19
--- /dev/null
+++ b/tests/testProfileDeleteMfa.php
@@ -0,0 +1,314 @@
+ [
+ 'priority' => 10,
+ 'accepted_args' => 1,
+ ],
+ ];
+ // Same method hooked to both actions.
+ $this->assertHooked( 'edit_user_profile', 'WP_Auth0_Profile_Delete_Mfa', $expect_hooked );
+ $this->assertHooked( 'show_user_profile', 'WP_Auth0_Profile_Delete_Mfa', $expect_hooked );
+
+ $expect_hooked = [
+ 'delete_mfa' => [
+ 'priority' => 10,
+ 'accepted_args' => 1,
+ ],
+ ];
+ $this->assertHooked( 'wp_ajax_auth0_delete_mfa', 'WP_Auth0_Profile_Delete_Mfa', $expect_hooked );
+ }
+
+ /**
+ * Test that an AJAX call with no nonce fails.
+ */
+ public function testThatAjaxFailsWithNoNonce() {
+ $this->startAjaxHalting();
+
+ // No nonce set should fail.
+ $caught_exception = false;
+ try {
+ self::$delete_mfa->delete_mfa();
+ } catch ( Exception $e ) {
+ $caught_exception = ( 'bad_nonce' === $e->getMessage() );
+ }
+ $this->assertTrue( $caught_exception );
+ }
+
+ /**
+ * Test that an AJAX call with no user_id fails.
+ */
+ public function testThatAjaxFailsWithNoUserId() {
+ $this->startAjaxHalting();
+
+ // Set the nonce.
+ $_REQUEST['_ajax_nonce'] = wp_create_nonce( 'delete_auth0_mfa' );
+
+ ob_start();
+ $caught_exception = false;
+ try {
+ self::$delete_mfa->delete_mfa();
+ } catch ( Exception $e ) {
+ $caught_exception = ( 'die_ajax' === $e->getMessage() );
+ }
+
+ $this->assertTrue( $caught_exception );
+ $this->assertEquals( '{"success":false,"data":{"error":"Empty user_id"}}', ob_get_clean() );
+ }
+
+ /**
+ * Test that an AJAX call with no admin user fails.
+ */
+ public function testThatAjaxFailsWithNoAdmin() {
+ $this->startAjaxHalting();
+
+ // Set the nonce.
+ $_REQUEST['_ajax_nonce'] = wp_create_nonce( 'delete_auth0_mfa' );
+
+ // Set the user ID.
+ $_POST['user_id'] = 1;
+
+ ob_start();
+ $caught_exception = false;
+ try {
+ self::$delete_mfa->delete_mfa();
+ } catch ( Exception $e ) {
+ $caught_exception = ( 'die_ajax' === $e->getMessage() );
+ }
+
+ $this->assertTrue( $caught_exception );
+ $this->assertEquals( '{"success":false,"data":{"error":"Forbidden"}}', ob_get_clean() );
+ }
+
+ /**
+ * Test that an AJAX call with no Auth0 data fails.
+ */
+ public function testThatAjaxFailsWithNoAuth0Data() {
+ $this->startAjaxHalting();
+
+ // Set the nonce.
+ $_REQUEST['_ajax_nonce'] = wp_create_nonce( 'delete_auth0_mfa' );
+
+ // Set the user ID.
+ $_POST['user_id'] = 1;
+
+ // Set the admin user and nonce.
+ $this->setGlobalUser();
+ $_REQUEST['_ajax_nonce'] = wp_create_nonce( 'delete_auth0_mfa' );
+
+ ob_start();
+ $caught_exception = false;
+ try {
+ self::$delete_mfa->delete_mfa();
+ } catch ( Exception $e ) {
+ $caught_exception = ( 'die_ajax' === $e->getMessage() );
+ }
+
+ $this->assertTrue( $caught_exception );
+ $this->assertEquals( '{"success":false,"data":{"error":"Auth0 profile data not found"}}', ob_get_clean() );
+ }
+
+ /**
+ * Test that the delete MFA action works as expected.
+ */
+ public function testDeleteMfaAjax() {
+ $this->startAjaxHalting();
+
+ // Set the user ID.
+ $_POST['user_id'] = 1;
+
+ // Set the admin user and nonce.
+ $this->setGlobalUser();
+ $_REQUEST['_ajax_nonce'] = wp_create_nonce( 'delete_auth0_mfa' );
+
+ // Set Auth0 profile.
+ $this->storeAuth0Data( 1 );
+
+ // Mocked to simulate a failed API call.
+ $delete_mfa = $this->getStub( false );
+ $caught_exception = false;
+
+ ob_start();
+ try {
+ $delete_mfa->delete_mfa();
+ } catch ( Exception $e ) {
+ $caught_exception = ( 'die_ajax' === $e->getMessage() );
+ }
+
+ $this->assertTrue( $caught_exception );
+ $this->assertEquals( '{"success":false,"data":{"error":"API call failed"}}', ob_get_clean() );
+ }
+
+ /**
+ * Test that an AJAX call will succeed.
+ */
+ public function testThatAjaxCallSucceeds() {
+ $this->startAjaxReturn();
+
+ $this->setGlobalUser();
+ $this->storeAuth0Data( 1 );
+ $_REQUEST['_ajax_nonce'] = wp_create_nonce( 'delete_auth0_mfa' );
+ $_POST['user_id'] = 1;
+
+ // Mocked to simulate a successful API call.
+ $delete_mfa = $this->getStub( true );
+
+ ob_start();
+ $delete_mfa->delete_mfa();
+ $this->assertEquals( '{"success":true}', ob_get_clean() );
+ }
+
+ /**
+ * Test that the ID delete control appears under certain conditions.
+ */
+ public function testShowDeleteMfa() {
+ // Should not show this control if not an admin.
+ ob_start();
+ self::$delete_mfa->show_delete_mfa();
+ $this->assertEmpty( ob_get_clean() );
+
+ $user_id = $this->setGlobalUser();
+
+ // Should not show this control if MFA is not turned on.
+ ob_start();
+ self::$delete_mfa->show_delete_mfa();
+ $this->assertEmpty( ob_get_clean() );
+
+ self::$options->set( 'mfa', 1 );
+
+ // Should not show this control if user is not an Auth0-connected user.
+ ob_start();
+ self::$delete_mfa->show_delete_mfa();
+ $this->assertEmpty( ob_get_clean() );
+
+ $this->storeAuth0Data( $user_id );
+
+ ob_start();
+ self::$delete_mfa->show_delete_mfa();
+ $delete_mfa_html = ob_get_clean();
+
+ $this->assertNotEmpty( $delete_mfa_html );
+
+ // Make sure we have the id attribute that connects to the AJAX action.
+ $input = $this->getDomListFromTagName( $delete_mfa_html, 'input' );
+ $this->assertEquals( 1, $input->length );
+ $this->assertEquals( 'auth0_delete_mfa', $input->item( 0 )->getAttribute( 'id' ) );
+
+ // Make sure we have a table with the right class.
+ $table = $this->getDomListFromTagName( $delete_mfa_html, 'table' );
+ $this->assertEquals( 1, $table->length );
+ $this->assertEquals( 'form-table', $table->item( 0 )->getAttribute( 'class' ) );
+ }
+
+ /*
+ * PHPUnit overrides to run after tests.
+ */
+
+ /**
+ * Runs after each test completes.
+ */
+ public function tearDown() {
+ parent::tearDown();
+ $this->stopAjaxHalting();
+ $this->stopAjaxReturn();
+ }
+
+ /*
+ * Test helper functions.
+ */
+
+ /**
+ * Creates a new WP_Auth0_Profile_Delete_Mfa object with a mocked API.
+ *
+ * @param boolean $return - Whether the call method should return true or false.
+ *
+ * @return WP_Auth0_Profile_Delete_Mfa
+ */
+ public function getStub( $return ) {
+ // Create a stub for the WP_Auth0_Api_Delete_User_Mfa class.
+ $mock_api_delete_mfa = $this
+ ->getMockBuilder( WP_Auth0_Api_Delete_User_Mfa::class )
+ ->setMethods( [ 'call', 'set_bearer' ] )
+ ->setConstructorArgs( [ self::$options, self::$api_client_creds ] )
+ ->getMock();
+ $mock_api_delete_mfa->method( 'set_bearer' )->willReturn( true );
+ $mock_api_delete_mfa->method( 'call' )->willReturn( $return );
+
+ return new WP_Auth0_Profile_Delete_Mfa( self::$options, $mock_api_delete_mfa );
+ }
+}
diff --git a/tests/traits/ajaxHelpers.php b/tests/traits/ajaxHelpers.php
index 03106094..7aa51d82 100644
--- a/tests/traits/ajaxHelpers.php
+++ b/tests/traits/ajaxHelpers.php
@@ -13,21 +13,25 @@
trait AjaxHelpers {
/**
- * Set filters for processing AJAX tests.
+ * Set a filter to halt AJAX requests with an exception.
* Call at the top of tests that use AJAX handler functions.
*/
- public function start_ajax() {
+ public function startAjaxHalting() {
add_filter( 'wp_doing_ajax', '__return_true' );
- add_filter(
- 'wp_die_ajax_handler',
- function() {
- return [ $this, 'stop_ajax' ];
- }
- );
+ add_filter( 'wp_die_ajax_handler', [ $this, 'startAjaxHaltingHook' ] );
}
/**
- * Stop AJAX requests from dying.
+ * Returns the function used to halt an AJAX request.
+ *
+ * @return array
+ */
+ public function startAjaxHaltingHook() {
+ return [ $this, 'haltAjax' ];
+ }
+
+ /**
+ * Stop AJAX requests by throwing an exception.
* Hooked to: wp_die_ajax_handler
*
* @param string}int $message - Message for die page.
@@ -36,7 +40,7 @@ function() {
*
* @throws Exception - Always, to stop AJAX process.
*/
- public function stop_ajax( $message, $title, $args ) {
+ public function haltAjax( $message, $title, $args ) {
if ( -1 === $message && ! empty( $args['response'] ) && 403 === $args['response'] && empty( $title ) ) {
$error_msg = 'bad_nonce';
} else {
@@ -44,4 +48,50 @@ public function stop_ajax( $message, $title, $args ) {
}
throw new Exception( $error_msg );
}
+
+ /**
+ * Remove the filter that halts AJAX requests.
+ * Call this in a test suites tearDown method.
+ */
+ public function stopAjaxHalting() {
+ remove_filter( 'wp_doing_ajax', '__return_true' );
+ remove_filter( 'wp_die_ajax_handler', [ $this, 'startAjaxHaltingHook' ] );
+ }
+
+ /**
+ * Return AJAX request messages.
+ * Call at the top of tests that use AJAX handler functions.
+ */
+ public function startAjaxReturn() {
+ add_filter( 'wp_doing_ajax', '__return_true' );
+ add_filter( 'wp_die_ajax_handler', [ $this, 'startAjaxReturnHook' ] );
+ }
+
+ /**
+ * Returns the function used to return an AJAX request message.
+ *
+ * @return array
+ */
+ public function startAjaxReturnHook() {
+ return [ $this, 'ajaxReturn' ];
+ }
+
+ /**
+ * Prevent the wp_die page from dying and echo the message passed.
+ * Hooked to: wp_die_handler
+ *
+ * @param string $message - HTML to show on the wp_die page.
+ */
+ public function ajaxReturn( $message ) {
+ echo $message;
+ }
+
+ /**
+ * Remove the filter that returns AJAX messages.
+ * Call this in a test suites tearDown method.
+ */
+ public function stopAjaxReturn() {
+ remove_filter( 'wp_doing_ajax', '__return_true', 10 );
+ remove_filter( 'wp_die_ajax_handler', [ $this, 'startAjaxReturnHook' ], 10 );
+ }
}
diff --git a/tests/traits/hookHelpers.php b/tests/traits/hookHelpers.php
index 84c44f29..b301a8b4 100644
--- a/tests/traits/hookHelpers.php
+++ b/tests/traits/hookHelpers.php
@@ -19,7 +19,7 @@ trait HookHelpers {
*
* @return array
*/
- public function getHooked( $hook = '' ) {
+ public function get_hook( $hook = '' ) {
global $wp_filter;
if ( isset( $wp_filter[ $hook ]->callbacks ) ) {
@@ -58,6 +58,16 @@ public function getHooked( $hook = '' ) {
return $hooks;
}
+ /**
+ * Remove all hooked functions from a hook.
+ *
+ * @param string $hook - Hook to clear.
+ */
+ public function clear_hooks( $hook = '' ) {
+ global $wp_filter;
+ unset( $wp_filter[ $hook ] );
+ }
+
/**
* Assert that hooked functions exists with the correct priority and arg numbers.
*
@@ -68,7 +78,7 @@ public function getHooked( $hook = '' ) {
* @return void
*/
public function assertHooked( $hook_name, $function, array $hooked ) {
- $hooks = $this->getHooked( $hook_name );
+ $hooks = $this->get_hook( $hook_name );
$found = 0;
foreach ( $hooks as $hook ) {
diff --git a/tests/traits/usersHelper.php b/tests/traits/usersHelper.php
index 4138daac..235dcc59 100644
--- a/tests/traits/usersHelper.php
+++ b/tests/traits/usersHelper.php
@@ -11,6 +11,13 @@
*/
trait UsersHelper {
+ /**
+ * WP_Auth0_UsersRepo instance.
+ *
+ * @var WP_Auth0_UsersRepo
+ */
+ protected static $users_repo;
+
/**
* Create a new User.
*
@@ -46,4 +53,28 @@ public function getUserinfo( $strategy = 'test-strategy' ) {
$userinfo->email = $name . '@example.com';
return $userinfo;
}
+
+ /**
+ * Set the global WP user.
+ *
+ * @param int $set_uid - WP user ID to set.
+ *
+ * @return int
+ */
+ public function setGlobalUser( $set_uid = 1 ) {
+ global $user_id;
+ $user_id = $set_uid;
+ wp_set_current_user( $user_id );
+ return $user_id;
+ }
+
+ /**
+ * Store dummy Auth0 data.
+ *
+ * @param int $user_id - WP user ID to set.
+ * @param string $strategy - Auth0 user strategy to use.
+ */
+ public function storeAuth0Data( $user_id, $strategy = 'auth0' ) {
+ self::$users_repo->update_auth0_object( $user_id, $this->getUserinfo( $strategy ) );
+ }
}