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!












