WordPress: Replace Built-in User Authentication

February 27, 2012

Synopsis

WordPress is a powerful multi-user web content management system. Due to its multi-user nature, WordPress has long instituted a full fledged user system, complete with separated roles and permissions for each user. What happens, however, when WordPress is brought into an environment with an existing user repository? Out of the box WordPress has no support for interacting with other user authentication methods other than built-in, however there exist WordPress filters that allow developers to hook into WordPress core actions, such as user authentication, registration, etc and alter their logic. In this post I will show you how to replace WordPress’ built-in authentication with authentication based off a very simple web service (Which we will also create).

Unless otherwise noted, all development will be assumed to exist at http://localhost for the purposes of this tutorial.

Now with video!

Check out the following video of the presentation over this topic I gave at the March of 2012 Orange County WordPress Meetup. While containing lots of good information, it should be used along with the rest of this post, not as a replacement for.

Creating a simple web based authentication service

We are going to create a simple web based authentication service to use as proof of concept when replacing the built-in WordPress authentication mechanism. Though this service will be simple, it will help illustrate how easy it is to authenticate using other databases, applications, even oAuth. Let’s begin.

Create a new PHP file at http://localhost/auth_serv.php with the following contents:

if($_GET['user'] == 'bob' && $_GET['pass'] == 'asdf') {
    echo json_encode(array('result' => '1', 'username' => 'bob', 'first_name' => 'Bob', 'last_name' => 'Jacobsen', 'email' => 'aaben@lobaugh.net'));
} else {
    echo json_encode(array('result' => '0'));
}

The code simply returns a JSON encoded array. This array contains a ‘result’ element, which is a boolean, holding whether the user is valid or not. This service only support one user with the following credentials:

Username: bob
Password: asdf

Let’s talk filters

WordPress allows developers to extend and manipulate virtually all of the system through hooks. There are two types of hooks in WordPress, actions and filters. Actions allow developers to tap into the power of WordPress and create new functionality. Filters allow developers to interrupt the flow of WordPress and alter how it runs. For example, there is a filter called ‘the_content’. This filter allows plugins to tap into post content before it is displayed and alter it as deemed fit.

For our purposes we will be using the ‘authenticate’ filter. Whenever a login is triggered the authenticate filter is hit and custom logic can be applied to it. The filter can be setup with the following code.

add_filter( 'authenticate', 'demo_auth', 10, 3 );

function demo_auth( $user, $username, $password ){
    return $user;
}

The demo_auth function will replace the built-in authentication method. The authenticate filter sends along three parameters:

  • $user – A WP_User object.
  • $username – From the username text box. This parameter is optional
  • $password – From the password text box. This parameter is optional

Because we will be using the built-in login box to obtain a username and password to send to our pretend service we will be using the $username and $password parameter. It is important to note that the username and password parameters are not required for this filter to function properly. Why would you not need a username and password you ask? If you are using oAuth to authenticate against another provider, such as Meetup, that site will provide it’s own login form to protect user information.

 What to keep in mind when replacing the built-in authentication

WordPress relies heavily on it’s built-in user system. Because of this there are lots of references to users in the WordPress database that are made. While slightly annoying, it is still fairly simple to work around these limitations.

WordPress requires that a real user (WordPress user) be present in the WordPress database in order to perform operations on that user. Luckily WordPress contains function to create, manipulate, and delete users. So when we build our service we will actually be taking the following steps, which should be fairly authentication type agnostic:

  • Authenticate user via alternate method
    • If invalid user display invalid login message
    • If valid user
      • Check to see if the user exists in the WordPress user table
      • If user exists load and return user data in a WP_User object
      • If user does not exist
        • Automagically create a new user from alternate authentication service user information
        • After creating the user load and return user data in a WP_User object

Enough blabbing, show me the code!

Here is a quick dump of the code. Then I will walk through the more important pieces

function demo_auth( $user, $username, $password ){
    // Make sure a username and password are present for us to work with
    if($username == '' || $password == '') return;

    $response = wp_remote_get( "http://localhost/auth_serv.php?user=$username&pass=$password" );
    $ext_auth = json_decode( $response['body'], true );

     if( $ext_auth['result']  == 0 ) {
        // User does not exist,  send back an error message
        $user = new WP_Error( 'denied', __("ERROR: User/pass bad") );

     } else if( $ext_auth['result'] == 1 ) {
         // External user exists, try to load the user info from the WordPress user table
         $userobj = new WP_User();
         $user = $userobj->get_data_by( 'email', $ext_auth['email'] ); // Does not return a WP_User object :(
         $user = new WP_User($user->ID); // Attempt to load up the user with that ID

         if( $user->ID == 0 ) {
             // The user does not currently exist in the WordPress user table.
             // You have arrived at a fork in the road, choose your destiny wisely

             // If you do not want to add new users to WordPress if they do not
             // already exist uncomment the following line and remove the user creation code
             //$user = new WP_Error( 'denied', __("ERROR: Not a valid user for this system") );

             // Setup the minimum required user information for this example
             $userdata = array( 'user_email' => $ext_auth['email'],
                                'user_login' => $ext_auth['email'],
                                'first_name' => $ext_auth['first_name'],
                                'last_name' => $ext_auth['last_name']
                                );
             $new_user_id = wp_insert_user( $userdata ); // A new user has been created

             // Load the new user info
             $user = new WP_User ($new_user_id);
         } 

     }

     // Comment this line if you wish to fall back on WordPress authentication
     // Useful for times when the external service is offline
     remove_action('authenticate', 'wp_authenticate_username_password', 20);

     return $user;
}

Now let’s go through some of this code:

  • wp_remote_get() – WordPress function that will fetch a remote resource with GET.
  • WP_Error – WordPress class to handle when errors occur. The authenticate filter automagically checks WP_Error before allowing login to continue
  • WP_User – The WordPress User Class allows accessing properties, roles and capabilities of a specific user.
  • wp_insert_user – Inserts a new user into the WordPress database based on the passed in user fields

The remove_action line is of particular importance. If that line exists WordPress will only authenticate based on the external service. If that line were to be commented out WordPress would fall back on the local user table for authentication if external authentication fails.

Additional considerations

WordPress contains several additional hooks that may be important to know about in your situation:

  • lost_password – This action allows you to alter how WordPress retrieves lost passwords. If passwords may only be retrieved from the external service developers can use this hook to send users to that service to set a new password
  • user_register – Provides access to the user’s data after registration. If the external authentication service allows new users to be posted to it this hook will enable developers to easily push the information out
  • register_form – Allows developers to extend the user registration form
  • retrieve_password – Helps with lost passwords
  • password_reset – Helps with lost passwords

In this demo we checked to see if a user exists in WordPress based on the email supplied by the authentication provider, but what happens if that user were to change their email address on the provider’s side? Suddenly they would lose all their information stored in the WordPress site, which could include access to all posts and settings. Make sure that whatever method you chose makes sense for your audience and usage patterns. Bad things happen when users think their data has been lost…

And more importantly, what will happen if/when the external provider goes offline? Maybe your service relies on other resources from the external provider and it is ok for your site to be down when theirs is. Generally though you will probably want your service to stay up. If this is the case then you will probably not want to have the remove_action line in the code above. Removing that line will allow WordPress to fallback on it’s own user system. However, since most external authentication systems purposefully do not send back a user’s password you may need to have the user reset their password on your system, or even when their user is first created have them insert an “emergency” pin to access your site when the external provider is down.

Helpful links

I found the following links helpful while researching this topic. If I do not make sense maybe they will