Tag: PHP

DIY Simple Whitelist API Tutorial Part 3

Now that we have a list of whitelisted IPs we can use, it makes sense for us to create another end-point in order to return a list of all the whitelisted IPs. Any servers under our control can then send requests to this end-point to receive a list of the current IP whitelist and add them to iptables or other ACLs. This end-point will be created in the file whitelist-get.php.

The first thing I realised is that I want successful authentication to occur before returning a list of whitelisted IPs. Instead of copy/pasting the existing authentication code into the new file and duplicating code, it is better that we add the authentication code to its own file (auth.php) and then requiring auth.php in both whitelist-url.php and whitelist-get.php.

I also decided that the functionality for whitelist-url.php and whitelist-get.php are quite distinct with no real overlap, so it makes sense to have two user types. The first type would be end users who only need to whitelist their IPs and the second type would be servers which only need to grab a current list of whitelisted IPs. As such I have separated their authenticated file directory structure which allows slightly different permissions.

So here is the new auth.php file:

<?php

function login($type) {

	function auth_fail() {
		header('HTTP/1.1 403 Forbidden');
		echo "You are forbidden!\n";
		exit;
	}

	if ( !isset($_GET['user']) || !isset($_GET['auth']) ) 
	{ 
		auth_fail();
	}

	$user = $_GET['user'];
	$pass = $_GET['auth'];
	if ( $type == "user" )
    {
    	$auth_file = "data/auth/user/{$user}";
    }
    elseif ( $type == "infra" ) 
    {
    	$auth_file = "data/auth/infra/{$user}";
	} 
	else 
	{
		auth_fail();
	} 

	if (!file_exists("$auth_file")) {
		auth_fail();
	}

	$userauth = file_get_contents($auth_file);

	if ( $userauth != $pass ) {

		auth_fail();
	}

	return $user;
}

You can see above that I have placed all the authentication code into the login function which also requires a type to be passed to it. The type is either an end user who needs their IP whitelisted (user) or a server that needs a list of IPs to whitelist (infra). The type is then used to define where the authentication file resides.

I also used this chance to cleanup the duplicate code used to return the forbidden response in multiple locations. The 403 forbidden response was added as its own function auth_fail() and called as needed.

With the authentication now sorted out the rest is very simple in our whitelist-get.php file:

<?php 

# Include authentication code for login function
require 'auth.php';
$user = login("infra");

$path    = 'data/ip-data/';
$files = array_diff(scandir($path), array('.', '..'));

echo "---Whitelisted IPs---\n";

foreach($files as $file){
    echo file_get_contents("{$path}{$file}");;
}

echo "----End Whitelist----\n";

After including auth.php and setting the user type to infra all the code does is grab the list of saved user IP whitelist files from ‘data/ip-data/’ and echo their contents. The only additional thing I added was to add a ‘header’ and ‘footer’ to the IP list in order to allow some basic validation by the servers pulling this data.

And just for good measure I have included the updated version of whitelist-url.php after the authentication code was refactored:

<?php 

# Include authentication code for login function
require 'auth.php';
$user = login("user");

$whitelist_file = "data/ip-data/{$user}";
$current_ip = $_SERVER['REMOTE_ADDR'];
$whitelist_max = 5;

if (file_exists($whitelist_file)) {
	$user_whitelist = file($whitelist_file, FILE_IGNORE_NEW_LINES);
}
else {
	$user_whitelist = [];
}

if (in_array($current_ip, $user_whitelist)) {
    echo "$current_ip already whitelisted\n";
    exit;
}

elseif (count($user_whitelist) < $whitelist_max ) {
	array_unshift($user_whitelist , $current_ip);
}
elseif (count($user_whitelist) == $whitelist_max) {
	array_unshift($user_whitelist , $current_ip);
	array_pop($user_whitelist);
}

$i = 0;
while ($i < count($user_whitelist))
{
	if ($i == 0) {
		file_put_contents($whitelist_file, $user_whitelist[$i] . "\n");
	}
	else {
		file_put_contents($whitelist_file, $user_whitelist[$i] . "\n", FILE_APPEND);   
	}
    $i++;
}

echo "$current_ip added to whitelist\n";

?>

The web api for our whitelisting functionality is now complete. End users can whitelist their IPs as needed and our infrastructure servers can requests the list of IPs to whitelist. In the 4th and final part of this tutorial I will show how our infrastructure servers can pull this list of IPs and allow access using iptables and ipset.

DIY Simple Whitelist API Tutorial Part 2

Ok, so we have authenticated the user in part 1, we can now start the process of adding the users IP to their whitelist.

if (file_exists($whitelist_file)) {
	$user_whitelist = file($whitelist_file, FILE_IGNORE_NEW_LINES);
}
else {
	$user_whitelist = [];
}

First we check to see if the user already has a corresponding file with previously whitelisted IPs. If they do then we simply load the existing IPs into an array, otherwise we create a new empty array.

if (in_array($current_ip, $user_whitelist)) {
	echo "IP Already whitelisted";
	print_r($user_whitelist);
	exit;
}
elseif (count($user_whitelist) < $whitelist_max ) {
	array_unshift($user_whitelist , $current_ip);
}
elseif (count($user_whitelist) == $whitelist_max) {
	array_unshift($user_whitelist , $current_ip);
	array_pop($user_whitelist);
}

Above we are create the logic for how IPs are added to the whitelist. If the IP has already been whitelisted then we can simply stop here.

As I mentioned previously, I want to make the whitelisting process as painless as possible for the end user. Because of this I have decided that the last 5 user IPs (this is defined earlier using $whitelist_max) should be whitelisted. This will help to reduce the need to whitelist every single time they swap between common wifi networks, or if they want multiple devices to be whitelisted at the same time but they are on different networks.

The final bit of logic handles the whitelisted IP list when 5 IPs are already whitelisted and a new IP is added. The new IP is added to the beginning of the array and then the final array element is removed.

$i = 0;
while ($i < count($user_whitelist))
{
	if ($i == 0) {
		file_put_contents($whitelist_file, $user_whitelist[$i] . "\n");
	}
	else {
		file_put_contents($whitelist_file, $user_whitelist[$i] . "\n", FILE_APPEND);   
	}
    $i++;
}

print_r($user_whitelist);

All that’s left to do is save the new IP list to the users whitelisted IP file. We iterate through the array, on the first loop it writes over the file and the subsequent loops append to the end of the file. Finally the new $user_whitelist is printed so the user can see their current IP whitelist.

And here is the completed PHP file (whitelist-url.php) that authenticates requests and stores successfully whitelisted IPs:

<?php

if ( !isset($_GET['user']) || !isset($_GET['auth']) ) 
{ 
	header('HTTP/1.0 403 Forbidden');
	echo 'You are forbidden!';
	exit;
}

$user = $_GET['user'];
$pass = $_GET['auth'];
$auth_file = "data/auth/{$user}";
$whitelist_file = "data/ip-data/{$user}";
$current_ip = $_SERVER['REMOTE_ADDR'];
$whitelist_max = 5;

if (!file_exists($auth_file)) {
	header('HTTP/1.0 403 Forbidden');
	echo 'You are forbidden!';
	exit;
}

$userauth = file_get_contents($auth_file);

if ( $userauth != $pass ) {
	header('HTTP/1.0 403 Forbidden');
	echo 'You are forbidden!';
	exit;
}

if (file_exists($whitelist_file)) {
	$user_whitelist = file($whitelist_file, FILE_IGNORE_NEW_LINES);
}
else {
	$user_whitelist = [];
}

if (in_array($current_ip, $user_whitelist)) {
	echo "IP Already whitelisted";
	print_r($user_whitelist);
	exit;
}
elseif (count($user_whitelist) < $whitelist_max ) {
	array_unshift($user_whitelist , $current_ip);
}
elseif (count($user_whitelist) == $whitelist_max) {
	array_unshift($user_whitelist , $current_ip);
	array_pop($user_whitelist);
}

$i = 0;
while ($i < count($user_whitelist))
{
	if ($i == 0) {
		file_put_contents($whitelist_file, $user_whitelist[$i] . "\n");
	}
	else {
		file_put_contents($whitelist_file, $user_whitelist[$i] . "\n", FILE_APPEND);   
	}
    $i++;
}

print_r($user_whitelist);

Our API is now authenticating user requests and storing a list of whitelisted IPs for each user in a file. In part 3 of the tutorial I will create a new endpoint that only returns a list of IPs whitelisted for each user. This can then be used by any server/service we like to pull these trusted IPs and allow them access.

DIY Simple Whitelist API Tutorial Part 1

A small side project I am working on requires some very simple IP whitelisting in order to restrict access. The side project isn’t particularly sensitive, but placing the restriction allows me to more easily set it and forget it while also being a better net citizen (think custom DNS resolvers). Only a small group of personal contacts will be using the whitelisting API, but they will have dynamic IPs and I want to make the process of whitelisting as painless as possible. While this ‘whitelist API’ is far from best practice or Enterprise ready, for my purposes I believe it is sufficiently secure and adds minimal complexity for the end users.

With the idea of making this API as simple for myself create as it is for people to use, I decided to stick with PHP and avoid using a database. I also wanted users to be able to whitelist themselves simply by visiting a bookmark in their web browser of choice. Eg:

https://exampledomain.com/whitelist-url.php?user=XYZ&auth=123

User Authentication

So let’s get started. It makes sense to begin with the PHP authorization code, as nothing else should happen until the users have successfully validated. Note that If I were writing a proper web application, I would not pass data using query strings. However in this case I wanted to keep it nice and simple for the end users and I ensure that a valid SSL certificate is installed on the website so that the username and passwords are not transmitted in plain text.

if ( !isset($_GET['user']) || !isset($_GET['auth']) ) 
{ 
	header('HTTP/1.0 403 Forbidden');
	echo 'You are forbidden!';
	exit;
}

This is a basic check to make sure that the request has set a user and auth value in the query string. If they have not submitted these required authentication details then the request will receive a 403 response.

$user = $_GET['user'];
$pass = $_GET['auth'];
$auth_file = "data/auth/{$user}";
$whitelist_file = "data/ip-data/{$user}";
$current_ip = $_SERVER['REMOTE_ADDR'];
$whitelist_max = 5;

Now that we know the required values have been submitted we can set some variables. As I mentioned earlier, I wanted to avoid using a database, and I also did not want to hard code the authorization details in PHP either. So to make it quick and nasty I opted to store the user authorization details in files. The filename corresponds to the username and the file content is the password. A similar scheme has been used to save the whitelisted IPs. Each user has their own file and the file is a list of their whitelisted IPs.

So to breakdown the above code, the first two lines are defining the user submitted username and password data. The third and forth lines define the authorization file and whitelist file that should exist for a valid user. The fifth line defines the IP of the user making the request and the sixth line sets the maximum number of IPs that can be whitelisted for a user at any one time.

if (!file_exists($auth_file)) {
	header('HTTP/1.0 403 Forbidden');
	echo 'You are forbidden!';
	exit;
}

$userauth = file_get_contents($auth_file);

If the $auth_file does not exist then the username is invalid and the request fails authentication. Once the username is known to be valid then you can get the file contents for their password.

if ( $userauth != $pass ) {
	header('HTTP/1.0 403 Forbidden');
	echo 'You are forbidden!';
	exit;
}

This checks that the file contents matches the password submitted by the user.

Now this actually ended up being much wordier than I was expecting, so I will end part 1 here. In part 2 of this tutorial I will show how I am saving the whitelisted IPs.

Copyright © 2024 iGeckoDev

Theme by Anders NorenUp ↑