Skip to content
63 changes: 57 additions & 6 deletions lib/plausible/auth/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,33 @@ defmodule Plausible.Auth do

require Logger

if Mix.env() == :e2e_test do
@ip_rate_limit 100_000
@user_rate_limit 100_000
else
@ip_rate_limit 5
@user_rate_limit 5
case Mix.env() do
:e2e_test ->
@ip_rate_limit 100_000
@user_rate_limit 100_000
@activation_limit 100_000
@activation_ip_limit 100_000
@activation_request_limit 100_000
@totp_setup_limit 100_000
@totp_setup_ip_limit 100_000

env when env in [:test, :ce_test] ->
@ip_rate_limit 5
@user_rate_limit 5
@activation_limit 10
@totp_setup_limit 10
@activation_ip_limit 100_000
@totp_setup_ip_limit 100_000
@activation_request_limit 100_000

_ ->
@ip_rate_limit 5
@user_rate_limit 5
@activation_limit 10
@totp_setup_limit 10
@activation_ip_limit 2
@totp_setup_ip_limit 2
@activation_request_limit 5
end

@rate_limits %{
Expand All @@ -43,6 +64,36 @@ defmodule Plausible.Auth do
prefix: "password-change:user",
limit: 5,
interval: :timer.minutes(20)
},
activation_ip: %{
prefix: "activation:ip",
limit: @activation_ip_limit,
interval: :timer.minutes(1)
},
activation_user: %{
prefix: "activation:user",
limit: @activation_limit,
interval: :timer.minutes(5)
},
activation_request_ip: %{
prefix: "activation-request:ip",
limit: @activation_request_limit,
interval: :timer.minutes(1)
},
activation_request_user: %{
prefix: "activation-request:user",
limit: @activation_request_limit,
interval: :timer.minutes(10)
},
totp_setup_ip: %{
prefix: "totp-setup:ip",
limit: @totp_setup_ip_limit,
interval: :timer.minutes(1)
},
totp_setup_user: %{
prefix: "totp-setup:user",
limit: @totp_setup_limit,
interval: :timer.minutes(5)
}
}

Expand Down
51 changes: 42 additions & 9 deletions lib/plausible_web/controllers/auth_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ defmodule PlausibleWeb.AuthController do
def activate(conn, %{"code" => code}) do
user = conn.assigns[:current_user]

with :ok <- Auth.rate_limit(:activation_ip, conn),
:ok <- Auth.rate_limit(:activation_user, user) do
do_activate(conn, user, code)
else
{:error, {:rate_limit, _}} ->
render_error(
conn,
429,
"Too many activation attempts. Wait a few minutes before trying again."
)
end
end

defp do_activate(conn, user, code) do
has_any_invitations? = Plausible.Teams.Users.has_sites?(user, include_pending?: true)
has_any_memberships? = Plausible.Teams.Users.has_sites?(user, include_pending?: false)

Expand Down Expand Up @@ -167,11 +181,20 @@ defmodule PlausibleWeb.AuthController do

def request_activation_code(conn, _params) do
user = conn.assigns.current_user
Auth.EmailVerification.issue_code(user)

conn
|> put_flash(:success, "Activation code was sent to #{user.email}")
|> redirect(to: Routes.auth_path(conn, :activate_form))
with :ok <- Auth.rate_limit(:activation_request_ip, conn),
:ok <- Auth.rate_limit(:activation_request_user, user) do
Auth.EmailVerification.issue_code(user)

conn
|> put_flash(:success, "Activation code was sent to #{user.email}")
|> redirect(to: Routes.auth_path(conn, :activate_form))
else
{:error, {:rate_limit, _}} ->
conn
|> put_flash(:error, "Too many code requests. Please wait before requesting another.")
|> redirect(to: Routes.auth_path(conn, :activate_form))
end
end

def password_reset_request_form(conn, _) do
Expand Down Expand Up @@ -396,11 +419,21 @@ defmodule PlausibleWeb.AuthController do
end

def verify_2fa_setup(conn, %{"code" => code}) do
case Auth.TOTP.enable(conn.assigns.current_user, code) do
{:ok, _, %{recovery_codes: codes}} ->
conn
|> put_flash(:success, "Two-Factor Authentication is fully enabled")
|> render("generate_2fa_recovery_codes.html", recovery_codes: codes, from_setup: true)
user = conn.assigns.current_user

with :ok <- Auth.rate_limit(:totp_setup_ip, conn),
:ok <- Auth.rate_limit(:totp_setup_user, user),
{:ok, _, %{recovery_codes: codes}} <- Auth.TOTP.enable(user, code) do
conn
|> put_flash(:success, "Two-Factor Authentication is fully enabled")
|> render("generate_2fa_recovery_codes.html", recovery_codes: codes, from_setup: true)
else
{:error, {:rate_limit, _}} ->
render_error(
conn,
429,
"Too many attempts. Wait a minute before trying again."
)

{:error, :invalid_code} ->
conn
Expand Down
20 changes: 20 additions & 0 deletions test/plausible_web/controllers/auth_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,26 @@ defmodule PlausibleWeb.AuthControllerTest do

refute Repo.get_by(Auth.EmailActivationCode, user_id: user.id)
end

test "limits activation attempts to 10 per 5 minutes", %{conn: conn} do
conn = put_req_header(conn, "x-forwarded-for", "10.9.8.7")

response =
eventually(
fn ->
Enum.each(1..10, fn _ ->
post(conn, "/activate", %{code: "1111"})
end)

conn = post(conn, "/activate", %{code: "1111"})

{conn.status == 429, conn}
end,
500
)

assert html_response(response, 429) =~ "Too many activation attempts"
end
end

describe "GET /login_form" do
Expand Down
Loading