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

Fix Login Process Error Handling #409

Merged
merged 3 commits into from
Mar 15, 2018
Merged

Conversation

joshcanhelp
Copy link
Contributor

@joshcanhelp joshcanhelp commented Mar 9, 2018

WP_Auth0_LoginManager was not processing errors well, was poorly
documented, and might have been improperly exposing error messages.
Incoming URL param errors from Auth0 and configuration issues are caught
earlier in the login process. Error message are not exposed to the
user; instead they are logged for an admin. Thrown errors are
standarized and listed in docblocks.

Fixes #305

@joshcanhelp joshcanhelp added the WIP label Mar 9, 2018
@@ -144,12 +144,19 @@ public function login_auto() {
}

public function init_auth0() {
global $wp_query;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not used

public function redirect_login() {
global $wp_query;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not used

public function redirect_login() {
global $wp_query;

if ( $this->query_vars( 'auth0' ) === null ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These 3 if blocks were moved to init_auth0() to be caught earlier and for both redirect and implicit flows.

) );

if ( $response instanceof WP_Error ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Diff gets a little mangled from here down because of switching around how the errors are handled. This was removed in favor of checking a specific error code we're looking for, followed by a general check if we have the info we need (access_token)

WP_Auth0_Api_Client::get_client_token(),
$decoded_token->sub
);
// Unsuccessful for another reason
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Log the error if we've got something but don't show to end user


throw new WP_Auth0_LoginFlowValidationException( );
}
if ( empty( $data->access_token ) ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only need the access token here

WP_Auth0_LoginManager was not processing errors well, was poorly
documented, and might have been improperly exposing error messages.
Incoming URL param errors from Auth0 and configuration issues are caught
earlier in the login process. Error message are not exposed to the
user; instead they are logged for an admin. Thrown errors are
standarized and listed in docblocks.

Fixes #305
@joshcanhelp joshcanhelp force-pushed the fixed-login-error-handling branch from 325594a to 46e20ec Compare March 12, 2018 22:02
@joshcanhelp joshcanhelp changed the base branch from fixed-handle-state-checking to dev March 12, 2018 22:04
$this->die_on_login( $e->getMessage(), $e->getCode(), FALSE );

} catch (Exception $e) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Blank general Exception catching lead to issues like #305


$domain = $this->a0_options->get( 'domain' );

$client_id = $this->a0_options->get( 'client_id' );
$client_secret = $this->a0_options->get( 'client_secret' );

if ( empty( $client_id ) ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Handled earlier in init_auth0() by WP_Auth0::ready()


$decoded_token = JWT::decode(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved down


$data->id_token = null;
$response = WP_Auth0_Api_Client::get_user(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved down

@joshcanhelp joshcanhelp changed the title [WIP] Fix login process error reporting Fix Login Process Error Handling Mar 13, 2018
@joshcanhelp joshcanhelp added CH: Fixed and removed WIP labels Mar 13, 2018
@joshcanhelp joshcanhelp added this to the v3-Next milestone Mar 13, 2018
Copy link
Member

@cocojoe cocojoe left a comment

Choose a reason for hiding this comment

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

Kind of hard to read, but looks okay. Lot of handling cleaned up which is nice.

Copy link
Contributor

@lbalmaceda lbalmaceda left a comment

Choose a reason for hiding this comment

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

left some comments 👍

WP_Auth0.php Outdated
@@ -123,6 +123,19 @@ public function init() {
WP_Auth0_Email_Verification::init();
}

/**
* Is the Auth0 plugin ready to proc
Copy link
Contributor

Choose a reason for hiding this comment

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

if "proc" is the short for something I didn't get it. To process, maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe I just fell asleep in the middle of that docblock or something ...

WP_Auth0.php Outdated
*/
public static function ready() {
$options = WP_Auth0_Options::Instance();
if ( ! $options->get( 'domain' ) || ! $options->get( 'client_id' ) || ! $options->get( 'client_id' ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

duplicated client_id condition

@@ -240,9 +253,11 @@ public static function get_plugin_dir_url() {
}

public function a0_register_query_vars( $qvars ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

what's the purpose of this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It registers query variables (URL params) with the main WP_Query instance that basically runs the whole site.


if ( $this->query_vars( 'auth0' ) === null ) {
// Not an Auth0 login process or settings are not configured to allow logins
if ( ! $this->query_vars( 'auth0' ) || ! WP_Auth0::ready() ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

auth0 was the query param you use to track context, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, can be 1 for a "regular" login or implicit

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm.. That's confusing, but as long as it's "internal" I don't care. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree but it's legacy and dangerous to unwire at this point. I'd like to just deprecate implicit altogether ...

// See https://auth0.com/docs/libraries/error-messages
if ( $this->query_vars( 'error_description' ) ) {
$error_msg = sanitize_text_field( $this->query_vars( 'error_description' ) );
$this->die_on_login( $error_msg ? $error_msg : sanitize_text_field( $this->query_vars( 'error' ) ) );
Copy link
Contributor

Choose a reason for hiding this comment

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

the if already checks if error_description is present on the query. Is there any case where the sanitize_test_field(error_description) will fail or give you something null/undefined? if not, it doesn't make sense to have this line's if that calls sanitize_text_field(error)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll only ever get a scalar back from query_vars() and sanitize_text_field() is just there to sanitize. We're expecting error descriptions from the Auth0 server so if something comes through that's wonky then it's probably someone screwing around. If sanitize_text_field() returns an empty value then it was empty to begin with or invalid symbols:

https://developer.wordpress.org/reference/functions/sanitize_text_field/


$msg = __( 'Error: the Client Secret configured on the Auth0 plugin is wrong. Make sure to copy the right one from the Auth0 dashboard.', 'wp-auth0' );
if ( $userinfo_resp instanceof WP_Error || 200 !== $userinfo_resp_code || empty( $userinfo_resp_body ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

To be honest I think the "error handler" should catch the last 2 conditions already and create/raise a WP_ERROR. You should only be checking for the error existence, if not you are "extending" the error handler logic to every call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also a good call. If $userinfo_resp is a WP_Error then this will also be true (resolves the code to 0).

if ( ! empty( $state_decoded->interim ) ) {
include WPA0_PLUGIN_DIR . 'templates/login-interim.php';
} else {
if ( ! empty( $state_decoded->redirect_to ) && wp_login_url() !== $state_decoded->redirect_to ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

what about

     $redirectURL= $state_decoded->redirect_to
      if ( empty( $redirectURL ) || wp_login_url() !==  $redirectURL ) {
         $redirectURL = $this->a0_options->get( 'default_login_redirection' );
       }
     wp_safe_redirect( $redirectURL );

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What if $state_decoded->redirect_to is not set?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know, I guess from your answer that it will throw an unhandled exception. If that's the case then fine 👍. I only suggested this as a way to avoid calling $state_decoded->redirect_to 3 times

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If $state_decoded->redirect_to is not set and you try to access it (the exception being empty()) then you get a notice. $redirectURL ends up false-y and things progress fine.

"avoid calling $state_decoded->redirect_to 3 times" - just for the sake of style? Accessing a property like that shouldn't have any kind of performance impact.

WP_Auth0_ErrorManager::insert_auth0_error( __METHOD__ . ' => $this->login_user()', $error );
if ( $this->login_user( $userinfo, $data->id_token, $data->access_token ) ) {
$state_decoded = $this->get_state();
if ( ! empty( $state_decoded->interim ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

what's interim? and what is the structure of the object state_decoded?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TBH, I'm not sure about interim at the moment, has never come up so I left it in-tact.

State is an object with interim (bool), nonce (state validation), and state (always set to "nonce" ... which can probably go). Sometimes redirect_to is set to a URL but it can be blank.

Copy link
Contributor

Choose a reason for hiding this comment

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

@joshcanhelp are you saying that state is the string "nonce" or that state==nonce ? In any case, remember this are individual values and should be different, secure random strings.

Copy link
Contributor Author

@joshcanhelp joshcanhelp Mar 15, 2018

Choose a reason for hiding this comment

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

Currently (in JSON for clarity):

{
  "interim" : true || false,
  "nonce" : "2309u29j23o28j2je8",
  "state" : "nonce"
}

* @param object $userinfo - the Auth0 profile of the user
* @param bool $is_new - `true` if the user was created in the WordPress database, `false` if not
* @param string $id_token - user's ID token returned from Auth0
* @param string $access_token - user's access token returned from Auth0; not provided when using implicit_login()
Copy link
Contributor

Choose a reason for hiding this comment

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

AFAIK the AcT is always returned. The one that might be missing is the IdT.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's true ... but the $id_token param was already there

Copy link
Contributor

Choose a reason for hiding this comment

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

So why isn't the access token provided "when using implicit_login()"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're given an ID token via POST which we use to ID the user. Access token isn't used for anything in that flow (at least in WP). Implicit in WP is a bit of a strange animal ...

* that the cookie will be kept. Default false.
* }
*/
// See wp_signon() for documentation on this filter
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah that's lazy 😛 I don't see it in this diff. Was this a copy of that method documentation? What's the reason behind removing this one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really lazy, more like an unnecessary duplicate of WP core documentation. Seems silly to have that here and in the core location and try to keep the two updated.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's fair. 👍

@joshcanhelp
Copy link
Contributor Author

@lbalmaceda - Fixes up and ready for re-review!

if ( $this->login_user( $userinfo, $data->id_token, $data->access_token ) ) {
$state_decoded = $this->get_state();
if ( ! empty( $state_decoded->interim ) ) {
include WPA0_PLUGIN_DIR . 'templates/login-interim.php';
Copy link
Contributor

Choose a reason for hiding this comment

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

there was an exit() before right below this line. Are you sure it's no longer required?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

exit() was moved to catch this and wp_safe_redirect() (which requires an exit/die). Here


$userinfo_resp_code = (int) wp_remote_retrieve_response_code( $userinfo_resp );
$userinfo_resp_body = wp_remote_retrieve_body( $userinfo_resp );

if ( $userinfo_resp instanceof WP_Error || 200 !== $userinfo_resp_code || empty( $userinfo_resp_body ) ) {
// Management API call failed
if ( 200 !== $userinfo_resp_code || empty( $userinfo_resp_body ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What I meant is remove this 2 conditions and only keep the $userinfo_resp instanceof WP_Error one. Why did you choose to remove the other?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

$userinfo_resp is only a WP_Error if something went wrong during the POST call. If it succeeds but you get a 401 or 404, the response code is what needs to be checked.

Copy link
Contributor

@lbalmaceda lbalmaceda left a comment

Choose a reason for hiding this comment

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

:)

@joshcanhelp joshcanhelp merged commit 477eacd into dev Mar 15, 2018
@joshcanhelp joshcanhelp deleted the fixed-login-error-handling branch March 15, 2018 21:10
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants