From c3f226b51efe70e4f8d85b0c493b80f2625a04d9 Mon Sep 17 00:00:00 2001 From: humancopy Date: Thu, 11 Apr 2019 19:39:02 +0200 Subject: [PATCH 1/3] Add an oAuth HMAC authentication --- README.md | 4 ++-- lib/shopify/oauth.ex | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dc8f312..698f530 100644 --- a/README.md +++ b/README.md @@ -58,14 +58,14 @@ To gain access to a shop via OAuth, first, generate a permission url based on yo ```elixir params = %{scope: "read_orders,read_products", redirect_uri: "http://my-redirect_uri.com/"} -permission_url = "shop-name" |> Shopify.session() |> Shopify.OAuth.permission_url(params) +permission_url = "shop-name" |> Shopify.session() |> Shopify.OAuth.authenticate(params) |> Shopify.OAuth.permission_url(params) ``` After a shop has authorized access, they will be redirected to your URI above. The redirect will include a payload that contains a 'code'. We can now generate an access token. ```elixir -{:ok, %Shopify.Response{data: oauth}} = "shop-name" |> Shopify.session() |> Shopify.OAuth.request_token(code) +{:ok, %Shopify.Response{data: oauth}} = "shop-name" |> Shopify.session() |> Shopify.OAuth.authenticate(params) |> Shopify.OAuth.request_token(code) ``` We can now easily create a new OAuth API session. diff --git a/lib/shopify/oauth.ex b/lib/shopify/oauth.ex index 0481ca0..92018b3 100644 --- a/lib/shopify/oauth.ex +++ b/lib/shopify/oauth.ex @@ -55,4 +55,34 @@ defmodule Shopify.OAuth do |> Request.new("oauth/access_token", %{}, %Shopify.OAuth{}, body) |> Client.post() end + + @doc """ + Validates the hmac signature of a Shopify request using the Session's secret + + Returns `Shopify.Session` or `nil` + + ## Parameters + - session: A %Shopify.Session{} struct. + - params: A map of additional query params. + + ## Examples + iex> Shopify.session("shop-name") |> Shopify.OAuth.authenticate(params) + %Shopify.Session{} + + """ + def authenticate(session, params) do + case valid_hmac?(session.client_secret, params) do + true -> session + _ -> nil + end + end + + defp valid_hmac?(secret, params) do + hmac = params["hmac"] + query = params |> Map.delete("hmac") |> URI.encode_query + + :crypto.hmac(:sha256, secret, query) + |> Base.encode16(case: :lower) + |> String.equivalent?(hmac) + end end From d04c075856c868e8a2e6d185b3d602912fa109ab Mon Sep 17 00:00:00 2001 From: humancopy Date: Thu, 11 Apr 2019 20:03:50 +0200 Subject: [PATCH 2/3] Add a plug to verify the authenticity of the request. This makes it easier to make a pipeline and make sure the requests are authenticated. --- lib/shopify/plugs/verify_oauth.ex | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 lib/shopify/plugs/verify_oauth.ex diff --git a/lib/shopify/plugs/verify_oauth.ex b/lib/shopify/plugs/verify_oauth.ex new file mode 100644 index 0000000..eaea7be --- /dev/null +++ b/lib/shopify/plugs/verify_oauth.ex @@ -0,0 +1,42 @@ +defmodule Shopify.Plugs.VerifyOAuth do + @moduledoc """ + Validates the request originated from Shopify. + If one exists, it let's the request continue, otherwise it haults the request and calls the handler function from the handler module. + + Note that this *does not* authenticate a shop, it only verifies that the request came from Shopify. + + # Usage + + Add this to your `router.ex`, possibly inside a pipeline: + + plug Shopify.Plugs.VerifyOAuth, + handler: MyApp.PageController # (required) The handler module + handler_fn: :handle_error # (optional) Customize the handler function, defaults to :unauthenticated + """ + + import Plug.Conn + + @doc false + def init(opts), do: opts + + @doc false + def call(conn, opts) do + handler = Keyword.get(opts, :handler) + handler_fn = Keyword.get(opts, :handler_fn, :unauthenticated) + case verify_hmac(conn.params) do + nil -> + conn = conn |> halt + apply(handler, handler_fn, [conn, conn.params]) + _ -> + conn + |> assign(:shop, conn.params["shop"]) + end + end + + defp verify_hmac(params) do + params["shop"] + |> Shopify.session() + |> Shopify.OAuth.authenticate(params) + end + +end From 1ed38f82c179e4d2dab05cfad7271437db0bb525 Mon Sep 17 00:00:00 2001 From: humancopy Date: Fri, 12 Apr 2019 14:13:02 +0200 Subject: [PATCH 3/3] The ids parameter should be prepared differently for the HMAC --- lib/shopify/oauth.ex | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/shopify/oauth.ex b/lib/shopify/oauth.ex index 92018b3..76b2775 100644 --- a/lib/shopify/oauth.ex +++ b/lib/shopify/oauth.ex @@ -79,10 +79,38 @@ defmodule Shopify.OAuth do defp valid_hmac?(secret, params) do hmac = params["hmac"] - query = params |> Map.delete("hmac") |> URI.encode_query - :crypto.hmac(:sha256, secret, query) + :crypto.hmac(:sha256, secret, query_string(params)) |> Base.encode16(case: :lower) |> String.equivalent?(hmac) end + + defp query_string(query, nil) do + query + end + + defp query_string(query, ids) do + # Convert the ids to a string representing and array of numeric strings: + # ["1", "2", "3"] + ids = ids + |> Enum.map(fn x -> "\"#{x}\"" end) + |> Enum.join(", ") + + # Concatenate the ids back to the query - they must not be URI encoded! + # https://community.shopify.com/c/Shopify-APIs-SDKs/HMAC-calculation-vs-ids-arrays/m-p/261154 + "ids=[#{ids}]&#{query}" + end + + defp query_string(params) when is_map(params) do + # Extract the ids + ids = params["ids"] + + # Remove the ids & hmac parameters and make a query string + query = params + |> Map.delete("ids") + |> Map.delete("hmac") + |> URI.encode_query + + query_string(query, ids) + end end