defmodule SyntropyWeb.ApiAuth do @moduledoc """ Runtime auth policy for Syntropy's external REST and channel surface. """ import Plug.Conn, only: [get_req_header: 2] @type validation_error :: :missing_token | :invalid_token @type validation_result :: :ok | {:error, validation_error()} @spec required?() :: boolean() def required? do config() |> Keyword.get(:required, false) end @spec validate_conn(Plug.Conn.t()) :: validation_result() def validate_conn(conn) do if required?() do conn |> get_req_header("authorization") |> List.first() |> extract_bearer() |> validate_token() else :ok end end @spec validate_socket(map()) :: validation_result() def validate_socket(params) do if required?() do params |> Map.get("token") |> validate_token() else :ok end end @spec error_message(validation_error()) :: String.t() def error_message(:missing_token), do: "Missing bearer token." def error_message(:invalid_token), do: "Invalid bearer token." @spec token() :: String.t() | nil def token do config() |> Keyword.get(:token) end defp config do Application.get_env(:syntropy, __MODULE__, []) end defp extract_bearer(header) when is_binary(header) do case String.split(header, ~r/\s+/, parts: 2, trim: true) do [scheme, token] when byte_size(token) > 0 -> if String.downcase(scheme) == "bearer", do: token, else: nil _other -> nil end end defp extract_bearer(_header), do: nil defp validate_token(nil), do: {:error, :missing_token} defp validate_token(provided_token) when is_binary(provided_token) do case token() do expected when is_binary(expected) and byte_size(expected) == byte_size(provided_token) -> if Plug.Crypto.secure_compare(provided_token, expected) do :ok else {:error, :invalid_token} end _other -> {:error, :invalid_token} end end end