defmodule Dialup.Auth.Plug do @moduledoc """ HTTP authentication plug for Dialup applications. use Dialup, auth_accounts: MyApp.Accounts, plugs: [{Dialup.Auth.Plug, accounts: MyApp.Accounts}] """ @behaviour Plug import Plug.Conn @auth_paths ~w(/log_in /log_out /register /reset_password/request /reset_password/confirm) @impl Plug def init(opts) do accounts = Keyword.fetch!(opts, :accounts) unless Code.ensure_loaded?(accounts) and function_exported?(accounts, :get_user_by_session_token, 1) do raise ArgumentError, "#{inspect(accounts)} must implement Dialup.Auth.Accounts callbacks" end Keyword.merge(opts, accounts: accounts) end @impl Plug def call(conn, opts) do accounts = Keyword.fetch!(opts, :accounts) conn = fetch_cookies(conn) case conn.method do "POST" when conn.request_path in @auth_paths -> handle_auth_post(conn, accounts) _ -> {conn, _user} = Dialup.Auth.fetch_current_user(conn, accounts) conn end end defp handle_auth_post(conn, accounts) do conn = fetch_query_params(conn) with :ok <- validate_csrf(conn), {:ok, conn} <- dispatch_auth(conn, accounts) do conn else {:error, :invalid_credentials} -> conn |> put_resp_content_type("text/html") |> send_resp(401, "Invalid email or password") |> halt() {:error, reason} -> conn |> put_resp_content_type("text/html") |> send_resp(400, auth_error_message(reason)) |> halt() end end defp validate_csrf(conn) do if Dialup.Auth.valid_csrf?(conn, conn.params), do: :ok, else: {:error, :invalid_csrf} end defp dispatch_auth(%{request_path: "/log_in"} = conn, accounts) do email = param(conn, "email") password = param(conn, "password") if is_binary(email) and is_binary(password) do case accounts.get_user_by_email_and_password(email, password) do {:ok, user} -> conn = user |> then(&Dialup.Auth.log_in_user(conn, &1, accounts)) |> redirect_to(Dialup.Auth.safe_redirect_path(param(conn, "return_to"))) |> halt() {:ok, conn} {:error, :invalid_credentials} -> {:error, :invalid_credentials} end else {:error, :invalid_credentials} end end defp dispatch_auth(%{request_path: "/log_out"} = conn, accounts) do conn = conn |> Dialup.Auth.log_out_user(accounts) |> redirect_to("/") |> halt() {:ok, conn} end defp dispatch_auth(%{request_path: "/register"} = conn, accounts) do if function_exported?(accounts, :register_user, 1) do attrs = %{"email" => param(conn, "email"), "password" => param(conn, "password")} case accounts.register_user(attrs) do {:ok, user} -> conn = user |> then(&Dialup.Auth.log_in_user(conn, &1, accounts)) |> redirect_to("/") |> halt() {:ok, conn} {:error, _changeset} -> {:error, :registration_failed} end else {:error, :registration_unavailable} end end defp dispatch_auth(%{request_path: "/reset_password/request"} = conn, accounts) do email = param(conn, "email") if is_binary(email) and function_exported?(accounts, :get_user_by_email, 1) and function_exported?(accounts, :deliver_user_reset_password_instructions, 2) do case accounts.get_user_by_email(email) do nil -> :ok user -> accounts.deliver_user_reset_password_instructions(user, &"/reset_password/#{&1}") end end conn = conn |> redirect_to("/log_in") |> halt() {:ok, conn} end defp dispatch_auth(%{request_path: "/reset_password/confirm"} = conn, accounts) do if function_exported?(accounts, :get_user_by_reset_password_token, 1) and function_exported?(accounts, :reset_user_password, 2) do token = param(conn, "token") password = param(conn, "password") if is_binary(token) and is_binary(password) do with user when not is_nil(user) <- accounts.get_user_by_reset_password_token(token), {:ok, user} <- accounts.reset_user_password(user, %{"password" => password}) do conn = user |> then(&Dialup.Auth.log_in_user(conn, &1, accounts)) |> redirect_to("/") |> halt() {:ok, conn} else _ -> {:error, :invalid_reset_token} end else {:error, :invalid_reset_token} end else {:error, :reset_password_unavailable} end end defp param(conn, key), do: conn.params[key] || conn.params[:"#{key}"] defp redirect_to(conn, path) do conn |> put_resp_header("location", path) |> send_resp(302, "") end defp auth_error_message(:invalid_csrf), do: "Invalid CSRF token" defp auth_error_message(:registration_failed), do: "Could not register user" defp auth_error_message(:registration_unavailable), do: "Registration is not available" defp auth_error_message(:reset_password_unavailable), do: "Password reset is not available" defp auth_error_message(:invalid_reset_token), do: "Invalid or expired reset token" defp auth_error_message(_), do: "Authentication failed" end