Example implementation of the Query Auth library
In order to run this example implementation, you'll need to have the following installed:
- 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
All code samples below can be found in /public/index.php
.
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());
}
};
};
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));
});
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;
});
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));
});
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;
});
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');
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;
});
This example implementation makes use of the following external dependencies:
- Slim Framework: A PHP microframework
- Twig: PHP template engine
- Guzzle: A PHP HTTP client, used here to send requests
- JSend: Jamie Schembri's PHP implementation of the OmniTI JSend specifiction
- Parsedown PHP: Emanuil Rusev's Markdown parser for PHP
- Composer Dependency Manager for PHP