If you did not receive a verification code, or it has expired:
- -diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62ad433a..0041d1a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: mix local.hex --force - name: Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: cache with: key: elixir-${{ hashFiles('Dockerfile', 'mix.lock') }}-${{ github.ref }}-test @@ -114,7 +114,7 @@ jobs: mix local.hex --force - name: Cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: cache with: key: elixir-${{ hashFiles('Dockerfile', 'mix.lock') }}-${{ github.ref }}-credo diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index ab57ee2a..37615d3d 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -64,7 +64,7 @@ jobs: image: ${{ steps.publish.outputs.image }} - name: Deploy - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 with: task-definition: ${{ steps.template.outputs.task-definition }} service: production-system76-recognizer diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 81638780..3c794400 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -64,7 +64,7 @@ jobs: image: ${{ steps.publish.outputs.image }} - name: Deploy - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 with: task-definition: ${{ steps.template.outputs.task-definition }} service: staging-genesis76-recognizer diff --git a/lib/recognizer/accounts.ex b/lib/recognizer/accounts.ex index 75656b4f..cd68477b 100644 --- a/lib/recognizer/accounts.ex +++ b/lib/recognizer/accounts.ex @@ -626,23 +626,21 @@ defmodule Recognizer.Accounts do Sends a new notification message to the user to verify their _new_ two factor settings. """ - def send_new_two_factor_notification(user) do + def check_two_factor_notification_time(user) do {:ok, attrs} = get_new_two_factor_settings(user) - send_new_two_factor_notification(user, attrs) + check_two_factor_notification_time(attrs, 100) end - def send_new_two_factor_notification(user, attrs) do + def check_two_factor_notification_time(attrs, two_factor_issue_time) do %{ - notification_preference: %{two_factor: preference}, - two_factor_seed: seed + notification_preference: %{two_factor: preference} } = attrs if preference != "app" do - token = Authentication.generate_token(seed) - Notification.deliver_two_factor_token(user, token, String.to_existing_atom(preference)) + {:ok, two_factor_issue_time} + else + {:ok, nil} end - - :ok end @doc """ @@ -660,15 +658,17 @@ defmodule Recognizer.Accounts do @doc """ Confirms the user's two factor settings and persists them to the database from our cache """ - def confirm_and_save_two_factor_settings(code, user) do - with {:ok, %{two_factor_seed: seed} = attrs} <- get_new_two_factor_settings(user), - true <- Authentication.valid_token?(code, seed) do + + def confirm_and_save_two_factor_settings(code, counter, user) do + with {:ok, %{notification_preference: %{two_factor: preference}, two_factor_seed: seed} = attrs} <- + get_new_two_factor_settings(user), + true <- Authentication.valid_token?(preference, code, counter, seed) do user |> Repo.preload([:notification_preference, :recovery_codes]) |> User.two_factor_changeset(attrs) |> Repo.update() else - _ -> :error + _ -> {:error, nil} end end diff --git a/lib/recognizer/ecto_enums.ex b/lib/recognizer/ecto_enums.ex index a1c04e02..fd481e2d 100644 --- a/lib/recognizer/ecto_enums.ex +++ b/lib/recognizer/ecto_enums.ex @@ -1,6 +1,6 @@ import EctoEnum -defenum(Recognizer.TwoFactorPreference, ["app", "text", "voice"]) +defenum(Recognizer.TwoFactorPreference, ["app", "text", "voice", "email"]) defenum(Recognizer.UserType, ["individual", "business"]) defenum(Recognizer.OAuthService, ["facebook", "github", "google"]) diff --git a/lib/recognizer/notifications/account.ex b/lib/recognizer/notifications/account.ex index 741bac58..afafd575 100644 --- a/lib/recognizer/notifications/account.ex +++ b/lib/recognizer/notifications/account.ex @@ -70,6 +70,7 @@ defmodule Recognizer.Notifications.Account do def two_factor_method(:text), do: :TWO_FACTOR_METHOD_SMS def two_factor_method(:voice), do: :TWO_FACTOR_METHOD_VOICE + def two_factor_method(:email), do: :TWO_FACTOR_METHOD_EMAIL @doc """ Deliver user recovery code used notification. diff --git a/lib/recognizer_web/authentication.ex b/lib/recognizer_web/authentication.ex index 6d81fd21..23a59827 100644 --- a/lib/recognizer_web/authentication.ex +++ b/lib/recognizer_web/authentication.ex @@ -180,15 +180,42 @@ defmodule RecognizerWeb.Authentication do @doc """ Generate a Time Based One Time Password """ - def generate_token(%{two_factor_seed: two_factor_seed}), do: generate_token(two_factor_seed) - def generate_token(two_factor_seed), do: :pot.totp(two_factor_seed, addwindow: 1) + def generate_token(preference, counter, %{two_factor_seed: two_factor_seed}), + do: generate_token(preference, counter, two_factor_seed) + + def generate_token(preference, counter, two_factor_seed) do + if preference == :app || preference == "app" do + generate_token_app(two_factor_seed) + else + generate_token_external(two_factor_seed, counter) + end + end + + def generate_token_app(two_factor_seed), do: :pot.totp(two_factor_seed, interval: 30) + + def generate_token_external(two_factor_seed, counter), do: :pot.hotp(two_factor_seed, counter) @doc """ Validate a user provided token is valid """ - def valid_token?(token, %{two_factor_seed: two_factor_seed}), do: valid_token?(token, two_factor_seed) - def valid_token?(_token, nil), do: false - def valid_token?(token, two_factor_seed), do: :pot.valid_totp(token, two_factor_seed, window: 1, addwindow: 1) + + def valid_token?(preference, token, counter, %{two_factor_seed: two_factor_seed}), + do: valid_token?(preference, token, counter, two_factor_seed) + + def valid_token?(preference, token, counter, two_factor_seed) do + if preference == :app || preference == "app" do + valid_token_app?(token, two_factor_seed) + else + valid_token_external?(token, two_factor_seed, counter) + end + end + + def valid_token_app?(token, two_factor_seed), do: :pot.valid_totp(token, two_factor_seed, interval: 30) + + def valid_token_external?(token, two_factor_seed, counter) do + # :pot.valid_hotp(token, two_factor_seed, last: counter) + token == :pot.hotp(two_factor_seed, counter) + end defp config(key) do Application.get_env(:recognizer, __MODULE__)[key] diff --git a/lib/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller.ex b/lib/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller.ex index 714673a7..0bb90051 100644 --- a/lib/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller.ex +++ b/lib/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller.ex @@ -5,7 +5,7 @@ defmodule RecognizerWeb.Accounts.Api.UserSettingsTwoFactorController do alias RecognizerWeb.{Authentication, ErrorView} @one_minute 60_000 - @one_day 86_400_000 + @one_hour 3_600_000 plug Hammer.Plug, [ @@ -14,13 +14,17 @@ defmodule RecognizerWeb.Accounts.Api.UserSettingsTwoFactorController do ] when action in [:send] + # when action in [:send, :update] + plug Hammer.Plug, [ - rate_limit: {"api:two_factor_daily", @one_day, 6}, + rate_limit: {"api:two_factor_hour", @one_hour, 6}, by: {:conn, &get_user_id_from_request/1} ] when action in [:send] + # when action in [:send, :update] + def show(conn, _params) do user = Authentication.fetch_current_user(conn) @@ -48,8 +52,9 @@ defmodule RecognizerWeb.Accounts.Api.UserSettingsTwoFactorController do def update(conn, %{"verification" => code}) do user = Authentication.fetch_current_user(conn) + counter = get_session(conn, :two_factor_issue_time) - case Accounts.confirm_and_save_two_factor_settings(code, user) do + case Accounts.confirm_and_save_two_factor_settings(code, counter, user) do {:ok, updated_user} -> render(conn, "show.json", user: updated_user) @@ -67,11 +72,34 @@ defmodule RecognizerWeb.Accounts.Api.UserSettingsTwoFactorController do def send(conn, _params) do user = Authentication.fetch_current_user(conn) - with {:ok, settings} <- Accounts.get_new_two_factor_settings(user), - :ok <- Accounts.send_new_two_factor_notification(user, settings) do - conn - |> put_status(202) - |> render("show.json", settings: settings, user: user) + with {:ok, settings} <- Accounts.get_new_two_factor_settings(user) do + # Get or initialize the two_factor_issue_time from the session + + conn = + case get_session(conn, :two_factor_issue_time) do + nil -> put_session(conn, :two_factor_issue_time, System.system_time(:second)) + _ -> conn + end + + issue_time = get_session(conn, :two_factor_issue_time) + + case Accounts.check_two_factor_notification_time(settings, issue_time) do + {:ok, updated_issue_time} when not is_nil(updated_issue_time) -> + conn + |> put_session(:two_factor_issue_time, updated_issue_time) + |> put_status(202) + |> render("show.json", settings: settings, user: user) + + {:ok, nil} -> + conn + |> put_status(202) + |> render("show.json", settings: settings, user: user) + + {:error, reason} -> + conn + |> put_status(400) + |> json(%{error: reason}) + end end end end diff --git a/lib/recognizer_web/controllers/accounts/prompt/two_factor_controller.ex b/lib/recognizer_web/controllers/accounts/prompt/two_factor_controller.ex index cac4e76a..c4942adc 100644 --- a/lib/recognizer_web/controllers/accounts/prompt/two_factor_controller.ex +++ b/lib/recognizer_web/controllers/accounts/prompt/two_factor_controller.ex @@ -36,8 +36,9 @@ defmodule RecognizerWeb.Accounts.Prompt.TwoFactorController do def update(conn, params) do user = conn.assigns.user two_factor_code = Map.get(params, "two_factor_code", "") + counter = get_session(conn, :two_factor_issue_time) - case Accounts.confirm_and_save_two_factor_settings(two_factor_code, user) do + case Accounts.confirm_and_save_two_factor_settings(two_factor_code, counter, user) do {:ok, updated_user} -> Authentication.log_in_user(conn, updated_user, params) diff --git a/lib/recognizer_web/controllers/accounts/prompt/verification_controller.ex b/lib/recognizer_web/controllers/accounts/prompt/verification_controller.ex index 5fa29b37..b1896d23 100644 --- a/lib/recognizer_web/controllers/accounts/prompt/verification_controller.ex +++ b/lib/recognizer_web/controllers/accounts/prompt/verification_controller.ex @@ -5,7 +5,7 @@ defmodule RecognizerWeb.Accounts.Prompt.VerificationController do alias RecognizerWeb.Authentication @one_minute 60_000 - @one_day 86_400_000 + @one_hour 3_600_000 plug :ensure_user @@ -16,13 +16,17 @@ defmodule RecognizerWeb.Accounts.Prompt.VerificationController do ] when action in [:resend] + # when action in [:resend, :new] + plug Hammer.Plug, [ - rate_limit: {"user:verification_daily", @one_day, 6}, + rate_limit: {"user:verification_hour", @one_hour, 6}, by: {:conn, &get_user_id_from_unverified_request/1} ] when action in [:resend] + # when action in [:resend, :new] + def new(%{assigns: %{user: %{verified_at: nil} = user}} = conn, _params) do render(conn, "new.html", resend?: false, email: user.email) end diff --git a/lib/recognizer_web/controllers/accounts/user_reset_password_controller.ex b/lib/recognizer_web/controllers/accounts/user_reset_password_controller.ex index 286bd7ac..5579bac1 100644 --- a/lib/recognizer_web/controllers/accounts/user_reset_password_controller.ex +++ b/lib/recognizer_web/controllers/accounts/user_reset_password_controller.ex @@ -15,7 +15,7 @@ defmodule RecognizerWeb.Accounts.UserResetPasswordController do plug Hammer.Plug, [ - rate_limit: {"user:reset_password_daily", @one_day, 10}, + rate_limit: {"user:reset_password_daily", @one_day, 6}, by: {:conn, &get_email_from_request/1} ] when action in [:create] diff --git a/lib/recognizer_web/controllers/accounts/user_session_controller.ex b/lib/recognizer_web/controllers/accounts/user_session_controller.ex index 4fabfbe0..51d0777b 100644 --- a/lib/recognizer_web/controllers/accounts/user_session_controller.ex +++ b/lib/recognizer_web/controllers/accounts/user_session_controller.ex @@ -19,6 +19,7 @@ defmodule RecognizerWeb.Accounts.UserSessionController do conn |> put_session(:two_factor_user_id, user.id) |> put_session(:two_factor_sent, false) + |> put_session(:two_factor_issue_time, System.system_time(:second)) |> redirect(to: Routes.user_two_factor_path(conn, :new)) {:oauth, _user} -> diff --git a/lib/recognizer_web/controllers/accounts/user_settings_controller.ex b/lib/recognizer_web/controllers/accounts/user_settings_controller.ex index ad4d1ba5..273dc7d3 100644 --- a/lib/recognizer_web/controllers/accounts/user_settings_controller.ex +++ b/lib/recognizer_web/controllers/accounts/user_settings_controller.ex @@ -2,6 +2,7 @@ defmodule RecognizerWeb.Accounts.UserSettingsController do use RecognizerWeb, :controller alias Recognizer.Accounts + alias Recognizer.Notifications.Account alias Recognizer.Accounts.Role alias Recognizer.BigCommerce alias RecognizerWeb.Authentication @@ -31,17 +32,53 @@ defmodule RecognizerWeb.Accounts.UserSettingsController do end end + def resend(conn, _params) do + current_user = Authentication.fetch_current_user(conn) + current_time = System.system_time(:second) + + conn = put_session(conn, :two_factor_issue_time, current_time) + + conn + |> send_two_factor_notification(current_user) + + conn + |> put_flash(:info, "Two factor code has been resent") + |> redirect(to: Routes.user_settings_path(conn, :two_factor_confirm)) + end + @doc """ Generate codes for a new two factor setup """ def two_factor_init(conn, _params) do user = Authentication.fetch_current_user(conn) + # %{two_factor_seed: seed, notification_preference: %{two_factor: method} } = user - {:ok, %{two_factor_seed: seed, notification_preference: %{two_factor: method}} = settings} = + {:ok, %{two_factor_seed: seed, notification_preference: %{two_factor: method}}} = Accounts.get_new_two_factor_settings(user) - if method == "text" || method == "voice" do - :ok = Accounts.send_new_two_factor_notification(user, settings) + method_atom = normalize_to_atom(method) + + if method_atom == :text || method_atom == :voice || method_atom == :email do + current_time = System.system_time(:second) + # conn = put_session(conn, :two_factor_issue_time, current_time) + + conn = + if get_session(conn, :two_factor_issue_time) == nil do + put_session(conn, :two_factor_issue_time, current_time) + else + conn + end + + two_factor_sent = get_session(conn, :two_factor_sent) + + conn = + if two_factor_sent do + conn + else + conn + |> send_two_factor_notification(user, method_atom) + end + render(conn, "confirm_two_factor_external.html") else render(conn, "confirm_two_factor.html", @@ -51,6 +88,85 @@ defmodule RecognizerWeb.Accounts.UserSettingsController do end end + @doc """ + Confirming and saving a new two factor setup with user-provided code + """ + def two_factor_confirm(conn, params) do + user = Authentication.fetch_current_user(conn) + two_factor_code = Map.get(params, "two_factor_code", "") + + case Accounts.get_new_two_factor_settings(user) do + {:ok, %{notification_preference: %{two_factor: method}} = _settings} -> + handle_two_factor_settings(conn, user, two_factor_code, method) + + {:ok, nil} -> + conn + |> put_flash(:error, "Two factor code is invalid") + |> redirect(to: Routes.user_settings_path(conn, :two_factor_confirm)) + + {:error, reason} -> + conn + |> put_flash(:error, "Error: #{inspect(reason)}") + |> redirect(to: Routes.user_settings_path(conn, :two_factor_confirm)) + end + end + + defp handle_two_factor_settings(conn, user, two_factor_code, method) do + current_time = System.system_time(:second) + conn = ensure_two_factor_issue_time(conn, current_time) + + two_factor_issue_time = get_session(conn, :two_factor_issue_time) + method_atom = normalize_to_atom(method) + + case Accounts.confirm_and_save_two_factor_settings(two_factor_code, two_factor_issue_time, user) do + {:ok, updated_user} -> + process_confirm_result(conn, user, updated_user, current_time, two_factor_issue_time, method_atom) + end + end + + defp ensure_two_factor_issue_time(conn, current_time) do + if get_session(conn, :two_factor_issue_time) == nil do + put_session(conn, :two_factor_issue_time, current_time) + else + conn + end + end + + defp process_confirm_result(conn, user, updated_user, current_time, two_factor_issue_time, method_atom) do + if current_time - two_factor_issue_time > 900 do + conn + |> put_session(:two_factor_issue_time, current_time) + |> send_two_factor_notification(user, method_atom) + |> put_flash( + :error, + "Two-factor code has expired. A new code has been sent. Please check your email for the newest two-factor code and try again." + ) + |> redirect(to: Routes.user_settings_path(conn, :two_factor_confirm)) + else + if is_nil(updated_user) do + conn + |> put_flash(:error, "Two factor code is invalid") + |> redirect(to: Routes.user_settings_path(conn, :two_factor_confirm)) + else + Accounts.clear_two_factor_settings(user) + + conn + |> put_session(:two_factor_sent, false) + |> put_session(:two_factor_issue_time, nil) + |> put_flash(:info, "Two factor code verified") + |> redirect(to: Routes.user_settings_path(conn, :edit)) + end + end + end + + def normalize_to_atom(input) do + cond do + is_atom(input) -> input + is_binary(input) -> String.to_existing_atom(input) + true -> raise ArgumentError, "Input must be a string or an atom" + end + end + @doc """ Rate limit 2fa setup only for text & voice, bypass for app. """ @@ -76,28 +192,6 @@ defmodule RecognizerWeb.Accounts.UserSettingsController do |> halt() end - @doc """ - Confirming and saving a new two factor setup with user-provided code - """ - def two_factor_confirm(conn, params) do - two_factor_code = Map.get(params, "two_factor_code", "") - user = Authentication.fetch_current_user(conn) - - case Accounts.confirm_and_save_two_factor_settings(two_factor_code, user) do - {:ok, _updated_user} -> - Accounts.clear_two_factor_settings(user) - - conn - |> put_flash(:info, "Two factor code verified") - |> redirect(to: Routes.user_settings_path(conn, :edit)) - - _ -> - conn - |> put_flash(:error, "Two factor code is invalid") - |> redirect(to: Routes.user_settings_path(conn, :two_factor_confirm)) - end - end - @doc """ Form submission for settings applied """ @@ -170,6 +264,8 @@ defmodule RecognizerWeb.Accounts.UserSettingsController do case Accounts.get_new_two_factor_settings(user) do {:ok, %{recovery_codes: recovery_codes}} -> + conn = put_session(conn, :two_factor_sent, false) + recovery_block = recovery_codes |> Enum.map_join("\n", & &1.code) @@ -206,4 +302,43 @@ defmodule RecognizerWeb.Accounts.UserSettingsController do |> assign(:redirect_home, home_uri) |> assign(:allow_phone_methods, !is_admin) end + + defp send_two_factor_notification(conn, %{notification_preference: %{two_factor: method}} = current_user) do + send_two_factor_notification(conn, current_user, method) + end + + defp send_two_factor_notification(conn, current_user, method) do + if method == "app" do + conn + else + two_factor_issue_time = get_session(conn, :two_factor_issue_time) + current_time = System.system_time(:second) + + if two_factor_issue_time == nil do + conn + |> deliver_and_update_token(current_user, method, current_time) + else + conn + |> deliver_and_update_token(current_user, method, two_factor_issue_time) + end + end + end + + defp deliver_and_update_token(conn, current_user, method, issue_time) do + # %{two_factor_seed: two_factor_seed} = current_user + + {:ok, %{two_factor_seed: two_factor_seed}} = Accounts.get_new_two_factor_settings(current_user) + + # method_atom = String.to_existing_atom(method) + + token = + if method == :app || method == "app" do + Authentication.generate_token_app(two_factor_seed) + else + Authentication.generate_token_external(two_factor_seed, issue_time) + end + + conn + |> tap(fn _conn -> Account.deliver_two_factor_token(current_user, token, method) end) + end end diff --git a/lib/recognizer_web/controllers/accounts/user_two_factor_controller.ex b/lib/recognizer_web/controllers/accounts/user_two_factor_controller.ex index 4806d1f9..bcd14d07 100644 --- a/lib/recognizer_web/controllers/accounts/user_two_factor_controller.ex +++ b/lib/recognizer_web/controllers/accounts/user_two_factor_controller.ex @@ -7,7 +7,7 @@ defmodule RecognizerWeb.Accounts.UserTwoFactorController do alias RecognizerWeb.Authentication @one_minute 60_000 - @one_day 86_400_000 + @one_hour 3_600_000 plug :verify_user_id @@ -18,24 +18,49 @@ defmodule RecognizerWeb.Accounts.UserTwoFactorController do ] when action in [:resend] + # when action in [:resend, :create, :new] + plug Hammer.Plug, [ - rate_limit: {"user:two_factor_daily", @one_day, 6}, + rate_limit: {"user:two_factor_hour", @one_hour, 6}, by: {:session, :two_factor_user_id} ] when action in [:resend] + # when action in [:resend, :create, :new] + @doc """ Prompt the user for a two factor code on login """ def new(conn, _params) do current_user_id = get_session(conn, :two_factor_user_id) current_user = Accounts.get_user!(current_user_id) - + current_time = System.system_time(:second) %{notification_preference: %{two_factor: two_factor_method}} = Accounts.load_notification_preferences(current_user) + conn = + if get_session(conn, :two_factor_issue_time) == nil do + conn + |> put_session(:two_factor_issue_time, current_time) + else + conn + end + + two_factor_sent = get_session(conn, :two_factor_sent) + + conn = + if two_factor_sent == false do + conn + |> maybe_send_two_factor_notification(current_user, two_factor_method) + + conn + |> put_session(:two_factor_sent, true) + |> put_session(:two_factor_issue_time, current_time) + else + conn + end + conn - |> maybe_send_two_factor_notification(current_user, two_factor_method) |> render("new.html", two_factor_method: two_factor_method) end @@ -45,42 +70,79 @@ defmodule RecognizerWeb.Accounts.UserTwoFactorController do def create(conn, %{"user" => %{"two_factor_code" => two_factor_code}}) do current_user_id = get_session(conn, :two_factor_user_id) current_user = Accounts.get_user!(current_user_id) + current_time = System.system_time(:second) + %{notification_preference: %{two_factor: two_factor_method}} = Accounts.load_notification_preferences(current_user) + + two_factor_issue_time = get_session(conn, :two_factor_issue_time) + + # 15 minutes + if current_time - two_factor_issue_time > 900 do + conn = put_session(conn, :two_factor_issue_time, current_time) + + conn + |> maybe_send_two_factor_notification(current_user, two_factor_method) - if Authentication.valid_token?(two_factor_code, current_user) do - Authentication.log_in_user(conn, current_user) - else conn - |> put_flash(:error, "Invalid security code") + |> put_flash( + :error, + "Two-factor code has expired. A new code has been sent. Please check your email for the newest two-factor code and try again." + ) |> redirect(to: Routes.user_two_factor_path(conn, :new)) + else + if Authentication.valid_token?(two_factor_method, two_factor_code, two_factor_issue_time, current_user) do + conn + |> put_session(:two_factor_sent, false) + |> put_session(:two_factor_issue_time, nil) + |> Authentication.log_in_user(current_user) + else + conn + |> put_flash(:error, "Invalid security code") + |> redirect(to: Routes.user_two_factor_path(conn, :new)) + end end end + @spec resend(Plug.Conn.t(), any()) :: Plug.Conn.t() def resend(conn, _params) do - current_user_id = get_session(conn, :two_factor_user_id) - current_user = Accounts.get_user!(current_user_id) + current_time = System.system_time(:second) conn - |> send_two_factor_notification(current_user) - |> put_flash(:info, "Two factor code has been resent") + |> put_session(:two_factor_sent, false) + |> put_session(:two_factor_issue_time, current_time) + |> put_flash(:info, "Two factor code has been reset") |> redirect(to: Routes.user_two_factor_path(conn, :new)) end - defp send_two_factor_notification(conn, %{notification_preference: %{two_factor: method}} = current_user) do - send_two_factor_notification(conn, current_user, method) + defp deliver_and_update_token(conn, current_user, method, issue_time) do + token = Authentication.generate_token(method, issue_time, current_user) + + conn + |> tap(fn _conn -> Account.deliver_two_factor_token(current_user, token, method) end) end defp send_two_factor_notification(conn, current_user, method) do - token = Authentication.generate_token(current_user) - Account.deliver_two_factor_token(current_user, token, method) - put_session(conn, :two_factor_sent, true) - end + if method == :app || method == "app" do + conn + else + two_factor_issue_time = get_session(conn, :two_factor_issue_time) + current_time = System.system_time(:second) + + conn = + if two_factor_issue_time == nil do + conn + |> deliver_and_update_token(current_user, method, current_time) + else + conn + |> deliver_and_update_token(current_user, method, current_time) + end - defp maybe_send_two_factor_notification(conn, current_user, method) do - if get_session(conn, :two_factor_sent) == false and :app != method do - send_two_factor_notification(conn, current_user, method) + conn end + end + defp maybe_send_two_factor_notification(conn, current_user, method) do conn + |> send_two_factor_notification(current_user, method) end defp verify_user_id(conn, _params) do diff --git a/lib/recognizer_web/router.ex b/lib/recognizer_web/router.ex index 6071e142..0aa358ed 100644 --- a/lib/recognizer_web/router.ex +++ b/lib/recognizer_web/router.ex @@ -109,7 +109,7 @@ defmodule RecognizerWeb.Router do get "/two-factor", UserTwoFactorController, :new post "/two-factor", UserTwoFactorController, :create - post "/two-factor/resend", UserTwoFactorController, :resend + get "/two-factor/resend", UserTwoFactorController, :resend get "/recovery-code", UserRecoveryCodeController, :new post "/recovery-code", UserRecoveryCodeController, :create @@ -140,5 +140,6 @@ defmodule RecognizerWeb.Router do get "/settings/two-factor/review", UserSettingsController, :review get "/settings/two-factor", UserSettingsController, :two_factor_init post "/settings/two-factor", UserSettingsController, :two_factor_confirm + get "/setting/two-factor/resend", UserSettingsController, :resend end end diff --git a/lib/recognizer_web/templates/accounts/user_settings/confirm_two_factor_external.html.eex b/lib/recognizer_web/templates/accounts/user_settings/confirm_two_factor_external.html.eex index 4053e4e3..13a2b88c 100644 --- a/lib/recognizer_web/templates/accounts/user_settings/confirm_two_factor_external.html.eex +++ b/lib/recognizer_web/templates/accounts/user_settings/confirm_two_factor_external.html.eex @@ -19,12 +19,10 @@ Cancel -
If you did not receive a verification code, or it has expired:
- -+ <%= link "Resend Two Factor Code", to: Routes.user_settings_path(@conn, :resend) %> +
<% end %> diff --git a/lib/recognizer_web/templates/accounts/user_settings/edit.html.eex b/lib/recognizer_web/templates/accounts/user_settings/edit.html.eex index 240c4b00..0f59553d 100644 --- a/lib/recognizer_web/templates/accounts/user_settings/edit.html.eex +++ b/lib/recognizer_web/templates/accounts/user_settings/edit.html.eex @@ -170,15 +170,10 @@ <%= hidden_input f, :two_factor_enabled, value: "0" %> <%= case two_factor_method(@two_factor_changeset) do %> - <% :text -> %> + <% :email -> %>Two factor authentication is enabled for your account. You will - receive a text message every time you try to log in. -
- <% :voice -> %> -- Two factor authentication is enabled for your account. You will - receive a phone call every time you try to log in. + receive an Email every time you try to log in.
<% _ -> %>@@ -214,7 +209,7 @@ Authenticator App <% end %> - <%= if @allow_phone_methods do %> + <%= if @allow_phone_methods && false do %> <%= label class: "label" do %> <%= radio_button n, :two_factor, "text" %> @@ -225,12 +220,18 @@ <%= radio_button n, :two_factor, "voice" %> Phone Call <% end %> + <% end %> + <%= label class: "label" do %> + <%= radio_button n, :two_factor, "email" %> + Email + <% end %> + <% end %> - <%= if @allow_phone_methods do %> + <%= if @allow_phone_methods && false do %>
Message and data rates may apply for text message and phone call methods. diff --git a/lib/recognizer_web/templates/accounts/user_settings/recovery_codes.html.eex b/lib/recognizer_web/templates/accounts/user_settings/recovery_codes.html.eex index 7440906c..257dc605 100644 --- a/lib/recognizer_web/templates/accounts/user_settings/recovery_codes.html.eex +++ b/lib/recognizer_web/templates/accounts/user_settings/recovery_codes.html.eex @@ -3,7 +3,7 @@
- Recovery codes are used to access your account if you have lost access to your device.
+ Copy your recovery codes and save them in safe location before continuing two-factor authentication setup.
@@ -15,13 +15,33 @@
<%= @recovery_block %>
+ <%
+ base_recovery_block =
+ @recovery_block
+ |> String.split("\n")
+ |> Enum.with_index(1)
+ |> Enum.map(fn {line, index} ->
+ "Code #{String.pad_leading(Integer.to_string(index), 2, "0")}: #{line}"
+ end)
+ |> Enum.join("\n")
+
+ formatted_recovery_block =
+ base_recovery_block <>
+ "\n\nOne time use recovery codes.\nUse one code per recovery attempt."
+ %>
+
+ <%= formatted_recovery_block %>
- We have sent a text message to your registered phone number. -
- <% :voice -> %> -- You will receive an automated phone call with your security code. + An email with your security code was sent to your registered email address.
<% _ -> %>@@ -31,8 +27,8 @@ autofocus: true, class: "is-medium #{input_classes(f, :two_factor_code)}", inputmode: "numeric", - pattern: "[0-9]*", - required: true + required: true, + pattern: "[0-9]*" %>
- <%= link "Resend Two Factor Code", to: Routes.user_two_factor_path(@conn, :resend), method: :create %> -
- <% end %> +<%= if @two_factor_method != :app do %> ++ <%= link "Resend Two Factor Code", to: Routes.user_two_factor_path(@conn, :resend) %> +
+<% end %> + + diff --git a/mix.exs b/mix.exs index f6bcd9b6..befded90 100644 --- a/mix.exs +++ b/mix.exs @@ -33,7 +33,7 @@ defmodule Recognizer.MixProject do defp deps do [ {:argon2_elixir, "~> 2.0"}, - {:bottle, github: "system76/bottle", ref: "bd102de771893e0135e566926e8f571578b5177b"}, + {:bottle, github: "system76/bottle", ref: "1a49e7bc7d8f7bf556c5780b70e9eb60a06a8ca7"}, {:cors_plug, "~> 2.0"}, {:cowboy, "~> 2.8", override: true}, {:cowlib, "~> 2.9.1", override: true}, @@ -65,7 +65,7 @@ defmodule Recognizer.MixProject do {:phoenix_view, "~> 2.0.3"}, {:phoenix, "~> 1.7.1"}, {:plug_cowboy, "~> 2.4"}, - {:pot, "~> 1.0"}, + {:pot, "~> 1.0.2"}, {:saxy, "~> 1.1"}, {:spandex, "~> 3.0.3"}, {:spandex_datadog, "~> 1.1.0"}, diff --git a/mix.lock b/mix.lock index 8da3d1e5..9dd9104c 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "amqp": {:hex, :amqp, "3.3.0", "056d9f4bac96c3ab5a904b321e70e78b91ba594766a1fc2f32afd9c016d9f43b", [:mix], [{:amqp_client, "~> 3.9", [hex: :amqp_client, repo: "hexpm", optional: false]}], "hexpm", "8d3ae139d2646c630d674a1b8d68c7f85134f9e8b2a1c3dd5621616994b10a8b"}, "amqp_client": {:hex, :amqp_client, "3.12.10", "dcc0d5d0037fa2b486c6eb8b52695503765b96f919e38ca864a7b300b829742d", [:make, :rebar3], [{:credentials_obfuscation, "3.4.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:rabbit_common, "3.12.10", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm", "16a23959899a82d9c2534ed1dcf1fa281d3b660fb7f78426b880647f0a53731f"}, "argon2_elixir": {:hex, :argon2_elixir, "2.4.1", "edb27bdd326bc738f3e4614eddc2f73507be6fedc9533c6bcc6f15bbac9c85cc", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "0e21f52a373739d00bdfd5fe6da2f04eea623cb4f66899f7526dd9db03903d9f"}, - "bottle": {:git, "https://github.com/system76/bottle.git", "bd102de771893e0135e566926e8f571578b5177b", [ref: "bd102de771893e0135e566926e8f571578b5177b"]}, + "bottle": {:git, "https://github.com/system76/bottle.git", "1a49e7bc7d8f7bf556c5780b70e9eb60a06a8ca7", [ref: "1a49e7bc7d8f7bf556c5780b70e9eb60a06a8ca7"]}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, diff --git a/test/recognizer/accounts_test.exs b/test/recognizer/accounts_test.exs index 86ff3881..232d4c77 100644 --- a/test/recognizer/accounts_test.exs +++ b/test/recognizer/accounts_test.exs @@ -163,7 +163,7 @@ defmodule Recognizer.AccountsTest do user = :user |> build() - |> add_two_factor(:text) + |> add_two_factor(:email) |> add_organization_policy(two_factor_app_required: true) |> insert() diff --git a/test/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller_test.exs b/test/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller_test.exs index 310680e5..fc6d4176 100644 --- a/test/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller_test.exs +++ b/test/recognizer_web/controllers/accounts/api/user_settings_two_factor_controller_test.exs @@ -16,7 +16,7 @@ defmodule RecognizerWeb.Api.UserSettingsTwoFactorControllerTest do conn = Phoenix.ConnTest.build_conn() user = :user |> insert() - Recognizer.Accounts.generate_and_cache_new_two_factor_settings(user, "text") + Recognizer.Accounts.generate_and_cache_new_two_factor_settings(user, "email") log_in_user(conn, user) end @@ -65,9 +65,12 @@ defmodule RecognizerWeb.Api.UserSettingsTwoFactorControllerTest do test "confirms the two factor code and updates the user's settings", %{conn: conn, user: user} do %{recovery_codes: recovery_codes, two_factor_seed: seed} = - Recognizer.Accounts.generate_and_cache_new_two_factor_settings(user, "text") + Recognizer.Accounts.generate_and_cache_new_two_factor_settings(user, :email) - valid_code = RecognizerWeb.Authentication.generate_token(seed) + counter = System.system_time(:second) + conn = put_session(conn, :two_factor_issue_time, counter) + + valid_code = RecognizerWeb.Authentication.generate_token("email", counter, seed) conn = put(conn, "/api/settings/two-factor", %{"verification" => valid_code}) assert json_response(conn, 200) diff --git a/test/recognizer_web/controllers/accounts/user_settings_controller_test.exs b/test/recognizer_web/controllers/accounts/user_settings_controller_test.exs index e9884b68..3b5ea139 100644 --- a/test/recognizer_web/controllers/accounts/user_settings_controller_test.exs +++ b/test/recognizer_web/controllers/accounts/user_settings_controller_test.exs @@ -17,7 +17,7 @@ defmodule RecognizerWeb.Accounts.UserSettingsControllerTest do conn = get(conn, Routes.user_settings_path(conn, :edit)) response = html_response(conn, 200) assert response =~ "Update Profile" - assert response =~ "Text Message" + assert response =~ "Authenticator App" end test "redirects if user is not logged in" do @@ -124,20 +124,6 @@ defmodule RecognizerWeb.Accounts.UserSettingsControllerTest do assert response =~ "must not contain special characters" end - test "update two-factor redirects for text method without phone number", %{conn: conn, user: user} do - stub(HTTPoisonMock, :put, fn _, _, _ -> ok_bigcommerce_response() end) - Accounts.update_user(user, %{phone_number: nil}) - - conn = - put(conn, Routes.user_settings_path(conn, :update), %{ - "action" => "update_two_factor", - "user" => %{"notification_preference" => %{"two_factor" => "text"}} - }) - - assert redirected_to(conn) =~ "/settings" - assert Flash.get(conn.assigns.flash, :error) =~ "Phone number required" - end - test "update two-factor allows app setup without a phone number", %{conn: conn, user: user} do stub(HTTPoisonMock, :put, fn _, _, _ -> ok_bigcommerce_response() end) Accounts.update_user(user, %{phone_number: nil}) @@ -145,7 +131,7 @@ defmodule RecognizerWeb.Accounts.UserSettingsControllerTest do conn = put(conn, Routes.user_settings_path(conn, :edit), %{ "action" => "update_two_factor", - "user" => %{"notification_preference" => %{"two_factor" => "app"}} + "user" => %{"notification_preference" => %{"two_factor" => :app}} }) assert redirected_to(conn) =~ "/settings/two-factor/review" @@ -155,7 +141,7 @@ defmodule RecognizerWeb.Accounts.UserSettingsControllerTest do describe "GET /users/settings/two-factor/review (backup codes)" do test "gets review page after 2fa setup", %{conn: conn, user: user} do - Accounts.generate_and_cache_new_two_factor_settings(user, "app") + Accounts.generate_and_cache_new_two_factor_settings(user, :app) conn = get(conn, Routes.user_settings_path(conn, :review)) assert html_response(conn, 200) =~ "copy your recovery codes" end @@ -169,7 +155,7 @@ defmodule RecognizerWeb.Accounts.UserSettingsControllerTest do describe "GET /users/settings/two-factor (init)" do test "/two-factor page is rendered for with settings for app, doesn't rate limit", %{conn: conn, user: user} do - Accounts.generate_and_cache_new_two_factor_settings(user, "app") + Accounts.generate_and_cache_new_two_factor_settings(user, :app) conn = get(conn, Routes.user_settings_path(conn, :two_factor_init)) assert html_response(conn, 200) =~ "Configure App" result2 = get(conn, Routes.user_settings_path(conn, :two_factor_init)) @@ -179,8 +165,8 @@ defmodule RecognizerWeb.Accounts.UserSettingsControllerTest do refute Flash.get(result3.assigns.flash, :error) end - test "/two-factor loads for text, limits retries", %{conn: conn, user: user} do - Accounts.generate_and_cache_new_two_factor_settings(user, "text") + test "/two-factor loads for email, limits retries", %{conn: conn, user: user} do + Accounts.generate_and_cache_new_two_factor_settings(user, :email) result1 = get(conn, Routes.user_settings_path(conn, :two_factor_init)) assert html_response(result1, 200) =~ "Enter the provided 6-digit code" result2 = get(conn, Routes.user_settings_path(conn, :two_factor_init)) @@ -191,11 +177,66 @@ defmodule RecognizerWeb.Accounts.UserSettingsControllerTest do end end - describe "POST /users/settings/two-factor (confirm)" do + describe "POST /users/settings/two-factor App (confirm)" do + test "confirm saves and clears cache", %{conn: conn, user: user} do + settings = Accounts.generate_and_cache_new_two_factor_settings(user, :app) + + token = Authentication.generate_token(:app, 0, settings) + params = %{"two_factor_code" => token} + + conn = post(conn, Routes.user_settings_path(conn, :two_factor_confirm), params) + + assert redirected_to(conn) =~ "/settings" + assert Flash.get(conn.assigns.flash, :info) =~ "Two factor code verified" + + %{recovery_codes: recovery_codes} = + User + |> Repo.get(user.id) + |> Repo.preload(:recovery_codes) + + refute Enum.empty?(recovery_codes) + + assert {:ok, nil} = Accounts.get_new_two_factor_settings(user) + end + + test "confirm redirects without cached settings", %{conn: conn, user: user} do + settings = Accounts.generate_and_cache_new_two_factor_settings(user, :app) + token = Authentication.generate_token(:app, 0, settings) + Accounts.clear_two_factor_settings(user) + params = %{"two_factor_code" => token} + conn = post(conn, Routes.user_settings_path(conn, :two_factor_confirm), params) + assert redirected_to(conn) =~ "/two-factor" + assert Flash.get(conn.assigns.flash, :error) =~ "Two factor code is invalid" + end + end + + describe "POST /users/settings/two-factor Email (confirm)" do + test "confirm take timeout genereated token with expire_time", %{conn: conn, user: user} do + settings = Accounts.generate_and_cache_new_two_factor_settings(user, :email) + + expired_time = System.system_time(:second) - 901 + conn = put_session(conn, :two_factor_issue_time, expired_time) + conn = put_session(conn, :two_factor_sent, true) + + token = Authentication.generate_token(:email, expired_time, settings) + params = %{"two_factor_code" => token} + + conn = post(conn, Routes.user_settings_path(conn, :two_factor_confirm), params) + + assert redirected_to(conn) =~ "/two-factor" + + assert Flash.get(conn.assigns.flash, :error) =~ + "Two-factor code has expired. A new code has been sent. Please check your email for the newest two-factor code and try again." + end + test "confirm saves and clears cache", %{conn: conn, user: user} do - settings = Accounts.generate_and_cache_new_two_factor_settings(user, "app") + settings = Accounts.generate_and_cache_new_two_factor_settings(user, :email) + + current_time = System.system_time(:second) + conn = put_session(conn, :two_factor_issue_time, current_time) + conn = put_session(conn, :two_factor_sent, true) - token = Authentication.generate_token(settings) + token = Authentication.generate_token(:email, current_time, settings) params = %{"two_factor_code" => token} conn = post(conn, Routes.user_settings_path(conn, :two_factor_confirm), params) @@ -214,8 +255,12 @@ defmodule RecognizerWeb.Accounts.UserSettingsControllerTest do end test "confirm redirects without cached settings", %{conn: conn, user: user} do - settings = Accounts.generate_and_cache_new_two_factor_settings(user, "app") - token = Authentication.generate_token(settings) + current_time = System.system_time(:second) + conn = put_session(conn, :two_factor_issue_time, current_time) + conn = put_session(conn, :two_factor_sent, true) + + settings = Accounts.generate_and_cache_new_two_factor_settings(user, :email) + token = Authentication.generate_token(:app, 0, settings) Accounts.clear_two_factor_settings(user) params = %{"two_factor_code" => token} conn = post(conn, Routes.user_settings_path(conn, :two_factor_confirm), params) diff --git a/test/recognizer_web/controllers/accounts/user_two_factor_controller_test.exs b/test/recognizer_web/controllers/accounts/user_two_factor_controller_test.exs index f91ad7d1..3f61443b 100644 --- a/test/recognizer_web/controllers/accounts/user_two_factor_controller_test.exs +++ b/test/recognizer_web/controllers/accounts/user_two_factor_controller_test.exs @@ -3,6 +3,7 @@ defmodule RecognizerWeb.Accounts.UserTwoFactorControllerTest do import Recognizer.AccountFactory + alias Recognizer.Accounts alias RecognizerWeb.Authentication setup %{conn: conn} do @@ -36,12 +37,19 @@ defmodule RecognizerWeb.Accounts.UserTwoFactorControllerTest do describe "POST /two-factor" do test "redirects to user settings for successful security codes", %{conn: conn, user: user} do - token = Authentication.generate_token(user) + current_time = System.system_time(:second) + conn = put_session(conn, :two_factor_issue_time, current_time) + conn = put_session(conn, :two_factor_user_id, user.id) + %{notification_preference: %{two_factor: two_factor_method}} = Accounts.load_notification_preferences(user) + + token = Authentication.generate_token(two_factor_method, current_time, user) conn = post(conn, Routes.user_two_factor_path(conn, :create), %{"user" => %{"two_factor_code" => token}}) assert redirected_to(conn) == "/settings" end test "emits error message with invalid security code", %{conn: conn} do + conn = put_session(conn, :two_factor_issue_time, System.system_time(:second) - 60) + conn = post(conn, Routes.user_two_factor_path(conn, :create), %{ "user" => %{"two_factor_code" => "INVALID"} @@ -52,17 +60,17 @@ defmodule RecognizerWeb.Accounts.UserTwoFactorControllerTest do end end - describe "POST /two-factor/resend" do + describe "GET /two-factor/resend" do test "redirects with flash message", %{conn: conn} do - conn = post(conn, Routes.user_two_factor_path(conn, :resend)) + conn = get(conn, Routes.user_two_factor_path(conn, :resend)) assert redirected_to(conn) == "/two-factor" - assert Flash.get(conn.assigns.flash, :info) =~ "resent" + assert Flash.get(conn.assigns.flash, :info) =~ "Two factor code has been reset" end test "rate limited", %{conn: conn} do - Enum.each(0..20, fn _ -> post(conn, Routes.user_two_factor_path(conn, :resend)) end) - conn = post(conn, Routes.user_two_factor_path(conn, :resend)) + Enum.each(0..20, fn _ -> get(conn, Routes.user_two_factor_path(conn, :resend)) end) + conn = get(conn, Routes.user_two_factor_path(conn, :resend)) assert response(conn, 429) end @@ -70,13 +78,61 @@ defmodule RecognizerWeb.Accounts.UserTwoFactorControllerTest do test "rate limited only by user id", %{conn: conn} do Enum.each(0..20, fn _ -> c = new_user_conn() - post(c, Routes.user_two_factor_path(c, :resend)) + get(c, Routes.user_two_factor_path(c, :resend)) end) - conn = post(conn, Routes.user_two_factor_path(conn, :resend)) + conn = get(conn, Routes.user_two_factor_path(conn, :resend)) assert redirected_to(conn) == "/two-factor" - assert Flash.get(conn.assigns.flash, :info) =~ "resent" + assert Flash.get(conn.assigns.flash, :info) =~ "Two factor code has been reset" + end + end + + describe "POST /users/two-factor Email (confirm)" do + test "confirm take timeout genereated token with expire_time", %{conn: conn, user: user} do + settings = Accounts.generate_and_cache_new_two_factor_settings(user, :email) + + expired_time = System.system_time(:second) - 901 + conn = put_session(conn, :two_factor_issue_time, expired_time) + conn = put_session(conn, :two_factor_sent, true) + + token = Authentication.generate_token(:email, expired_time, settings) + params = %{"user" => %{"two_factor_code" => token}} + + conn = post(conn, Routes.user_two_factor_path(conn, :create), params) + + assert redirected_to(conn) =~ "/two-factor" + + assert Flash.get(conn.assigns.flash, :error) =~ + "Two-factor code has expired. A new code has been sent. Please check your email for the newest two-factor code and try again." + end + + test "confirm saves and clears cache", %{conn: conn, user: user} do + %{notification_preference: %{two_factor: two_factor_method}} = Accounts.load_notification_preferences(user) + + current_time = System.system_time(:second) + conn = put_session(conn, :two_factor_issue_time, current_time) + conn = put_session(conn, :two_factor_sent, true) + + token = Authentication.generate_token(two_factor_method, current_time, user) + params = %{"user" => %{"two_factor_code" => token}} + + conn = post(conn, Routes.user_two_factor_path(conn, :create), params) + assert redirected_to(conn) =~ "/settings" + end + + test "confirm redirects without cached settings", %{conn: conn, user: user} do + current_time = System.system_time(:second) + conn = put_session(conn, :two_factor_issue_time, current_time) + conn = put_session(conn, :two_factor_sent, true) + + settings = Accounts.generate_and_cache_new_two_factor_settings(user, :email) + token = Authentication.generate_token(:app, 0, settings) + Accounts.clear_two_factor_settings(user) + params = %{"user" => %{"two_factor_code" => token}} + conn = post(conn, Routes.user_two_factor_path(conn, :create), params) + assert redirected_to(conn) =~ "/two-factor" + assert Flash.get(conn.assigns.flash, :error) =~ "Invalid security code" end end end diff --git a/test/support/factories/account_factory.ex b/test/support/factories/account_factory.ex index 4989ecd6..84119895 100644 --- a/test/support/factories/account_factory.ex +++ b/test/support/factories/account_factory.ex @@ -13,7 +13,7 @@ defmodule Recognizer.AccountFactory do def notification_preference_factory do %Accounts.NotificationPreference{ - two_factor: :text + two_factor: :email } end