Opened 17 months ago

Last modified 6 months ago

#19714 new defect (bug)

plugins which use the 'authenticate' hook unable to return errors

Reported by: willnorris Owned by:
Priority: normal Milestone: Awaiting Review
Component: General Version: 3.3
Severity: normal Keywords: has-patch
Cc: abackstrom

Description

The 'authenticate' hook is designed to allow functions to return either an authenticated WP_User object (which will cause the user to be logged in), or a WP_Error object, which will cause the errors to be displayed to the user.

In practice, most plugins that use this hook don't rely on the username and password at all, but instead on other means entirely. So what is happening with these plugins (the OpenID plugin chief among them), is that they are returning a WP_Error object that describes the error, but then the wp_authenticate_username_password function is ignoring that and returning its own WP_Error object which rightfully shows that the username and password fields were left empty. Unfortunately, this error object (containing both an empty username AND password) is explicitly checked for and removed in the wp_signon method. This is normally the right behavior and handles the case of a user who simply clicks "Log In" without entering anything... we don't show them an error, we just redraw the login form. However, in the case described above, an actual error did occur with an authentication plugin, but the user simply sees the normal login form with no error displayed.

(patch forthcoming)

Attachments (1)

authenticate.diff (407 bytes) - added by willnorris 17 months ago.

Download all attachments as: .zip

Change History (12)

If you hook into authenticate at a priority > 20, would this still be a problem?

  • Keywords has-patch added

The way the 'authenticate' hook is designed, it's up to each function which implements the hook to look at the passed in user value and decide what to do with it. WordPress core has two functions which implement this hook, wp_authenticate_username_password and wp_authenticate_cookie. Both of these will immediately return if the passed in user is a WP_User object, but they don't do anything special if it's a WP_Error object. In some cases, the error will get passed through untouched, but in other cases it will get overwritten, as described above.

We have to be careful about returning immediately if a WP_Error object is present, however. The authentication system currently allows for failover. Even though one authentication method may have failed, there's nothing to say that a later one might not still succeed and successfully authenticate the user (this is especially true of wp_authenticate_cookie which pretty much exclusively relies on this approach).

The provided patch modifies only the wp_authenticate_username_password function to check, at the point where it would have otherwise overwritten an existing WP_Error, and return the passed in WP_Error if it exists. The rationale here being that if a WP_Error is already present, it must have been set by a plugin trying to handle authentication, and should therefore trump the "no username or password" error. If a plugin actually does care about the absence of the username or password, it will be up to them to add that error as well. This also still allows wp_authenticate_username_password to attempt to authenticate the user using the username and password if present.

@nacin: that would also be one solution. However, at least for any auth plugins I've written, users typically want the alternate authentication method to take precedence over the username/password approach. I'd have to think a bit more if this would actually introduce any problems in practice though.

(that said, the provided patch is pretty minimal and has very low risk for introducing problems)

Thank you very much for the detailed explanation.

hmm, okay now I'm torn. I still feel like auth plugins should typically happen before the username/password though I'm having trouble articulating exactly why.

However, I just noticed that wp_authenticate_username_password also has checks for the user or their site being marked as spam. Based on how things work today, the spammer would still be able to login using an auth plugin like OpenID, since that happens before the spammer checks. My gut reaction is that the spammer check should not happen inside the wp_authenticate_username_password method, since that actually has nothing to do with the original intent of that method... authenticating a user by username and password. Instead, I think it should be its own function that hooks into 'authenticate' much later. If you're okay with the patch as written, I'll open a separate bug to track this.

Last edited 17 months ago by willnorris (previous) (diff)

ping. any new thoughts on this?

I am trying to think about what problems the patch could introduce, and whether we should be concerned. I don't think we should, and I think I like the patch. As long as $user is already a WP_Error object, then clearly a plugin has stepped in, and the login won't be successful.

The other thing I've thought about is allowing add() to run on the existing WP_Error object, thus creating an object with at least three error codes, and then tweaking it so empty_username and empty_password is still filtered out. But the end result is the same, AFAICT.

I agree that wp_authenticate_username_password() is being abused with the spam checks. In MU, it did exist as a separate function hooked into wp_authenticate_user. That was merged in with [12879]. The main reason for why that occurred was to clean up the MU codebase, but I think ryan would support bringing that back out. The wp_authenticate_user hook does seem better than authenticate for this, though, so I'm a bit torn as to the specifics of how we'd handle it.

Sidebar — This code in wp_authenticate() made me think about just letting add() get called:

	$ignore_codes = array('empty_username', 'empty_password');

	if (is_wp_error($user) && !in_array($user->get_error_code(), $ignore_codes) ) {
		do_action('wp_login_failed', $username);
	}

My take on this code is that it should instead be checking to see if get_error_codes() != array( 'empty_username', 'empty_password' ), per the check in wp_signon(). wp_login_failed seems like a fringe hook, though.

  • Cc abackstrom added
  • Version set to 3.3

#22516 was marked as a duplicate.

Note: See TracTickets for help on using tickets.