Current section
Files
Jump to
Current section
Files
lib/mpx/authentication/mpx_authentication.ex
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