Problem:

There are many considerations to make when building and securing a public-facing API, but I wanted to look at one issue in particular: Making sure that only authenticated clients can access it.

The aim of this tutorial is to create a flow where a client only needs to make one request to the server to get back a response. In this simple application flow, we can avoid the added complexity of managing request and/or access tokens:

Client sends a request for data along with their authentication details Server sends data back to the authenticated client

This flow will be familiar if you have used Amazon Web Services (AWS) before, as they use a similar “signed-message” system.

Solution:

Use a simple version of the OAuth 1.0a standard which is known as “2-legged” authentication. In this system, no access tokens are used. Authentication is performed using a public and private key system. These public and private keys are known by the client (consumer) and the server (provider).

The private key (the secret) is NEVER passed over the wire

The provider uses the private key within a hash-based message authentication code (HMAC) to generate a signature

The consumer uses its own copy of the private key to verify the signature is authentic

You should use always SSL/TLS to encrypt traffic between the consumer and provider

My solution uses PHP OAuth library by Andy Smith (MIT licence) for the heavy lifting. I have made this library available as a Composer package which can be installed like this:

$ php composer.phar require glenscott/oauth

Here are two simple examples for the provider and consumer sides:

Provider side

In this example, the list of valid consumer keys and secrets are hardcoded, but you probably want to store these in a DB somewhere. The provider will return "true" if it is a valid authenticated request, or otherwise it will spit out and error message "Exception: ..." .

File: provider.php

<?php require_once dirname(__FILE__) . '/vendor/autoload.php'; use GlenScott\OAuth; use GlenScott\OAuthProvider; require_once 'datastore.php'; $server = new OAuth\Server(new OAuthProvider\ExampleDataStore()); $server->add_signature_method(new OAuth\SignatureMethod_HMAC_SHA1()); $request = OAuth\Request::from_request(); try { if ( $server->verify_request($request) ) { echo json_encode(true); } } catch (Exception $e) { echo json_encode("Exception: " . $e->getMessage()); }

file: datastore.php

<?php namespace GlenScott\OAuthProvider; require_once dirname(__FILE__) . '/vendor/autoload.php'; use GlenScott\OAuth; class ExampleDataStore extends OAuth\DataStore { function lookup_consumer($consumer_key) { $consumer_secrets = array( 'thisisakey' => 'thisisasecret', 'anotherkey' => 'f3ac5b093f3eab260520d8e3049561e6', ); if ( isset($consumer_secrets[$consumer_key])) { return new OAuth\Consumer($consumer_key, $consumer_secrets[$consumer_key], NULL); } else { return false; } } function lookup_token($consumer, $token_type, $token) { // we are not using tokens, so return empty token return new OAuth\Token("", ""); } function lookup_nonce($consumer, $token, $nonce, $timestamp) { // @todo lookup nonce and make sure it hasn't been used before (perhaps in combination with timestamp?) return NULL; } function new_request_token($consumer, $callback = null) { } function new_access_token($token, $consumer, $verifier = null) { } }

Consumer side

file: consumer.php

<?php require_once dirname(__FILE__) . '/vendor/autoload.php'; use GlenScott\OAuth; // this is sent with each request, and doesn't matter if it is public $consumer_key = 'thisisakey'; // this should never be sent directly over the wire $private_key = 'thisisasecret'; // API endpoint -- note that in prodution, you _must_ use https rather than http $url = 'http://localhost:8080/provider.php'; // the custom paramters you want to send to the endpoint $params = array( 'foo' => 'bar', 'bar' => 'foo', ); $consumer = new OAuth\Consumer($consumer_key, $private_key); $request = OAuth\Request::from_consumer_and_token($consumer, NULL, 'GET', $url, $params); $sig = new OAuth\SignatureMethod_HMAC_SHA1(); $request->sign_request($sig, $consumer, null); $opts = array( 'http' => array( 'header' => $request->to_header() ) ); $context = stream_context_create($opts); $url = $url . '?' . http_build_query($params); echo "Making request: " . $url . PHP_EOL; echo "Authorization HTTP Header: " . $request->to_header() . PHP_EOL; echo "Response: " . file_get_contents($url, false, $context) . PHP_EOL;

To run the above example, run the PHP development server using php -S localhost:8080 and run php consumer.php :

Making request: http://localhost:8080/provider.php?foo=bar&bar=foo Authorization HTTP Header: Authorization: OAuth oauth_version="1.0",oauth_nonce="7620de430a896a5594fbf76e96f3b3d3",oauth_timestamp="1510494497",oauth_consumer_key="thisisakey",oauth_signature_method="HMAC-SHA1",oauth_signature="4V8Ft368ZBWxh5V10jv1AW%2FJwls%3D" Response: true

Using the samples above should give you a head-start when creating your own authenticated API.

Any questions? Please use the comments section below!