lib/api_auth.ex

defmodule ApiAuth do
  @moduledoc """
  This is the ApiAuth module.

  It provides an HMAC authentication system for APIs.
  """

  alias ApiAuth.HeaderValues
  alias ApiAuth.HeaderCompare
  alias ApiAuth.Utils

  alias ApiAuth.ContentTypeHeader
  alias ApiAuth.DateHeader
  alias ApiAuth.UriHeader
  alias ApiAuth.ContentHashHeader
  alias ApiAuth.ContentHashHeader
  alias ApiAuth.AuthorizationHeader

  @doc """
  Takes a keyword list of headers and arguments necessary for generating the
  Authorization header and returns an updated keyword list of headers.

  ## Examples

      iex> [DATE: "Sat, 01 Jan 2000 00:00:00 GMT", "Content-Type": "application/json"]
      ...> |> ApiAuth.headers("/path", "client_id", "secret_key",
      ...>                    method: "PUT", content: "{\\"foo\\": \\"bar\\"}")
      [Authorization: "APIAuth-HMAC-SHA256 client_id:v5+Ooq88txd0cFyfSXYn03EFK/NQW9Gepk5YIdkZ4qM=",
       "X-APIAuth-Content-Hash": "Qm/ATwS/j9tYMdw3u7bc9w9jo34FpoxupfY+ha5Xk3Y=",
       DATE: "Sat, 01 Jan 2000 00:00:00 GMT",
       "Content-Type": "application/json"]

  """
  def headers(request_headers, uri, client_id, secret_key, opts \\ []) do
    parsed = parse(opts)

    request_headers
    |> Utils.convert()
    |> HeaderValues.wrap()
    |> ContentTypeHeader.headers()
    |> DateHeader.headers()
    |> UriHeader.headers(uri)
    |> ContentHashHeader.headers(parsed.method, parsed.content, parsed.content_algorithm)
    |> AuthorizationHeader.override(
      parsed.method,
      client_id,
      secret_key,
      parsed.signature_algorithm
    )
    |> HeaderValues.unwrap()
  end

  @doc """
  Takes a request header and arguments necessary for validating the Authorization header
  and returns true if the request is authentic and false otherwise

  ## Examples

      iex> headers = ApiAuth.headers([], "/path", "client_id", "secret_key")
      ...> ApiAuth.authentic?(headers, "/path", "client_id", "secret_key")
      true

      iex> headers = ApiAuth.headers([], "/path", "client_id", "secret_key")
      ...> ApiAuth.authentic?(headers, "/path", "client_id", "hacker")
      false

  """
  def authentic?(request_headers, uri, client_id, secret_key, opts \\ []) do
    parsed = parse(opts)

    converted_headers = Utils.convert(request_headers)

    valid = valid_headers(converted_headers, uri, client_id, secret_key, opts)

    valid
    |> HeaderCompare.wrap(converted_headers)
    |> ContentHashHeader.compare(parsed.method)
    |> AuthorizationHeader.compare()
    |> DateHeader.compare()
    |> HeaderCompare.to_boolean()
  end

  @doc """
  Takes a keyword list of headers and pulls the client id from the Authorization header
  returns the {:ok, client_id} or {:error}

  ## Examples

      iex> headers = [Authorization: "APIAuth-HMAC-SHA256 client_id:v5+Ooq88txd0cFyfSXYn03EFK/NQW9Gepk5YIdkZ4qM="]
      ...> ApiAuth.client_id(headers)
      {:ok, "client_id"}

      iex> headers = []
      ...> ApiAuth.client_id(headers)
      :error

  """
  def client_id(headers) do
    headers
    |> Utils.convert()
    |> AuthorizationHeader.extract_client_id()
  end

  defp valid_headers(request_headers, uri, client_id, secret_key, opts) do
    parsed = parse(opts)

    request_headers
    |> HeaderValues.wrap()
    |> ContentTypeHeader.headers()
    |> DateHeader.headers()
    |> UriHeader.override(uri)
    |> ContentHashHeader.override(parsed.method, parsed.content, parsed.content_algorithm)
    |> AuthorizationHeader.override(
      parsed.method,
      client_id,
      secret_key,
      parsed.signature_algorithm
    )
    |> HeaderValues.unwrap()
  end

  defp parse(opts) do
    %{
      method: opts |> Keyword.get(:method, "GET") |> String.upcase(),
      content: opts |> Keyword.get(:content, ""),
      content_algorithm: opts |> Keyword.get(:content_algorithm, :sha256),
      signature_algorithm: opts |> Keyword.get(:signature_algorithm, :sha256)
    }
  end
end