Skip to content

Commit 4db2f26

Browse files
Created API model for our access_token endpoint
1 parent be5e7a9 commit 4db2f26

7 files changed

Lines changed: 153 additions & 46 deletions

File tree

pfSense-pkg-API/files/etc/inc/api.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ function api_whitelist_check() {
352352
$if_conf = $config["interfaces"];
353353
$whitelist = array("any");
354354
$srv_ip = $_SERVER["SERVER_ADDR"];
355+
355356
// Check that we have a package configuration
356357
if (is_numeric($pkg_id)) {
357358
// Override defaults with user specified config
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<?php
2-
require_once("api/framework/APITools.inc");
32
require_once("api/framework/APIBaseModel.inc");
43
require_once("api/framework/APIResponse.inc");
5-
require_once("api/framework/APIAuth.inc");
64

75
class APIAccessToken extends APIBaseModel {
86
# Create our method constructor
97
public function __construct() {
108
parent::__construct();
11-
$this->methods = ["GET"];
12-
$this->validators = ["validateAuthMode"];
9+
$this->set_auth_mode = "local";
10+
$this->methods = ["POST"];
11+
$this->validators = [
12+
$this->validateAuthMode()
13+
];
1314
}
1415

1516
# Validate our API configurations auth mode (must be JWT)
@@ -18,13 +19,13 @@ class APIAccessToken extends APIBaseModel {
1819

1920
# Add error if our auth mode is invalid
2021
if ($api_config["authmode"] !== "jwt") {
21-
$this->errors[] = APIResponse\get(9);
22+
return APIResponse\get(9);
2223
}
2324
}
2425

2526
# Override action subclass to create a JWT and return it to the user
2627
public function action() {
27-
$jwt = api_create_jwt($this->client->username);
28+
$jwt = APITools\create_jwt($this->client->username);
2829
return APIResponse\get(0, ["token" => $jwt]);
2930
}
3031
}

pfSense-pkg-API/files/etc/inc/api/framework/APIAuth.inc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<?php
22
require_once("api/framework/APITools.inc");
33

4-
54
# Creates an object capable of verifying authentication and authorization based on the configuration
65
class APIAuth {
76
private $api_config;
@@ -14,9 +13,9 @@ class APIAuth {
1413
public $is_authorized;
1514

1615
# Create our method constructor
17-
public function __construct($req_privs){
16+
public function __construct($req_privs, $enforce_auth_mode=null){
1817
$this->api_config = APITools\get_api_config()[1];
19-
$this->auth_mode = $this->api_config["authmode"];
18+
$this->auth_mode = (is_null($enforce_auth_mode)) ? $this->api_config["authmode"] : $enforce_auth_mode;
2019
$this->request = APITools\get_request_data();
2120
$this->req_privs = $req_privs;
2221
$this->privs = [];

pfSense-pkg-API/files/etc/inc/api/framework/APIBaseModel.inc

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,21 @@ class APIBaseModel {
1212
public $errors;
1313
public $methods;
1414
public $requires_auth;
15+
public $set_auth_mode;
1516

1617
public function __construct() {
1718
$this->methods = ["GET", "POST"];
1819
$this->privileges = ["page-all"];
19-
$this->client = new APIAuth($this->privileges);
20+
$this->client = null;
2021
$this->requires_auth = true;
22+
$this->set_auth_mode = null;
2123
$this->validators = [];
2224
$this->initial_data = APITools\get_request_data();
2325
$this->validated_data = [];
2426
$this->errors = [];
2527

2628
}
2729

28-
private function check_authentication() {
29-
if ($this->requires_auth === true) {
30-
if (!$this->client->is_authenticated) {
31-
$this->errors[] = APIResponse\get(3);
32-
}
33-
}
34-
}
35-
36-
private function check_authorization() {
37-
if (!$this->client->is_authorized) {
38-
$this->errors[] = APIResponse\get(4);
39-
}
40-
}
41-
42-
private function check_method() {
43-
if (!in_array($_SERVER["REQUEST_METHOD"], $this->methods)) {
44-
$this->errors[] = APIResponse\get(2);
45-
}
46-
}
47-
4830
public function action() {
4931
# This function is intended to be overridden by an API model extended class
5032
# Any configuration writes, system configurations, etc should be added when overriding this base class
@@ -53,12 +35,15 @@ class APIBaseModel {
5335
}
5436

5537
public function validate() {
38+
$this->check_enable();
39+
$this->check_server_ip();
40+
$this->check_version();
5641
$this->check_method();
5742
if ($this->requires_auth) {
5843
$this->check_authentication();
5944
$this->check_authorization();
6045
}
61-
$this->errors = array_merge($this->errors, $this->validators);
46+
$this->errors = array_filter(array_merge($this->errors, $this->validators));
6247

6348

6449
if (count($this->errors) === 0) {
@@ -77,10 +62,78 @@ class APIBaseModel {
7762
}
7863

7964
public function listen() {
65+
header("Content-Type: application/json", true);
66+
header("Referer: no-referrer");
8067
$resp = $this->call();
8168
http_response_code($resp["code"]);
8269
echo json_encode($resp) . PHP_EOL;
8370
exit();
8471
}
8572

73+
private function check_authentication() {
74+
$this->client = new APIAuth($this->privileges, $this->set_auth_mode);
75+
if ($this->requires_auth === true) {
76+
if (!$this->client->is_authenticated) {
77+
$this->errors[] = APIResponse\get(3);
78+
}
79+
}
80+
}
81+
82+
private function check_authorization() {
83+
if (!$this->client->is_authorized) {
84+
$this->errors[] = APIResponse\get(4);
85+
}
86+
}
87+
88+
private function check_method() {
89+
if (!in_array($_SERVER["REQUEST_METHOD"], $this->methods)) {
90+
$this->errors[] = APIResponse\get(2);
91+
}
92+
}
93+
94+
# Check if the API is enabled before answering calls, if not, redirect to wc login
95+
private function check_enable() {
96+
$api_config = APITools\get_api_config()[1];
97+
if (!isset($api_config["enable"])) {
98+
header("Location: /");
99+
die();
100+
}
101+
}
102+
103+
# Check if server is running a supported version of pfSense
104+
private function check_version() {
105+
# Local variables
106+
$curr_ver = str_replace(".", "", explode("-", APITools\get_pfsense_version()["version"])[0]);
107+
$min_ver = 244;
108+
$curr_ver = is_numeric($curr_ver) ? intval($curr_ver) : 0;
109+
if ($curr_ver < $min_ver) {
110+
$this->errors[] = APIResponse\get(5);
111+
}
112+
}
113+
114+
# Check if server IP is allowed to answer API calls. Redirects to login if not
115+
private function check_server_ip() {
116+
$pkg_conf = APITools\get_api_config()[1];
117+
$allow_ifs = $pkg_conf["allowed_interfaces"];
118+
$whitelist = explode(",", $allow_ifs);
119+
120+
// Check if our server IP is in our whitelist
121+
foreach ($whitelist as $wif) {
122+
$if_info = get_interface_info($wif);
123+
// Check if our server IP is a valid if address, localhost, or any
124+
if ($_SERVER["SERVER_ADDR"] === $if_info["ipaddr"]) {
125+
return;
126+
} elseif ($_SERVER["SERVER_ADDR"] === $if_info["ipaddrv6"]) {
127+
return;
128+
} elseif (in_array($_SERVER["SERVER_ADDR"], ["::1", "127.0.0.1", "localhost"]) and $wif === "localhost") {
129+
return;
130+
}elseif ($wif === "any") {
131+
return;
132+
}
133+
}
134+
135+
# Return 444 response if we did not find a previous match
136+
$this->errors[] = APIResponse\get(6);
137+
}
138+
86139
}

pfSense-pkg-API/files/etc/inc/api/framework/APITools.inc

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<?php
22
namespace APITools;
3-
require_once("apiresp.inc");
43
require_once("php-jwt/src/JWT.php");
54
require_once("php-jwt/src/ExpiredException.php");
65
require_once("php-jwt/src/SignatureInvalidException.php");
@@ -29,6 +28,40 @@ function get_request_data() {
2928
return $data;
3029
}
3130

31+
# Check our local pfSense version
32+
function get_pfsense_version() {
33+
# VARIABLES
34+
$ver_path = "/etc/version"; // Assign the path to our version file
35+
$ver_patch_path = "/etc/version.patch"; // Assign the path to our version patch file
36+
$ver_bt_path = "/etc/version.buildtime"; // Assign the path to our version build time file
37+
$ver_lc_path = "/etc/version.lastcommit"; // Assign the path to our version last commit file
38+
$ver_data = array(); // Init an empty array for our version data
39+
40+
// Check that our files exist, if so read the files. Otherwise return error
41+
if (file_exists($ver_path)) {
42+
$ver_file = fopen($ver_path, "r"); // Open our file
43+
$ver = str_replace(PHP_EOL, "", fread($ver_file, filesize($ver_path))); // Save our version data
44+
$ver_data["version"] = $ver; // Save to array
45+
}
46+
if (file_exists($ver_patch_path)) {
47+
$ver_patch_file = fopen($ver_patch_path, "r"); // Open our file
48+
$ver_patch = str_replace(PHP_EOL, "", fread($ver_patch_file, filesize($ver_patch_path))); // Save patch
49+
$ver_data["patch"] = $ver_patch; // Save to array
50+
}
51+
if (file_exists($ver_bt_path)) {
52+
$ver_bt_file = fopen($ver_bt_path, "r"); // Open our file
53+
$ver_bt = str_replace(PHP_EOL, "", fread($ver_bt_file, filesize($ver_bt_path))); // Save bt data
54+
$ver_data["buildtime"] = $ver_bt; // Save to array
55+
}
56+
if (file_exists($ver_lc_path)) {
57+
$ver_lc_file = fopen($ver_lc_path, "r"); // Open our file
58+
$ver_lc = str_replace(PHP_EOL, "", fread($ver_lc_file, filesize($ver_lc_path))); // Save bt data
59+
$ver_data["lastcommit"] = $ver_lc; // Save to array
60+
}
61+
$ver_data["program"] = floatval(str_replace(".", "", explode("-", $ver)[0]).".".$ver_patch);
62+
return $ver_data;
63+
}
64+
3265
# Locates our API configuration from pfSense's XML configuration. Returns
3366
function get_api_config() {
3467
global $config;
@@ -58,8 +91,8 @@ function is_user_disabled($username) {
5891
# Creates JWT server key if one does not exist, or optionally allows rotation of the JWT server key
5992
function create_jwt_server_key($rotate=false) {
6093
global $config;
61-
$pkg_index = get_api_configuration()[0]; // Save our current API configs pkg index
62-
$api_config = get_api_configuration()[1]; // Save our current API config
94+
$pkg_index = get_api_config()[0]; // Save our current API configs pkg index
95+
$api_config = get_api_config()[1]; // Save our current API config
6396
# Create a new server key if one is not set
6497
if (empty($api_config["server_key"]) or $rotate === true) {
6598
$config["installedpackages"]["package"][$pkg_index]["conf"]["server_key"] = bin2hex(random_bytes(32));
@@ -84,6 +117,7 @@ function create_jwt($data) {
84117

85118
# Decodes a JWT using our store server key
86119
function decode_jwt($token) {
120+
$token = (is_string($token)) ? $token : "";
87121
$key = get_api_config()[1]["server_key"]; // Save our current server key
88122
try {
89123
$decoded = (array) JWT::decode($token, $key, array('HS256'));
@@ -123,4 +157,27 @@ function authenticate_token($cid, $ctoken) {
123157
}
124158
}
125159
return $authenticated;
160+
}
161+
162+
// Generate new API tokens for token auth mode
163+
function generate_token($username) {
164+
// Local variables
165+
global $config;
166+
$pkg_index = get_api_config()[0]; // Save our current API configs pkg index
167+
$api_config = get_api_config()[1]; // Save our current API config
168+
$key_hash_algo = $api_config["keyhash"]; // Pull our configured key hash algorithm
169+
$key_bit_strength = $api_config["keybytes"]; // Pull our configured key bit strength
170+
$key_user = bin2hex($username); // Save our user's dedicated API client-ID
171+
$key_new = bin2hex(random_bytes(intval($key_bit_strength))); // Generate a new key
172+
$key_hash = hash($key_hash_algo, $key_new); // Hash our key using our configured algos
173+
174+
// Loop through our existing keys to see
175+
$api_config["keys"] = !is_array($api_config["keys"]) ? array("key" => []) : $api_config["keys"];
176+
$api_config["keys"]["key"][] = array("client_id" => $key_user, "client_token" => $key_hash, "algo" => $key_hash_algo);
177+
178+
// Write our changes
179+
$config["installedpackages"]["package"][$pkg_index]["conf"] = $api_config; // Write change to config
180+
$change_note = " Generated API key"; // Add a change note
181+
write_config(sprintf(gettext($change_note))); // Apply our configuration change
182+
return $key_new;
126183
}

pfSense-pkg-API/files/usr/local/www/api/index.php

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
include_once("util.inc");
33
include_once("guiconfig.inc");
4-
require_once("api.inc");
4+
require_once("api/framework/APITools.inc");
55

66
// Variables
77
global $config; // Define our config globally
@@ -12,24 +12,23 @@
1212
display_top_tabs($tab_array, true); // Ensure the tabs are written to the top of page
1313
$user = $_SESSION["Username"]; // Save our username
1414
$sec_client_id = bin2hex($user); // Save our secure username client ID (token mode)
15-
$b64_client_id = base64_encode($user); // Save a base64 encoded version of our username
16-
$pkg_config = get_api_configuration(); // Save our entire pkg config
15+
$pkg_config = APITools\get_api_config(); // Save our entire pkg config
1716
$pkg_index = $pkg_config[0]; // Save our pkg configurations index value
1817
$api_config = $pkg_config[1]; // Save our api configuration from our pkg config
19-
$available_auth_modes = array("local" => "Local Database", "base64" => "Base64", "token" => "API Token", "jwt" => "JWT");
18+
$available_auth_modes = array("local" => "Local Database", "token" => "API Token", "jwt" => "JWT");
2019
$available_hash_algos = array("sha256" => "SHA256", "sha384" => "SHA384", "sha512" => "SHA512", "md5" => "MD5");
2120
$available_key_bytes = array("16", "32", "64"); // Save our allowed key bitlengths
2221
$non_config_ifs = array("any" => "Any", "localhost" => "Link-local"); // Save non-configurable interface ids
2322
$availabe_api_if = array_merge($non_config_ifs, get_configured_interface_with_descr(true)); // Combine if arrays
2423

2524
// UPON POST
2625
if ($_POST["gen"] === "1") {
27-
$new_key = api_generate_token($user);
26+
$new_key = APITools\generate_token($user);
2827
print_apply_result_box(0, "\nSave this API key somewhere safe, it cannot be viewed again: \n".$new_key);
2928
}
3029
// Rotate JWT server key requested
3130
if ($_POST["rotate_server_key"] === "1") {
32-
api_create_jwt_server_key(true);
31+
APITools\create_jwt_server_key(true);
3332
print_apply_result_box(0, "\nRotated JWT server key.\n");
3433
}
3534

@@ -220,7 +219,7 @@
220219
<?php
221220
if ($api_config["authmode"] === "token") {
222221
// Pull credentials if configured
223-
$user_creds = api_get_existing_tokens($user);
222+
$user_creds = APITools\get_existing_tokens($user);
224223
echo "<div class=\"panel panel-default\">".PHP_EOL;
225224
echo " <div class=\"panel-heading\">".PHP_EOL;
226225
echo " <h2 class=\"panel-title\">API Credentials</h2>".PHP_EOL;
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
<?php
22
# Copyright 2020 - Jared Hendrickson
33
# IMPORTS
4-
require_once("apicalls.inc");
4+
require_once("api/api_calls/APIAccessToken.inc");
55

66
# RUN API CALL
7-
$resp = api_access_token();
8-
http_response_code($resp["code"]);
9-
echo json_encode($resp) . PHP_EOL;
10-
exit();
7+
(new APIAccessToken())->listen();

0 commit comments

Comments
 (0)