defmodule Mpx.Authentication do @moduledoc """ Handles all aspects of authenticating with the Ministry Platform API. Ministry Platform expects a username and password AND a clientId and Client Secret that are set in the Ministy Platform Api Clients table. We send these parameters along with the grant type of "password" to the server and recieve back a token and refresh token that needs to be sent on subsequent requests to the MP. This also supports authenticating as a Client. For this case, the configuration is used to authenticate as the default user whose token we use to get the client details from the Clients table. Once we have the client_secret, we then attempt to authenticate as the client using the grant_type of client_credentials. """ use GenServer import Mpx.Config alias Mpx.Tables require Logger require IEx @type authentication_opts :: [username: String.t, password: String.t, client_id: String.t, client_secret: String.t] @name __MODULE__ @client_id Application.get_env(:mpx, :mp_client_id) @mp_username Application.get_env(:mpx, :mp_username) @doc false def start_link(opts \\ []) do opts = Keyword.put_new(opts, :name, @name) GenServer.start_link(__MODULE__, :ok, opts) end @doc false def init(:ok) do {:ok, %{}} end @doc """ Authenticate with Ministry Platform. Takes a keyword list of options in the shape: ``` [ username: "mp-username", password: "mp-password", client_id: "mp-client-id", client_secret: "mp-client-secret" ] ``` If keyword options are not passed in, defaults to configuration parameters: ``` mpx, mp_username: "mp-username", mp_password: System.get_env("MP_PASSWORD"), mp_client_id: {:system, "MP_CLIENT_ID"}, mp_client_secret: "mp-client-secret" ``` Returns `{:ok, token}` where `token` can be used in subsequent calls to the API that need to be authenticated or `{:error, reason}` """ @spec authenticate(authentication_opts | nil) :: {:ok, String.t} | {:error, String.t} def authenticate(opts \\ []) do GenServer.call(__MODULE__, {:authenticate, opts}, 5000) end @doc """ Uses the default authentication token to get a token for an api client. Assumes that the authentication parameters are set in the config: ``` mpx, mp_username: "mp-username", mp_password: System.get_env("MP_PASSWORD"), mp_client_id: {:system, "MP_CLIENT_ID"}, mp_client_secret: "mp-client-secret" ``` """ @spec authenticate_client(String.t) :: {:ok, String.t} | {:error, String.t} def authenticate_client(client_id) do GenServer.call(__MODULE__, {:authenticate_client, client_id}, 5000) end @doc """ Clears any cached authentication tokens ## Examples iex> Mpx.Authentication.clear() :ok """ @spec clear() :: :ok def clear() do GenServer.call(__MODULE__, :clear, 5000) end @doc """ Invalidates a token, so the next call to authenticate will go to the server """ @spec invalidate_token(String.t) :: :ok | :error def invalidate_token(client_id) do GenServer.call(@name, {:invalidate_token, client_id}, 5000) end @doc false def handle_call(:clear, _from, _state) do {:reply, :ok, %{}} end @doc false def handle_call({:authenticate, opts}, _from, state) do state = generate_auth_token_in_state(state, opts) auth = from_cache(state, opts) {:reply, auth.response, state} end @doc false def handle_call({:authenticate_client, client_id}, _from, state) do state = generate_auth_token_in_state(state, []) state |> from_cache([]) |> case do %{:response => {:ok, token}} -> state = generate_auth_token_in_state(state, client_id: client_id) auth = Map.get(state, client_id) {:reply, auth.response, state} %{:response => {:error, reason}} -> {:reply, {:error, "Unable to authenticate as the default user"}, state} end end def handle_call({:invalidate_token, client_id}, _from, state) do #TODO {:reply, nil, state} end defp get_authentication_token(nil, state, client_id: client_id) do # client token has expired %{:response => {:ok, token}} = from_cache(state, []) secret = get_client_secret(client_id, token) response = Mpx.Http.authenticate_client(client_id: client_id, client_secret: secret) Map.put(state, client_id, %{:response => response, :expires_at => expiration_time()}) end defp get_authentication_token(nil, state, opts) do # user token has expired response = Mpx.Http.authenticate_user(opts) Map.put(state, get_cache_key(opts), %{:expires_at => expiration_time(), :response => response }) end defp get_authentication_token(client, state, opts), do: state defp generate_auth_token_in_state(state, opts \\ []) do state |> from_cache(opts) |> client_expired |> get_authentication_token(state, opts) end defp from_cache(state, opts), do: Map.get(state, get_cache_key(opts)) defp client_expired(nil), do: nil defp client_expired(client_map) do now = DateTime.utc_now |> DateTime.to_unix case now < client_map.expires_at do true -> client_map #not expired false -> nil #expired end end defp expiration_time() do DateTime.utc_now |> DateTime.to_unix |> (fn(unix) -> unix + 30*60 end).() end defp get_cache_key(username: username, password: _, client_id: _, client_secret: _), do: username defp get_cache_key(client_id: client_id), do: client_id defp get_cache_key(opts), do: from_config(:mp_username, @mp_username) defp get_client_secret(client_id, token) do "dp_API_Clients" |> Tables.get_records(token, "$filter": "Client_ID='#{client_id}'") |> case do {:ok, [client|rest]} -> Map.get(client, "Client_Secret", nil) {:error, reason} -> nil end end end