Adding HTTP Authentication to a WordPress site

When you’re working on multiple environments, particularly when you have a publicly accessible testing or staging environment, often you need to keep these environments protected.

One option in WordPress is to restrict access to users who aren’t logged in to WordPress; i.e. make them login first. But that makes it hard to test the site for users who aren’t logged in – for example, how does your custom login page look and does it work?

Another option is restricting IP’s to only a handful you need. But IP’s change, unless you opt for a static one. And even when this is an option for you and your development team, when you need to demo the site to a client, it can make life rather difficult.

My preferred option is Basic HTTP Authentication; it’s simple and effective and there are several ways to set this up.

If you’re using Apache, you could use your .htaccess file – but this can lead to complications when the same codebase (inc. the .htaccess file) needs to be pushed to the live environment (you’d need to set the .htaccess file to only trigger the HTTP Authentication where certain conditions are / are not met); or if you’re using Nginx configuration you’d add this to the testing / staging environment configurations – much more straight forward to keep control over things.

However, the Apache and Nginx options have one crucial drawback.. updating user credentials can be cumbersome and usually requires a developer to make changes – not something your typical client would be able to do.

Sometimes, there are times where you only have control of the codebase, but not the infrastructure. Client hosting and some platform as a service (PAAS) offerings limit what you can do. For example, a provider using Nginx doesn’t give you an option directly to add this, the better ones tend to though.

I’ve put together a simple WordPress plugin, also available on Github Gist, that deals with HTTP Authentication in PHP – there is an assumption here that you’re using environment variables to determine which site you’re looking at, but it would be relatively simple to update this to a static URL or for example any URL containing example.com except www.example.com..

This could be extended to have an options page that controls access to the site; for example by allowing access to the WordPress login page and excluding specific roles from being required to use HTTP authentication – you could even add an options page to add / edit / remove the valid credentials.

A quick side note – to clear your credentials, add ?logout to any URL – this resets the username and password to be blank.

Alternatively, you can just filter the environments and credentials that can be used to access the site with a WordPress filter, or simply take this, change it, and make it your own 🙂

<?php
/**
 * Plugin Name: WP Basic HTTP Authentication
 * Description: Adds HTTP Authentication to a WordPress site
 * Author: James Morrison
 * Version: 1.0.0
 * Author URI: https://www.jamesmorrison.me
 **/

// Namespace
namespace WP_Basic_HTTP_Authentication;

// Security check
defined( 'ABSPATH' ) || die( 'Direct file access is forbidden' );

// Define valid usernames / passwords
function valid_credentials() {

	return apply_filters( 'wp_basic_auth_credentials', [
		'example_user' => 'example_password',
	] );

}

// Define restricted environments
function restricted_environments() {

	return apply_filters( 'wp_basic_auth_environments', [
		'staging',
	] );

}

// Is the user authenticated?
function is_authenticated() {

	// Username and Password defaults
	$user = false;
	$password = false;

	// Sanitize username if set
	if ( isset( $_SERVER['PHP_AUTH_USER'] ) ) {
		$user = sanitize_text_field( $_SERVER['PHP_AUTH_USER'] );
	}

	// Sanitize password if set
	if ( isset( $_SERVER['PHP_AUTH_PW'] ) ) {
		$password = sanitize_text_field( $_SERVER['PHP_AUTH_PW'] );
	}

	// Retrieve the valid credentials
	$valid_credentials = valid_credentials();

	// Loop through the valid credentials to authenticate user
	foreach ( $valid_credentials as $valid_username => $valid_password ) {

		// If the username doesn't match, skip to the next record
		if ( $user !== $valid_username ) {
			continue;
		}

		// Validate the password; we already know we have a valid username
		if ( $password === $valid_password ) {
			return true;
			break;
		}
	}

	// User is not authenticated
	return false;

}

// Failed authentication.. return 401
function failed_authentication() {

	header( 'WWW-Authenticate: Basic realm="Private Site"' );
	header( 'HTTP/1.0 401 Unauthorized' );
	echo 'FAILED LOGIN';
	die();

}

// Check if we need to authenticate this user; if we do, check the user is authenticated
add_action( 'init',
	function() {

		// Default to no environment
		$environment = false;

		// Work out if there is a defined environment
		if ( isset( $_SERVER['environment'] ) ) {
			$environment = sanitize_text_field( $_SERVER['environment'] );
		}

		// Bail early if there's no environment set
		if ( ! $environment ) {
			return;
		}

		// Retrieve the restricted environments
		$restricted_environments = restricted_environments();

		// Check the current environment is not one of the restricted ones; bail if it's not
		if ( ! in_array( $environment, $restricted_environments ) ) {
			return;
		}

		// We have to authenticate this user
		if ( ! is_authenticated() ) {
			failed_authentication();
		}

	}, 1, 0
);


// Logout
add_action( 'init',
	function() {

		if ( isset( $_GET[ 'logout' ] ) ) {
			$_SERVER['PHP_AUTH_USER'] = $_SERVER['PHP_AUTH_PW'] = '';
		}		

	}, 1, 0
);

Photo by James Sutton on Unsplash

Leave a Reply

Your email address will not be published. Required fields are marked *