101 lines
3.3 KiB
Elixir
101 lines
3.3 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 (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, "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
|