defmodule Doorman.Authenticator do alias Doorman.{Repo, User, Mailer, Device, Users} import Ecto.Query defexception message: "not_authenticated" defp pin_range() do Application.get_env(:doorman, :pin_range, 100000..999999) end defp pin_lifespan_mins() do Application.get_env(:doorman, :pin_lifespan_mins, 60) end defp random_bytes() do (:crypto.hash :sha512, (:crypto.strong_rand_bytes 512)) |> Base.encode64 end def create_user_by_email(email, extra \\ nil) do create_user%{"email" => email, "username" => email}, extra end def create_user_by_mobile(mobile, extra \\ nil) do create_user%{"mobile" => mobile, "username" => mobile}, extra end def send_welcome_email(user) do {:ok, token, _} = generate_login_claim(user, user.requested_email) {:ok, pin, user} = generate_pin(user) Mailer.send_welcome_email(user, token, pin) end def send_confirm_email(user) do {:ok, token, _} = generate_login_claim(user, user.requested_email) Mailer.send_confirm_email(user, token) end def send_login_email(user) do {:ok, token, _} = generate_login_claim(user, user.requested_email) {:ok, pin, user} = generate_pin(user) Mailer.send_login_link(user, token, pin) end def create_user_by_username(username, password, extra \\ nil) do map = %{"username" => username, "password" => password} create_user(map, extra) end defp insert_user(changeset) do case Repo.insert(changeset) do {:ok, user} -> {:ok, jwt, _full_claims} = generate_access_claim(user) {:ok, user, jwt} {:error, changeset} -> {:error, changeset} end end def create_and_confirm_user(user_map) do case create_user(user_map) do {:ok, user, jwt, extra} -> send_welcome_email(user) {:ok, user, jwt, extra} other -> other end end def create_user(user_map, extra \\ nil) when is_map(user_map) do #Only accept very few keys when creating user user = Map.take(user_map, ["username", "password", "password_confirmation", "fullname", "locale"]) email = Map.get(user_map, "email") user = Map.put(user, "requested_email", email) mobile = Map.get(user_map, "mobile") user = Map.put(user, "requested_mobile", mobile) username = Map.get(user, "username") username = if is_nil(username) do email || mobile else username end user = Map.put(user, "username", username) user = Map.put(user, "confirmation_token", random_bytes()) #Make sure user does not try to set permissions user = Map.delete(user, "perms") #Generate password if user has not provided one user = unless (Map.get user, "password") do password = random_bytes() Map.put(user, "password", password) else user end changeset = User.changeset(%User{}, user) if is_nil(extra) do with {:ok, user, jwt} <- insert_user(changeset) do {:ok, user, jwt, nil} else {:error, changeset} -> {:error, Repo.changeset_errors(changeset), changeset} end else Repo.transaction(fn -> with {:ok, user, jwt} <- insert_user(changeset), {:ok, response} <- (try do extra.(user) rescue error -> {:error, error} end) do {:ok, user, jwt, response} else error -> Repo.rollback(error) end end) |> case do {:ok, response} -> response {:error, {:error, %Ecto.Changeset{} = changeset}} -> {:error, Repo.changeset_errors(changeset), changeset} {:error, {:error, error}} -> {:error, Doorman.Controller.translate_error(error), error} {:error, error} -> {:error, Doorman.Controller.translate_error(error), error} end end end def current_user(conn) do Guardian.Plug.current_resource(conn) end def authenticated_user!(conn) do user = Guardian.Plug.current_resource(conn) if user do user else raise Doorman.Authenticator, message: "not_authenticated" end end def confirm_email(user, confirmation_token) do if confirmation_token == user.confirmation_token do Users.update_user(user, %{email: user.requested_email, requested_email: nil, confirmation_token: nil}) else {:error, "wrong_confirmation_token"} end end def change_email(user, email) do Users.update_user(user, %{"confirmation_token" => random_bytes(), "requested_email" => email}) end def change_password(user, new_password) do Users.update_user(user, %{password: new_password}) end @doc """ Check that the given user has all the provider permissions ## Examples iex> Doorman.Authenticator.has_perms?(user, %{"admin" => ["read", "write"]}) false """ def has_perms?(user, %{}=required_perms) do !is_nil(user.perms) && Enum.reduce_while(required_perms, true, fn {key, ps}, _acc -> case Map.get(user.perms, key) do nil -> {:halt, false} users_perms -> user_has_perm = Enum.reduce_while(ps, true, fn p, _acc -> if Enum.any?(users_perms, fn up -> p == up end) do {:cont, true} else {:halt, false} end end) if user_has_perm do {:cont, true} else {:halt, false} end end end) end def has_perms?(user, [_|_] = perm_names) do Enum.reduce_while(perm_names, true, fn v, _acc -> if has_perms?(user, v) do {:cont, true} else {:cont, false} end end) end def has_perms?(user, perm_name) do !is_nil(user.perms) && Map.has_key?(user.perms, perm_name) end def add_perms(user, perms) do case user do nil -> {:error} user -> old_perms = user.perms || %{} Repo.update(User.changeset(user, %{"perms" => Map.merge(old_perms, perms)})) end end def drop_perm(user, name) do case user do nil -> {:error} user -> perms = user.perms || %{} Repo.update(User.changeset(user, %{"perms" => Map.delete(perms, name)})) end end def bump_to_admin(username) do case Users.get_by_username(username) do nil -> {:error} user -> Repo.update(User.changeset(user, %{"perms" => %{"admin" => ["read", "write"]}})) end end def drop_admin(username) do case Users.get_by_username(username) do nil -> {:error} user -> Repo.update(User.changeset(user, %{"perms" => %{}})) end end defp process_perms(perms) do if perms do Enum.to_list(perms) |> Enum.map(fn({k,v}) -> {k, Enum.map(v, fn(v) -> String.to_atom(v) end)} end) |> Enum.into(%{}) else nil end end def generate_access_claim(%User{} = user) do perms = process_perms(user.perms) Doorman.Guardian.encode_and_sign(user, %{}, token_type: "access", perms: perms || %{}) end def generate_login_claim(%User{} = user, email \\ nil) do Doorman.Guardian.encode_and_sign(user, %{requested_email: email}, token_type: "login", token_ttl: Application.get_env(:doorman, :login_ttl, {12, :hours})) end def generate_password_reset_claim(%User{} = user) do Doorman.Guardian.encode_and_sign(user, %{}, token_type: "password_reset", token_ttl: Application.get_env(:doorman, :login_ttl, {12, :hours})) end def get_user_devices(user) do Repo.all(from d in Device, where: d.user_id == ^user.id) end def current_claims(conn) do conn |> Guardian.Plug.current_token |> Doorman.Guardian.decode_and_verify end def current_claim_type(conn) do case conn |> current_claims do {:ok, claims} -> Map.get(claims, "typ") {:error, _} -> nil end end def use_pin(user, pin) do if User.check_pin(user, pin) do clear_pin(user) else {:error, :wrong_pin} end end def generate_pin(user) do pin = to_string(Enum.random(pin_range())) {:ok, exp_time} = (DateTime.utc_now() |> DateTime.to_unix()) + 60*pin_lifespan_mins() |> DateTime.from_unix {:ok, user} = Users.update_user(user, %{"pin" => pin, "pin_expiration" => exp_time}) {:ok, pin, user} end def clear_pin(user) do Users.update_user(user, %{"enc_pin" => nil, "pin_expiration" => nil}) end end