diff --git a/pfSense-pkg-API/files/etc/inc/api/framework/APIAuth.inc b/pfSense-pkg-API/files/etc/inc/api/framework/APIAuth.inc
index 44885ee97..a4c5f7bbd 100644
--- a/pfSense-pkg-API/files/etc/inc/api/framework/APIAuth.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/framework/APIAuth.inc
@@ -118,8 +118,9 @@ class APIAuth {
$resp = false;
}
- # Set our class is_authenticated attribute to our authentication resp and return the resp
+ # Set our object is_authenticated attribute to our authentication resp, log auth, and return the resp
$this->is_authenticated = $resp;
+ $this->__log_authentication($this->is_authenticated);
return $this->is_authenticated;
}
@@ -174,4 +175,27 @@ class APIAuth {
return true;
}
}
+
+ # Logs the authentication attempt if login protection is enabled for the API
+ private function __log_authentication($authenticated) {
+ # Variables
+ $username = ($this->username) ?: "unknown";
+ $ip_address = $this->ip_address;
+
+ # Log authentication attempts if enabled
+ if (isset($this->api_config["enable_login_protection"])) {
+ # Log successful authentication
+ if ($authenticated) {
+ log_auth(
+ gettext("Successful login for user '{$username}' from: {$ip_address} (Local Database)")
+ );
+ }
+ # Log failed authentication
+ else {
+ log_auth(
+ gettext("webConfigurator authentication error for user '{$username}' from: {$ip_address}")
+ );
+ }
+ }
+ }
}
diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APISystemAPIUpdate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APISystemAPIUpdate.inc
index 921d36bf8..1078343da 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APISystemAPIUpdate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APISystemAPIUpdate.inc
@@ -67,6 +67,15 @@ class APISystemAPIUpdate extends APIModel {
}
}
+ private function __validate_enable_login_protection() {
+ # Check for our optional 'enable_login_protection' payload value
+ if ($this->initial_data['enable_login_protection'] === true) {
+ $this->validated_data["enable_login_protection"] = "";
+ } elseif ($this->initial_data['enable_login_protection'] === false) {
+ unset($this->validated_data["enable_login_protection"]);
+ }
+ }
+
private function __validate_hasync() {
# Check for our optional 'hasync' payload value
if ($this->initial_data['hasync'] === true) {
@@ -259,6 +268,7 @@ class APISystemAPIUpdate extends APIModel {
$this->__validate_keybytes();
$this->__validate_custom_headers();
$this->__validate_access_list();
+ $this->__validate_enable_login_protection();
$this->__validate_hasync();
$this->__validate_hasync_hosts();
$this->__validate_hasync_username();
diff --git a/pfSense-pkg-API/files/usr/local/share/pfSense-pkg-API/info.xml b/pfSense-pkg-API/files/usr/local/share/pfSense-pkg-API/info.xml
index 1128b031e..6ed49468e 100644
--- a/pfSense-pkg-API/files/usr/local/share/pfSense-pkg-API/info.xml
+++ b/pfSense-pkg-API/files/usr/local/share/pfSense-pkg-API/info.xml
@@ -20,6 +20,7 @@
sha256
16
+
\ No newline at end of file
diff --git a/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml b/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml
index 92a52db2e..db26fc3c6 100644
--- a/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml
+++ b/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml
@@ -12073,6 +12073,9 @@ paths:
will be disabled and you will no longer be able to API requests
until the API enabled via the webConfigurator.
type: boolean
+ enable_login_protection:
+ description: Enable or disable Login Protection for API authentication requests.
+ type: boolean
hasync:
description: Enable or disable HA sync for API configurations.
type: boolean
diff --git a/pfSense-pkg-API/files/usr/local/www/api/index.php b/pfSense-pkg-API/files/usr/local/www/api/index.php
index 1499fbcb9..e4607071a 100644
--- a/pfSense-pkg-API/files/usr/local/www/api/index.php
+++ b/pfSense-pkg-API/files/usr/local/www/api/index.php
@@ -158,6 +158,13 @@
$pkg_config["access_list"] = "";
}
+ # Validate login protection settings
+ if (!empty($_POST["enable_login_protection"])) {
+ $pkg_config["enable_login_protection"] = "";
+ } else {
+ unset($pkg_config["enable_login_protection"]);
+ }
+
# Validate HA Sync settings if enabled
if (!empty($_POST["hasync"])) {
$pkg_config["hasync"] = "";
@@ -319,6 +326,16 @@
### Populate the ADVANCED section of the UI form
$advanced_section->addClass("hide-api-advanced-settings");
+$advanced_section->addInput(new Form_Checkbox(
+ 'enable_login_protection',
+ 'Login Protection',
+ 'Enable API Login Protection',
+ isset($pkg_config["enable_login_protection"])
+))->setHelp(
+ "Include API authentication in pfSense's Login Protection feature. When enabled, all API authentication requests
+ will be logged and monitored for brute force attacks. Login Protection can be configured in
+ System > Advanced"
+);
$advanced_section->addInput(new Form_Checkbox(
'hasync',
'Sync API Configuration',
diff --git a/tests/e2e_test_framework/__init__.py b/tests/e2e_test_framework/__init__.py
index 66f38c48b..eae0ca7e9 100644
--- a/tests/e2e_test_framework/__init__.py
+++ b/tests/e2e_test_framework/__init__.py
@@ -54,6 +54,7 @@ def __init__(self):
self.get()
self.put()
self.delete()
+ self.custom_tests()
sys.exit(self.exit_code)
except KeyboardInterrupt:
sys.exit(1)
@@ -106,6 +107,9 @@ def delete(self):
if test_params.get("status", 200) == 200:
time.sleep(self.time_delay)
+ def custom_tests(self):
+ """Allows child classes to specify custom tests. This is inteded to be overwritten by the child class."""
+
# PRE/POST REQUEST METHODS. These are intended to be overwritten by a child class.
def pre_post(self):
"""
diff --git a/tests/test_login_protection.py b/tests/test_login_protection.py
new file mode 100644
index 000000000..dee48882d
--- /dev/null
+++ b/tests/test_login_protection.py
@@ -0,0 +1,35 @@
+"""Script used to test the login protection API integration"""
+import sys
+import requests
+import e2e_test_framework
+
+
+class APIE2ETestLoginProtection(e2e_test_framework.APIE2ETest):
+ """Class used to test the login protection API integration."""
+ def custom_tests(self):
+ self.test_login_protection()
+
+ def test_login_protection(self):
+ """Custom test method to ensure login protection locks out too many failed auth attempts."""
+ # Variables
+ test_params = {"name": "Ensure login protection blocks many failed auth attempts"}
+
+ # Fail authentication many times to initiate the lockout
+ for _ in range(0, 5):
+ try:
+ requests.get(
+ self.format_url("/api/v1/system/api"),
+ auth=("bad_username", "bad_password"),
+ verify=False,
+ timeout=(5, 5)
+ )
+ except (requests.exceptions.ReadTimeout, requests.exceptions.ConnectTimeout):
+ print(self.__format_msg__("GET", test_params, "Response is valid", mode="ok"))
+ sys.exit(0)
+
+ # Test fails if we were not locked out
+ print(self.__format_msg__("GET", test_params, "Expected lockout for too many failed requests"))
+ sys.exit(1)
+
+
+APIE2ETestLoginProtection()