diff --git a/.gitignore b/.gitignore index d65284a3..376591ba 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ npm-debug.log # we ignore priv/static. You may want to comment # this depending on your deployment strategy. /priv/static/ + +# Mnesia +/Mnesia* \ No newline at end of file diff --git a/.iex.exs b/.iex.exs new file mode 100644 index 00000000..826000ae --- /dev/null +++ b/.iex.exs @@ -0,0 +1,2 @@ +alias Philomena.{Repo, Users.User} +import Ecto.Query \ No newline at end of file diff --git a/config/config.exs b/config/config.exs index 740e247d..63dcf03f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -16,7 +16,8 @@ config :philomena, config :philomena, :pow, user: Philomena.Users.User, repo: Philomena.Repo, - extensions: [PhilomenaWeb.HaltTotp], + web_module: PhilomenaWeb, + extensions: [PowResetPassword, PowPersistentSession, PowMultiFactor], controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks config :bcrypt_elixir, diff --git a/lib/philomena/application.ex b/lib/philomena/application.ex index d1186c20..a78bf76a 100644 --- a/lib/philomena/application.ex +++ b/lib/philomena/application.ex @@ -11,9 +11,10 @@ defmodule Philomena.Application do # Start the Ecto repository Philomena.Repo, # Start the endpoint when the application starts - PhilomenaWeb.Endpoint + PhilomenaWeb.Endpoint, # Starts a worker by calling: Philomena.Worker.start_link(arg) # {Philomena.Worker, arg}, + Pow.Store.Backend.MnesiaCache ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/lib/philomena/images/query.ex b/lib/philomena/images/query.ex index aebd8743..28a4db54 100644 --- a/lib/philomena/images/query.ex +++ b/lib/philomena/images/query.ex @@ -53,12 +53,12 @@ defmodule Philomena.Images.Query do tag_include = %{terms: %{tag_ids: user.watched_tag_ids}} {:ok, include_query} = - Philomena.Images.Query.user_parser(ctx, user.watched_images_query |> normalize()) + Philomena.Images.Query.user_parser(ctx, user.watched_images_query_str |> normalize()) {:ok, exclude_query} = Philomena.Images.Query.user_parser( ctx, - user.watched_images_exclude_query |> normalize() + user.watched_images_exclude_str |> normalize() ) should = [tag_include, include_query] @@ -125,12 +125,12 @@ defmodule Philomena.Images.Query do tag_include = %{terms: %{tag_ids: user.watched_tag_ids}} {:ok, include_query} = - Philomena.Images.Query.moderator_parser(ctx, user.watched_images_query |> normalize()) + Philomena.Images.Query.moderator_parser(ctx, user.watched_images_query_str |> normalize()) {:ok, exclude_query} = Philomena.Images.Query.moderator_parser( ctx, - user.watched_images_exclude_query |> normalize() + user.watched_images_exclude_str |> normalize() ) should = [tag_include, include_query] diff --git a/lib/philomena/users/user.ex b/lib/philomena/users/user.ex index dbf6b505..9a4b6307 100644 --- a/lib/philomena/users/user.ex +++ b/lib/philomena/users/user.ex @@ -6,6 +6,9 @@ defmodule Philomena.Users.User do use Pow.Ecto.Schema, password_hash_methods: {&Password.hash_pwd_salt/1, &Password.verify_pass/2} + use Pow.Extension.Ecto.Schema, + extensions: [PowResetPassword] + import Ecto.Changeset schema "users" do @@ -100,6 +103,7 @@ defmodule Philomena.Users.User do def changeset(user, attrs) do user |> pow_changeset(attrs) + |> pow_extension_changeset(attrs) |> cast(attrs, []) |> validate_required([]) end diff --git a/lib/philomena_web/endpoint.ex b/lib/philomena_web/endpoint.ex index 7667b112..f787a00d 100644 --- a/lib/philomena_web/endpoint.ex +++ b/lib/philomena_web/endpoint.ex @@ -43,7 +43,10 @@ defmodule PhilomenaWeb.Endpoint do signing_salt: "signed cookie", encryption_salt: "authenticated encrypted cookie" - plug PhilomenaWeb.Plugs.Session, otp_app: :philomena + plug Pow.Plug.Session, otp_app: :philomena + plug PowPersistentSession.Plug.Cookie, otp_app: :philomena + + plug PhilomenaWeb.Plugs.ReloadUser plug PhilomenaWeb.Plugs.RenderTime plug PhilomenaWeb.Plugs.CurrentFilter plug PhilomenaWeb.Router diff --git a/lib/philomena_web/halt_totp.ex b/lib/philomena_web/halt_totp.ex deleted file mode 100644 index 77a01861..00000000 --- a/lib/philomena_web/halt_totp.ex +++ /dev/null @@ -1,35 +0,0 @@ -defmodule PhilomenaWeb.HaltTotp.Phoenix.ControllerCallbacks do - use Pow.Extension.Phoenix.ControllerCallbacks.Base - alias Pow.Plug - import Phoenix.Controller - - def before_respond(Pow.Phoenix.SessionController, :create, {:ok, conn}, _config) do - conn - |> Plug.current_user() - |> halt_totp(conn) - end - - defp halt_totp(%{otp_required_for_login: true}, conn) do - {:ok, conn} = Plug.clear_authenticated_user(conn) - - conn = - conn - |> put_flash(:error, "Cannot yet authenticate accounts with TOTP enabled") - |> redirect(to: "/") - - {:halt, conn} - end - - defp halt_totp(_, conn) do - {:ok, conn} - end - - def before_process(Pow.Phoenix.RegistrationController, _method, conn, _config) do - conn = - conn - |> put_flash(:error, "Registrations are disabled") - |> redirect(to: "/") - - {:halt, conn} - end -end diff --git a/lib/philomena_web/plugs/current_filter.ex b/lib/philomena_web/plugs/current_filter.ex index e9b6d659..4ef1cd1b 100644 --- a/lib/philomena_web/plugs/current_filter.ex +++ b/lib/philomena_web/plugs/current_filter.ex @@ -15,7 +15,7 @@ defmodule PhilomenaWeb.Plugs.CurrentFilter do filter = if user do - user = user |> preload(:current_filter) + user = user |> Repo.preload(:current_filter) user.current_filter else filter_id = conn |> get_session(:filter_id) diff --git a/lib/philomena_web/plugs/reload_user.ex b/lib/philomena_web/plugs/reload_user.ex new file mode 100644 index 00000000..a3ee8975 --- /dev/null +++ b/lib/philomena_web/plugs/reload_user.ex @@ -0,0 +1,21 @@ +defmodule PhilomenaWeb.Plugs.ReloadUser do + alias Pow.Plug + alias Philomena.Users.User + alias Philomena.Repo + + def init(opts), do: opts + + def call(conn, _opts) do + config = Plug.fetch_config(conn) + + case Plug.current_user(conn, config) do + nil -> + conn + + user -> + reloaded_user = Repo.get!(User, user.id) + + Plug.assign_current_user(conn, reloaded_user, config) + end + end +end \ No newline at end of file diff --git a/lib/philomena_web/plugs/session.ex b/lib/philomena_web/plugs/session.ex deleted file mode 100644 index e3bac93b..00000000 --- a/lib/philomena_web/plugs/session.ex +++ /dev/null @@ -1,52 +0,0 @@ -defmodule PhilomenaWeb.Plugs.Session do - use Pow.Plug.Base - - alias Plug.Conn - alias Philomena.{Repo, Users.User} - - @session_key :philomena_session - - def fetch(conn, _config) do - conn = Conn.fetch_session(conn) - user = Conn.get_session(conn, @session_key) - - conn - |> maybe_load_user(user) - end - - def create(conn, user, _config) do - value = session_value(user) - - conn = - conn - |> Conn.fetch_session() - |> Conn.put_session(@session_key, value) - - {conn, user} - end - - def delete(conn, _config) do - conn - |> Conn.fetch_session() - |> Conn.delete_session(@session_key) - end - - defp maybe_load_user(conn, {:ok, user}) do - with {:ok, [user_id, hash]} <- Jason.decode(user), - %User{} = user <- Repo.get(User, user_id), - true <- SecureCompare.compare(hash, binary_part(user.encrypted_password, 0, 25)) do - {conn, user} - else - _ -> - {conn, nil} - end - end - - defp maybe_load_user(conn, _) do - {conn, nil} - end - - defp session_value(user) do - Jason.encode([user.id, binary_part(user.encrypted_password, 0, 25)]) - end -end diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 8f960520..9c4e39c7 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -16,11 +16,11 @@ defmodule PhilomenaWeb.Router do plug :accepts, ["json"] end - #scope "/" do - # pipe_through :browser - # - # pow_routes() - #end + scope "/" do + pipe_through :browser + + pow_routes() + end scope "/", PhilomenaWeb do pipe_through :browser diff --git a/lib/philomena_web/templates/pow/registration/edit.html.eex b/lib/philomena_web/templates/pow/registration/edit.html.eex new file mode 100644 index 00000000..69130ee0 --- /dev/null +++ b/lib/philomena_web/templates/pow/registration/edit.html.eex @@ -0,0 +1,30 @@ +

Edit profile

+ +<%= form_for @changeset, @action, [as: :user], fn f -> %> + <%= if @changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + + <%= label f, :current_password %> + <%= password_input f, :current_password %> + <%= error_tag f, :current_password %> + + <%= label f, Pow.Ecto.Schema.user_id_field(@changeset) %> + <%= text_input f, Pow.Ecto.Schema.user_id_field(@changeset) %> + <%= error_tag f, Pow.Ecto.Schema.user_id_field(@changeset) %> + + <%= label f, :password %> + <%= password_input f, :password %> + <%= error_tag f, :password %> + + <%= label f, :confirm_password %> + <%= password_input f, :confirm_password %> + <%= error_tag f, :confirm_password %> + +
+ <%= submit "Update" %> +
+<% end %> + diff --git a/lib/philomena_web/templates/pow/registration/new.html.eex b/lib/philomena_web/templates/pow/registration/new.html.eex new file mode 100644 index 00000000..decf913b --- /dev/null +++ b/lib/philomena_web/templates/pow/registration/new.html.eex @@ -0,0 +1,28 @@ +

Register

+ +<%= form_for @changeset, @action, [as: :user], fn f -> %> + <%= if @changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + + <%= label f, Pow.Ecto.Schema.user_id_field(@changeset) %> + <%= text_input f, Pow.Ecto.Schema.user_id_field(@changeset) %> + <%= error_tag f, Pow.Ecto.Schema.user_id_field(@changeset) %> + + <%= label f, :password %> + <%= password_input f, :password %> + <%= error_tag f, :password %> + + <%= label f, :confirm_password %> + <%= password_input f, :confirm_password %> + <%= error_tag f, :confirm_password %> + +
+ <%= submit "Register" %> +
+<% end %> + + +<%= link "Sign in", to: Routes.pow_session_path(@conn, :new) %> diff --git a/lib/philomena_web/templates/pow/session/new.html.eex b/lib/philomena_web/templates/pow/session/new.html.eex new file mode 100644 index 00000000..86c7e7f8 --- /dev/null +++ b/lib/philomena_web/templates/pow/session/new.html.eex @@ -0,0 +1,24 @@ +

Sign in

+ +<%= form_for @changeset, @action, [as: :user], fn f -> %> + <%= if @changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + + <%= label f, Pow.Ecto.Schema.user_id_field(@changeset) %> + <%= text_input f, Pow.Ecto.Schema.user_id_field(@changeset) %> + <%= error_tag f, Pow.Ecto.Schema.user_id_field(@changeset) %> + + <%= label f, :password %> + <%= password_input f, :password %> + <%= error_tag f, :password %> + +
+ <%= submit "Sign in" %> +
+<% end %> + + +<%= link "Register", to: Routes.pow_registration_path(@conn, :new) %> diff --git a/lib/philomena_web/views/pow/registration_view.ex b/lib/philomena_web/views/pow/registration_view.ex new file mode 100644 index 00000000..940fa244 --- /dev/null +++ b/lib/philomena_web/views/pow/registration_view.ex @@ -0,0 +1,3 @@ +defmodule PhilomenaWeb.Pow.RegistrationView do + use PhilomenaWeb, :view +end diff --git a/lib/philomena_web/views/pow/session_view.ex b/lib/philomena_web/views/pow/session_view.ex new file mode 100644 index 00000000..ddf846a6 --- /dev/null +++ b/lib/philomena_web/views/pow/session_view.ex @@ -0,0 +1,3 @@ +defmodule PhilomenaWeb.Pow.SessionView do + use PhilomenaWeb, :view +end diff --git a/lib/pow_multi_factor/phoenix/controller_callbacks.ex b/lib/pow_multi_factor/phoenix/controller_callbacks.ex new file mode 100644 index 00000000..f92658e9 --- /dev/null +++ b/lib/pow_multi_factor/phoenix/controller_callbacks.ex @@ -0,0 +1,69 @@ +defmodule PowMultiFactor.Phoenix.ControllerCallbacks do + @moduledoc """ + Controller callback logic for multi-factor authentication. + + ### 2FA code not submitted + + Triggers on `Pow.Phoenix.SessionController.create/2`. + + When a user with 2FA enabled attempts to sign in without submitting their + TOTP token, the session will be cleared, and the user redirected back to + `Pow.Phoenix.Routes.session_path/1`. + + ### User updates account + + Triggers on `Pow.Phoenix.RegistrationController.update/2` + + When a user changes their account settings, they are required to confirm a + current 2FA token. + + See `PowMultiFactor.Ecto.Schema` for more. + """ + + use Pow.Extension.Phoenix.ControllerCallbacks.Base + + alias Pow.Plug + alias PowMultiFactor.Plug, as: PowMultiFactorPlug + + def before_respond(Pow.Phoenix.SessionController, :create, {:ok, conn}, _config) do + return_path = routes(conn).session_path(conn, :new) + + clear_unauthorized(conn, {:ok, conn}, return_path) + end + + def before_respond(Pow.Phoenix.RegistrationController, :update, {:ok, user, conn}, _config) do + return_path = routes(conn).registration_path(conn, :edit) + + halt_unauthorized(conn, {:ok, user, conn}, return_path) + end + + defp clear_unauthorized(conn, success_response, return_path) do + case PowMultiFactorPlug.mfa_unauthorized?(conn) do + true -> clear_auth(conn) |> go_back(return_path) + false -> success_response + end + end + + defp halt_unauthorized(conn, success_response, return_path) do + case PowMultiFactorPlug.mfa_unauthorized?(conn) do + true -> go_back(conn, return_path) + false -> success_response + end + end + + def clear_auth(conn) do + {:ok, conn} = Plug.clear_authenticated_user(conn) + + conn + end + + defp go_back(conn, return_path) do + error = extension_messages(conn).invalid_multi_factor(conn) + conn = + conn + |> Phoenix.Controller.put_flash(:error, error) + |> Phoenix.Controller.redirect(to: return_path) + + {:halt, conn} + end +end diff --git a/lib/pow_multi_factor/plug.ex b/lib/pow_multi_factor/plug.ex new file mode 100644 index 00000000..e5b0eeec --- /dev/null +++ b/lib/pow_multi_factor/plug.ex @@ -0,0 +1,26 @@ +defmodule PowMultiFactor.Plug do + @moduledoc """ + Plug helper methods. + """ + + alias Pow.Plug + #alias PowMultiFactor.Ecto.Context + + def mfa_unauthorized?(conn) do + user = Plug.current_user(conn) + + if user.otp_required_for_login do + true + else + false + end + end + + #defp otp_secret(user) do + + #end + + #defp otp_shared_key do + # Application.get_env + #end +end diff --git a/mix.exs b/mix.exs index 08d45af8..05e92221 100644 --- a/mix.exs +++ b/mix.exs @@ -52,7 +52,8 @@ defmodule Philomena.MixProject do {:elastix, "~> 0.7.1"}, {:nimble_parsec, "~> 0.5.1"}, {:canary, "~> 1.1.1"}, - {:scrivener_ecto, "~> 2.0"} + {:scrivener_ecto, "~> 2.0"}, + {:elixir2fa, "~> 0.1.0"} ] end diff --git a/mix.lock b/mix.lock index 1cb5eff4..9a40e583 100644 --- a/mix.lock +++ b/mix.lock @@ -13,6 +13,7 @@ "ecto_network": {:hex, :ecto_network, "1.1.0", "7062004b9324ff13e50c02dab84877f8a55e06db9eabbf2d04bda21da6fc6e8a", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm"}, "ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "elastix": {:hex, :elastix, "0.7.1", "8e199a764a0bc018e0a97afeea950a8069b988867d87f8d25ae121d8b3288612", [:mix], [{:httpoison, "~> 1.4", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}, {:retry, "~> 0.8", [hex: :retry, repo: "hexpm", optional: false]}], "hexpm"}, + "elixir2fa": {:hex, :elixir2fa, "0.1.0", "4585154695ad13a01c17c46e61b0884b0ce1569ebcc667d1d09cd1cbbb4f4ba8", [:mix], [], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm"}, "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"},