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.