Writing a Secure RESTful service in Zend Framework

So you're up with the RESTful Services buzz but you're concerned about security; as you should be! So what do you do? Well, like all good OOP practitioners, you don't reinvent the wheel. As Steve Jobs said, "Good Designers copy, Great Designers Steal", or borrow in our case. So let's look at the Amazon S3 model and implement a security layer for our RESTful services with that in mind with one of the frameworks of choice - Zend Framework.

Whilst this article will go in to a certain amount of detail, it will assume that the user has already gone through the process of setting up a basic Zend Framework application and the relevant database schema with the required user table. So, without further ado, let's get started.

Amazon S3 enforces security through a series of request headers, including a hash of the request and the api key of the requesting user. This makes it simple to ensure that every request is made by a valid user and that that user has sufficient privileges for the information that they are requesting.

When a request is made by the user, the request will contain their api key and a hash of their request. The hash is made, in our case, with the hash_hmac function, using the sha1 algorithm, with the secret key of the user, from their user credentials, as the hash seed.

When the REST server receives the request, it will do a look up to see if a valid user exists, identified by that api key. If so, then the secret key of the user is retrieved and a hash of the request is made, using the secret key as the hash seed. If the hash the server generates, matches the hash that the client submitted, then the request is accepted. If not, then the request is denied. In a nutshell, that's it.

So how do we do this with Zend Framework? Personally, I'm a big fan of controller plugins. They can make adding partial functionality a breeze. So we're going to create a controller plugin called Common_Controller_Plugin_Auth which will hook into the preDispatch phase of each request. In your bootstrap, we're going to load the plugin via a resource init method, providing to it a database handle with which to verify the user's credentials.

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
  protected function _initPlugins()
  {
    $this->bootstrap('Db');
    $this->_db = $this->getResource('Db');
    $front = Zend_Controller_Front::getInstance();
    $front->registerPlugin(
        new Common_Controller_Plugin_Auth(
          array('db' => $this->_db)
        )
    );
  }
}

Now let's have a look at the plugin that we're going to use. As mentioned earlier, we're going to use the preDispatch method to ensure that before a request is executed on each request we check that the requesting user is allowed to access the information that they've requested.

class Common_Controller_Plugin_Auth extends Zend_Controller_Plugin_Abstract
{
  protected $_authObj = NULL;
  protected $_db = NULL;
  protected $_responseObj = NULL;
  const CACHE_ID_PREFIX = 'CommonAuth_';
  const HEADER_APIKEY = 'CommonApikey';
  const HEADER_REQUEST_HASH = 'CommonRequestHash';
  const AUTH_NAMESPACE = 'CommonAuth';
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
// get the api key from the header
    $apiKey = $this->getRequest()->getHeader(self::HEADER_APIKEY);
    // get the hash of the request
    $requestHash = $this->getRequest()->getHeader(self::HEADER_REQUEST_HASH);
    // create a basic auth object
    $authObject = NULL;
    // both the api key and request hash are required
    if (!empty($apiKey) && !empty($requestHash)) {
      $authStorage = new Zend_Session_Namespace(self::AUTH_NAMESPACE);
      $cacheKey = self::CACHE_ID_PREFIX . $apiKey;
      if (isset($authStorage->$cacheKey)) {
        $authObject = $authStorage->$cacheKey;
        if (Common_Auth_Adapter_Rest::validRequestHash(
          $authObject, $requestHash, $request->getParams()
        )) {
          return TRUE;
        }
      } else {
        $auth = Zend_Auth::getInstance();
        $authAdapter = new Common_Auth_Adapter_Rest(
          $this->_db,
          $request->getParams()
        );
        $authAdapter->setApiKey($apiKey)
                    ->setRequestHash($requestHash);
        try {
          $result = $authAdapter->authenticate();
        } catch (Zend_Auth_Exception $e) {
        $this->_redirectNoAuth($request);
      }
    }
  } else {
    $this->_redirectNoAuth($request);
  }
}
protected function _redirectNoAuth(Zend_Controller_Request_Abstract $request)
{
  if (($request->getParam('controller') == 'error') &&
    ($request->getParam('module') == 'default') &&
    ($request->getParam('action') == 'noauth')) {
      return;
  }
  $redir = Zend_Controller_Action_HelperBroker::getStaticHelper(
    'Redirector'
  );
  $redir->setGotoRoute(array(), 'noauth', true);
  $redir->redirectAndExit();
  }
}

Now let's go over that a piece at a time to explain it propely:

$apiKey = $this->getRequest()->getHeader(self::HEADER_APIKEY);
$requestHash = $this->getRequest()->getHeader(self::HEADER_REQUEST_HASH);
$authObject = NULL;

Firstly, we retrieve the two request header variables used for validation, the api key and the request hash and set the authObject, which will be a Zend_Session_Namespace variable to null.

if (!empty($apiKey) && !empty($requestHash)) {

If either of these variables is unavailable, then we refuse the request immediately

$authStorage = new Zend_Session_Namespace(self::AUTH_NAMESPACE);
$cacheKey = self::CACHE_ID_PREFIX . $apiKey;

We now initialise our authStorage variable to an instance of Zend_Session_Namespace. The reason for this is that if we're doing a database lookup on each and every request then this is likely to get very intensive and expensive. So, in this simple example, we're using Zend_Session_Namespace to cache the information and avoid, where possible, database lookups. Next, we initialise a cache key which which to set and retrieve our user's details with.

if (isset($authStorage->$cacheKey)) {
  $authObject = $authStorage->$cacheKey;
  if (Common_Auth_Adapter_Rest::validRequestHash(
    $authObject, $requestHash, $request->getParams()
  )) {
    return TRUE;
  }
}

Here, if we have a cached copy of the user's credentials, we retrieve them and pass them to our function to validate the request hash supplied. For simplicity, this example uses the cache key above. Whilst this isn't the best, necessarily, for a production system, for our simple example here, it's fine. If the hash is valid, then we return TRUE and we can move on to retrieve the information requested for by user.

else {
  $auth = Zend_Auth::getInstance();
  $authAdapter = new Common_Auth_Adapter_Rest(
    $this->_db,
    $request->getParams()
  );
  $authAdapter->setApiKey($apiKey)
              ->setRequestHash($requestHash);
  try {
    $result = $authAdapter->authenticate();
  } catch (Zend_Auth_Exception $e) {
    $this->_redirectNoAuth($request);
  }
}

If we reach this case, then we weren't able to find a copy of the users credentials in the cache. So we have to attempt to find them in the database; and save them to the database if we find them. To attempt to simplify this process, we've extended Zend_Auth_Adapter. Whereas in Zend_Auth_Db, you set the name of the identity columns, eg., username and password, we're setting our api key and request hash variables. Then we call authenticate to run the process and if they are successful, then their details are cached. If not, then we call our _redirectNoAuth method.

The _redirectNoAuth is really simple; it redirects to a new action in the, default, Error controller we've called noauth and exits - displaying a message to the user. However, to avoid an infinite dispatch loop, we check if we're already at that point and simply return.

class ErrorController extends Zend_Controller_Action
{
  public function noauthAction()
  {
    $this->getResponse()->setHttpResponseCode(401);
    $this->_helper->json('Request unauthorised');
  }
}

So, what is the noauth action? Not much really. As the service is a RESTful service, it simply set's the appropriate http response code and the JSON body output. And that's it! In this case, a 401 Unauthorised.

Now there are a series of points that are not covered, or covered fully, here. However I hope that this post provides you with enough of a primer to research further and write your own secure service. Hopefully you've seen that this really isn't, necessarily, a complex operation and is a nice way of starting to protect your RESTful service without too much hassle.

I hope that you enjoyed that and can take something meaningful away from it. Next time, in part 2, you'll learn about using the correct HTTP status codes.

**Till next, **

Matt



About Matthew

Matthew Setter Matthew Setter is a PHP & Zend Framework specialist. If you're in need of a custom software application, need to migrate an existing legacy application, or want to know your current application's GPA - get in touch.