Skip to content

Commit

Permalink
Issue 3: add new forgot password wizard (#21)
Browse files Browse the repository at this point in the history
For Cyclos versions 4.13 and up, the plugin will show a wizard to handle forgotten password requests.
  • Loading branch information
sandrab authored Oct 8, 2020
1 parent f895052 commit 9698169
Show file tree
Hide file tree
Showing 9 changed files with 511 additions and 48 deletions.
50 changes: 50 additions & 0 deletions app/Components/LoginComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public function init() {
add_action( 'wp_ajax_nopriv_cyclos_captcha', array( $this, 'handle_captcha_ajax_request' ) );
add_action( 'wp_ajax_cyclos_forgot_password', array( $this, 'handle_forgot_password_ajax_request' ) );
add_action( 'wp_ajax_nopriv_cyclos_forgot_password', array( $this, 'handle_forgot_password_ajax_request' ) );
add_action( 'wp_ajax_cyclos_forgot_password_wizard', array( $this, 'handle_forgot_password_wizard_ajax_request' ) );
add_action( 'wp_ajax_nopriv_cyclos_forgot_password_wizard', array( $this, 'handle_forgot_password_wizard_ajax_request' ) );
}

/**
Expand Down Expand Up @@ -104,13 +106,17 @@ public function render_loginform() {
set_query_var( 'cyclos_error', __( 'Something is wrong with the Cyclos server. The login form cannot be used at the moment.', 'cyclos' ) );
set_query_var( 'cyclos_is_forgot_password_enabled', false );
set_query_var( 'cyclos_is_captcha_enabled', false );
set_query_var( 'cyclos_use_forgot_password_wizard', false );
set_query_var( 'cyclos_forgot_password_mediums', array() );
set_query_var( 'cyclos_return_to', '' );
} else {
// Cyclos can not send us a nonce, so ignore the recommended nonce verification.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$return_to = isset( $_GET['returnTo'] ) ? sanitize_text_field( wp_unslash( $_GET['returnTo'] ) ) : '';
set_query_var( 'cyclos_is_forgot_password_enabled', $login_configuration['is_forgot_password_enabled'] );
set_query_var( 'cyclos_is_captcha_enabled', $login_configuration['is_captcha_enabled'] );
set_query_var( 'cyclos_use_forgot_password_wizard', $login_configuration['has_complex_forgot_password'] );
set_query_var( 'cyclos_forgot_password_mediums', $login_configuration['forgot_password_mediums'] );
set_query_var( 'cyclos_return_to', $return_to );
}

Expand Down Expand Up @@ -242,4 +248,48 @@ public function handle_forgot_password_ajax_request() {
$response = $this->cyclos->forgot_password( $principal, $captcha_id, $captcha_response );
wp_send_json( $response );
}

/**
* Handle the AJAX request from the forgot password wizard.
*/
public function handle_forgot_password_wizard_ajax_request() {
// Die if the nonce is incorrect.
check_ajax_referer( 'cyclos_login_nonce' );

// Do a remote request to Cyclos. Check the step field to determine which route we must call.
$step = isset( $_POST['step'] ) ? sanitize_text_field( wp_unslash( $_POST['step'] ) ) : '';
switch ( $step ) {
case 'request':
$principal = isset( $_POST['principal'] ) ? wp_unslash( $_POST['principal'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$captcha_id = isset( $_POST['captcha_id'] ) ? sanitize_text_field( wp_unslash( $_POST['captcha_id'] ) ) : '';
$captcha_response = isset( $_POST['captcha_response'] ) ? sanitize_text_field( wp_unslash( $_POST['captcha_response'] ) ) : '';
$send_medium = isset( $_POST['send_medium'] ) ? sanitize_text_field( wp_unslash( $_POST['send_medium'] ) ) : '';

$response = $this->cyclos->forgot_password_step_request( $principal, $captcha_id, $captcha_response, $send_medium );
break;
case 'code':
$principal = isset( $_POST['principal'] ) ? wp_unslash( $_POST['principal'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$code = isset( $_POST['code'] ) ? sanitize_text_field( wp_unslash( $_POST['code'] ) ) : '';

$response = $this->cyclos->forgot_password_step_code( $principal, $code );
break;
case 'change':
// Note: we can not sanitize the password and principal fields, because they may contain legitimate special characters.
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$principal = isset( $_POST['principal'] ) ? wp_unslash( $_POST['principal'] ) : '';
$code = isset( $_POST['code'] ) ? sanitize_text_field( wp_unslash( $_POST['code'] ) ) : '';
$new_password = isset( $_POST['new_pw'] ) ? wp_unslash( $_POST['new_pw'] ) : '';
$confirm_password = isset( $_POST['confirm_pw'] ) ? wp_unslash( $_POST['confirm_pw'] ) : '';
$security_answer = isset( $_POST['sec_answer'] ) ? sanitize_text_field( wp_unslash( $_POST['sec_answer'] ) ) : '';
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

$response = $this->cyclos->forgot_password_step_change( $principal, $code, $new_password, $confirm_password, $security_answer );
break;
default:
$response = array(
'errorMessage' => __( 'Unidentified wizard step', 'cyclos' ) . ': ' . $step,
);
}
wp_send_json( $response );
}
}
14 changes: 14 additions & 0 deletions app/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ protected function initialize_settings() {
'forgot_pw_email' => new Setting( 'login_form', __( 'Forgotten password user', 'cyclos' ), 'text', false, __( 'User', 'cyclos' ), __( 'The placeholder in the username field in the forgotten password form', 'cyclos' ) ),
'forgot_pw_captcha' => new Setting( 'login_form', __( 'Forgotten password captcha', 'cyclos' ), 'text', false, __( 'Visual validation', 'cyclos' ), __( 'The placeholder in the captcha field in the forgotten password form', 'cyclos' ) ),
'forgot_pw_newcaptcha' => new Setting( 'login_form', __( 'Forgotten password new captcha', 'cyclos' ), 'text', false, __( 'New code', 'cyclos' ), __( 'The text for the new captcha link in the forgotten password form', 'cyclos' ) ),
'forgot_pw_medium' => new Setting( 'login_form', __( 'Forgotten password send medium label', 'cyclos' ), 'text', false, __( 'Send verification code by', 'cyclos' ), __( 'The label near the (optional) send medium choice in the forgotten password form', 'cyclos' ) ),
'forgot_pw_mail' => new Setting( 'login_form', __( 'Forgotten password e-mail', 'cyclos' ), 'text', false, __( 'E-mail', 'cyclos' ), __( 'The (optional) e-mail send medium choice in the forgotten password form', 'cyclos' ) ),
'forgot_pw_sms' => new Setting( 'login_form', __( 'Forgotten password SMS', 'cyclos' ), 'text', false, __( 'SMS', 'cyclos' ), __( 'The (optional) SMS send medium choice in the forgotten password form', 'cyclos' ) ),
'forgot_pw_code' => new Setting( 'login_form', __( 'Forgotten password code', 'cyclos' ), 'text', false, __( 'Verification code', 'cyclos' ), __( 'The placeholder in the verification code step in the forgotten password form', 'cyclos' ) ),
'forgot_pw_sec_answer' => new Setting( 'login_form', __( 'Forgotten password security answer', 'cyclos' ), 'text', false, __( 'Your answer', 'cyclos' ), __( 'The placeholder in the answer to the security question field in the forgotten password form', 'cyclos' ) ),
'forgot_pw_new_pw' => new Setting( 'login_form', __( 'Forgotten password new password', 'cyclos' ), 'text', false, __( 'New password', 'cyclos' ), __( 'The placeholder in the new password field in the forgotten password form', 'cyclos' ) ),
'forgot_pw_confirm_pw' => new Setting( 'login_form', __( 'Forgotten password confirm password', 'cyclos' ), 'text', false, __( 'Confirm new password', 'cyclos' ), __( 'The placeholder in the confirmation field of the new password in the forgotten password form', 'cyclos' ) ),
'forgot_pw_submit' => new Setting( 'login_form', __( 'Forgotten password submit', 'cyclos' ), 'text', false, __( 'Submit', 'cyclos' ), __( 'The text on the submit button in the forgotten password form', 'cyclos' ) ),
'forgot_pw_cancel' => new Setting( 'login_form', __( 'Forgotten password cancel', 'cyclos' ), 'text', false, __( 'Cancel', 'cyclos' ), __( 'The text for the cancel link in the forgotten password form', 'cyclos' ) ),
);
Expand Down Expand Up @@ -356,6 +363,13 @@ public function get_loginform_labels() {
'forgot_newcaptcha' => $this->get_setting( 'forgot_pw_newcaptcha' ),
'forgot_submit' => $this->get_setting( 'forgot_pw_submit' ),
'forgot_cancel' => $this->get_setting( 'forgot_pw_cancel' ),
'forgot_medium' => $this->get_setting( 'forgot_pw_medium' ),
'forgot_email' => $this->get_setting( 'forgot_pw_mail' ),
'forgot_sms' => $this->get_setting( 'forgot_pw_sms' ),
'forgot_code' => $this->get_setting( 'forgot_pw_code' ),
'forgot_security' => $this->get_setting( 'forgot_pw_sec_answer' ),
'forgot_new_pw' => $this->get_setting( 'forgot_pw_new_pw' ),
'forgot_confirm_pw' => $this->get_setting( 'forgot_pw_confirm_pw' ),
);
}

Expand Down
53 changes: 51 additions & 2 deletions app/Services/Cyclos4/AuthService.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public function get_data_for_login() {
$this->method = 'GET';
$this->route = '/auth/data-for-login';
$this->authenticate_as_guest();
// Note: we don't need to specify a channel, because the information we need from data-for-login does not depend on the channel.
// Note: In this case we must specify the channel to be 'Main', because the information we need from data-for-login might depend on the channel.
$this->use_explicit_main_channel();
return $this->run();
}

Expand All @@ -43,9 +44,10 @@ public function get_data_for_login() {
* @param string $principal The principal (i.e. username, e-mail, ..) to identify the user with.
* @param string $captcha_id The ID of the captcha challenge.
* @param string $captcha_response The response for the captcha challenge.
* @param string $send_medium (Optional) The medium (email/sms) to use for sending the verification code to the visitor.
* @return object|\WP_Error The body from the Cyclos server response or a WP_Error object on failure.
*/
public function forgotten_password_request( string $principal, string $captcha_id, string $captcha_response ) {
public function forgotten_password_request( string $principal, string $captcha_id, string $captcha_response, string $send_medium = null ) {
$this->method = 'POST';
$this->route = '/auth/forgotten-password/request';
$data = array(
Expand All @@ -55,6 +57,53 @@ public function forgotten_password_request( string $principal, string $captcha_i
'response' => $captcha_response,
),
);
if ( $send_medium ) {
$data['sendMedium'] = $send_medium;
}
return $this->run( $data );
}

/**
* Returns configuration data used to change a forgotten password after the initial request.
*
* @param string $principal The principal (i.e. username, e-mail, ..) to identify the user with.
* @param string $code The verification code which was sent to the user.
*
* @return object|\WP_Error The body from the Cyclos server response or a WP_Error object on failure.
*/
public function forgotten_password_data_for_change( string $principal, string $code ) {
$this->method = 'GET';
$this->route = '/auth/forgotten-password/data-for-change';
$this->route .= '?user=' . $principal . '&code=' . $code;
$this->authenticate_as_guest();
return $this->run();
}

/**
* Changes the forgotten password after the user has completed the request.
*
* @param string $principal The principal (i.e. username, e-mail, ..) to identify the user with.
* @param string $code The verification code which was sent to the user.
* @param string $new_password The new password.
* @param string $confirm_password The new password again as a way of confirmation.
* @param string $security_answer (Optional) The answer to the security question if one was used.
*
* @return object|\WP_Error The body from the Cyclos server response or a WP_Error object on failure.
*/
public function forgotten_password( string $principal, string $code, string $new_password, string $confirm_password, string $security_answer = null ) {
$this->method = 'POST';
$this->route = '/auth/forgotten-password';
$data = array(
'user' => $principal,
'code' => $code,
'newPassword' => $new_password,
'newPasswordConfirmation' => $confirm_password,
'checkConfirmation' => true, // This is needed to have Cyclos check the confirmation password value.
);
if ( $security_answer ) {
$data['securityAnswer'] = $security_answer;
}
$this->authenticate_as_guest();
return $this->run( $data );
}

Expand Down
19 changes: 18 additions & 1 deletion app/Services/Cyclos4/Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ class Service {
*/
protected $authentication;

/**
* The channel to use in the request. Defaults to empty, to follow the default channel per Cyclos route.
*
* @var array $channel The channel.
*/
protected $channel = array();

/**
* The configuration with the plugin settings.
*
Expand Down Expand Up @@ -92,6 +99,15 @@ protected function authenticate_with_basic_login( string $username, string $pass
// phpcs:enable
}

/**
* Configure the channel used in the request to be Main.
*/
protected function use_explicit_main_channel() {
$this->channel = array(
'Channel' => 'main',
);
}

/**
* Execute a request to the Cyclos API.
*
Expand All @@ -106,7 +122,8 @@ protected function run( array $data = array() ) {
array(
'Content-Type' => 'application/json',
),
$this->authentication
$this->authentication,
$this->channel
),
);

Expand Down
Loading

0 comments on commit 9698169

Please sign in to comment.