towerops/lib/towerops_web/controllers/user_sudo_controller.ex
Graham McIntire 9ff9c4ddc8
fix: address UserSudoController critical issues
Critical fixes:
- Renamed controller actions to follow Phoenix conventions (verify→new, create→verify)
- Added sudo mode check in GET action using last_sudo_at timestamp
- Added TOTP enrollment check in GET action with redirect
- Changed default return path from /orgs to /users/settings
- Updated route paths from /users/sudo/verify to /users/sudo-verify (dash separator)
- Added success flash message "Identity verified."

Implementation details:
- new/2 (GET): Checks recently_verified_sudo?/1 helper that examines last_sudo_at
  instead of authenticated_at to distinguish sudo verification from regular login
- verify/2 (POST): Adds info flash and defaults to /users/settings
- recently_verified_sudo?/1: Private helper checking last_sudo_at within 10 minutes
- Updated all routes, templates, and tests to use new paths and action names

Tests:
- Added tests for already-in-sudo-mode redirect behavior
- Added test for TOTP enrollment requirement
- Updated all test assertions for new route paths and success messages
- All 15 controller tests passing
- Full test suite passing (4076 tests, 0 failures)
2026-02-01 14:45:05 -06:00

101 lines
3.2 KiB
Elixir

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, "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
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, "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, "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, "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