Alexa skills with Elixir

PUBLISHED ON FEB 26, 2017 — DEVELOPMENT

As I delve into working with Elixir and Phoenix, I decided to write an Alexa Skill. The first thing I needed was a nice api to do so, so I wrote that.

Modeling the Alexa Request and Response

The first thing I did is write Elixir structs for the Alexa response and request objects. I like knowing the shape of my data ahead of time and the dot syntax is more concise than a map access. This step is fairly easy.

JSON Decoders for the nested objects.

I had to write custom Poison decoders to be able to provide deep parsing/typing of my structs. Overall, it’s fairly straightforward. Here’s a sample:

defmodule Alexa.RequestBody do
  defstruct [:version, :session, :context, :request]

  defimpl Poison.Decoder, for: Alexa.RequestBody do
    def decode(data, options) do
      data
      |> Map.update!(:session, fn session ->
        Poison.Decode.decode(session, Keyword.merge(options, as: %Alexa.Session{}))
      end)
      |> Map.update!(:request, fn request ->
        Poison.Decode.decode(request, Keyword.merge(options, as: %Alexa.Request{}))
      end)
      |> Map.update!(:context, fn context ->
        Poison.Decode.decode(context, Keyword.merge(options, as: %Alexa.Context{}))
      end)
    end
  end
end

We simply update the keys where we want typed structs.

Request validation

If you ever want to ship an Alexa Skill, there are a few things you need to do to ensure that the request is valid and coming from Amazon. If any of these fail, the request should fail. If I understand Phoenix’s design, the correct way to implement this behavior is through plugs. I wrote some small plugs to handle each of the validation bits.

Verifying the signature

I said that most of the validations are plugs, well, that is mostly true. plug lets you read the body data only once, and we need to read it during parsing. That’s why signature verification is implemented as a plug parser. It’s also the most complex of these validations. Lets dig into it.

You verify the signature using the signature an signaturecertchainurl headers. signaturecertchainurl has the certificate chain that you use to verify that signature is signed by the server. I used an OTP process to handle certificate validation, since I wanted to cache certificates and OTP processes can have state. Here is my handle_call function.

def handle_call({:validate, chain_uri, signature, body}, _from, state) do
    if uri_from_amazon?(chain_uri) do
      case validate_certificates(chain_uri, state[chain_uri]) do
        {:ok, certificates} ->
          state = Map.put(state, chain_uri, certificates)
          [ certificate | _] = certificates

          case is_signature_valid?(certificate, signature, body) do
            {:ok} ->
              {:reply, :ok, state}
            {:error, reason} ->
              {:reply, {:error, reason}, state}
          end

        {:error, reason} ->
          {:reply, {:error, reason}, state}
      end
    else
      {:reply, {:error, :not_amazon}, state}
    end
  end

The first step of this validation is to confirm that the url in signaturecertchainurl is from Amazon. This is fairly easy:

defp uri_from_amazon?(uri) do
  uri = URI.parse(uri)
  uri.scheme == "https" && uri.host == "s3.amazonaws.com" && (uri.port == nil || uri.port == 443) && String.starts_with?(uri.path, "/echo.api")
end

If the URL is from Amazon, you need to ensure that the certificate chain isn’t expired. This is done by validate_certificates which receives the chain_uri or the cached certificates.

def validate_certificates(chain_uri, nil) do
    case HTTPoison.get(chain_uri) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        certificates = :public_key.pem_decode(body)
        |> decoded_certs
        validate_certificates(chain_uri, certificates)
      {_, _} ->
        {:error, :no_cert}
    end
  end

  def validate_certificates(_chain_uri, certificates) do
    certificates
    |> valid_chain?
  end

Here, if we don’t have cached certificates, we fetch the certificate and decode them using the public_key module. If we have certificates, we verify that the chain is valid.

Down to Erlang

In order to read the certificates, I needed to use Erlang’s public key library. I spent quite a bit of time figuring out how to properly read out the certificates. This is where I discovered Erlang Records. Wow. They are basically tagged tuples. Elixir provides the Record module to help interact with these.

This is how you use the Record module.

defmodule Alexa.Records do
  require Record
  import Record
  defrecord :certificate, :OTPCertificate, extract(:OTPCertificate, from_lib: "public_key/include/public_key.hrl")
  defrecord :tbs_certificate, :OTPTBSCertificate, extract(:OTPTBSCertificate, from_lib: "public_key/include/public_key.hrl")
  defrecord :signature_algorithm, :SignatureAlgorithm, extract(:SignatureAlgorithm, from_lib: "public_key/include/public_key.hrl")
  defrecord :public_key_algorithm, :PublicKeyAlgorithm, extract(:PublicKeyAlgorithm, from_lib: "public_key/include/public_key.hrl")
  defrecord :attribute, :AttributeTypeAndValue, extract(:AttributeTypeAndValue, from_lib: "public_key/include/public_key.hrl")
  defrecord :subject_public_key_info, :OTPSubjectPublicKeyInfo, extract(:OTPSubjectPublicKeyInfo, from_lib: "public_key/include/public_key.hrl")
  defrecord :extension, :Extension, extract(:Extension, from_lib: "public_key/include/public_key.hrl")
  defrecord :validity, :Validity, extract(:Validity, from_lib: "public_key/include/public_key.hrl")
end

defrecord is a macro that generates methods with the tag you specify and you use those methods to set/get values of an Erlang record as follows.

Alexa.Records.certificate(tbsCertificate: tbsCertificate) = cert
Alexa.Records.tbs_certificate(validity: v) = tbsCertificate
Alexa.Records.validity(notBefore: before) = v
Alexa.Records.validity(notAfter: until) = v

Frankly, I find this non-idiomatic, since you can’t really create a pipe that chains record accesses.

Verifying the chain

Once I learned of records, validating the chain was easy. we just reduce the certificate list, verifying that Today is within the validity range. I used Timex to parse out the ASN.1 dates.

defp valid_chain?(decoded_certs) do
  valid_chain = Enum.reduce(decoded_certs, true, fn(cert, acc) ->
    Alexa.Records.certificate(tbsCertificate: tbsCertificate) = cert
    Alexa.Records.tbs_certificate(validity: v) = tbsCertificate
    Alexa.Records.validity(notBefore: before) = v
    Alexa.Records.validity(notAfter: until) = v
    {_, not_before} = before
    {_, not_after} = until
    {:ok, not_before} = Timex.parse("#{not_before}", "{ASN1:UTCtime}")
    {:ok, not_after} = Timex.parse("#{not_after}", "{ASN1:UTCtime}")
    now = DateTime.utc_now
    acc && (DateTime.compare(not_before, now) == :lt && DateTime.compare(now, not_after) == :lt)
  end)
  if valid_chain do
    {:ok, decoded_certs}
  else
    {:error, :invalid_chain}
  end
end

If the certificates are valid, we add them to the state, using the uri as the key. We now go on to verify the signature. Step 1. here is to verify that the certificate is from Amazon. We do this by reading the dNSName entry of the record and comparing it with a predefined value.


def is_signature_valid?(certificate, signature, body) do
  Alexa.Records.certificate(tbsCertificate: tbs_certificate) = certificate
  certificate_from_amazon?(certificate)
  |> verify_signature(tbs_certificate, signature, body)
end

defp certificate_from_amazon?(certificate) do
    Alexa.Records.certificate(tbsCertificate: tbs_certificate) = certificate
    Alexa.Records.tbs_certificate(extensions: extensions) = tbs_certificate
    Enum.reduce(extensions, false, fn(extension, acc) ->
      acc || case extension do
        {_, {2, 5, 29, 17}, _, [dNSName: 'echo-api.amazon.com']} ->
          true
        _ ->
          false
      end
    end)
  end

If the certificate is from amazon, we then go on to verifying the signature by extracting the public key, verifying the signature header by comparing it with the post body.

defp verify_signature(true, tbs_certificate, signature, body) do
  Alexa.Records.tbs_certificate(subjectPublicKeyInfo: public_key_info) = tbs_certificate
  Alexa.Records.subject_public_key_info(subjectPublicKey: key) = public_key_info
  {:ok, signature} = Base.decode64(signature)

  case :public_key.verify(body, :sha, signature, key) do
    true ->
      {:ok}
    false ->
      {:error, :invalid_signature}
  end
end

defp verify_signature(false, _, _, _) do
  {:error, :not_amazon}
end


Verifying the App ID

Verifying the Application ID is trivial, you just need to make sure that the identifier in the request body is the same as your application. There isn’t much interesting here.

defmodule Alexa.ValidateAppID do
  import Plug.Conn

  def init(opts) do
    Keyword.fetch!(opts, :app_id)
  end

  def call(conn, app_id) do
    case conn.body_params.session.application.applicationId do
      ^app_id ->
        conn
      _ ->
        conn
        |> resp(401, "Invalid App ID")
        |> halt()
    end
  end
end

Verifying the timestamp

Verifying the timestamp is also trivial. We just parse out the request timestamp using Timex, and check if it’s within the last 10 minutes.

defmodule Alexa.ValidateTimestamp do
  import Plug.Conn

  def init(opts) do
    opts
  end

  def call(conn, _) do
    timestamp = conn.body_params.request.timestamp
    {:ok, date} =  Timex.parse(timestamp, "{ISO:Extended}")
    date = date
    now = Timex.now

    if Timex.diff(date, now, :minutes) > 10 do
      resp(conn, 401, "expired request")
      |> halt()
    else
      conn
    end
  end
end

Providing a “controller” API

I am building this on top of Phoenix, but I wanted specific entry points for the Alexa requests instead of handling everything in a post action. I did this by doing a few things. First, I created a simple phoenix controller that handles the post action. Then I wrote a plug, where you pass in an module that handles alexa requests. These are then brought together by a macro such that you can do the following in your router:

  Alexa.skill "/alexa/kitchen", App.KitchenSkill, app_id: "amzn1.ask.skill.xxx", auth_handler: Alexa.AuthHandler

Passing the handler module through.

This is fairly trivial, we just add the skill to assigns

defmodule Alexa.SkillPlug do
  import Plug.Conn

  def init(opts) do
    Keyword.fetch!(opts, :skill)
  end

  def call(conn, skill) do
    assign(conn, :skill, skill)
  end

end

Handling user Authentication

I added an optional user authentication pass. This is specified using the auth_handler: option above. This is a module that exposes an authenticate_user method and returns either {:ok, user} or {:error, response} as demonstrated below. If the error tuple is returned, the plug will halt the connection.

defmodule App.AlexaAuthHandler do
  def authenticate_user(access_token) do
    case  Guardian.decode_and_verify(access_token) do
      {:ok, claims} ->
        App.GuardianSerializer.from_token(claims["sub"])
      {:error, _reason} ->
        response = %Alexa.ResponseBody{}
        {:error, response}
    end
  end
end

Handling request types

There are a few request types that Alexa can send. * Launch Requests * Handle Intent * Session Ended Requests.

This work is done via the skill handler specified above. Here is my skill behavior.

defmodule Alexa.Skill do
  defmacro __using__(_opts) do
    quote do
      import Alexa.API
      @behaviour Alexa.Skill

      def handle_intent(conn, name, _request) do
        {conn, nil}
      end

      def handle_launch(conn, _request) do
        {conn, nil}
      end

      def handle_session_end(conn, _request) do
        {conn, nil}
      end
      defoverridable [handle_intent: 3, handle_launch: 2, handle_session_end: 2]
    end
  end

  @callback handle_intent(Plug.Conn.T, String.T, Alexa.RequestBody) :: {Plug.Conn.T, Alexa.ResponseBody}
  @callback handle_launch(Plug.Conn.T, Alexa.RequestBody) :: {Plug.Conn.T, Alexa.ResponseBody}
  @callback handle_session_end(Plug.Conn.T, Alexa.RequestBody) :: {Plug.Conn.T, Alexa.ResponseBody}

end

handle_intent is the most interesting of these as it allows you to match on the intent type in your skill schema.

Finally, the macro puts it all together:

defmacro skill(path, controller, options) do
  path_atom = String.to_atom(path)
  quote bind_quoted: [options: options], unquote: true do
    pipeline unquote(path_atom) do
      plug :accepts, ["json"]
      plug Alexa.ValidateAppID, app_id: Keyword.get(options, :app_id)
      plug Alexa.ValidateTimestamp
      plug Alexa.AuthenticateUser, auth_handler: Keyword.get(options, :auth_handler)
      plug Alexa.SkillPlug, skill: unquote(controller)
    end

    scope unquote(path) do
      pipe_through [unquote(path_atom)]
      post "/", Alexa.Controller, :index
    end
  end
end

First, we create a pipeline, we atomize the path to name the pipeline to avoid conflicts. Then we configure all the plugs we created.

Then, we create a scope that passes through the generic Alexa.Controller, which reads the skill and passes control over to it.

Providing a response composition API

As in my apns response objects, I like composing them using elixir pipes. When I set out to build an API for creating Alexa responses, I wanted the same style. For these, I created an Alexa.API module that has chainable modules. These are then automatically imported into the Alexa Controllers.


defmodule Alexa.API do
  def continue_session(response \\ %Alexa.ResponseBody{}) do
    Kernel.put_in(response.response.shouldEndSession, false)
  end

  def put_session_attribute(response \\ %Alexa.ResponseBody{}, key, value) do
    Map.put(response, :sessionAttributes, Map.put(response.sessionAttributes, key, value))
  end

  def say(response \\ %Alexa.ResponseBody{}, text) do
    Kernel.put_in(response.response.outputSpeech, %Alexa.OutputSpeech{text: text})
  end

  def say_ssml(response \\ %Alexa.ResponseBody{}, text) do
    Kernel.put_in(response.response.outputSpeech, %Alexa.OutputSpeech{type: "SSML", ssml: text})
  end

  def card(response \\ %Alexa.ResponseBody{}, type, title, content, text, image) do
    Kernel.put_in(response.response.card, %Alexa.Card{type: type, title: title, content: content, text: text, image: image})
  end

  def card(response \\ %Alexa.ResponseBody{}, card) do
    Kernel.put_in(response.response.card, card)
  end
end

This can then be used as follows:

def handle_intent(conn, "For", _request) do
  response = say("hello")
  |> continue_session()
  |> put_session_attribute("hello", "world")

  {conn, response}
end

And that’s that. I hope to deploy some skills soon with this framework.

What’s missing?

Tests, so many tests. I hope to get to those soon.

TAGS: ALEXA, ELIXIR