defmodule ToweropsWeb.UserSudoController do @moduledoc """ Controller for sudo mode verification. Handles re-authentication for sensitive operations by requiring TOTP verification (no recovery codes allowed). """ use ToweropsWeb, :controller import ToweropsWeb.UserAuth, only: [require_authenticated_user: 2] alias Towerops.Accounts plug :require_authenticated_user def new(conn, _params) do user = conn.assigns.current_scope.user cond do # Already in sudo mode (verified within last 10 minutes), redirect to destination recently_verified_sudo?(user) -> return_to = get_session(conn, :user_return_to) || ~p"/users/settings" conn |> delete_session(:user_return_to) |> redirect(to: return_to) # No TOTP devices, must enroll first !Accounts.totp_enabled?(user) -> conn |> put_flash(:error, t("Two-factor authentication is required for this action.")) |> redirect(to: ~p"/account/totp-enrollment") # Show verification form true -> form = Phoenix.Component.to_form(%{}, as: "user") render(conn, :verify, form: form) end end # Check if user verified sudo mode within the last 10 minutes # Uses last_sudo_at timestamp instead of authenticated_at to distinguish # between regular login and explicit sudo verification defp recently_verified_sudo?(%{last_sudo_at: nil}), do: false defp recently_verified_sudo?(%{last_sudo_at: last_sudo_at}) do ten_minutes_ago = DateTime.add(DateTime.utc_now(), -10, :minute) DateTime.after?(last_sudo_at, ten_minutes_ago) end def verify(conn, %{"user" => %{"totp_code" => totp_code}}) do user = conn.assigns.current_scope.user case Accounts.verify_totp_only(user, totp_code) do {:ok, _verified_user} -> # Grant sudo mode (updates last_sudo_at in database) case Accounts.grant_sudo_mode(user) do {:ok, _updated_user} -> # Redirect to return_to path or default return_to = get_session(conn, :user_return_to) || ~p"/users/settings" conn |> put_flash(:info, t("Identity verified.")) |> delete_session(:user_return_to) |> redirect(to: return_to) {:error, _changeset} -> # Shouldn't happen, but handle gracefully form = Phoenix.Component.to_form(%{"totp_code" => totp_code}, as: "user") conn |> put_flash(:error, t("Failed to grant sudo mode. Please try again.")) |> render(:verify, form: form) end {:error, :recovery_code_not_allowed} -> form = Phoenix.Component.to_form(%{"totp_code" => totp_code}, as: "user") conn |> put_flash( :error, "Recovery codes are not allowed for sudo mode verification. Please use your authenticator app." ) |> render(:verify, form: form) {:error, :invalid_code} -> form = Phoenix.Component.to_form(%{"totp_code" => totp_code}, as: "user") conn |> put_flash(:error, t("Invalid authentication code. Please try again.")) |> render(:verify, form: form) end end def verify(conn, _params) do # Handle missing totp_code parameter form = Phoenix.Component.to_form(%{}, as: "user") render(conn, :verify, form: form) end end