add LOG_FAILED_LOGIN_ATTEMPTS (#2936)

* add failed login logs

* put failed login attempt logs behind a config option

* add changelog entry

* add config test

* add auth_controller tests

* move tests to separate non-async test module

---------

Co-authored-by: Uku Taht <Uku.taht@gmail.com>
This commit is contained in:
ruslandoga 2023-05-25 15:37:10 +08:00 committed by GitHub
parent ce7401dd83
commit 40e95ffd3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 87 additions and 1 deletions

View File

@ -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

View File

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

View File

@ -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

View File

@ -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)

View File

@ -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