Skip to content

Commit

Permalink
Merge pull request #11 from jeremykendall/develop
Browse files Browse the repository at this point in the history
Implements replay prevention capability
  • Loading branch information
jeremykendall committed Aug 23, 2013
2 parents 23ed1cb + c4b182f commit 5030b53
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 67 deletions.
63 changes: 61 additions & 2 deletions src/QueryAuth/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace QueryAuth;

use QueryAuth\KeyGenerator;
use QueryAuth\Signer;

/**
Expand All @@ -21,14 +22,25 @@ class Client
*/
private $signer;

/**
* @var KeyGenerator Instance of KeyGenerator
*/
private $keyGenerator;

/**
* @var int Unix timestamp
*/
private $timestamp;

/**
* Public constructor
*
* @param Signer $signer Instance of singature creation class
*/
public function __construct(Signer $signer)
public function __construct(Signer $signer, KeyGenerator $keyGenerator)
{
$this->signer = $signer;
$this->keyGenerator = $keyGenerator;
}

/**
Expand All @@ -45,7 +57,8 @@ public function __construct(Signer $signer)
public function getSignedRequestParams($key, $secret, $method, $host, $path, array $params = array())
{
$params['key'] = $key;
$params['timestamp'] = (int) gmdate('U');
$params['timestamp'] = $this->getTimestamp();
$params['cnonce'] = $this->keyGenerator->generateNonce();
// Ensure path is absolute
$path = '/' . ltrim($path, '/');
$signature = $this->signer->createSignature($method, $host, $path, $secret, $params);
Expand Down Expand Up @@ -73,4 +86,50 @@ public function setSigner(Signer $signer)
{
$this->signer = $signer;
}

/**
* Gets instance of KeyGenerator
*
* @return KeyGenerator Instance of KeyGenerator
*/
public function getKeyGenerator()
{
return $this->keyGenerator;
}

/**
* Sets instance of KeyGenerator
*
* @param KeyGenerator Instance of KeyGenerator
*/
public function setKeyGenerator(KeyGenerator $keyGenerator)
{
$this->keyGenerator = $keyGenerator;
}

/**
* Get timestamp
*
* Returns GMT timestamp if timestamp has not been set.
*
* @return int timestamp
*/
public function getTimestamp()
{
if ($this->timestamp === null) {
$this->timestamp = (int) gmdate('U');
}

return $this->timestamp;
}

/**
* Set timestamp
*
* @param int $timestamp
*/
public function setTimestamp($timestamp)
{
$this->timestamp = $timestamp;
}
}
17 changes: 0 additions & 17 deletions src/QueryAuth/Exception/MinimumDriftExceededException.php

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
namespace QueryAuth\Exception;

/**
* Exception thrown when maximum drift is exceeded
* Thrown when request timestamp is beyond allowable clock drift
*/
class MaximumDriftExceededException extends \Exception
class TimeOutOfBoundsException extends \OutOfBoundsException
{
}
2 changes: 1 addition & 1 deletion src/QueryAuth/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class Factory
*/
public function newClient()
{
return new Client($this->newSigner());
return new Client($this->newSigner(), $this->newKeyGenerator());
}

/**
Expand Down
12 changes: 11 additions & 1 deletion src/QueryAuth/KeyGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace QueryAuth;

use RandomLib\Generator as Generator;
use RandomLib\Generator;

/**
* Creates API keys and secrets
Expand Down Expand Up @@ -52,4 +52,14 @@ public function generateSecret()
{
return $this->generator->generateString(60);
}

/**
* Returns 64 character alphanumeric plus '.' and '/' random string
*
* @return string Nonce
*/
public function generateNonce()
{
return $this->generator->generateString(64);
}
}
52 changes: 14 additions & 38 deletions src/QueryAuth/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@

namespace QueryAuth;

use QueryAuth\Exception\MaximumDriftExceededException;
use QueryAuth\Exception\MinimumDriftExceededException;
use QueryAuth\Exception\TimeOutOfBoundsException;
use QueryAuth\Exception\SignatureMissingException;
use QueryAuth\Signer;

Expand Down Expand Up @@ -42,14 +41,13 @@ public function __construct(Signer $signer)
/**
* Is signature valid?
*
* @param string $secret API secret
* @param string $method Request method (GET, POST, PUT, HEAD, etc)
* @param string $host Host portion of API resource URL (including subdomain, excluding scheme)
* @param string $path Path portion of API resource URL (excluding query and fragment)
* @param array $params Request params
* @throws MaximumDriftExceededException If drift is greater than $drift
* @throws MinimumDriftExceededException If drift is less than $drift
* @throws SignatureMissingException If signature is missing from request
* @param string $secret API secret
* @param string $method Request method (GET, POST, PUT, HEAD, etc)
* @param string $host Host portion of API resource URL (including subdomain, excluding scheme)
* @param string $path Path portion of API resource URL (excluding query and fragment)
* @param array $params Request params
* @throws TimeOutOfBoundsException If timestamp greater than or less than allowable drift
* @throws SignatureMissingException If signature is missing from request
* @return boolean
*/
public function validateSignature($secret, $method, $host, $path, array $params)
Expand All @@ -60,15 +58,9 @@ public function validateSignature($secret, $method, $host, $path, array $params)

$currentTimestamp = (int) gmdate('U');

if ($this->exceedsMaximumDrift($currentTimestamp, $params['timestamp'])) {
throw new MaximumDriftExceededException(
sprintf('Timestamp is more than %d seconds in the future.', $this->getDrift())
);
}

if ($this->exceedsMinimumDrift($currentTimestamp, $params['timestamp'])) {
throw new MinimumDriftExceededException(
sprintf('Timestamp is more than %d seconds in the past.', $this->getDrift())
if ($this->timeOutOfBounds($currentTimestamp, $params['timestamp'])) {
throw new TimeOutOfBoundsException(
sprintf('Timestamp is beyond the +-%d second difference allowed.', $this->getDrift())
);
}

Expand All @@ -85,31 +77,15 @@ public function validateSignature($secret, $method, $host, $path, array $params)
}

/**
* Is $timestamp more than $drift seconds in the future?
*
* @param int $now GMT server timestamp
* @param int $timestamp GMT timestamp from request
* @return boolean
*/
protected function exceedsMaximumDrift($now, $timestamp)
{
if ($timestamp > $now && ($timestamp - $now) > $this->drift) {
return true;
}

return false;
}

/**
* Is $timestamp more than $drift seconds in the past?
* Is $timestamp greater than or less than $drift seconds?
*
* @param int $now GMT server timestamp
* @param int $timestamp GMT timestamp from request
* @return boolean
*/
protected function exceedsMinimumDrift($now, $timestamp)
protected function timeOutOfBounds($now, $timestamp)
{
if ($timestamp < $now && ($now - $timestamp) > $this->drift) {
if (abs($timestamp - $now) > $this->drift) {
return true;
}

Expand Down
54 changes: 54 additions & 0 deletions src/QueryAuth/Storage/SignatureStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
/**
* Query Auth: Signature generation and validation for REST API query authentication
*
* @copyright 2013 Jeremy Kendall
* @license https://github.com/jeremykendall/query-auth/blob/master/LICENSE MIT
* @link https://github.com/jeremykendall/query-auth
*/

namespace QueryAuth\Storage;

/**
* Interface for dealing with signature persistence
*
* Use to prevent replay attacks by checking a persistence layer to see if the
* requesting signature is already present. If it is present, the request should
* be denied. If it is not present, the signature should be persisted and the
* request should be approved.
*
* In order to minimize reads and writes, it's highly recommended to do so only
* after the signature has been otherwise validated.
*/
interface SignatureStorage
{
/**
* Checks persistence layer to see if a signature exists for the requester.
* If a signature is found in the persistence layer, then it has already
* been used and the associated request should be denied.
*
* If the persistence layer will return an error or throw an exception when
* a duplicate apikey and signature are inserted, you don't have to use
* this method to check for a key. Simply attempt to save the signature and
* check for the exception.
*
* @param string $key API key of the requster
* @param string $signature Request signature
* @return boolean True if signature exists, false if not
*/
public function exists($key, $signature);

/**
* Saves a key, signature, and the signature's expiration date
*
* @param string $key API key of the requster
* @param string $signature Request signature
* @param integer $expires Expiration timestamp
*/
public function save($key, $signature, $expires);

/**
* Deletes any signature with an expiration date <= now
*/
public function purge();
}
52 changes: 50 additions & 2 deletions tests/QueryAuth/Tests/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

use QueryAuth\Client;
use QueryAuth\Factory;
use QueryAuth\KeyGenerator;
use QueryAuth\ParameterCollection;
use QueryAuth\Signer;
use RandomLib\Factory as RandomFactory;

class ClientTest extends \PHPUnit_Framework_TestCase
{
Expand Down Expand Up @@ -58,9 +60,10 @@ public function testGetSignedRequestParamsForGetRequestWithoutParams()
$this->assertInternalType('array', $result);
$this->assertNotEmpty($result);
$this->assertArrayHasKey('timestamp', $result);
$this->assertArrayHasKey('cnonce', $result);
$this->assertArrayHasKey('key', $result);
$this->assertArrayHasKey('signature', $result);
$this->assertEquals(3, count($result));
$this->assertEquals(4, count($result));
}

public function testGetSignedRequestParamsForPostRequestWithParams()
Expand All @@ -78,7 +81,32 @@ public function testGetSignedRequestParamsForPostRequestWithParams()
$this->assertNotEmpty($result);
$this->assertArrayHasKey('foo', $result);
$this->assertArrayHasKey('baz', $result);
$this->assertEquals(5, count($result));
$this->assertEquals(6, count($result));
}

public function testSignaturesWithSameDataAndTimestampAreUnique()
{
$this->client->setTimestamp(gmdate('U'));

$result1 = $this->client->getSignedRequestParams(
$this->key,
$this->secret,
'POST',
$this->host,
$this->path,
$params = array('foo' => 'bar', 'baz' => 'bat')
);

$result2 = $this->client->getSignedRequestParams(
$this->key,
$this->secret,
'POST',
$this->host,
$this->path,
$params = array('foo' => 'bar', 'baz' => 'bat')
);

$this->assertNotEquals($result1, $result2);
}

public function testGetSetSigner()
Expand All @@ -88,4 +116,24 @@ public function testGetSetSigner()
$this->client->setSigner($signature);
$this->assertSame($signature, $this->client->getSigner());
}

public function testGetSetKeyGenerator()
{
$this->assertInstanceOf('QueryAuth\KeyGenerator', $this->client->getKeyGenerator());
$randomFactory = new RandomFactory();
$keyGenerator = new KeyGenerator($randomFactory->getMediumStrengthGenerator());
$this->client->setKeyGenerator($keyGenerator);
$this->assertSame($keyGenerator, $this->client->getKeyGenerator());
}

public function testGetSetTimestamp()
{
$default = $this->client->getTimestamp();
$this->assertLessThanOrEqual(gmdate('U'), $default);
$this->assertNotNull($default);
$this->assertInternalType('int', $default);
$new = gmdate('U');
$this->client->setTimestamp($new);
$this->assertEquals($new, $this->client->getTimestamp());
}
}
6 changes: 6 additions & 0 deletions tests/QueryAuth/Tests/KeyGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ public function testGenerateSecret()
$secret = $this->keyGenerator->generateSecret();
$this->assertRegexp('/^[0-9A-Za-z\/\.]{60}$/', $secret);
}

public function testGenerateNonce()
{
$secret = $this->keyGenerator->generateNonce();
$this->assertRegexp('/^[0-9A-Za-z\/\.]{64}$/', $secret);
}
}
Loading

0 comments on commit 5030b53

Please sign in to comment.