diff --git a/CHANGELOG.md b/CHANGELOG.md index 1451c7ed2..20f74be07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - 'Last updated X seconds ago' info to 'current visitors' tooltips - Add support for more Bamboo adapters, i.e. `Bamboo.MailgunAdapter`, `Bamboo.MandrillAdapter`, `Bamboo.SendGridAdapter` plausible/analytics#2649 - Ability to change domain for existing site (requires numeric IDs data migration, instructions will be provided separately) UI + API (`PUT /api/v1/sites`) +- Add `LOG_FAILED_LOGIN_ATTEMPTS` environment variable to enable failed login attempts logs plausible/analytics#2936 - Add `MAILER_NAME` environment variable support plausible/analytics#2937 - Add `MAILGUN_BASE_URI` support for `Bamboo.MailgunAdapter` plausible/analytics#2935 diff --git a/config/runtime.exs b/config/runtime.exs index 315a9040a..b38da09ec 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -217,6 +217,11 @@ disable_cron = |> get_var_from_path_or_env("DISABLE_CRON", "false") |> String.to_existing_atom() +log_failed_login_attempts = + config_dir + |> get_var_from_path_or_env("LOG_FAILED_LOGIN_ATTEMPTS", "false") + |> String.to_existing_atom() + config :plausible, environment: env, mailer_email: mailer_email, @@ -224,7 +229,8 @@ config :plausible, site_limit: site_limit, site_limit_exempt: site_limit_exempt, is_selfhost: is_selfhost, - custom_script_name: custom_script_name + custom_script_name: custom_script_name, + log_failed_login_attempts: log_failed_login_attempts config :plausible, :selfhost, enable_email_verification: enable_email_verification, diff --git a/lib/plausible_web/controllers/auth_controller.ex b/lib/plausible_web/controllers/auth_controller.ex index 2def542c3..97bc4211f 100644 --- a/lib/plausible_web/controllers/auth_controller.ex +++ b/lib/plausible_web/controllers/auth_controller.ex @@ -371,12 +371,15 @@ defmodule PlausibleWeb.AuthController do |> redirect(to: login_dest) else :wrong_password -> + maybe_log_failed_login_attempts("wrong password for #{email}") + render(conn, "login_form.html", error: "Wrong email or password. Please try again.", layout: {PlausibleWeb.LayoutView, "focus.html"} ) :user_not_found -> + maybe_log_failed_login_attempts("user not found for #{email}") Plausible.Auth.Password.dummy_calculation() render(conn, "login_form.html", @@ -385,6 +388,8 @@ defmodule PlausibleWeb.AuthController do ) {:rate_limit, _} -> + maybe_log_failed_login_attempts("too many logging attempts for #{email}") + render_error( conn, 429, @@ -393,6 +398,12 @@ defmodule PlausibleWeb.AuthController do end end + defp maybe_log_failed_login_attempts(message) do + if Application.get_env(:plausible, :log_failed_login_attempts) do + Logger.warning("[login] #{message}") + end + end + @login_interval 60_000 @login_limit 5 defp check_ip_rate_limit(conn) do diff --git a/test/plausible/config_test.exs b/test/plausible/config_test.exs index 62ffe8702..3d1741182 100644 --- a/test/plausible/config_test.exs +++ b/test/plausible/config_test.exs @@ -149,6 +149,23 @@ defmodule Plausible.ConfigTest do end end + describe "log_failed_login_attempts" do + test "can be true" do + env = {"LOG_FAILED_LOGIN_ATTEMPTS", "true"} + assert get_in(runtime_config(env), [:plausible, :log_failed_login_attempts]) == true + end + + test "can be false" do + env = {"LOG_FAILED_LOGIN_ATTEMPTS", "false"} + assert get_in(runtime_config(env), [:plausible, :log_failed_login_attempts]) == false + end + + test "is false by default" do + env = {"LOG_FAILED_LOGIN_ATTEMPTS", nil} + assert get_in(runtime_config(env), [:plausible, :log_failed_login_attempts]) == false + end + end + defp runtime_config(env) do put_system_env_undo(env) Config.Reader.read!("config/runtime.exs", env: :prod) diff --git a/test/plausible_web/controllers/auth_controller/logs_test.exs b/test/plausible_web/controllers/auth_controller/logs_test.exs new file mode 100644 index 000000000..e2b1bc293 --- /dev/null +++ b/test/plausible_web/controllers/auth_controller/logs_test.exs @@ -0,0 +1,51 @@ +defmodule PlausibleWeb.AuthController.LogsTest do + use PlausibleWeb.ConnCase + import ExUnit.CaptureLog + + describe "POST /login" do + setup do + patch_env(:log_failed_login_attempts, true) + end + + test "logs on missing user", %{conn: conn} do + logs = + capture_log(fn -> + post(conn, "/login", email: "user@example.com", password: "password") + end) + + assert logs =~ "[warning] [login] user not found for user@example.com" + end + + test "logs on wrong password", %{conn: conn} do + user = insert(:user, password: "password") + + logs = + capture_log(fn -> + post(conn, "/login", email: user.email, password: "wrong") + end) + + assert logs =~ "[warning] [login] wrong password for #{user.email}" + end + + test "logs on too many login attempts", %{conn: conn} do + user = insert(:user, password: "password") + + capture_log(fn -> + for _ <- 1..5 do + build_conn() + |> put_req_header("x-forwarded-for", "1.1.1.1") + |> post("/login", email: user.email, password: "wrong") + end + end) + + logs = + capture_log(fn -> + conn + |> put_req_header("x-forwarded-for", "1.1.1.1") + |> post("/login", email: user.email, password: "wrong") + end) + + assert logs =~ "[warning] [login] too many logging attempts for #{user.email}" + end + end +end