How to secure the endpoint of my Skills Alexa application

Welcome to this second tutorial on the development of Skills Alexa, in the first tutorial we saw how to make a helloworld, today in this tutorial we will see how to secure our script. Today I will switch to Symfony to continue the tutorials, but you can of course adapt it to any PHP framework.

Step 1 - Secure the endpoint that receives the Alexa Skills request, the theory:

Currently our script is doing a helloworld but let's be honest with ourselves... Security level is not great.
Indeed, anyone can use your webservice.

We will therefore add several controls:
- Check that the request comes from an Amazon IP.
- Check that it is our ID application that is called
- Check the SSL
signature - check that the signature of the request corresponds to the Amazon
signature - Check the subjectActName (SAN)
- Check the expiry of the SSL certificate
- Check that the request was sent less than 60 seconds ago

Step 2 - Check on my request, practice

Here is already what my action looks like for the moment:

 /**
   * @Route("/helloworld.json",  defaults={"_format"="json"}, name="alexa_search_helloworld")
   */
  public function helloworldAction(Request $request)
  {
      $response = array(
          'outputSpeech' => array(
              'type' => 'PlainText',
              'text' => "Hello World"
          )
      );
      $data = array(
              'version' => "0.1",
              'sessionAttributes' => array(
                  'countActionList' => array(
                      'read' => true,
                      'category' => true
                  )
              ) ,
              'response' => $response,
              'shouldEndSession' => false
          );

      return new JsonResponse($data);
  }
In order to secure my request I will add at the beginning of my action a call to the validate() method of my AlexaSkills class (which we will create later). To use it you must declare the object:

use AlexaBundle\Service\AlexaSkills;

Then add in the controller, the instantiation of the class and the call to the validate method:

    $json = file_get_contents('php://input');
    $requete = json_decode($json);
    $alexaSkillsService = new AlexaSkills();
    $alexaSkillsService->validate($requete);

We will then create our class like this:

class AlexaSkills
{
    private $_appId;

    public function __construct() {
        // Remplacer ici par votre app ID
        $this->_appId = 'amzn1.ask.skill.XXXXX';
     }

    public function validate($requete)
    {
    }
}

You will notice that I put in private attribute the ID app of my Amazon skill, which you can get in the "Alexa Skills Kit Developer Console" on the "Alexa Skills" page which lists all your skills, you have below the name of your skill a link "View Skill ID" which allows you to get the ID of your skill, to insert in our example in our attribute _appId. Then we will do each check one by one in the validate method.

Check that the request comes from an Amazon IP

Amazon's IPs for Alexa requests are: 72.21.217 and 54.240.197
We will start by creating a function, which allows to check these 2 IPs (by checking the server variable REMOTE_ADDR):

    /**
     * Check if the request is from Amazon Ip
     */
    public function isRequestFromAmazon()
    {
        $amazon_ip = array("72.21.217.","54.240.197.");
        foreach($amazon_ip as $ip) {
        	if (stristr($_SERVER['REMOTE_ADDR'], $ip)) {
        		return true;
        	}
        }
        return false;

    }

Then we will use it in our validate method like this:

     //check if the request come from amazon
        $isAmazonIp = $this->isRequestFromAmazon();
        if ( !$isAmazonIp) {
            throw new BadRequestHttpException("Forbidden, your Host is not allowed to make this request!", null, 400);
        }

Check the application ID

We will then check that it is the correct Skill Id that makes the request via the applicationID contained in the JSON content (in session):

  //check my Amazon IP
        if (strtolower($requete->session->application->applicationId) != strtolower($this->_appId)) {
            throw new BadRequestHttpException("Forbidden, your App ID is not allowed to make this request!", null, 400);
        }

Verify SSL signature

Then we will check the SSL signature, by comparing the HTTP_SIGNATURECERTCHAINURL server variable with the address s3.amazonaws.com/echo.api (see the regex for the exact address):

 // Check SSL signature
        if (preg_match("/https:\/\/s3.amazonaws.com(\:443)?\/echo.api\/*/i", $_SERVER['HTTP_SIGNATURECERTCHAINURL']) == false) {
        	throw new BadRequestHttpException( "Forbidden, unkown SSL Chain Origin!", null, 400);
        }

Check that the signature of the request matches the Amazon signature


 $pem_file = sys_get_temp_dir() . '/' . hash("sha256", $_SERVER['HTTP_SIGNATURECERTCHAINURL']) . ".pem";
        if (!file_exists($pem_file)) {
        	file_put_contents($pem_file, file_get_contents($_SERVER['HTTP_SIGNATURECERTCHAINURL']));
        }
        $pem = file_get_contents($pem_file);

        $json = file_get_contents('php://input');
        if (openssl_verify($json, base64_decode($_SERVER['HTTP_SIGNATURE']) , $pem) !== 1){
        	throw new BadRequestHttpException( "Forbidden, failed to verify SSL Signature!", null, 400);
        }

Then we check that we can parse the pem with Open SSL:

       // check we can parse the pem content
        $cert = openssl_x509_parse($pem);
        if (empty($cert)) {
             throw new BadRequestHttpException("Certificate parsing failed!", null, 400);
        }

Check the subjectActName (SAN)

We check that subject alt name is echo-api.amazon.com :

        // Check subjectAltName
        if (stristr($cert['extensions']['subjectAltName'], 'echo-api.amazon.com') != true) {
            throw new BadRequestHttpException( "Forbidden! Certificate subjectAltName Check failed!", null, 400);
        }

Check the expiry of the SSL certificate

And we check the date of the certificate:

        // check expiration date of the certificate
        if ($cert['validTo_time_t'] < time()){
        	throw new BadRequestHttpException( "Forbidden! Certificate no longer Valid!", null, 400);
        	if (file_exists($pem_file)){
                unlink($pem_file);
            }
        }

Check that the request was issued less than 60 seconds ago

The title speaks for itself, we look in request, the timestamp like this to do it:

       if (time() - strtotime($requete->request->timestamp) > 60) {
            throw new BadRequestHttpException( "Request Timeout! Request timestamp is to old.",null, 400);
        }

Which gives you in the end:

  <?php
namespace AlexaBundle\Service;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class AlexaSkills
{
    private $_appId;

    public function __construct() {
        // Remplacer ici par votre app ID
        $this->_appId = 'amzn1.ask.skill.XXXXX';
     }

    /**
     * Check if the request is from Amazon Ip
     */
    public function isRequestFromAmazon()
    {
        $amazon_ip = array("72.21.217.","54.240.197.");
        foreach($amazon_ip as $ip) {
        	if (stristr($_SERVER['REMOTE_ADDR'], $ip)) {
        		return true;
        	}
        }
        return false;

    }

    public function validate($requete)
    {

        //check if the request come from amazon
        $isAmazonIp = $this->isRequestFromAmazon();
        if ( !$isAmazonIp) {
            throw new BadRequestHttpException("Forbidden, your Host is not allowed to make this request!", null, 400);
        }

        //check my Amazon IP
        if (strtolower($requete->session->application->applicationId) != strtolower($this->_appId)) {
            throw new BadRequestHttpException("Forbidden, your App ID is not allowed to make this request!", null, 400);
        }

        // Check SSL signature
        if (preg_match("/https:\/\/s3.amazonaws.com(\:443)?\/echo.api\/*/i", $_SERVER['HTTP_SIGNATURECERTCHAINURL']) == false) {
        	throw new BadRequestHttpException( "Forbidden, unkown SSL Chain Origin!", null, 400);
        }

        $pem_file = sys_get_temp_dir() . '/' . hash("sha256", $_SERVER['HTTP_SIGNATURECERTCHAINURL']) . ".pem";
        if (!file_exists($pem_file)) {
        	file_put_contents($pem_file, file_get_contents($_SERVER['HTTP_SIGNATURECERTCHAINURL']));
        }
        $pem = file_get_contents($pem_file);

        $json = file_get_contents('php://input');
        if (openssl_verify($json, base64_decode($_SERVER['HTTP_SIGNATURE']) , $pem) !== 1){
        	throw new BadRequestHttpException( "Forbidden, failed to verify SSL Signature!", null, 400);
        }
        // check we can parse the pem content
        $cert = openssl_x509_parse($pem);
        if (empty($cert)) {
             throw new BadRequestHttpException("Certificate parsing failed!", null, 400);
        }
        // Check subjectAltName
        if (stristr($cert['extensions']['subjectAltName'], 'echo-api.amazon.com') != true) {
            throw new BadRequestHttpException( "Forbidden! Certificate subjectAltName Check failed!", null, 400);
        }

        // check expiration date of the certificate
        if ($cert['validTo_time_t'] < time()){
        	throw new BadRequestHttpException( "Forbidden! Certificate no longer Valid!", null, 400);
        	if (file_exists($pem_file)){
                unlink($pem_file);
            }
        }
        if (time() - strtotime($requete->request->timestamp) > 60) {
            throw new BadRequestHttpException( "Request Timeout! Request timestamp is to old.",null, 400);
        }
    }
}

And here is your endpoint is now secure properly, it will be able to pass the Amazon certification security tests.
Be careful, the fact that your app passes these tests does not guarantee that your app will be published...there are still many rules to follow.
In the next tutorial we will see how to take into account some variables called "Slots" in our requests to answer the user dynamically.
Questions about this lesson
No questions for this lesson. Be the first !

You must be logged in to ask for help on a lesson.