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!