Intro
Recently I was working on a WordPress plugin project where I needed to secure a custom API endpoint using basic authentication. (For those who don't know, basic authentication uses a username and password as opposed to a token or other method.) Saving passwords in git repos is incredibly insecure for a variety of reasons. It's honestly not even worth talking about it. Just don't do that.
One of the most common solutions is to use what's called a dotenv file (.env
). This is a file that doesn't get checked into source control. Usually a repo will have a .env.example
file. This file will show what environment variables need to be made available. Initally, just to get things working, I added the Symfony Dotenv package to my project. It worked really well, but then I was talking with my colleague Brian. We discussed problems we'd had in WordPress multisite with composer packages in the past. So that lead me to write my own (more basic) Dotenv class to handle the job.
So, in this post you'll learn about:
- the PHP
file
function - how to use the
preg_match_all
function to create key/value pairs - how to assign those pairs to the PHP [
$_ENV
](https://www.php.net/manual/en/reserved.variables.environment " - PHP env super-global documentation") super-global variable and use them.
Creating .env files
Really, there will be two .env
files as I mentioned. They might look like this.
.env.example
USERNAME=samSmith
PASSWORD=password
.env
USERNAME=samSmith
PASSWORD=superSecret1234
The .env
file values will only be available in a secure location like a CI/CD service. They can be written into the project safely and sent to the project server where they can be used.
Setting up a Dotenv Class
The first thing I want to do is set up a class that will allow me to keep my code organized. When the class is constructed, I want it to find the .env
file. Then, create an initial array of data.
<?php
namespace MyPlugin;
class Dotenv {
// the path to the file. this will depend on what server we're at
private $envPath;
// the initial pairs array. this will be raw data from the file.
private $pairsArray;
// the processed key/value pairs
private $envPairs;
public function __construct() {
$this->envPath = $_SERVER['HTTP_HOST'] !== 'localhost' ?
MY_PLUGIN_DIR . '.env' :
MY_PLUGIN_DIR . '.env.example';
$this->pairs = file($this->envPath);
}
}
As you can see, the path of the file will depend on the server. If we're just developing locally, we can use the example file that was checked into source control. This will give people a quick start to using the class. They won't have to set up their own file. Otherwise, the "real" .env
file will be used.
Now that I have the path to that file, I can use it in the file
function. This function will split the file into an array. In the examples above, that means something like this...
[
"USERNAME=samSmith",
"PASSWORD=password"
]
For now, this is fine.
Setting the pairs
Now I wanted to turn the array from the file
function into this format.
[
"USERNAME" => "samSmith",
"PASSWORD" => "password"
]
This is how the $_ENV
super-global needs to appear in order to easily check against it.
To do that, I wanted to split each item int the $pairsArray
at the equal sign. A good way to do that is with the preg_match_all
function. It takes three arguments:
- a regular expression
- a string to test
- the matches found by the regular expression. ok... this isn't really an argument I guess...
Here's a class method that will do that. I'll show you then talk through it.
<?php
private function setPairs() {
foreach ($this->pairsArray as $pair) {
preg_match_all('/[\w-]+/', $pair, $matches);
$this->envVars[$matches[0][0]] = $matches[0][1];
}
}
That's it! Let's talk it out.
RegEx
The regular expression [\w-]+
will match:
- all letters (lowercase and captial)
- all whole numbers 0-9
- the underscore
- the dash
- between 1 and unlimited times
The great part is that it will split on the equal sign which is not included in the expression.
Pairs
The $pair
argument is the the thing to test against the regular expression.
Matches
The $matches
are essentially the output of the function. Rather than assign the returned result of preg_match_all
to a variable, you can use this directly. In our case, each $matches
variable will be an array that looks like this...
array {
[0]=>
array(2) {
[0]=>
string(8) "USERNAME"
[1]=>
string(8) "samSmith"
}
}
Now, this method can be called inside the class constructor.
<?php
namespace MyPlugin;
class Dotenv {
// the path to the file. this will depend on what server we're at
private $envPath;
// the initial pairs array. this will be raw data from the file.
private $pairsArray;
// the processed key/value pairs
private $envPairs;
public function __construct() {
$this->envPath = $_SERVER['HTTP_HOST'] !== 'localhost' ?
MY_PLUGIN_DIR . '.env' :
MY_PLUGIN_DIR . '.env.example';
$this->pairs = file($this->envPath);
$this->setPairs();
}
}
Creating the ENV Pairs
Now, let's say that we have an endpoint that needs to be secured. Perhaps it's meant to POST
some data or perform an action. Only authorized users should do that. In WordPress we can extend the WP_REST_Controller
class. This is a great way to manage using the Dotenv
class.
<?php
namespace MyPlugin;
use MyPlugin\Dotenv;
use WP_REST_Controller;
class Controller extends WP_REST_Controller {
private $userName;
private $password;
public function __construct(string $namespace = 'my-namespace/v1', string $resource = 'my-resources') {
$dotenv = new Dotenv();
$dotenv->createEnvGlobals();
$this->userName = $_ENV['USERNAME'];
$this->password = $_ENV['PASSWORD'];
}
}
Now the environment variables have been created and are part of the class. From here, it's we can write a method to compare the credentials coming in from a POST
request for instance with those on the server.
/**
* Checks basic authentication of the requests to an endpoint.
* Also confirms that the Authorization header has been passed successfully.
*
* @param string $user
* @param string $pass
* @return array
*/
private function authenticateRequest(string $user, string $pass): array { $authentication = [
'isAuthenticated' => false,
'hasAuthorizationHeader' => false
];
$headers = getallheaders();
// check to make sure the Auth header exists.
// believe it or not, this can be a problem.
if (isset($headers['Authorization'])) {
$authentication['hasAuthorizationHeader'] = true;
} else {
return $authentication;
}
// separate out the incoming encoded authentication
$auth = explode(' ', $headers['Authorization']);
$authArray = explode(':', base64_decode($auth[1]));
// compare
if ($authArray[0] === $user && $authArray[1] === $pass) {
$authentication['isAuthenticated'] = true;
}
// return true or false
return $authentication;
}
Here's what the whole thing looks like together.
<?php
namespace MyPlugin;
use MyPlugin\Dotenv;
use WP_REST_Controller;
class Controller extends WP_REST_Controller {
private $userName;
private $password;
public function __construct(string $namespace = 'my-namespace/v1', string $resource = 'my-resources') {
$dotenv = new Dotenv();
$dotenv->createEnvGlobals();
$this->userName = $_ENV['USERNAME'];
$this->password = $_ENV['PASSWORD'];
}
/*
*
* Register a route at my-namespace/v1/my-endpoint
* Give it the ability to doSomething when a POST request is made
*
*/
public function registerRoutes() {
register_rest_route($this->namespace, '/' . $this->resource . '/my-endpoint', [
'methods' => 'POST',
'callback' => [$this, 'doSomething']
]);
}
public function doSomething() {
$isAuthenticated = $this->authenticateRequest($this->user, $this->password);
if (!$isAuthenticated) {
// bail immediately and return an error
}
// continue with the rest of the method.
}
/**
* Checks basic authentication of the requests to an endpoint.
* Also confirms that the Authorization header has been passed successfully.
*
* @param string $user
* @param string $pass
* @return array
*/
private function authenticateRequest(string $user, string $pass): array { $authentication = [
'isAuthenticated' => false,
'hasAuthorizationHeader' => false
];
$headers = getallheaders();
// check to make sure the Auth header exists.
// believe it or not, this can be a problem.
if (isset($headers['Authorization'])) {
$authentication['hasAuthorizationHeader'] = true;
} else {
return $authentication;
}
// separate out the incoming encoded authentication
$auth = explode(' ', $headers['Authorization']);
$authArray = explode(':', base64_decode($auth[1]));
// compare
if ($authArray[0] === $user && $authArray[1] === $pass) {
$authentication['isAuthenticated'] = true;
}
// return true or false
return $authentication;
}
}
Wrapping up
Obviously this is a very simple example. It won't handle super complex passwords for instance. However, the regular expression could be changed to do that. But sometimes the simplest approach is best.
That's all for now.