defmodule PlugHmacAuth do @moduledoc """ `PlugHmacAuth` provides a `Plug` for `HMAC authentication`. """ @behaviour Plug import Plug.Conn @type opts :: [ key_access_id: String.t(), key_signature: String.t(), hmac_hash_algo: :md4 | :md5 | :sha | :sha224 | :sha256 | :sha384 | :sha3_224 | :sha3_256 | :sha3_384 | :sha3_512 | :sha512, secret_handler: module(), error_handler: module() ] @spec init(opts :: Keyword.t()) :: Keyword.t() def init(opts), do: opts @spec call(Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t() def call(conn, opts) do key_access_id = Keyword.get(opts, :key_access_id, "no-key-access-id") key_signature = Keyword.get(opts, :key_signature, "no-key-signature") hmac_hash_algo = Keyword.get(opts, :hmac_hash_algo, :no_hash_algo) secret_handler = Keyword.get(opts, :secret_handler, :no_secret_handler) error_handler = Keyword.get(opts, :error_handler, :no_error_handler) with {:ok, access_key} <- fetch_token_from_header(conn, key_access_id), {:ok, access_signature} <- fetch_token_from_header(conn, key_signature), {:ok, secret_key} <- secret_handler.get_secret_key(access_key), :ok <- verify_payload(conn, secret_key, access_signature, hmac_hash_algo) do conn else {:error, code} -> conn |> error_handler.auth_error(code) |> halt() end end @spec verify_payload( Plug.Conn.t(), String.t(), String.t(), :md4 | :md5 | :sha | :sha224 | :sha256 | :sha384 | :sha3_224 | :sha3_256 | :sha3_384 | :sha3_512 | :sha512 ) :: :ok | {:error, :invalid_signature} def verify_payload(conn, secret_key, access_signature, hmac_hash_algo) def verify_payload(conn, secret_key, access_signature, hmac_hash_algo) do if conn |> get_payload() |> gen_signature(secret_key, hmac_hash_algo) == access_signature do :ok else {:error, :invalid_signature} end end @spec get_payload(Plug.Conn.t()) :: String.t() def get_payload(%Plug.Conn{method: "GET", query_string: query_string}), do: query_string def get_payload(%Plug.Conn{assigns: %{raw_body: raw_body}}), do: raw_body @doc """ Returns signatre generated by payload and secret key. """ @spec gen_signature(String.t(), String.t(), atom()) :: String.t() def gen_signature(payload, secret_key, hmac_hash_algo), do: :crypto.hmac(hmac_hash_algo, secret_key, payload) |> Base.encode64() @doc """ Returns the first token from http request header by specific key. """ @spec fetch_token_from_header(Plug.Conn.t(), binary()) :: {:error, :invalid_key} | {:ok, binary()} def fetch_token_from_header(conn, key) def fetch_token_from_header(conn, key) when is_binary(key) do case get_req_header(conn, key) do [] -> {:error, :invalid_key} [token | _] -> {:ok, String.trim(token)} end end def fetch_token_from_header(_conn, key) when not is_binary(key) do {:error, :invalid_key} end end