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