|
<? |
|
/* |
|
Copyright BAFTA 2019 |
|
|
|
Authoritative source: https://gist.github.com/skwirrel/dd95cdce81006ff21a4c3da7111e7952 |
|
|
|
This class handles signing and validating API requests. |
|
It handles passing data in as a raw POST blob (e.g.JSON), or as an associative array (GET or POST style). |
|
It also handles other request methods (PUT, DELETE - anything you want) |
|
|
|
To make requests you do this... |
|
$apiHandler = new SignedRequester( $baseUrl, $keyId, $secretKey ); |
|
list( $error, $httpResponseCode, $responseBody) = $apiHandler->GET($path, $array_of_params); |
|
list( $error, $httpResponseCode, $responseBody) = $apiHandler->GET($path, $query_string); |
|
list( $error, $httpResponseCode, $responseBody) = $apiHandler->POST($path, $array_of_params); |
|
// N.B. raw_post_data shouldn't be stupidly large as it has to be loaded entirely into RAM for hashing |
|
list( $error, $httpResponseCode, $responseBody) = $apiHandler->POST($path, $raw_post_data); |
|
list( $error, $httpResponseCode, $responseBody) = $apiHandler->DELETE($path, $params); |
|
list( $error, $httpResponseCode, $responseBody) = $apiHandler->SOME_OTHER_METHOD($path, $params); |
|
$error will be false on success. |
|
|
|
At the other end you validate requests list this... |
|
|
|
list( $error, $method, $data ) = SignedRequester::validate( $secretKeyLookupCallback, $hashValiditySeconds ); |
|
$error will be false on success. |
|
$method and $data will both be empty ('') on error. |
|
|
|
$secretKeyLookupCallback is a callback which gets passed the keyId as a parameter and must return the secret key |
|
(or empty string on error e.g. keyId is invalid). |
|
|
|
*/ |
|
|
|
/* Sample code for testing purposes |
|
if (isset($_REQUEST['mode']) && $_REQUEST['mode'] == 'test') { |
|
list($error, $method, $data) = signedRequester::validate( function(){ return 'SECRET'; }, 60 ); |
|
|
|
if ($error===false) $error = 'OK'; |
|
|
|
echo "$error ($method)<br />"; |
|
echo "data: <pre>"; print_r($data); echo '</pre>'; |
|
exit; |
|
} |
|
|
|
$path = $_SERVER["SCRIPT_URL"]; |
|
$base = substr($_SERVER["SCRIPT_URI"],0,strlen($path)*-1); |
|
echo "base=$base path=$path<br />"; |
|
|
|
$apiHandler = new SignedRequester( $base, 123, 'SECRET' ); |
|
|
|
echo "<hr />SIMPLE GET<br /><br />"; |
|
list( $error, $code, $response ) = $apiHandler->GET($path,array('foo'=>'bar','mode'=>'test')); |
|
echo "Code: $code<br />"; |
|
echo "Response:<blockQuote>$response</blockQuote>"; |
|
|
|
echo "<hr />QUERY STRING<br /><br />"; |
|
list( $error, $code, $response ) = $apiHandler->GET($path,'foo=bar&mode=test'); |
|
echo "Code: $code<br />"; |
|
echo "Response:<blockQuote>$response</blockQuote>"; |
|
|
|
echo "<hr />SIMPLE POST<br /><br />"; |
|
list( $error, $code, $response ) = $apiHandler->POST($path,array('foo'=>'bar','mode'=>'test')); |
|
echo "Code: $code<br />"; |
|
echo "Response:<blockQuote>$response</blockQuote>"; |
|
|
|
echo "<hr />RAW POST<br /><br />"; |
|
list( $error, $code, $response ) = $apiHandler->POST($path.'?mode=test', 'This is the raw post data'); |
|
echo "Code: $code<br />"; |
|
echo "Response:<blockQuote>$response</blockQuote>"; |
|
|
|
echo "<hr />RAW DELETE<br /><br />"; |
|
list( $error, $code, $response ) = $apiHandler->DELETE($path.'?mode=test', 'This is the raw post data'); |
|
echo "Code: $code<br />"; |
|
echo "Response:<blockQuote>$response</blockQuote>"; |
|
|
|
exit; |
|
*/ |
|
|
|
class SignedRequester { |
|
|
|
private $baseUrl; |
|
private $keyId; |
|
private $secretKey; |
|
const authHeaderName = 'Authentication'; |
|
|
|
function __construct($baseUrl, $keyId, $secretKey) { |
|
$this->baseUrl = $baseUrl; |
|
$this->keyId = $keyId; |
|
$this->secretKey = $secretKey; |
|
} |
|
|
|
public function request() { |
|
$args = func_get_args(); |
|
return call_user_func_array(array($this, array_shift($args)), $args); |
|
} |
|
|
|
public function __call($method, $arguments) { |
|
$debug = false; |
|
|
|
list( $path, $params ) = $arguments; |
|
|
|
$curl = curl_init(); |
|
|
|
$url = $this->baseUrl.$path; |
|
|
|
if (!isset($params)) $params=''; |
|
|
|
$method = preg_replace('/[^A-Z]/','',strtoupper($method)); |
|
$now = time(); |
|
|
|
$fullPath = parse_url($this->baseUrl,PHP_URL_PATH).$path; |
|
|
|
$digestString = $method.':'.rawUrlEncode($fullPath).':'.$now.':'; |
|
|
|
if (is_array($params)) { |
|
ksort($params); |
|
$query = http_build_query($params); |
|
$digestString.=hash('sha256',$query); |
|
} else { |
|
if (strlen($params)) $digestString.=hash('sha256',$params); |
|
$query = $params; |
|
} |
|
|
|
if ($method=='GET') { |
|
if (strlen($query)) $url .= '?'.$query; |
|
} else { |
|
curl_setopt($curl,CURLOPT_POSTFIELDS,$params); |
|
} |
|
|
|
if ($debug) echo "Digest string = \"$digestString\"<br />"; |
|
|
|
$hmac = base64_encode(hash_hmac('sha256',$digestString,$this->secretKey,true)); |
|
$authHeader = self::authHeaderName.': HMAC '.$now.':'.rawUrlEncode($this->keyId).':'.$hmac; |
|
|
|
if ($debug) echo "Auth header=$authHeader<br />"; |
|
|
|
curl_setopt($curl, CURLOPT_URL, $url); |
|
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); |
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); |
|
curl_setopt($curl, CURLOPT_HTTPHEADER, array($authHeader)); |
|
|
|
$response = curl_exec($curl); |
|
if( $response === false) { |
|
return array( curl_error($curl), 0, ''); |
|
} |
|
|
|
$responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); |
|
|
|
return array( false, $responseCode, $response ); |
|
} |
|
|
|
public static function validate( $secretKeyLookupCallback, $hashValiditySeconds ) { |
|
$debug = false; |
|
|
|
$method = strtoupper($_SERVER['REQUEST_METHOD']); |
|
|
|
if ($debug) echo "Received incoming $method request<br />"; |
|
$path = $_SERVER['REQUEST_URI']; |
|
|
|
if (!isset($_SERVER['HTTP_'.strtoupper(self::authHeaderName)])) return array('Request is missging authentication header','','' ); |
|
$authData = $_SERVER['HTTP_'.strtoupper(self::authHeaderName)]; |
|
|
|
|
|
if ( substr($authData,0,5)!=='HMAC ' ) return array('Invalid authentication token','','' ); |
|
$authData = trim(substr($authData,5)); |
|
|
|
list( $time, $keyId, $hmac ) = explode( ':', $authData.'::::' ); |
|
if ( !(strlen($time) && strlen($keyId) && strlen($time)) ) return array('Invalid authentication token','','' ); |
|
|
|
if ($time<(time()-$hashValiditySeconds)) return array('Token timed out - check clocks are in sync','','' ); |
|
|
|
$secretKey = $secretKeyLookupCallback( $keyId ); |
|
if (!strlen($secretKey)) return array('Unknown key ID','','' ); |
|
|
|
$paramsHash = ''; |
|
$return = ''; |
|
|
|
if ($method=='GET') { |
|
$query = $_SERVER["QUERY_STRING"]; |
|
|
|
if (strlen($query)) { |
|
// Chop the query string (and the question mark) of the end of the path |
|
$path = substr($path, 0, (strlen($query)+1) * -1); |
|
$paramsHash = hash('sha256',$_SERVER["QUERY_STRING"]); |
|
} |
|
|
|
$return = &$_GET; |
|
} else { |
|
$contentType = strtoupper(trim($_SERVER['CONTENT_TYPE'])); |
|
if (strpos($contentType,'APPLICATION/X-WWW-FORM-URLENCODED')===0 || strpos($contentType,'MULTIPART/FORM-DATA')===0) { |
|
$params = $_POST; |
|
ksort($params); |
|
if ($debug) echo "Query data = \"".http_build_query($params)."\"<br />"; |
|
$paramsHash = hash('sha256',http_build_query($params)); |
|
$return = &$_POST; |
|
} else if ($method='POST') { |
|
$rawPostData = file_get_contents('php://input'); |
|
$paramsHash = hash('sha256',$rawPostData); |
|
$return = &$rawPostData; |
|
} |
|
} |
|
|
|
$digestString = $method.':'.urlEncode($path).':'.$time.':'.$paramsHash; |
|
if ($debug) echo "Reconstructed digest string = \"$digestString\"<br />"; |
|
|
|
$desiredHmac = base64_encode(hash_hmac('sha256',$digestString,$secretKey,true)); |
|
|
|
if (!hash_equals( $desiredHmac, $hmac )) return array( 'Unauthorized','','' ); |
|
|
|
return array( false, $method, $return ); |
|
} |
|
} |