Trick to share Wordpress login across domains

Today I bring you a little trick that will prevent some headaches when working with headless WordPress.  As an example I will present a real use case.

Use Case: We have an MVC app with a headless WordPress CMS. How do we show extra content in the MVC frontend when the user is also logged in to WordPress CMS as an administrator?

This sounds easy, right? Not quite. Both parts, frontend and content (CMS), use different domains, reading the WordPress session is not an option.

The Solution

In our app, there is a frontend that consist of an angular UI and a backend section implemented with Erdiko PHP framework. The content is being administered within WordPress, and Erdiko pull this data to be shown in the UI via AJAX.

To begin, we need to split the content in two big parts, a WordPress plugin and a helper function in Erdiko.

WordPress “logged_users” plugin

The strategy is create a new “option” where we will store a list of User ID’s, that will be grouped by client IP.

In order to maintain this list updated, we’ll hook two WordPress events: “wp_login” and “clear_auth_cookie“.
The former will be triggered by wp-admin login and there we will capture client IP and User and will update our option.

Here’s an example:

// when a user logs in, add them to our list of logged in users
add_action('wp_login', 'addUser', 10, 2);
function addUser($user_login, $user)
{
    switch_to_blog(1);
    $logged_in_users = get_option('logged_in_users');
    if (!empty($logged_in_users)) {
        if (!in_array($user->user_login, $logged_in_users)) {
            $clientIp = getClientIP();
            $logged_in_users[$clientIp] = $user->ID;
            update_option('logged_in_users', $logged_in_users);
        }
    } else {
        update_option('logged_in_users', [$_SERVER["REMOTE_ADDR"] => $user->ID]);
    }
}

The latter will be triggered right before logout and will remove the user from database.

// when a user logs out, remove them from our list of logged in users
add_action('clear_auth_cookie', 'removeUser');
function removeUser()
{
    switch_to_blog(1);
    $logged_in_users = get_option('logged_in_users');
    if (!empty($logged_in_users)) {
        $clientIp = getClientIP();
        if (!in_array($clientIp, $logged_in_users)) {
            unset($logged_in_users[$clientIp]);
            update_option('logged_in_users', $logged_in_users);
        }
    } else {
        delete_option('logged_in_users');
    }
}

Note: We hook “clear_auth_cookie” instead of “wp_logout” because we need to know the user_id of our current  logged user. If we opt for “wp_logout”, the user_id becomes unavailable.

Get Client Real IP

You may also need a function to get client’s real IP from request, in particular if you are behind a proxy.

function getClientIP()
{
    $ipaddress = '';

    if (isset($_SERVER['HTTP_TRUE_CLIENT_IP'])) {
        $ipaddress = $_SERVER['HTTP_TRUE_CLIENT_IP'];
    } elseif (isset($_SERVER['TRUE_CLIENT_IP'])) {
        $ipaddress = $_SERVER['TRUE_CLIENT_IP'];
    } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
        $ipaddress = $_SERVER['HTTP_CLIENT_IP'];
    } elseif (isset($_SERVER['HTTP_X_REAL_IP'])) {
        $ipaddress = $_SERVER['HTTP_X_REAL_IP'];
    } elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
    } elseif (isset($_SERVER['HTTP_X_FORWARDED'])) {
        $ipaddress = $_SERVER['HTTP_X_FORWARDED'];
    } elseif (isset($_SERVER['HTTP_FORWARDED_FOR'])) {
        $ipaddress = $_SERVER['HTTP_FORWARDED_FOR'];
    } elseif (isset($_SERVER['HTTP_FORWARDED'])) {
        $ipaddress = $_SERVER['HTTP_FORWARDED'];
    } elseif (isset($_SERVER['REMOTE_ADDR'])) {
        $ipaddress = $_SERVER['REMOTE_ADDR'];
    }

    /*
     * As per https://en.wikipedia.org/wiki/X-Forwarded-For#Format we can have multiple IPs
     * Added  new function to overcome this behaviour and get only client IP
     */
    return checkMultiIp($ipaddress);
}

function checkMultiIp(string $ips): string
{
    $result = $ips;
    if (strpos($ips, ',') > 0) {
        $_array = explode(',', $ips);
        $result = trim(array_shift($_array));
    }
    return $result;
}

The last function was added because of X-Forwarded-For, to extract only an IP address and not all Forwarded ones.

The helper

Finally, we need a helper function that checks for logged CMS administrator to show / hide private content.

public static function isAdminLogged()
{
    $result = false;
    try {
        ob_start();
        $model = @new WModel();
        $logged_in_users = @$model->get_option('logged_in_users');
        if(!empty($logged_in_users) && is_array($logged_in_users)) {
            foreach ($logged_in_users as $user_ip => $user_id) {
                if ($user_ip == Acl::getClientIP()) {
                    $_user = @$model->get_user_by('ID', $user_id);
                    if (!empty($_user) && ($_user instanceof \WP_User) && $_user->has_cap('administrator')) {
                        $result = true;
                        break;
                    }
                }
            }
        }
        @ob_get_clean();
    } catch (\Exception $e) {
        $result = false;
    }

    return $result;
}

The block that interacts with WordPress stuff is wrapped between ob_start(); and @ob_get_clean(); to prevent any headers errors. With $model we get all “logged_in_users” and iterate them until find current request IP. If it is successful, it attempts to load a WordPress user by user_id and check if is an Administrator.

Thoughts

Although reading WordPress Session from different domain brings easy solution, I must warn you that it can also bring you a security flaw. At this point, with just a simple plugin and sharing a database we still can reach the goal.

Hope these tips help. Thanks for reading and see you on the next post.

Next Post

Comments

See how we can help

Lets talk!

Stay up to date on the latest technologies

Join our mailing list, we promise not to spam.