# `Oasis.HMACToken`
[🔗](https://github.com/xinz/oasis/blob/main/lib/oasis/hmac_token.ex#L1)

## Callback

There are two callback functions reserved for use in the generated modules when we use the hmac security
scheme of the OpenAPI Specification.

* `c:crypto_config/3`, provides a way to define the crypto-related key information for the high level usage,
  it required to return a `Oasis.HMACToken.Crypto` struct (or nil).
* `c:verify/3`, an optional function to provide a way to custom the verification of the token, you may
  want to validate request datetime, HTTP body or other more rules to verify it.

## Example

Here is an example to verify that HTTP request time does not exceed the current time by 1 minute.

```elixir
defmodule Oasis.Gen.HMACAuth do
  @behaviour Oasis.HMACToken
  alias Oasis.HMACToken.Crypto

  # in seconds
  @max_diff 60

  @impl true
  def crypto_config(_conn, _opts, _credential) do
    %Crypto{
      credential: "...",
      secret: "..."
    }
  end

  @impl true
  def verify(conn, token, opts) do
    with {:ok, _} <- Oasis.HMACToken.verify(conn, token, opts),
         {:ok, timestamp} <- conn |> get_header_date() |> parse_header_date() do
      timestamp_now = DateTime.utc_now() |> DateTime.to_unix()

      if abs(timestamp_now - timestamp) < @max_diff do
        {:ok, timestamp}
      else
        {:error, "expired"}
      end
    end
  end

  defp get_header_date(conn) do
    conn
    |> Plug.Conn.get_req_header("x-oasis-date")
    |> case do
      [date] -> date
      _ -> nil
    end
  end

  defp parse_header_date(str) when is_binary(str) do
    with {:ok, datetime, 0} <- DateTime.from_iso8601(str) do
      {:ok, DateTime.to_unix(datetime)}
    end
  end

  defp parse_header_date(_otherwise), do: {:error, "expired"}
end
```

# `opts`

```elixir
@type opts() :: Plug.opts()
```

# `token`

```elixir
@type token() :: %{
  credential: String.t(),
  signed_headers: String.t(),
  signature: String.t()
}
```

# `verify_error`

```elixir
@type verify_error() :: {:error, verify_error_status()}
```

Structured verification error returned by `verify/3`.

The second tuple element is a fixed string status code rather than an atom.

# `verify_error_status`

```elixir
@type verify_error_status() :: String.t()
```

String status code used in `verify_error()`.

Allowed values are:

- `"header_mismatch"`
- `"invalid_credential"`
- `"invalid_token"`
- `"expired"`

Elixir typespecs cannot enumerate string literals directly, so this remains
`String.t()` at the type level and the fixed set is documented here.

# `crypto_config`

```elixir
@callback crypto_config(
  conn :: Plug.Conn.t(),
  opts :: Keyword.t(),
  credential :: String.t()
) ::
  Oasis.HMACToken.Crypto.t() | nil
```

# `verify`
*optional* 

```elixir
@callback verify(conn :: Plug.Conn.t(), token :: token(), opts :: opts()) ::
  {:ok, term()} | verify_error()
```

# `sign`

```elixir
@spec sign(
  conn :: Plug.Conn.t(),
  signed_headers :: String.t(),
  secret :: String.t(),
  algorithm :: Oasis.Plug.HMACAuth.algorithm()
) :: String.t()
```

Sign HTTP requests according to settings.

# `verify`

```elixir
@spec verify(
  conn :: Plug.Conn.t(),
  token :: token(),
  opts :: Oasis.Plug.HMACAuth.opts()
) :: {:ok, term()} | verify_error()
```

Default implementation of the callback `verify`, only verify the signature.

Error statuses are normalized to strings for Oasis's public callback
contract.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
