feat: update UserAuth to redirect to sudo verify page
This commit is contained in:
parent
9ff9c4ddc8
commit
f0d6ae42d0
3 changed files with 110 additions and 76 deletions
|
|
@ -273,16 +273,30 @@ defmodule ToweropsWeb.UserAuth do
|
|||
|
||||
@doc """
|
||||
Plug for routes that require sudo mode.
|
||||
|
||||
Checks if the user's sudo mode is active (within 10 minutes).
|
||||
If TOTP is enabled, redirects to TOTP-only verification page.
|
||||
If TOTP is not enabled, redirects to enrollment.
|
||||
"""
|
||||
def require_sudo_mode(conn, _opts) do
|
||||
if Accounts.sudo_mode?(conn.assigns.current_scope.user, -10) do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> put_flash(:error, "You must re-authenticate to access this page.")
|
||||
|> maybe_store_return_to()
|
||||
|> redirect(to: ~p"/users/log-in")
|
||||
|> halt()
|
||||
user = conn.assigns.current_scope.user
|
||||
|
||||
case {Accounts.sudo_mode?(user, -10), Accounts.totp_enabled?(user)} do
|
||||
{true, _} ->
|
||||
conn
|
||||
|
||||
{false, true} ->
|
||||
conn
|
||||
|> put_flash(:error, "Please verify your identity to continue.")
|
||||
|> maybe_store_return_to()
|
||||
|> redirect(to: ~p"/users/sudo-verify")
|
||||
|> halt()
|
||||
|
||||
{false, false} ->
|
||||
conn
|
||||
|> put_flash(:error, "Two-factor authentication is required for this action.")
|
||||
|> redirect(to: ~p"/account/totp-enrollment")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -540,15 +554,28 @@ defmodule ToweropsWeb.UserAuth do
|
|||
def on_mount(:require_sudo_mode, _params, _session, socket) do
|
||||
user = socket.assigns.current_scope && socket.assigns.current_scope.user
|
||||
|
||||
if user && Accounts.sudo_mode?(user, -10) do
|
||||
{:cont, socket}
|
||||
else
|
||||
socket =
|
||||
socket
|
||||
|> LiveView.put_flash(:error, "You must re-authenticate to access this page.")
|
||||
|> LiveView.redirect(to: ~p"/users/log-in")
|
||||
case {user && Accounts.sudo_mode?(user, -10), user && Accounts.totp_enabled?(user)} do
|
||||
{true, _} ->
|
||||
{:cont, socket}
|
||||
|
||||
{:halt, socket}
|
||||
{false, true} ->
|
||||
socket =
|
||||
socket
|
||||
|> LiveView.put_flash(:error, "Please verify your identity to continue.")
|
||||
|> LiveView.redirect(to: ~p"/users/sudo-verify")
|
||||
|
||||
{:halt, socket}
|
||||
|
||||
{false, false} ->
|
||||
socket =
|
||||
socket
|
||||
|> LiveView.put_flash(:error, "Two-factor authentication is required for this action.")
|
||||
|> LiveView.redirect(to: ~p"/account/totp-enrollment")
|
||||
|
||||
{:halt, socket}
|
||||
|
||||
_ ->
|
||||
{:halt, LiveView.redirect(socket, to: ~p"/users/log-in")}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ defmodule Towerops.Repo.Migrations.DropUserCredentials do
|
|||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
drop table(:user_credentials)
|
||||
drop_if_exists table(:user_credentials)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -188,7 +188,40 @@ defmodule ToweropsWeb.UserAuthTest do
|
|||
end
|
||||
|
||||
describe "require_sudo_mode/2" do
|
||||
test "allows users that have authenticated in the last 10 minutes", %{conn: conn, user: user} do
|
||||
test "redirects to sudo verify when sudo mode expired and TOTP enabled", %{conn: conn} do
|
||||
user = user_fixture()
|
||||
|
||||
# Create TOTP device
|
||||
{:ok, _device, _secret} =
|
||||
Accounts.create_totp_device(user.id, "Test Device", NimbleTOTP.secret())
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> fetch_flash()
|
||||
|> assign(:current_scope, Scope.for_user(user))
|
||||
|> UserAuth.require_sudo_mode([])
|
||||
|
||||
assert redirected_to(conn) == ~p"/users/sudo-verify"
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "verify your identity"
|
||||
end
|
||||
|
||||
test "redirects to TOTP enrollment when sudo mode expired and no TOTP", %{conn: conn} do
|
||||
user = user_fixture(enable_totp: false)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> fetch_flash()
|
||||
|> assign(:current_scope, Scope.for_user(user))
|
||||
|> UserAuth.require_sudo_mode([])
|
||||
|
||||
assert redirected_to(conn) == ~p"/account/totp-enrollment"
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "Two-factor authentication"
|
||||
end
|
||||
|
||||
test "allows access when sudo mode is active", %{conn: conn} do
|
||||
user = user_fixture()
|
||||
{:ok, user} = Accounts.grant_sudo_mode(user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> fetch_flash()
|
||||
|
|
@ -196,37 +229,14 @@ defmodule ToweropsWeb.UserAuthTest do
|
|||
|> UserAuth.require_sudo_mode([])
|
||||
|
||||
refute conn.halted
|
||||
refute conn.status
|
||||
end
|
||||
|
||||
test "redirects when authentication is too old", %{conn: conn, user: user} do
|
||||
eleven_minutes_ago = DateTime.add(DateTime.utc_now(), -11, :minute)
|
||||
test "stores return path on GET requests when redirecting", %{conn: conn} do
|
||||
user = user_fixture()
|
||||
|
||||
# Update the user in the database with old authenticated_at
|
||||
user =
|
||||
user
|
||||
|> Ecto.Changeset.change(authenticated_at: eleven_minutes_ago)
|
||||
|> Towerops.Repo.update!()
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> fetch_flash()
|
||||
|> assign(:current_scope, Scope.for_user(user))
|
||||
|> UserAuth.require_sudo_mode([])
|
||||
|
||||
assert redirected_to(conn) == ~p"/users/log-in"
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
|
||||
"You must re-authenticate to access this page."
|
||||
end
|
||||
|
||||
test "stores return path on GET requests when redirecting", %{conn: conn, user: user} do
|
||||
eleven_minutes_ago = DateTime.add(DateTime.utc_now(), -11, :minute)
|
||||
|
||||
user =
|
||||
user
|
||||
|> Ecto.Changeset.change(authenticated_at: eleven_minutes_ago)
|
||||
|> Towerops.Repo.update!()
|
||||
# Create TOTP device
|
||||
{:ok, _device, _secret} =
|
||||
Accounts.create_totp_device(user.id, "Test Device", NimbleTOTP.secret())
|
||||
|
||||
conn =
|
||||
%{conn | path_info: ["admin", "users"], query_string: "", method: "GET"}
|
||||
|
|
@ -234,7 +244,7 @@ defmodule ToweropsWeb.UserAuthTest do
|
|||
|> assign(:current_scope, Scope.for_user(user))
|
||||
|> UserAuth.require_sudo_mode([])
|
||||
|
||||
assert redirected_to(conn) == ~p"/users/log-in"
|
||||
assert redirected_to(conn) == ~p"/users/sudo-verify"
|
||||
assert get_session(conn, :user_return_to) == "/admin/users"
|
||||
end
|
||||
end
|
||||
|
|
@ -866,45 +876,42 @@ defmodule ToweropsWeb.UserAuthTest do
|
|||
end
|
||||
|
||||
describe "on_mount/4 - :require_sudo_mode" do
|
||||
test "allows users in sudo mode", %{user: user} do
|
||||
test "redirects to sudo verify when sudo mode expired and TOTP enabled" do
|
||||
user = user_fixture()
|
||||
|
||||
# Create TOTP device
|
||||
{:ok, _device, _secret} =
|
||||
Accounts.create_totp_device(user.id, "Test Device", NimbleTOTP.secret())
|
||||
|
||||
socket = %Socket{assigns: %{__changed__: %{}, flash: %{}}}
|
||||
socket = Phoenix.Component.assign(socket, :current_scope, Scope.for_user(user))
|
||||
|
||||
socket =
|
||||
Phoenix.Component.assign_new(socket, :current_scope, fn -> Scope.for_user(user) end)
|
||||
assert {:halt, redirected_socket} =
|
||||
UserAuth.on_mount(:require_sudo_mode, %{}, %{}, socket)
|
||||
|
||||
{:cont, result_socket} =
|
||||
UserAuth.on_mount(
|
||||
:require_sudo_mode,
|
||||
%{},
|
||||
%{},
|
||||
socket
|
||||
)
|
||||
|
||||
refute result_socket.redirected
|
||||
assert Phoenix.Flash.get(redirected_socket.assigns.flash, :error) =~ "verify your identity"
|
||||
end
|
||||
|
||||
test "redirects users not in sudo mode" do
|
||||
eleven_minutes_ago = DateTime.add(DateTime.utc_now(), -11, :minute)
|
||||
|
||||
user =
|
||||
user_fixture()
|
||||
|> Ecto.Changeset.change(authenticated_at: eleven_minutes_ago)
|
||||
|> Towerops.Repo.update!()
|
||||
test "redirects to TOTP enrollment when sudo mode expired and no TOTP" do
|
||||
user = user_fixture(enable_totp: false)
|
||||
|
||||
socket = %Socket{assigns: %{__changed__: %{}, flash: %{}}}
|
||||
socket = Phoenix.Component.assign(socket, :current_scope, Scope.for_user(user))
|
||||
|
||||
socket =
|
||||
Phoenix.Component.assign_new(socket, :current_scope, fn -> Scope.for_user(user) end)
|
||||
assert {:halt, redirected_socket} =
|
||||
UserAuth.on_mount(:require_sudo_mode, %{}, %{}, socket)
|
||||
|
||||
{:halt, result_socket} =
|
||||
UserAuth.on_mount(
|
||||
:require_sudo_mode,
|
||||
%{},
|
||||
%{},
|
||||
socket
|
||||
)
|
||||
assert Phoenix.Flash.get(redirected_socket.assigns.flash, :error) =~ "Two-factor authentication"
|
||||
end
|
||||
|
||||
assert result_socket.redirected
|
||||
test "allows access when sudo mode is active" do
|
||||
user = user_fixture()
|
||||
{:ok, user} = Accounts.grant_sudo_mode(user)
|
||||
|
||||
socket = %Socket{assigns: %{__changed__: %{}, flash: %{}}}
|
||||
socket = Phoenix.Component.assign(socket, :current_scope, Scope.for_user(user))
|
||||
|
||||
assert {:cont, _socket} = UserAuth.on_mount(:require_sudo_mode, %{}, %{}, socket)
|
||||
end
|
||||
|
||||
test "redirects unauthenticated users" do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue