Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for email update in migration-ws-get-user endpoint #674

Merged
merged 6 commits into from
Apr 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions lib/WP_Auth0_Routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ protected function migration_ws_login() {
$user = wp_authenticate( $_POST['username'], $_POST['password'] );

if ( is_wp_error( $user ) ) {
throw new Exception( __( 'Invalid Credentials', 'wp-auth0' ), 401 );
throw new Exception( __( 'Invalid credentials', 'wp-auth0' ), 401 );
}

unset( $user->data->user_pass );
Expand All @@ -198,6 +198,7 @@ protected function migration_ws_login() {

/**
* User migration get user route used by custom database Login script.
* This is used for email changes made in Auth0.
*
* @return array
*
Expand All @@ -221,7 +222,15 @@ protected function migration_ws_get_user() {
}

if ( ! $user ) {
throw new Exception( __( 'Invalid Credentials', 'wp-auth0' ), 401 );
throw new Exception( __( 'User not found', 'wp-auth0' ), 401 );
}

$updated_email = WP_Auth0_UsersRepo::get_meta( $user->ID, WP_Auth0_Profile_Change_Email::UPDATED_EMAIL );
if ( $updated_email === $user->data->user_email ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return early so no error is logged.

return array(
'status' => 200,
'error' => 'Email update in process',
);
}

unset( $user->data->user_pass );
Expand Down
47 changes: 38 additions & 9 deletions lib/WP_Auth0_UsersRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,16 +219,14 @@ public function find_auth0_user( $id ) {
* @param stdClass $userinfo - User profile object from Auth0.
*/
public function update_auth0_object( $user_id, $userinfo ) {
global $wpdb;

$auth0_user_id = isset( $userinfo->user_id ) ? $userinfo->user_id : $userinfo->sub;
update_user_meta( $user_id, $wpdb->prefix . 'auth0_id', $auth0_user_id );
self::update_meta( $user_id, 'auth0_id', $auth0_user_id );

$userinfo_encoded = WP_Auth0_Serializer::serialize( $userinfo );
$userinfo_encoded = wp_slash( $userinfo_encoded );
update_user_meta( $user_id, $wpdb->prefix . 'auth0_obj', $userinfo_encoded );
self::update_meta( $user_id, 'auth0_obj', $userinfo_encoded );

update_user_meta( $user_id, $wpdb->prefix . 'last_update', date( 'c' ) );
self::update_meta( $user_id, 'last_update', date( 'c' ) );
}

/**
Expand All @@ -237,10 +235,10 @@ public function update_auth0_object( $user_id, $userinfo ) {
* @param int $user_id - WordPress user ID.
*/
public function delete_auth0_object( $user_id ) {
global $wpdb;
delete_user_meta( $user_id, $wpdb->prefix . 'auth0_id' );
delete_user_meta( $user_id, $wpdb->prefix . 'auth0_obj' );
delete_user_meta( $user_id, $wpdb->prefix . 'last_update' );
self::delete_meta( $user_id, 'auth0_id' );
self::delete_meta( $user_id, 'auth0_obj' );
self::delete_meta( $user_id, 'last_update' );
self::delete_meta( $user_id, 'auth0_transient_email_update' );
}

/**
Expand All @@ -257,4 +255,35 @@ public static function get_meta( $user_id, $key ) {
global $wpdb;
return get_user_meta( $user_id, $wpdb->prefix . $key, true );
}

/**
* Update a user's Auth0 meta data.
*
* @param integer $user_id - WordPress user ID.
* @param string $key - Usermeta key to update.
* @param mixed $value - Usermeta value to use.
*
* @return int|bool
*
* @since 3.11.0
*/
public static function update_meta( $user_id, $key, $value ) {
global $wpdb;
return update_user_meta( $user_id, $wpdb->prefix . $key, $value );
}

/**
* Delete a user's Auth0 meta data.
*
* @param integer $user_id - WordPress user ID.
* @param string $key - Usermeta key to delete.
*
* @return bool
*
* @since 3.11.0
*/
public static function delete_meta( $user_id, $key ) {
global $wpdb;
return delete_user_meta( $user_id, $wpdb->prefix . $key );
}
}
26 changes: 20 additions & 6 deletions lib/profile/WP_Auth0_Profile_Change_Email.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
*/
class WP_Auth0_Profile_Change_Email {

/**
* Usermeta key used when updating the email address at Auth0.
*/
const UPDATED_EMAIL = 'auth0_transient_email_update';

/**
* WP_Auth0_Api_Change_Email instance.
*
Expand Down Expand Up @@ -42,7 +47,8 @@ public function init() {
}

/**
* Update the user's email at Auth0
* Update the user's email at Auth0 when changing email for a database connection user.
* This runs AFTER a successful email change is saved in WP.
* Hooked to: profile_update
* IMPORTANT: Internal callback use only, do not call this function directly!
*
Expand Down Expand Up @@ -74,11 +80,22 @@ public function update_email( $wp_user_id, $old_user_data ) {
return false;
}

// Password change was successful, nothing else to do.
// Set a flag so the Get User call to other processes know the email is in the process of changing.
WP_Auth0_UsersRepo::update_meta( $wp_user_id, self::UPDATED_EMAIL, $current_email );

// Attempt to update the email address at Auth0.
// For custom database setups, this will trigger a Get User script call from Auth0.
// See: WP_Auth0_Routes::migration_ws_get_user()
if ( $this->api_change_email->call( $auth0_id, $current_email ) ) {
WP_Auth0_UsersRepo::delete_meta( $wp_user_id, self::UPDATED_EMAIL );
return true;
}

// Past this point, email update with Auth0 has failed so we need to revert changes saved in WP.
// Remove the pending email address change flags so it can be tried again.
delete_user_meta( $wp_user_id, '_new_email' );
WP_Auth0_UsersRepo::delete_meta( $wp_user_id, self::UPDATED_EMAIL );

// Suppress the notification for email change.
add_filter( 'email_change_email', array( $this, 'suppress_email_change_notification' ), 100 );

Expand All @@ -89,13 +106,10 @@ public function update_email( $wp_user_id, $old_user_data ) {
$wp_user->data->user_email = $old_email;
wp_update_user( $wp_user );

// Revert hooks.
// Revert hooks from above.
add_action( 'profile_update', array( $this, __FUNCTION__ ), 100, 2 );
remove_filter( 'email_change_email', array( $this, 'suppress_email_change_notification' ), 100 );

// Remove the pending email address change so it can be tried again.
delete_user_meta( $wp_user_id, '_new_email' );

// Can't set a custom message here so redirect with an error for WP to pick up.
if ( in_array( $GLOBALS['pagenow'], array( 'user-edit.php', 'profile.php' ) ) ) {
$redirect_url = admin_url( $GLOBALS['pagenow'] );
Expand Down
28 changes: 28 additions & 0 deletions tests/testProfileChangeEmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class TestProfileChangeEmail extends WP_Auth0_Test_Case {

use HookHelpers;

use HttpHelpers;

use RedirectHelpers;

use UsersHelper;
Expand Down Expand Up @@ -64,6 +66,7 @@ public function testSuccessfulEmailUpdate() {

$this->assertTrue( $change_email->update_email( $user->ID, $old_user ) );
$this->assertEquals( $new_email, get_user_by( 'id', $user->ID )->data->user_email );
$this->assertEmpty( WP_Auth0_UsersRepo::get_meta( $user->ID, 'auth0_transient_email_update' ) );
}

/**
Expand Down Expand Up @@ -137,6 +140,7 @@ public function testThatFailedApiCallStopsEmailUpdate() {
$this->assertFalse( $change_email->update_email( $user->ID, $old_user ) );
$this->assertEquals( $old_user->data->user_email, get_user_by( 'id', $user->ID )->data->user_email );
$this->assertEmpty( get_user_meta( $user->ID, '_new_email', true ) );
$this->assertEmpty( WP_Auth0_UsersRepo::get_meta( $user->ID, 'auth0_transient_email_update' ) );
}

/**
Expand Down Expand Up @@ -177,6 +181,30 @@ public function testThatFailedApiRedirectsOnUserEditPage() {
$this->assertContains( 'error=new-email', $caught_redirect['location'] );
}

/**
* Test that the email update flag is set for a user before the Auth0 API call is made.
*/
public function testThatEmailUpdateFlagIsSetBeforeApiCall() {
$this->startHttpHalting();

$user = $this->createUser( [], false );
$new_email = $user->data->user_email;
$old_user = clone $user;
$old_user->data->user_email = 'OLD-' . $new_email;
$this->storeAuth0Data( $user->ID, 'auth0' );

$api_change_email = new WP_Auth0_Api_Change_Email( self::$opts, self::$api_client_creds );
$change_email = new WP_Auth0_Profile_Change_Email( $api_change_email );

try {
$change_email->update_email( $user->ID, $old_user );
} catch ( Exception $e ) {
// Just need to stop the API call.
}

$this->assertEquals( $new_email, WP_Auth0_UsersRepo::get_meta( $user->ID, 'auth0_transient_email_update' ) );
}

/**
* Get an API stub set to pass or fail.
*
Expand Down
65 changes: 32 additions & 33 deletions tests/testRoutesGetUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,14 @@ public function setUp() {
$_REQUEST = [];

parent::setUp();
self::$wp = new WP();
self::$wp = new WP();
self::$wp->query_vars['a0_action'] = 'migration-ws-get-user';
}

/**
* If migration services are off, the route should fail with an error.
*/
public function testThatGetUserRouteIsForbiddenByDefault() {
self::$wp->query_vars['a0_action'] = 'migration-ws-get-user';

$output = json_decode( self::$routes->custom_requests( self::$wp, true ) );

$this->assertEquals( 403, $output->status );
Expand All @@ -72,13 +71,11 @@ public function testThatGetUserRouteIsForbiddenByDefault() {
public function testThatGetUserRouteIsUnauthorizedIfWrongIp() {
self::$opts->set( 'migration_ws', 1 );
self::$opts->set( 'migration_ips_filter', 1 );
self::$wp->query_vars['a0_action'] = 'migration-ws-get-user';

$output = json_decode( self::$routes->custom_requests( self::$wp, true ) );

$this->assertEquals( 401, $output->status );
$this->assertEquals( 'Unauthorized', $output->error );

$this->assertEmpty( self::$error_log->get() );
}

Expand All @@ -87,7 +84,6 @@ public function testThatGetUserRouteIsUnauthorizedIfWrongIp() {
*/
public function testThatGetUserRouteIsUnauthorizedIfNoToken() {
self::$opts->set( 'migration_ws', 1 );
self::$wp->query_vars['a0_action'] = 'migration-ws-get-user';

$output = json_decode( self::$routes->custom_requests( self::$wp, true ) );

Expand All @@ -108,8 +104,7 @@ public function testThatGetUserRouteIsUnauthorizedIfWrongJti() {
self::$opts->set( 'client_secret', $client_secret );
self::$opts->set( 'migration_token_id', '__test_token_id__' );

self::$wp->query_vars['a0_action'] = 'migration-ws-get-user';
$_POST['access_token'] = JWT::encode( [ 'jti' => uniqid() ], $client_secret );
$_POST['access_token'] = JWT::encode( [ 'jti' => uniqid() ], $client_secret );

$output = json_decode( self::$routes->custom_requests( self::$wp, true ) );

Expand All @@ -125,15 +120,11 @@ public function testThatGetUserRouteIsUnauthorizedIfWrongJti() {
* If there is no username POSTed, the route should fail with an error.
*/
public function testThatGetUserRouteIsBadRequestIfNoUsername() {
$client_secret = '__test_client_secret__';
$token_id = '__test_token_id__';
$migration_token = JWT::encode( [ 'jti' => $token_id ], $client_secret );
$migration_token = uniqid();
self::$opts->set( 'migration_ws', 1 );
self::$opts->set( 'client_secret', $client_secret );
self::$opts->set( 'migration_token', $migration_token );

self::$wp->query_vars['a0_action'] = 'migration-ws-get-user';
$_POST['access_token'] = $migration_token;
$_POST['access_token'] = $migration_token;

$output = json_decode( self::$routes->custom_requests( self::$wp, true ) );

Expand All @@ -149,49 +140,57 @@ public function testThatGetUserRouteIsBadRequestIfNoUsername() {
* If there the username cannot be found, the route should fail with an error.
*/
public function testThatGetUserRouteIsUnauthorizedIfUserNotFound() {
$client_secret = '__test_client_secret__';
$token_id = '__test_token_id__';
$migration_token = JWT::encode( [ 'jti' => $token_id ], $client_secret );
$migration_token = uniqid();
self::$opts->set( 'migration_ws', 1 );
self::$opts->set( 'client_secret', $client_secret );
self::$opts->set( 'migration_token', $migration_token );

self::$wp->query_vars['a0_action'] = 'migration-ws-get-user';

$_POST['access_token'] = $migration_token;
$_POST['username'] = uniqid();

$output = json_decode( self::$routes->custom_requests( self::$wp, true ) );

$this->assertEquals( 401, $output->status );
$this->assertEquals( 'Invalid Credentials', $output->error );
$this->assertEquals( 'User not found', $output->error );

$log = self::$error_log->get();
$this->assertCount( 1, $log );
$this->assertEquals( $output->error, $log[0]['message'] );
}

/**
* Route should return a blank user profile when email is being updated.
*/
public function testThatGetUserRouteReturnsEmptyIfEmailUpdate() {
$migration_token = uniqid();
self::$opts->set( 'migration_ws', 1 );
self::$opts->set( 'migration_token', $migration_token );

$_POST['access_token'] = $migration_token;
$_POST['username'] = uniqid() . '@' . uniqid() . '.com';

$user = $this->createUser( [ 'user_email' => $_POST['username'] ] );
WP_Auth0_UsersRepo::update_meta( $user->ID, 'auth0_transient_email_update', $_POST['username'] );

$output = json_decode( self::$routes->custom_requests( self::$wp, true ) );

$this->assertFalse( isset( $output->ID ) );
$this->assertEquals( 200, $output->status );
$this->assertEquals( 'Email update in process', $output->error );
$this->assertEmpty( self::$error_log->get() );
}

/**
* Route should return a user with no password set if provided a valid username or email.
*/
public function testThatGetUserRouteReturnsUserIfSuccessful() {
$client_secret = '__test_client_secret__';
$token_id = '__test_token_id__';
$_POST['username'] = uniqid() . '@' . uniqid() . '.com';
$user = $this->createUser( [ 'user_email' => $_POST['username'] ] );
$migration_token = JWT::encode(
[
'jti' => $token_id,
'scope' => 'migration_ws',
],
$client_secret
);

$migration_token = uniqid();
self::$opts->set( 'migration_ws', 1 );
self::$opts->set( 'client_secret', $client_secret );
self::$opts->set( 'migration_token', $migration_token );

self::$wp->query_vars['a0_action'] = 'migration-ws-get-user';
$_POST['access_token'] = $migration_token;
$_POST['access_token'] = $migration_token;

$output_em = json_decode( self::$routes->custom_requests( self::$wp, true ) );

Expand Down
2 changes: 1 addition & 1 deletion tests/testRoutesLogin.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ public function testThatLoginRouteIsUnauthorizedIfNotAuthenticated() {
$output = json_decode( self::$routes->custom_requests( self::$wp, true ) );

$this->assertEquals( 401, $output->status );
$this->assertEquals( 'Invalid Credentials', $output->error );
$this->assertEquals( 'Invalid credentials', $output->error );

$log = self::$error_log->get();
$this->assertCount( 1, $log );
Expand Down
7 changes: 3 additions & 4 deletions tests/testUserRepoMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,20 +94,19 @@ public function testThatSpecialCharactersAreStoredProperly() {
*/
public function testThatDeleteMetaDeletesData() {
$users_repo = new WP_Auth0_UsersRepo( self::$opts );
$this->assertEmpty( $users_repo::get_meta( 1, 'auth0_id' ) );
$this->assertEmpty( $users_repo::get_meta( 1, 'auth0_obj' ) );
$this->assertEmpty( $users_repo::get_meta( 1, 'last_update' ) );

$this->storeAuth0Data( 1 );
$users_repo::update_meta( 1, 'auth0_transient_email_update', uniqid() );

$this->assertNotEmpty( $users_repo::get_meta( 1, 'auth0_id' ) );
$this->assertNotEmpty( $users_repo::get_meta( 1, 'auth0_obj' ) );
$this->assertNotEmpty( $users_repo::get_meta( 1, 'last_update' ) );
$this->assertNotEmpty( $users_repo::get_meta( 1, 'auth0_transient_email_update' ) );

$users_repo->delete_auth0_object( 1 );

$this->assertEmpty( $users_repo::get_meta( 1, 'auth0_id' ) );
$this->assertEmpty( $users_repo::get_meta( 1, 'auth0_obj' ) );
$this->assertEmpty( $users_repo::get_meta( 1, 'last_update' ) );
$this->assertEmpty( $users_repo::get_meta( 1, 'auth0_transient_email_update' ) );
}
}