Custom Alexa Skills and WordPress

The following posts are coming up!

Recent Posts



Before you start, please note that there is no Amazon Alexa SDK for PHP. And this is a big problem.

An SDK is a software development kit that helps standardize the development of apps for the software. Using an SDK is basically using a standard library that everyone can access and call and not reinvent the wheel all the bloody time. And Amazon, for whatever reason, has decided that they’d rather push their Lambda hosting, which yes they charge you for, instead of clearly and cleanly document PHP code. Node.js? No problem. PHP? You’re on your own.

Rant aside, I have now a custom Amazon Skill, self hosted, and powered by WordPress.

Amazon’s Requirements

On Monday I showed you how to build a very generic skill. It had no options, and it was actually missing a critical piece. You see, Amazon has six basic requirements to be an app:

  1. The service must be Internet-accessible.
  2. The service must adhere to the Alexa Skills Kit interface.
  3. The service must support HTTP over SSL/TLS, leveraging an Amazon-trusted certificate.
  4. The service must accept requests on port 443.
  5. The service must present a certificate with a subject alternate name that matches the domain name of the endpoint.
  6. The service must validate that incoming requests are coming from Alexa.

The first five are pretty normal. If it’s not internet accessible, its not going to work. Same with the adherence to the skills kit interface. But that last one was surprisingly difficult and annoying. Mostly because of that lack of a standardized PHP SDK.

Basically there isn’t a standard way to validate that incoming requests are coming from Alexa, but boy howdy, are there requirements.

Validating Requests for Alexa

While it says the requirement is to validate the requests, that’s only one aspect of the game. The three basic parts are these:

  • Verifying that the Request was Sent by Alexa
  • Checking the Signature of the Request
  • Checking the Timestamp of the Request

And none of those are really well documented for PHP. Thanks.

The Code

In Monday’s post, I framed out the majority of the code that will be used. The change will be in this section:

public function last_post_rest_api_callback( $data ) {
	$response = $this->last_post();
	return $response;
}

It now shows this:

public function bury_your_queers_rest_api_callback( WP_REST_Request $request ) {
	$date = ( isset( $request['request']['intent']['slots']['Date']['value'] ) )? $request['request']['intent']['slots']['Date']['value'] : false;
	$validate_alexa = $this->alexa_validate_request( $request );
	if ( $validate_alexa['success'] !== 1 ) {
		$error = new WP_REST_Response( array( 'message' => $validate_alexa['message'], 'data' => array( 'status' => 400 ) ) );
		$error->set_status( 400 );
		return $error;
	}
	$response = $this->last_post( $date );
	return $response;
}

This makes two changes. First it’s grabbing the date from the weirdly stored JSON POST from Alexa and passing it to my last_post function. That code I’m skipping since taking the date, parsing it, and changing your output from last_post is beyond the score. No, I’m going to concentrate on the alexa_validate_request function.

You should take note of the success check if ( $validate_alexa['success'] !== 1 ) however. You must use a rest response with a 400 because Amazon is very picky.

alexa_validate_request

The brunt of the validation is to check if the URL came from Amazon, if the URL is on the certificate chain, if the certificate is legit, and finally if the request was made in the last 60 seconds. Which is a lot to look for.

In order to write this function, I forked Rich Bowen’s Validate Echo request via PHP code for WordPress. This takes into account some WordPress code that isn’t otherwise available:

function alexa_validate_request( $request ) {
	$url            = $request->get_header( 'signaturecertchainurl' );
	$timestamp      = $request['request']['timestamp'];
	$signature      = $request->get_header( 'signature' );

	// Validate that it even came from Amazon ...
	if ( !isset( $url ) )
		return array( 'success' => 0, 'message' => 'This request did not come from Amazon.' );

	// Validate proper format of Amazon provided certificate chain url
	$valid_uri = $this->alexa_valid_key_chain_uri( $url );
	if ( $valid_uri != 1 )
	    	return array( 'success' => 0, 'message' => $valid_uri );

	// Validate certificate signature
	$valid_cert = $this->alexa_valid_cert( $request, $signature, $url );
	if ( $valid_cert != 1 )
	    	return array ( 'success' => 0, 'message' => $valid_cert );

	// Validate time stamp
	if (time() - strtotime( $timestamp ) > 60)
		return array ( 'success' => 0, 'message' => 'Timestamp validation failure. Current time: ' . time() . ' vs. Timestamp: ' . $timestamp );

	// If there was no error, it's a success!
	return array( 'success' => 1, 'message' => 'Success' );
}

Within that function, I reference two more: alexa_valid_key_chain_uri and alexa_valid_cert which parse the chain and validate the certificate.

function alexa_valid_key_chain_uri( $keychainUri ){

    $uriParts = parse_url($keychainUri);

    if (strcasecmp( $uriParts['host'], 's3.amazonaws.com' ) != 0 )
        return ( 'The host for the Certificate provided in the header is invalid' );

    if (strpos( $uriParts['path'], '/echo.api/' ) !== 0 )
        return ( 'The URL path for the Certificate provided in the header is invalid' );

    if (strcasecmp( $uriParts['scheme'], 'https' ) != 0 )
        return ( 'The URL is using an unsupported scheme. Should be https' );

    if (array_key_exists( 'port', $uriParts ) && $uriParts['port'] != '443' )
        return ( 'The URL is using an unsupported https port' );

    return 1;
}

/*
    Validate that the certificate and signature are valid
*/
function alexa_valid_cert( $request, $signature, $url ) {

    $md5pem     = get_temp_dir() . md5( $url ) . '.pem';
    $echoDomain = '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( $url ) );

    // Validate certificate chain and signature
    $pem = file_get_contents( $md5pem );
    $ssl_check = openssl_verify( $request->get_body(), base64_decode( $signature ), $pem, 'sha1' );
    if ($ssl_check != 1 ) return( openssl_error_string() );

    // Parse certificate for validations below
    $parsedCertificate = openssl_x509_parse( $pem );
    if ( !$parsedCertificate ) return( 'x509 parsing failed' );

    // 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'], $echoDomain) === false) {
        return( 'subjectAltName Check Failed' );
    }

    // 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( 'certificate expiration check failed' );
    }

    return 1;
}

A Word About Testing…

The problem with all this weird code is that the only way to test is to use Amazon’s testing platform and that doesn’t actually throw back errors. The testing environment is fun, because you can type in ‘when was last post’ and it prepends “Alexa, ask HalfElf…” for you. And it shows you exactly what JSON it’s passing to your API and what your API retuned.

But…

In the event your API throws an error, you don’t get to see what the error was. No, you get a message saying that the API returned an invalid output.

Basically the Amazon API has no actual debugging if you’re trying to debug the connection requirements.

There may have been a lot of swearing involved on my end.


Posted

in

by

%d bloggers like this: