Example implementation of query-auth library
Puppet Ruby Shell Perl PHP CSS Pascal
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
db
logs
manifests
modules
public
scripts/sql
src/Example
templates
.gitignore
LICENSE.md
README.md
Vagrantfile
composer.json
composer.lock
config.php
phpdoc.dist.xml
puphpet.json

README.md

Query Auth Example Implementation

Example implementation of the Query Auth library

Requirements

In order to run this example implementation, you'll need to have the following installed:

Usage

  • Clone repo
  • cd /path/to/repo
  • Run vagrant up
  • Add 192.168.56.102 query-auth.dev to /etc/hosts
  • Open a browser and visit http://query-auth.dev

Examples

All code samples below can be found in /public/index.php.

Using Slim Middleware to Validate Request Signatures

Since this example implementation includes multiple routes that require signature validation, I decided to use Slim Framework's Route Middleware so that I'd only have to write the code once. When you see $validateSignature attached to the /api/* routes below, know that the validation is being performed by the middleware before the code in those routes is being executed.

// Middleware to validate incoming request signatures
// See http://docs.slimframework.com/#Route-Middleware
$validateSignature = function (Slim $app, RequestValidator $requestValidator) {
    return function () use ($app, $requestValidator) {
        $response = $app->response;

        try {
            // Grabbing credentials from app container kind of mimics grabbing
            // credentials from persistent storage
            $credentials = $app->credentials;

            $isValid = $requestValidator->isValid(
                new SlimRequestAdapter($app->request),
                $credentials
            );

            if ($isValid === false) {
                $jsend = new JSendResponse('fail', array('message' => 'Invalid signature'));
                $response->setStatus(403);
                $response->headers->set('Content-Type', 'application/json');
                $response->setBody($jsend->encode());
            }
        } catch (\Exception $e) {
            $jsend = new JSendResponse('error', array(), $e->getMessage(), $e->getCode());
            $response->setStatus(403);
            $response->headers->set('Content-Type', 'application/json');
            $response->setBody($jsend->encode());
        }
    };
};

GET Example

Client: Sending a Signed GET Request

Visit http://query-auth.dev/get-example to see an example of a signed GET request:

/**
 * Sends a signed GET request which returns a famous mangled phrase
 */
$app->get('/get-example', function () use ($app, $requestSigner) {

    // Create request
    $guzzle = new GuzzleHttpClient(['base_url' => 'http://query-auth.dev']);
    $request = $guzzle->createRequest('GET', '/api/get-example');

    // Sign request
    $requestSigner->signRequest(new GuzzleHttpRequestAdapter($request), $app->credentials);

    // Send request
    try {
        $response = $guzzle->send($request);
    } catch (BadResponseException $bre) {
        $response = $bre->getResponse();
    }

    // Render template with data
    $app->render('get.html', array('request' => (string) $request, 'response' => (string) $response));
});

Server: Handling a GET Request

Uses $validateSignature to ensure the request signature is valid.

  • If not valid, return the response generated by $validateSignature.
  • If valid, return the famous mangled phrase.

Below is the /api/get-example GET route of the sample implementation:

/**
 * Validates a signed GET request and, if the request is valid, returns a
 * famous mangled phrase
 */
$app->get('/api/get-example', $validateSignature($app, $requestValidator), function () use ($app) {

    $response = $app->response();

    // If client error (400 - 499) because signature is invalid, return response
    if ($response->isClientError()) {
        return $response;
    }

    $mistakes = array('necktie', 'neckturn', 'nickle', 'noodle');
    $format = 'Klaatu... barada... n... %s!';
    $data = array('message' => sprintf($format, $mistakes[array_rand($mistakes)]));
    $jsend = new JSendResponse('success', $data);

    $response->headers->set('Content-Type', 'application/json');
    $response->setBody($jsend->encode());

    return $response;
});

POST Example

Client: Sending a Signed POST Request

Visit http://query-auth.dev/post-example to see an example of a signed POST request:

/**
 * Sends a signed POST request to create a new user
 */
$app->get('/post-example', function () use ($app, $requestSigner) {

    $params = array(
        'name' => 'Ash',
        'email' => 'ash@s-mart.com',
        'department' => 'Housewares',
    );

    // Create request
    $guzzle = new GuzzleHttpClient(['base_url' => 'http://query-auth.dev']);
    $request = $guzzle->createRequest('POST', '/api/post-example', ['body' => $params]);

    // Sign request
    $requestSigner->signRequest(new GuzzleHttpRequestAdapter($request), $app->credentials);

    // Send request
    try {
        $response = $guzzle->send($request);
    } catch (BadResponseException $bre) {
        $response = $bre->getResponse();
    }

    $app->render('post.html', array('request' => (string) $request, 'response' => (string) $response));
});

Server: Handling a POST Request

Uses $validateSignature to ensure the request signature is valid.

  • If valid, save the new user and return the new user data.
  • If not valid, return the response generated by $validateSignature.

Below is the /api/post-example POST route of the sample implementation:

/**
 * Validates a signed POST request and, if the request is valid, mimics creating
 * a new user
 */
$app->post('/api/post-example', $validateSignature($app, $requestValidator), function () use ($app) {

    $response = $app->response();

    // If client error (400 - 499) because signature is invalid, return response
    if ($response->isClientError()) {
        return $response;
    }

    $params = $app->request()->post();

    // Assume appropriate POST action of some sort, in this case saving
    // a new user and returning the persisted user data.
    $data = array(
        'user' => array(
            'id' => 666,
            'name' => $params['name'],
            'email' => $params['email'],
            'department' => $params['department'],
        ),
    );

    $jsend = new JSendResponse('success', $data);

    $response->headers->set('Content-Type', 'application/json');
    $response->setBody($jsend->encode());

    return $response;
});

Replay Prevention Example

Client: Sending a Signed POST Request OR Replaying a Previous Request

Visit http://query-auth.dev/replay-example to see an example of replay prevention:

/**
 * Sends a signed POST request to create a new user, OR replays a previous POST request
 */
$app->map('/replay-example', function () use ($app, $requestSigner) {

    // Create request
    $guzzle = new GuzzleHttpClient(['base_url' => 'http://query-auth.dev']);
    $request = $guzzle->createRequest('POST', '/api/replay-example');

    // Build a new request
    if ($app->request()->isGet()) {

        $params = array(
            'name' => 'Ash',
            'email' => 'ash@s-mart.com',
            'department' => 'Housewares',
        );

        $request->getBody()->replaceFields($params);

        // Sign request
        $requestSigner->signRequest(new GuzzleHttpRequestAdapter($request), $app->credentials);
    }

    // Build a replay request
    if ($app->request()->isPost()) {
        // Set a previous request's data on a new request
        $request->getBody()->replaceFields($app->request->post());
    }

    // Send request
    try {
        $response = $guzzle->send($request);
    } catch (BadResponseException $bre) {
        $response = $bre->getResponse();
    }

    $app->render('replay.html', array(
        'request' =>  (string) $request,
        'response' => (string) $response,
        'postFields' => $request->getBody()->getFields(),
    ));
})->via('GET', 'POST');

Server: Handling a Replayed Request

Uses $validateSignature to ensure the request signature is valid.

  • If valid, save the API key, request signature, and signature expiration timestamp
    • If the save is successful, this is a new request
    • If the save is unsuccessful, this is a replayed request and is denied
  • If not valid, return the response generated by $validateSignature.

Below is the /api/replay-example POST route of the sample implementation:

/**
 * Uses $validateSignature to ensure the request signature is valid.
 * If valid, save the API key, request signature, and signature expiration timestamp
 *     If the save is successful, this is a new request
 *     If the save is unsuccessful, this is a replayed request and is denied
 * If not valid, return the response generated by `$validateSignature`.
 */
$app->post('/api/replay-example', $validateSignature($app, $requestValidator), function () use ($app, $config) {

    $response = $app->response();

    // If client error (400 - 499) because signature is invalid, return response
    if ($response->isClientError()) {
        return $response;
    }

    try {
        $db = new \PDO(
            $config['pdo']['dsn'],
            $config['pdo']['username'],
            $config['pdo']['password'],
            $config['pdo']['options']
        );

        $params = $app->request()->post();

        $signatureDao = new SignatureDao($db);
        $signatureDao->save($params['key'], $params['signature'], (int) gmdate('U') + 3600);

        // Assume appropriate POST action of some sort, in this case saving
        // a new user and returning the persisted user data.
        $data = array(
            'user' => array(
                'id' => 666,
                'name' => $params['name'],
                'email' => $params['email'],
                'department' => $params['department'],
            ),
        );

        $jsend = new JSendResponse('success', $data);
    } catch (\PDOException $pe) {
        if ($pe->getCode() == 23000) {
            $response->setStatus(403);
            $jsend = new JSendResponse('error', array(), sprintf('REPLAYED REQUEST: %s', $pe->getMessage()), $pe->getCode());
        }
    } catch (\Exception $e) {
        $response->setStatus(400);
        $jsend = new JSendResponse('error', array(), $e->getMessage(), $e->getCode());
    }

    $response->headers->set('Content-Type', 'application/json');
    $response->setBody($jsend->encode());

    return $response;
});

Credits

This example implementation makes use of the following external dependencies: