defmodule Guard.Authenticator do alias Guard.{Repo, User, UserApiKey, Mailer, Users} defexception message: "not_authenticated" defp pin_range() do Application.get_env(:guard, :pin_range, 100_000..999_999) end defp pin_lifespan_mins() do Application.get_env(:guard, :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" => User.downcase(email)}, extra) end def create_user_by_mobile(mobile, extra \\ nil) do create_user(%{"mobile" => mobile, "username" => User.clean_mobile_number(mobile)}, extra) end def send_welcome_email(user) do {:ok, token, _} = generate_login_claim(user) {:ok, pin, user} = generate_email_pin(user) Mailer.send_welcome_email(user, token, pin) end def send_confirm_email(user) do {:ok, token, _} = generate_login_claim(user) Mailer.send_confirm_email(user, token) end def send_login_email(user) do {:ok, token, _} = generate_login_claim(user) {:ok, pin, user} = generate_email_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 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) # 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 Repo.transaction(fn -> with {:ok, user} <- Users.create_user(user), {:ok, jwt, _full_claims} <- generate_access_claim(user), {:ok, response} <- (try do if extra do extra.(user) else {:ok, nil} end 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, Guard.Controller.translate_error(error), error} {:error, error} -> {:error, Guard.Controller.translate_error(error), error} end end def current_user(conn) do case Guardian.Plug.current_resource(conn) do nil -> nil user = %User{} -> user key = %UserApiKey{} -> Users.get!(key.user_id) _ -> nil end end def authenticated_user!(conn) do user = Guardian.Plug.current_resource(conn) if user do user else raise Guard.Authenticator, message: "not_authenticated" end end def request_email_change(user, email) do Users.update_user(user, %{"requested_email" => email}) end def request_mobile_change(user, mobile) do Users.update_user(user, %{"requested_mobile" => mobile}) 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> Guard.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 -> add_perms(user, %{admin: [:read, :write]}) end end def drop_admin(username) do case Users.get_by_username(username) do nil -> {:error} user -> drop_perm(user, "admin") 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 -> if is_atom(v), do: v, else: String.to_atom(v) end)} end) |> Enum.into(%{}) else nil end end defp can_switch_user?(claims) do case Application.get_env(:guard, Guard.Guardian)[:switch_user_permission] do nil -> false true -> true required_permission -> claims |> Guard.Jwt.decode_permissions_from_claims() |> Guard.Jwt.all_permissions?(required_permission) end end def switch_user(conn, %User{} = user) do case current_claims(conn) do {:ok, claims} -> current_claims(conn) if can_switch_user?(claims) do root_user = authenticated_user!(conn) generate_switched_user_access_claim(user, root_user.id) else {:error, :forbidden} end other -> other end end def reset_user(conn) do case current_claims(conn) do {:ok, claims} -> current_claims(conn) case claims do %{"usr" => user_id} -> user = Users.get!(user_id) generate_access_claim(user) _ -> {:error, :not_switched} end other -> other end end defp generate_switched_user_access_claim(%User{} = user, root_user_id) do perms = process_perms(user.perms) Guard.Jwt.encode_and_sign(user, %{usr: root_user_id}, token_type: "access", perms: perms || %{} ) end def generate_access_claim(%User{} = user) do perms = process_perms(user.perms) Guard.Jwt.encode_and_sign(user, %{}, token_type: "access", perms: perms || %{}) end def generate_login_claim(%User{} = user, email \\ nil) do Guard.Jwt.encode_and_sign(user, %{requested_email: email || user.requested_email}, token_type: "login", token_ttl: Application.get_env(:guard, :login_ttl, {12, :hours}) ) end def generate_password_reset_claim(%User{} = user) do Guard.Jwt.encode_and_sign(user, %{}, token_type: "password_reset", token_ttl: Application.get_env(:guard, :login_ttl, {12, :hours}) ) end def current_claims(conn) do {:ok, Guardian.Plug.current_claims(conn)} 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 case User.validate_pin(user, pin) do :ok -> clear_pin(user) other -> other end end def use_email_pin(user, pin) do case User.validate_email_pin(user, pin) do :ok -> clear_email_pin(user) other -> other end end def use_either_pin(user, pin) do case User.validate_pin(user, pin) do :ok -> clear_pin(user) other -> if user.enc_email_pin, do: use_email_pin(user, pin), else: other end end def generate_pin(user) do {:ok, exp_time} = ((DateTime.utc_now() |> DateTime.to_unix()) + 60 * pin_lifespan_mins()) |> DateTime.from_unix() generate_pin(user, exp_time) end def generate_pin(user, exp_time) do pin = to_string(Enum.random(pin_range())) case Users.update_user(user, %{pin: pin, pin_expiration: exp_time}) do {:ok, user} -> {:ok, pin, user} other -> other end end def clear_pin(user) do Users.update_user(user, %{enc_pin: nil, pin_expiration: nil}) end def generate_email_pin(user) do {:ok, exp_time} = ((DateTime.utc_now() |> DateTime.to_unix()) + 60 * pin_lifespan_mins()) |> DateTime.from_unix() generate_email_pin(user, exp_time) end def generate_email_pin(user, exp_time) do pin = to_string(Enum.random(pin_range())) case Users.update_user(user, %{email_pin: pin, email_pin_expiration: exp_time}) do {:ok, user} -> {:ok, pin, user} other -> other end end def clear_email_pin(user) do Users.update_user(user, %{enc_email_pin: nil, email_pin_expiration: nil}) end def create_api_key(%User{} = user, permissions \\ %{}) do Users.create_api_key(user, permissions) end def delete_api_key(key) do Users.delete_api_key(key) end def list_api_keys(%User{} = user) do Users.list_api_keys(user) end end