Created
August 21, 2018 05:21
-
-
Save napolux/02e1c28bd976c42fe70d3e5676fc7779 to your computer and use it in GitHub Desktop.
A SlimFramework middleware to validate Amazon Alexa requests... See: https://developer.amazon.com/it/docs/custom-skills/host-a-custom-skill-as-a-web-service.html#checking-the-signature-of-the-request
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace API\Middleware; | |
use \Psr\Http\Message\ServerRequestInterface as Request; | |
use \Psr\Http\Message\ResponseInterface as Response; | |
/** | |
* Class AmazonMiddleware | |
* @package API\Middleware | |
*/ | |
class AmazonMiddleware | |
{ | |
// create the folder and give appropriate permissions | |
const AMZN_CERT_PATH = '/var/cache/amazon-echo-cert/'; | |
public function __invoke(Request $request, Response $response, callable $next) { | |
$validate = $this->validateAmazonRequest($request); | |
if ($validate['response'] !== true) { | |
return $response->withJSON(['Error' => $validate['msg']])->withStatus(400); | |
} | |
$response = $next($request, $response); | |
return $response; | |
} | |
/** | |
* From https://github.com/rbowen/validate-echo-request-php/ | |
*/ | |
private function validateAmazonRequest(Request $request) { | |
// validate timestamp of request... | |
if(!$this->validateTimestamp($request->getServerParam('REQUEST_TIME'))) { | |
return $this->error('Timestamp is not valid'); | |
} | |
$certificateUrl = $request->getServerParam('HTTP_SIGNATURECERTCHAINURL'); | |
$httpSignature = $request->getServerParam('HTTP_SIGNATURE'); | |
// validate that the request is coming from amazon | |
if($certificateUrl === null) { | |
return $this->error('Request not coming from amazon'); | |
} | |
// validate certificate url | |
if(!$this->validateCertificateUrl($certificateUrl)) { | |
return $this->error('Certificate URL is not valid'); | |
} | |
if(!$this->validateCertificateSignature($request->getBody(), $httpSignature, $certificateUrl)) { | |
return $this->error('Certificate signature is not valid'); | |
} | |
return $this->success('Request validated'); | |
} | |
private function validateCertificateUrl($certificateUrl) { | |
$urlParts = parse_url($certificateUrl); | |
if (strcasecmp($urlParts['host'], 's3.amazonaws.com') != 0) | |
return false; | |
if (strpos($urlParts['path'], '/echo.api/') !== 0) | |
return false; | |
if (strcasecmp($urlParts['scheme'], 'https') != 0) | |
return false; | |
if (array_key_exists('port', $urlParts) && $urlParts['port'] != '443') | |
return false; | |
return true; | |
} | |
private function validateTimestamp($timestamp) { | |
if (time() - (int)$timestamp > 60) { | |
// the request is too old... | |
return false; | |
} else { | |
return true; | |
} | |
} | |
private function validateCertificateSignature($requestBody, $httpSignature, $certificateUrl) { | |
// Determine if we need to download a new Signature Certificate Chain from Amazon | |
$md5pem = self::AMZN_CERT_PATH . md5($certificateUrl) . '.pem'; | |
$echoServiceDomain = 'echo-api.amazon.com'; | |
// If we haven't received a certificate with this URL before, | |
// store it as a cached copy | |
if (!file_exists($md5pem)) { | |
file_put_contents($md5pem, file_get_contents($certificateUrl)); | |
} | |
// Validate certificate chain and signature | |
$pem = file_get_contents($md5pem); | |
$sslCheck = openssl_verify($requestBody, base64_decode($httpSignature), $pem, 'sha1'); | |
if ($sslCheck != 1) { | |
return false; | |
} | |
// Parse certificate for validations below | |
$parsedCertificate = openssl_x509_parse($pem); | |
if (!$parsedCertificate) { | |
return false; | |
} | |
// Check that the domain echo-api.amazon.com is present in | |
// the Subject Alternative Names (SANs) section of the signing certificate | |
if(strpos($parsedCertificate['extensions']['subjectAltName'], $echoServiceDomain) === false) { | |
return false; | |
} | |
// Check that the signing certificate has not expired | |
// (examine both the Not Before and Not After dates) | |
$validFrom = $parsedCertificate['validFrom_time_t']; | |
$validTo = $parsedCertificate['validTo_time_t']; | |
$time = time(); | |
if (!($validFrom <= $time && $time <= $validTo)) { | |
return false; | |
} | |
return true; | |
} | |
// Utilities for messages | |
private function error($msg) { | |
return ['response' => false, 'msg' => $msg]; | |
} | |
private function success($msg) { | |
return ['response' => true, 'msg' => $msg]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment