Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion pfSense-pkg-API/files/etc/inc/api/framework/APIAuth.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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}")
);
}
}
}
}
10 changes: 10 additions & 0 deletions pfSense-pkg-API/files/etc/inc/api/models/APISystemAPIUpdate.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<keyhash>sha256</keyhash>
<keybytes>16</keybytes>
<keys></keys>
<enable_login_protection></enable_login_protection>
</conf>
</package>
</pfsensepkgs>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions pfSense-pkg-API/files/usr/local/www/api/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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"] = "";
Expand Down Expand Up @@ -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
<a href='/system_advanced_admin.php'>System > Advanced</a>"
);
$advanced_section->addInput(new Form_Checkbox(
'hasync',
'Sync API Configuration',
Expand Down
4 changes: 4 additions & 0 deletions tests/e2e_test_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
"""
Expand Down
35 changes: 35 additions & 0 deletions tests/test_login_protection.py
Original file line number Diff line number Diff line change
@@ -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()