cult3

Create a HMAC-SHA authentication implementation for PHP

May 21, 2014

Table of contents:

  1. How will this package work?
  2. Custom exceptions
  3. The Token object
  4. The factory class
  5. The Request class
  6. Authenticating requests
  7. Testing
  8. Conclusion

When it comes to providing authentication for your API, you basically have a few different options. HTTP Basic is a popular choice because of how easy it is to use. All you have to do is copy and paste your username and password or API key and you can start interacting with the API straight away.

However sending your username and password or API key across the wire isn’t the most secure approach (Why the hell does your API still use HTTP Basic Auth?).

Oauth is another popular choice, but it is probably overkill if all you want to do is to authenticate your application with your API.

A third option is to use a shared key and secret to hash the request. This means the request is sent as a hash, and can include a timestamp so the hash will be different every time you send it.

In this post I’m going to be creating a HMAC-SHA authentication implementation package for PHP. This package will allow you to create requests and hash them in your client code, and then authenticate the request on the API side.

As a side note, this package is a port of Signature, a Ruby gem that does exactly the same thing. If you are also interested in learning Ruby, then this will probably give you a good introduction to reading code in a different language.

How will this package work?

Just so we’re all clear, first I’ll outline exactly how this package will work from the perspective of a developer who wants to use it in their project.

Imagine a scenario where you want to create a new user using your API. You have the following data that you need to send to create the new user:

// Create data to send
$params = ["name" => "Philip Brown", "email" => "name@domain.com"];

In order to authenticate with the API, we have a key and a secret that only us and the API server know about. We can use this key and secret combination to create a Token;

// Create new Signplz instance
$signplz = new Signplz();

// Create Token
$token = $signplz->token("my_key", "my_secret");

Next we need to create a request, passing in the HTTP method, the endpoint and the parameters that we want to send:

// Create Request
$request = $signplz->request("POST", "/api/thing", $params);

Now that we have the request and the token, we can use the token to securely sign the request:

// Sign the request
$auth_params = $request->sign($token);

Finally we can merge the $auth_params with the original $params that we want to send to the request:

// Create query params
$query_params = array_merge($params, $auth_params);

var_dump($query_params);
/*
array(6) {
'name' => string(12) "Philip Brown"
'email' => string(16) "name@domain.com"
'auth_version' => string(3) "1.0"
'auth_key' => string(6) "my_key"
'auth_timestamp' => int(1387899087)
'auth_signature' => string(64) "f4d3e997fa469e393f63243c4659b698dd38aef849cf01a7fdaf53ce8821c13c"
}
*/

Now you can send $query_params to your API. The API will find the secret from the auth_key and then authenticate the auth_signature. Notice how there is also a timestamp? This will prevent relay attacks because the signature will expire.

This allows you to make requests to your API without ever having to expose your API secret.

Now that I’ve explained how this package will work, I’ll walk you through how the code is structured and implemented. This package is called Signplz. You can find the entire source code on GitHub.

I’m going to assume you’ve already set up the package structure from this tutorial, so I won’t cover the same ground again.

Custom exceptions

The first thing I will do will be to create a custom exception. This package will be used to create the requests, but also authenticate them. This means we need to handle the situation where a request has been made to the API but it is not valid, or it is missing part of the body that we need to authenticate.

To handle this situation I will create a custom Exception that can be caught and dealt with.

Create a new folder under src called Exception and copy the following code:

<?php namespace PhilipBrown\Signplz\Exception;

use Exception;

class AuthenticationException extends Exception
{
}

This is just your basic custom exception as I won’t need any additional methods.

The Token object

The first class I will make will be Token. This object will basically be a Value Object that will hold the key and the secret:

<?php namespace PhilipBrown\Signplz;

class Token
{
    /**
     * The key
     *
     * @var string
     */
    protected $key;

    /**
     * The secret
     *
     * @var secret
     */
    protected $secret;

    /**
     * Create a new instance of Token
     *
     * @param string $key
     * @param string $secret
     * @return void
     */
    public function __construct($key, $secret)
    {
        $this->key = $key;
        $this->secret = $secret;
    }

    /**
     * Get the key
     *
     * @return string
     */
    public function getKey()
    {
        return $this->key;
    }

    /**
     * Get the secret
     *
     * @return string
     */
    public function getSecret()
    {
        return $this->secret;
    }
}

When creating a new Token object, you pass the key and the secret in through the __construct() method.

With this being a Value Object, it will only require methods for retrieving class properties. In this case, getKey() and getSecret().

The factory class

For this package I’m going to use a factory class that will provide two convenient methods for creating the two types of objects that we’re going to need.

As I covered in What is the Factory Method Design Pattern?, a factory class provides a consistent and easy way to instantiate objects:

<?php namespace PhilipBrown\Signplz;

class Signplz
{
    /**
     * Token
     *
     * @param string $key
     * @param string $secret
     * @return Philipbrown\Signplz\Token
     */
    public function token($key, $secret)
    {
        return new Token($key, $secret);
    }

    /**
     * Request
     *
     * @param string $method
     * @param string $path
     * @param array $params
     * @return PhilipBrown\Signplz\Request
     */
    public function request($method, $path, array $params)
    {
        return new Request($method, $path, $params);
    }
}

I’ve already created the Token class, so now I need to create the Request class.

The Request class

The Request class is where the real magic happens in this package. There’s quite a bit going on in this class, so I’ll take it step by step.

The class properties

Before I get into the methods of the class, first I will talk about each of the class properties:

/**
 * The HTTP method
 *
 * @var string
 */
protected $method;

First we have the $method. This is the the HTTP method of the request, for example, POST.

/**
 * The API path
 *
 * @var string
 */
protected $path;

Next we have the actual path of the endpoint, for example users

/**
 * The data to send
 *
 * @var array
 */
protected $params;

Next we have the parameters of the request. This is the data that you want to send to the API.

/**
 * The version of Signplz
 *
 * @var string
 */
protected $version = '1.0';

This is the version number of the package so that you can ensure both ends of the request are using the same code.

/**
 * The default auth parameters
 *
 * @var array
 */
protected $auth_params = array(
    'auth_version' => null,
    'auth_key' => null,
    'auth_timestamp' => null,
    'auth_signature' => null
);

The signature will require some default parameters.

/**
 * The query params
 *
 * @var array
 */
protected $query_params = array();

And finally, we have the query parameters.

The Constructor

Next I need to implement the __construct() method for when the Request object is instantiated:

/**
 * Create a new instance of Request
 *
 * @param string $method
 * @param string $path
 * @param array $params
 * @return void
 */
public function __construct($method, $path, array $params)
{
    // Set method
    $this->method = strtoupper($method);

    // Set path
    $this->path = $path;

    // Set the params
    foreach ($params as $k => $v) {
        $k = strtolower($k);
        substr($k, 0, 5) == 'auth_' ? $this->auth_params[$k] = $v : $this->query_params[$k] = $v;
    }
}

As you can see from the code above, here I’m basically just setting the class properties.

The foreach statement will separate the $params into the auth_params and the query_params. This is important when you come to authenticate the request.

The sign method

In order to make the Request secure, we need to sign it using the Token:

/**
 * Sign the request with a token
 *
 * @param PhilipBrown\SignPlz\Token $token
 * @return array
 */
public function sign(Token $token)
{
    $this->auth_params = array(
        'auth_version' => '1.0',
        'auth_key' => $token->getKey(),
        'auth_timestamp' => time()
    );

    $this->auth_params['auth_signature'] = $this->signature($token);

    return $this->auth_params;
}

This method accepts a Token object as an argument.

First I set the $this->auth_params with the auth_version, auth_key and the auth_timestamp.

Next I can generate the auth_signature by passing the $token to the signature() method.

The signature method

The signature() method will return a hash of the request:

/**
 * Get the hashed signature
 *
 * @param PhilipBrown\Signplz\Token $token
 * @return string
 */
protected function signature(Token $token)
{
    return hash_hmac('sha256', $this->stringToSign(), $token->getSecret());
}

In this method I’m using the hash_hmac function and I choose the sha256 algorithm.

Next I pass in the secret from the Token as well as the return value from the stringToSign() method.

The stringToSign method

The stringToSign() method returns a string of values joined together using the \n character:

/**
 * String To Sign
 *
 * @return string
 */
protected function stringToSign()
{
    return implode("\n", array($this->method, $this->path, $this->parameterString()));
}

This method will use the return value of the parameterString() method.

The parameterString method

The parameterString() method will create a string based upon the parameters of the request:

/**
 * Parameter String
 *
 * @return string
 */
protected function parameterString()
{
    // Create an array to build the http query
    $array = array();

    // Merge the auth and query params
    $params = array_merge($this->auth_params, $this->query_params);

    // Convert keys to lowercase
    foreach ($params as $k => $v) {
        // Set each param on the array
        $array[strtolower($k)] = $v;
    }

    // Remove the signature key
    unset($array['auth_signature']);

    // Encode array to http string
    return http_build_query($array);
}

First I will create a blank array to hold the values.

Next I will merge the $this->auth_params and the $this->query_params arrays.

Next I will cycle through each of the keys and values and set them on the empty array ensuring to set the keys as lowercase strings.

Next I will remove the auth_signature key.

Finally I will use the http_build_query function to return a URL-encoded query string.

Quick recap

Phew, that was probably a lot to take in, so let me do a quick recap.

When we sign a request, we basically need to take each part of the request and then hash it all so we can generate a hash signature to send. The four methods above are basically just breaking down the request and then returning each bit so that it can be hashed together. It’s better to have really small methods that do one job, rather than one big sign() method that tries to do everything.

Authenticating requests

Now that everything is set up to create requests, we need to write the method that will be used to authenticate them.

The main method is the authenticate() method that accepts a Token and an optional $timestampGrace parameter.

When authenticating a request, you would take the auth_key and retrieve the secret from your database. You can then use the key and the secret to create a Token just as you did on the client side.

By default, the signature will expire after 600 milliseconds. You can overwrite this by passing in an optional $timestampGrace.

/**
 * Authenticate the request
 *
 * @param PhilipBrown\Signplz\Token $token
 * @param int $timestampGrace
 */
public function authenticate(Token $token, $timestampGrace = 600)
{
    // Check the authentication key is correct
    if ($this->auth_params['auth_key'] == $token->getKey()) {
        return $this->authenticateByToken($token, $timestampGrace);
    }

    throw new AuthenticationException('The auth_key is incorrect');
}

The authenticate() method will ensure that the auth_key of the request and the Token are the same. If they do not match, an AuthenticationException will be thrown.

If the two keys do match, the $token and the $timestampGrace are passed to the authenticateByToken() method.

Authenticate By Token

The authenticateByToken() method will run through the checks to ensure the request is valid:

/**
 * Authenticate By Token
 *
 * @param PhilipBrown\SignPlz\Token $token
 * @param int $timestampGrace
 * @return bool
 */
protected function authenticateByToken(Token $token, $timestampGrace)
{
    // Check token
    if ($token->getSecret() == null) {
        throw new AuthenticationException('The token secret is not set');
    }

    // Validate version
    $this->validateVersion();

    // Validate timestamp
    $this->validateTimestamp($timestampGrace);

    // Validate signature
    $this->validateSignature($token);

    return true;
}

First the secret is checked to make sure it has been set. If it has not been set, an AuthenticationException is thrown.

Next we can run through each of the tests.

Validate Version

First we check to see if the version number matches:

/**
 * Validate Version
 *
 * @return bool
 */
protected function validateVersion()
{
    if ($this->auth_params['auth_version'] !== $this->version) {
        throw new AuthenticationException('The auth_version is incorrect');
    }

    return true;
}

If the version number does not match it might mean that different parameters or a different hashing algorithm was used to generate the signature. If the two versions do not match, we throw a AuthenticationException.

Authenticate Timestamp

Next we check to see if the timestamp is still valid:

/**
 * Validate Timestamp
 *
 * @param int $timestampGrace
 * @return bool
 */
protected function validateTimestamp($timestampGrace)
{
    if ($timestampGrace == 0) {
        return true;
    }

    $difference = $this->auth_params['auth_timestamp'] - time();

    if ($difference >= $timestampGrace) {
        throw new AuthenticationException('The auth_timestamp is invalid');
    }

    return true;
}

First we check to see if the $timestampGrace has been set to 0. If it has we can just ignore this test.

Next we calculate the difference between the auth_timestamp and the current timestamp. If the different is bigger than the $timestampGrace, we can throw an AuthenticationException.

Authenticate Signature

Finally, if the request has passed all of the tests so far, we can validate that the auth_signature is correct:

/**
 * Validate Signature
 *
 * @param PhilipBrown\Signplz\Token $token
 * @return bool
 */
protected function validateSignature(Token $token)
{
    if ($this->auth_params["auth_signature"] !== $this->signature($token)) {
        throw new AuthenticationException('The auth_signature is incorrect');
    }

    return true;
}

This method will generate a hash based upon the request and then check to see if it matches the auth_signature. By using the same parameters from the body of the request and the secret we have stored for the key, we should get the exact same hash if the request has been signed with the correct secret.

If the two signatures do not match a AuthenticationException will be thrown.

Testing

With a package like this, it’s a good idea to unit test the basic methods to ensure nothing is accidentally broken. It would be a bit of a nightmare if you broke something which then prevented your application talking to your API!

Create a new file called SignplzTest under tests with the following code:

<?php namespace PhilipBrown\Signplz;

use PHPUnit_Framework_TestCase;
use PhilipBrown\Signplz\Signplz;

class SignplzTest extends PHPUnit_Framework_TestCase
{
}

Create a helper method

The first thing I will do will be to create a helper method that will generate a request that I can test against:

public function makeRequest()
{
    // Create new Signplz instance
    $signplz = new Signplz;

    // Create params
    $params = array('name' => 'Philip Brown', 'email' => 'name@domain.com');

    // Create Token
    $token = $signplz->token('my_key', 'my_secret');

    // Create Request
    $request = $signplz->request('POST', '/api/thing', $params);

    // Sign the request
    $auth_params = $request->sign($token);

    // Create query params
    return array_merge($params, $auth_params);
}

This method is not a test, it just means I don’t have to repeat the same set up stuff over and over again.

Testing the API

Your package should provide a clear and stable API for other developers to consume. This means you should ensure that only the correct inputs are accepted, and appropriate and consistent errors are returned:

/**
 * @expectedException Exception
 */
public function testExceptionWhenRequestParamsNotArray()
{
    // Create new Signplz instance
    $signplz = new Signplz;
    $request = $signplz->request('POST', '/api/thing', 'not an array');
}

In this test I’m ensure that the request() method enforces that the third argument should be an array.

Testing for success

Next I will test to ensure that the package is able to successfully create a request and that it authenticates correctly:

public function testAuthenticateRequestSuccess()
{
    // Create new Signplz instance
    $signplz = new Signplz;

    // Create Token
    $token = $signplz->token('my_key', 'my_secret');

    // Create Request
    $request = $signplz->request('POST', '/api/thing', $this->makeRequest());

    // Assert authenticated request
    $this->assertTrue($request->authenticate($token));
}

In this method I’m utilising the makeRequest() method from earlier. This test will ensure any changes to either side of the request / authenticate process do not ultimately break the authentication process.

Testing incorrect parameters

Finally I will test using incorrect parameters to ensure that I’m getting the correct exceptions being returned. If incorrect parameters do not trigger an exception something is drastically wrong with the package!

In each of these methods I’m triggering the exception and ensuring that the message is correct:

/**
 * @expectedException Philipbrown\Signplz\Exception\AuthenticationException
 * @expectedExceptionMessage The auth_key is incorrect
 */
public function testAuthenticationRequestIncorrectKey()
{
    // Create new Signplz instance
    $signplz = new Signplz;

    // Create Token
    $token = $signplz->token('not_my_key', 'my_secret');

    // Create Request
    $request = $signplz->request('POST', '/api/thing', $this->makeRequest());

    // Attempt to authenticate
    $request->authenticate($token);
}

/**
 * @expectedException Philipbrown\Signplz\Exception\AuthenticationException
 * @expectedExceptionMessage The token secret is not set
 */
public function testAuthenticationRequestIncorrectSecret()
{
    // Create new Signplz instance
    $signplz = new Signplz;

    // Create Token
    $token = $signplz->token('my_key', null);

    // Create Request
    $request = $signplz->request('POST', '/api/thing', $this->makeRequest());

    // Attempt to authenticate
    $request->authenticate($token);
}

/**
 * @expectedException Philipbrown\Signplz\Exception\AuthenticationException
 * @expectedExceptionMessage The auth_version is incorrect
 */
public function testAuthenticationRequestIncorrectVersion()
{
    // Create new Signplz instance
    $signplz = new Signplz;

    // Create Token
    $token = $signplz->token('my_key', 'not_my_secret');

    // Change params
    $params = $this->makeRequest();
    $params['auth_version'] = '1.1';

    // Create Request
    $request = $signplz->request('POST', '/api/thing', $params);

    // Attempt to authenticate
    $request->authenticate($token);
}

/**
 * @expectedException Philipbrown\Signplz\Exception\AuthenticationException
 * @expectedExceptionMessage The auth_timestamp is invalid
 */
public function testAuthenticationRequestIncorrectTimestamp()
{
    // Create new Signplz instance
    $signplz = new Signplz;

    // Create Token
    $token = $signplz->token('my_key', 'my_secret');

    // Change params
    $params = $this->makeRequest();
    $params['auth_timestamp'] = time() + (7 * 24 * 60 * 60);

    // Create Request
    $request = $signplz->request('POST', '/api/thing', $params);

    // Attempt to authenticate
    $request->authenticate($token);
}

/**
 * @expectedException Philipbrown\Signplz\Exception\AuthenticationException
 * @expectedExceptionMessage The auth_signature is incorrect
 */
public function testAuthenticationRequestIncorrectSignature()
{
    // Create new Signplz instance
    $signplz = new Signplz;

    // Create Token
    $token = $signplz->token('my_key', 'my_secret');

    // Change params
    $params = $this->makeRequest();
    $params['auth_signature'] = 'secure signature. many character. so wow';

    // Create Request
    $request = $signplz->request('POST', '/api/thing', $params);

    // Attempt to authenticate
    $request->authenticate($token);
}

Conclusion

HMAC-SHA authentication is a great solution to API authentication for your website or web application. As I’ve shown in this tutorial, implementing HMAC-SHA authentication is really not that difficult at all.

If you feel like HTTP Basic auth is not for you, and you don’t want to set up a full blown Oauth Server, HMAC-SHA authentication could be an excellent middle ground.

I think one of the best attributes a developer can posses is Polyglotism (the ability to master multiple languages). Programming languages are just our tools for the job, and so you should endeavour to understand and use more than one. Try and compare the Signplz and the Signature repositories to see how you would implement this package in PHP and Ruby. I think a great way of learning another language is to port something from one language to another.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.