Ben Lobaugh Online

I think, therefore I am. I am, therefore I sail

WordPress: Replace Built-in User Authentication

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

Previous

Crystal Cove, my new favorite beach

Next

I got bored and built a new sailboat…

32 Comments

  1. Mukut

    I added one function customAuth in functions.php and added filter in wp-app.php but some how it is not working with buddy press plug in.
    It gives FATAL error in undefined functions.
    I am trying to implement authentication with our application where all sign up/login happens.

    • Mukut – Did you include the wp-app.php file in your functions.php? The functions.php file will be automagically included by WordPress, but the wp-app.php file most likely will not.

  2. Mukut

    Thanks blobaugh for the reply. I have not included anything in functions.php. As per this article I wrote fustomAuth function in functions.php and added addfilter code in the functions.php file. After that I moved both customAuth function and addfilter in wp-app.php. I am java developer but trying integrate java app and wordpress. We have java app which takes care of user registration/sign up, password change etc.
    I want to integrate word press authentication with our java app.
    Which file I need to modify to change the authentication?
    Thanks

  3. Mukut – Sorry for the slow response. First lets make sure we are on the same page. Is this functions.php file in your theme folder, is it the name of the file your plugin is in, or is it a random file named functions.php? If either of the first two are true, then the functions.php file will be automagically included whenever WordPress runs. The “easiest” place to put all your custom code would be in that file, however, if you would like to use wp-app.php (assuming it is not the main plugin file) from functions.php you can include it using require_once( ‘wp-app.php’ ).

    Is that helpful?

  4. Do you know of a way while your capturing the user name and password is the check box for remember also available?

    • You might be able to check the $_POST variable. I have not tried to get the checkbox, but as long as WordPress has not forwarded the page anywhere $_POST will still be available locally

  5. neha

    I add the demo_auth to : functions.php (in wp-includes folder).
    I added the add_filter at wp-app.php (at htdocs folder).
    When I browze : mydomain/wp-login.php , it shows login and allows me to go throught, but doesnt seem to be using my filter… ?

    • You’re Doing It Wrong ™ :).

      You are touching core files and you should never need to do that. Put the code inside of a plugin you create or your theme’s functions.php file and that should get you pointed in the correct direction

  6. neha

    hi blob,
    ya I realized it later and I am now puting it in a plugin. 🙂 !!
    Will post you if I have questions. Thanks 🙂

  7. neha

    hey..ben.. it worked.. thanks for the wonderfull post.. 🙂 !!!

  8. Vida

    Hi Bob,

    Thank you very much!

    This works really well and you have explained it brilliantly.

    I got it working and I don’t even know how to create a wordpress plugin!!

    Just wondering, when the new user logs in for the first time, wordpress 3.5.1 is then redirecting the user to their profile page. Is there are way to just have them redirected back to the homepage instead?

    Thanks
    Vida

  9. Vida

    Whoops!
    Sorry, I meant Ben!
    Profuse apologies…

    Vida

  10. Vida

    Oh, doh…
    I should have googled this shouldn’t I?
    Apologies for bloating up your blog…
    The answer for anyone who needs it is to add the following to the themes’ functions.php

    /* redirect users to front page after login */
    function redirect_to_front_page() {
    global $redirect_to;
    if (!isset($_GET[‘redirect_to’])) {
    $redirect_to = get_option(‘siteurl’);
    }
    }
    add_action(‘login_form’, ‘redirect_to_front_page’);

  11. Andrew

    What I am hoping to do is authenticate a subscriber base using a service, such as the one above, OR using standard authentication for our general wordpress administrators and editors (i.e. against the users table).

    Has anyone come across this before?

  12. lesley

    Dear Ben,

    I am lost, not savvy enough to know where to put what.
    would it be possible, (asking too much, i know) to tell me how many files i need to put where ?

    i tried to make a plugin but i even failed that…. 🙁

    I am trying to make a simple members site by using a subsciption form on another site.

    i am totally new with wordpress, i would really appreciate any help

  13. Bahman

    Hi Ben,
    thank you for your great manual.
    In my case # if( $user->ID == 0 ) made issues, probably because $user->ID is rather empty than 0.
    I fixed the issue by using # if( empty($user->ID) ).
    Again thank you very much! Your manual made it very easy to implent an external authentication service.

  14. Thanks for this. I coded it up as a wordpress plugin available here: https://github.com/cogwheelcircuitworks/wordpress_ext_auth_demo

  15. Hey Ben, great article 🙂 Here are a couple things I was thinking about as I read it that might be important in certain use cases, or if someone were to build out a full-fledged version of this:

    * I know you just used HTTP since it was a demo, but it might be good to point out that, in production, the wp_remote_get() call should always use HTTPS to avoid sending the username/password in plaintext. It sounds like some of the other commenters are copy/pasting the demo into their sites without understanding the potential security implications. It might also be good to explicitly specify `sslverify` = true, even though that’s the default; that would make it obvious to other developers who work on the code that we want the call to fail if we can’t trust the authenticity of the remote certificate.

    * My preferred approach for the psuedo WP user would be to generate a strong random password instead of using their real one, in order to minimize attack vectors. Otherwise an attacker who gained access to the database could run rainbow tables against the hashes and get most of the passwords, then they could login to any other services that also used that same remote authentication endpoint.

    I’d then also always unhook `wp_authenticate_username_password` so that there wouldn’t be 2 valid passwords an attacker could login with, which would cut the brute force space in half. I’d also disable password resets just in case the custom auth plugin is ever disabled. In fact, it’d be better to have a uninstall/deactivation routine that removes all the pseudo users.

    * Instead of registering the custom `authenticate` callback at priority 10, I’d do it at 40, after `wp_authenticate_cookie`. That way auth cookies will be set in the user’s browser and you can avoid making a remote HTTPS call on every page load. That would probably be a noticeable improvement for page load times.

    * When users login, I’d update all their `user_meta` to make sure their name, etc is synced with the canonical source. You brought up a similar point with the changing e-mail address, but I figured it might be helpful to also mention this similar situation.

    * I think `$user = get_user_by( ‘login’, $username );` would do the same thing as the three lines you have to fetch the existing user.

    • All great points Ian!

      I wrote this on an airplane while learning about it by digging through core without an internet connection 🙂

      Now I am curious how it was done in Jetpack….

      • Actually, now that I think about it more, the cookie thing is probably wrong. It should already be setting cookies, even if your custom authenticate callback short-circuits the cookie callback. Otherwise you’d be getting redirected to login with everything request. I haven’t tested that out, though, so it may be worth double-checking the next time you implement this.

        • Yup. I was confusing `wp_authenticate_cookie()` with `wp_validate_auth_cookie()`. The `authenticate` callback only gets called during the initial login process. After that, `auth_redirect()` calls `wp_validate_auth_cookie()` on each page request. So, no need to worry about a remote request on each page load.

  16. Hi Ben,

    Thank you for your tutorial. I think I understand and updated the information to work with my site and web service. However, I have additional security constants that are included in my sites security that I want to pull into the authentication. In Dreamweaver I used constants but I am having a hard time deciphering where this code goes in WordPress. Here is the code I used in Dreamweaver that worked for me. Would you be able to tell me where I can add this information? I am new to WordPress but not new to coding. After months of research I am totally stumped. Your tutorial was the closest I could find to what I am looking for and hoping my search has ended! PS – I live in Orange County and am glad I found your site. I am looking into attending a meet up soon. Hope you can make it down from Seattle sometime again!

  17. bharathg1010

    Hi ben,
    I am getting the following error. can you please resolve it.
    ->I ve added the deom_auth in theme’s function.php
    ->Placed my auth_serv.php in a accessible folder.

    Fatal error: Cannot use object of type WP_Error as array .

    • Check the return value from the call to auth_serv.php. Without seeing your code I am guessing the remote call is failing and returning the default WordPress error instead of what you are expecting. The demo does not take into account any failures or external conditions

  18. David

    I have tried working with this under 4.1.1 but I get a blank screen after the login pointing to wp-login.php. Are there any modifications needed for this newer version of WordPress?

  19. Ben, this. is. awesome. Thank you for giving me a starting point and providing us with this easy to understand code!

    I’ve built a rails application with doorkeeper oauth services and i needed a way to implement it with WordPress and being newish to WordPress development this was the perfect starting point.

    I praise you with my thanks! Once I get some time and get my site up, you’ll be getting links from me!

  20. Douh. It’s of course the missing remove_action that’s been causing me a headache for two hours…

    Thank you, thank you, that you.

  21. CHANDAN

    WordPress: to replace built-in user login after taking data from cookie set by myAuthsite

    I am trying to load user profile of contributor after taking data from cookie set by http://www.myAuthSite.org. If username from cookie does not already exist in wp_users table of WordPress then I am inserting required user data in wp_users table but if username already exist then I want to load the current user dashboard so that he can post new articles on my site, same as he can do when he logs in using http://www.mysiteurl.org/wp-login.php.

    but when I am redirecting it to http//www.mysiteurl.org/wp-admin/post-new.php It is getting redirected to wp-login.php again and it is eventually getting redirected to http://myAuthsite.org (i.e, else part)

    below is the link to my code I have written in my functions.php file

    http://goo.gl/A1k8P2

  22. Very cool, man. Thank you very much for sharing this. In only ten minutes I am set, as long as I only have one user who’s name is Bob.

Leave a Reply

Powered by WordPress & Drip City Coffee