Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an oAuth HMAC authentication #71

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
58 changes: 58 additions & 0 deletions lib/shopify/oauth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,62 @@ 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"]

: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
42 changes: 42 additions & 0 deletions lib/shopify/plugs/verify_oauth.ex
Original file line number Diff line number Diff line change
@@ -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