Cognitip

Verify Shopify App Proxy in Elixir Phoenix with Plugs - Part 1: Expected Parameters

In future posts we’ll look at how we can check the timestamp to be within a reasonable timeframe and doing HMAC verification.

But the first thing we should do is create a method to check if the four parameters are set in the connection.

When Shopify receives an HTTP request for a proxied path, it will forward that request to the specified Proxy URL. The forwarded request adds the following parameters:

  1. shop: The myshopify.com domain for the shop.
  2. path_prefix: The proxy sub-path prefix at which the shop was accessed.
  3. timestamp: The time in seconds since midnight of January 1, 1970 UTC.
  4. signature: A hexadecimal encoded SHA-256 HMAC of the other parameters.

Given that the Proxy URL is set to https://proxy-domain.com/proxy, the client’s IP address is 123.123.123.123 and the applications shared secret is hush, the forwarded request will look like the following:

GET /proxy/extra/path/components?extra=1&extra=2&shop=shop-name.myshopify.com&path_prefix=%2Fapps%2Fawesome_reviews&timestamp=1317327555&signature=a9718877bea71c2484f91608a7eaea1532bdf71f5c56825065fa4ccabe549ef3
  HTTP/1.1
  Host: proxy-domain.com
  X-Forwarded-For: 123.123.123.123

— Paraphrased from Shopify Developer Resources

Our first Plug is called Paramv, using Paramv we can pattern match the call with a Plug.Conn tuple that contains a params key and inside that, each of the keys we’re expecting.

We then create an additional function that matches everything, in that function we call Cogn.Plug.Errors.failed_connection/2 which checks to see what environment we’re in. If we’re running in :dev, we send a warning via Logger and proceed, else we send a 401 halt the connection and raise(Cogn.BadRequestError).

defmodule Cogn.Plug.Paramv do
  def init(default), do: default

  @doc """
  Check that all the parameters we need are set in the connection.
  """
  def call(
        %Plug.Conn{
          params: %{
            "shop" => _,
            "path_prefix" => _,
            "timestamp" => _,
            "signature" => _
          }
        } = conn,
        _default
      ) do
    conn
  end

  @doc """
  If the parameters are not set we'll render an error and halt the pipeline
  """
  def call(conn, _default) do
    Cogn.Plug.Errors.failed_connection(conn, :Paramv)
  end
end

And now adding in our Error Handler.

Remember: Replace GenieWeb with the name of your application.

defmodule Cogn.Plug.Errors do
  require Logger

  @doc """
  Calls `failed_connection/3` with `Mix.env()` as the last parameter.
  """
  def failed_connection(conn, who) do
    failed_connection(conn, who, Mix.env())
  end

  defp failed_connection(conn, who, :dev) do
    Logger.warn(fn ->
      "#{who}: " <>
        "Accessing proxy route with bad params. " <>
        "Doing this from a production enviroment will fail with the error code 400."
    end)

    conn
  end

  defp failed_connection(conn, _who, _) do
    Logger.error(fn ->
      "Accessing proxy route with bad params. #{conn.params}"
    end)

    conn
    |> Plug.Conn.put_status(401)
    |> Phoenix.Controller.render(GenieWeb.ErrorView, :"401")
    |> Plug.Conn.halt()

    raise(Cogn.BadRequestError)
  end
end