Create a Basic PHP API with Token Authentication

Camilo Herrera
winkhosting
Published in
12 min readJan 18, 2023
Photo by Ben Griffiths on Unsplash

Today we are going to create in the shortest time possible an API with PHP and authentication using tokens. Come, walk with me through the wonderful world of scripting, hack and slash.

Motivational tip: Remember that programming is more than engineering, it is creativity, it is skill. You will be better every day as long as you keep practicing and solving problems with your code.

Let’s start with the assumption that you are using Apache http server and php 8.x (obviously on a hosting account at Winkhosting.co), or that you already have an environment configured on another provider or your PC and everything works correctly (if you use another web server like nginx or caddy you will have to look for the options to overwrite paths).

To continue, now that you have an environment where you can work, our API will require the management of routes or “endpoints”, let’s start by creating the structure and route management functionality.

Routing, mod_rewrite and .htaccess

Start by creating a directory, for example, in my case I have created the /simple-api directory.

In the directory create a file named .htaccess and in it write/paste the following lines:

<Files "*.php">
Require ip 127.0.0.1
</Files>

<Files "index.php">
Require all granted
</Files>

RewriteEngine On
#Remove the comments to force https, you must have an SSL certificate.
#RewriteCond %{HTTPS} off
#RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]

#Enable HTTP Authorization Header if you use php-cgi
# $_SERVER["HTTP_AUTHORIZATION"]
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]

RewriteBase /simple-api/
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]

Let’s walk through the sections of the file to explain what each one does:

PHP script access restrictions

<Files "*.php">
Require ip 127.0.0.1
</Files>

<Files "index.php">
Require all granted
</Files>

I use these sections for security purposes, what they state is that the only .php file that can be requested or “accessed” is index.php which will be in charge of managing the routes to the different endpoints of our API (if you don’t know the concept of endpoint, you can check it here).

In summary, what we indicate to Apache http server is:

  • Prevent access to files with .php extension to all sources except 127.0.0.1, the localhost address in IPv4, if your server already uses IPv6 you would need to add the IP ::1. This means that only local requests can be made to scripts in the directory.
  • Authorizes queries and requests from any source (source IPs) to the index.php file.

Force traffic redirection to HTTPS (With an SSL certificate)

We continue with the rules to use https in your API, this would be recommended in production, in tests you can use http without any problems.

RewriteEngine On
#Remove the comments to force https, you must have an SSL certificate.
#RewriteCond %{HTTPS} off
#RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]

The first line of the file activates the route writing engine with the value “On”, the following lines are the ones in charge of modifying the traffic and passing from a URL that starts in http to its corresponding https. I will not go into details about redirection conditions, this topic has taken whole books, if you want to know more, check the Apache documentation here

Warning: Normally mod_rewrite is enabled by default on Apache servers, if you receive any type of error associated with the absence of this module you would have to request support from your hosting provider to enable it or activate it in your environment as appropriate.

HTTP Authorization Header y php-cgi

#Enable HTTP Authorization Header if you use php-cgi 
# $_SERVER["HTTP_AUTHORIZATION"]
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]

These lines are optional if you don’t use php as cgi (php-cgi), my recommendation from the bottom of my heart is to use them in any case unless it generates some kind of error. What these lines allow is the activation and saving of the authentication information in the PHP $_SERVER array, with the key “HTTP_AUTHORIZATION”.

If you plan to use basic authentication through headers, this would be a requirement for PHP to receive the necessary information.

Route writing, base route and index.php file

RewriteBase /simple-api/
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]

These final lines do the magic of sending any path inside /simple-api/ to the index.php file, the path information will be processed by this file and we will see it later.

Finally, our .htaccess file has been created and we are ready to continue down that joyful path of PHP programming.

Photo by Nathan Dumlao on Unsplash

Yes, that’s how you should feel, like that image every time you open your favorite code editor.

Route processing

Let’s start by creating an index.php file, inside the same directory /simple-api, then open the php tag as you were taught in school, no short tags, we are not vulgar programmers:

<?php

Pro tip: Don’t use the php closing tag unless absolutely necessary for the context of use of your script. Thank me later.

We are going to define some initial variables to manage our endpoints, the base path and the parameters received in the requests:

<?php

$BASE_URI = "/simple-api/";

$endpoints = array();
$requestData = array();

$parsedURI = parse_url($_SERVER["REQUEST_URI"]);
$endpointName = str_replace($BASE_URI, "", $parsedURI["path"]);

if (empty($endpointName)) {
$endpointName = "/";
}

BASE_URI matches the base path of our directory, every request will contain this text at the beginning, it is the same we use in the .htaccess file.

$endPoints will contain anonymous functions (closures) with the logic to process a specific request. We use closures to reduce the complexity and amount of code in this example, but it is recommended to use a class or separate the logic of each endpoint from the route handling.

$endpointName is the name of the endpoint that was queried in the request, it is determined from the “REQUEST_URI” in the PHP $_SERVER array, first the text string is parsed using parse_url() and the path and query string are extracted with get parameters if they apply to the request, then a text replacement is done to remove the string from the base path URI. This way we get only the path of the endpoint.

If this variable is empty, we will understand that they are trying to enter the API base path and we define a default endpoint name “/”, this will help us later to detect this scenario and respond accordingly.

Now let’s define the three endpoints logic:

  • Base path, i.e. when the user tries to query /simple-api/ directly without using a specific endpoint.
  • “sayhello”, this endpoint will receive a “name” parameter and return a response with the text “hello! <name>”
  • “404”, this will not be used directly, it will serve to respond to any other endpoint name that does not exist in the API with a default message.
// closures to define each endpoint logic, 
// I know, this can be improved with some OOP but this is a basic example,
// don't do this at home, well, or if you want to do it, don't feel judged.

/**
* prints a default message if the API base path is queried.
* @param array $requestData contains the parameters sent in the request, for this endpoint they are ignored.
* @return void
*/
$endpoints["/"] = function (array $requestData): void {

echo json_encode("Welcome to my API!");
};

/**
* prints a greeting message with the name specified in the $requestData["name"] item.
* if the variable is empty a default name is used.
* @param array $requestData this array must contain an item with key "name"
* if you want to display a custom name in the greeting.
* @return void
*/
$endpoints["sayhello"] = function (array $requestData): void {

if (!isset($requestData["name"])) {
$requestData["name"] = "Misterious masked individual";
}

echo json_encode("hello! " . $requestData["name"]);
};

/**
* prints a default message if the endpoint path does not exist.
* @param array $requestData contains the parameters sent in the request,
* for this endpoint they are ignored.
* @return void
*/
$endpoints["404"] = function ($requestData): void {

echo json_encode("Endpoint " . $requestData["endpointName"] . " not found.");
};

As you can see in the code section, we define and store the functions in charge of each path in the $endpoints array, this taking advantage of the “closures” (anonymous functions) functionality within PHP. These closures must adhere to a function signature convention, which receives a $requestData array and has no return (void). In the $requestData array the parameters received by the endpoint are sent to be processed.

Let’s continue. Now let’s move on to the encoding of the response of our API, in this case and by preference, that is, my preference my dear reader, I will impose the encoding in JSON and UTF-8 format like this:

//we define the encoding of the response, by default we will use json
header("Content-Type: application/json; charset=UTF-8");

This way all data output to the browser will be encoded with JSON and UTF-8 characters, I recommend avoiding different character sets and always keep the standard, it can quickly become hell if you decide to use other types.

Next we will see where the API magic happens:

if (isset($endpoints[$endpointName])) {
$endpoints[$endpointName]($requestData);
} else {
$endpoints["404"](array("endpointName" => $endpointName));
}

By storing the endpoints in an array we can take advantage of the PHP isset() function to determine if the path really exists or not using an if-else block which is very practical and reduces the code considerably. What this section does is to check if the endpoint exists and execute it, if it does not exist it will call by default the 404 endpoint that reports the operation you are trying to perform does not exist.

Up to this point, we have a functional API without authentication, now we are going to implement authentication through tokens.

Token authentication

In this section we will start by implementing the detection and parameter collection sent on the request to the endpoint.

We can implement it in the following way:

//collect incoming parameters
switch ($_SERVER['REQUEST_METHOD']) {
case 'POST':
$requestData = $_POST;
break;
case 'GET':
$requestData = $_GET;
break;
case 'DELETE':
$requestData = $_DELETE;
break;
case 'PUT':
case 'PATCH':
parse_str(file_get_contents('php://input'), $requestData);

//if the information received cannot be interpreted as an arrangement it is ignored.
if (!is_array($requestData)) {
$requestData = array();
}

break;
default:
//TODO: implement here any other type of request method that may arise.
break;
}

We detect the type of request from the “REQUEST_METHOD” entry in the PHP $_SERVER array. For POST, GET and DELETE requests it is quite simple, but for PUT and PATCH requests you need to parse the string ‘php://input’ and convert it to an array using the parse_str() function. If in the future you want to cover other types of requests you can do so by extending the scope of the switch-case.

This section of the code will cover the reception of the parameters, including the security token to be used, if the origin decides to include it in the items sent via GET, POST, PUT, etc. If authentication using headers is used, we will need the following code:

//If the token is sent in a header X-API-KEY
if (isset($_SERVER["HTTP_X_API_KEY"])) {
$requestData["token"] = $_SERVER["HTTP_X_API_KEY"];
}

Our API will use one of the most common headers for token authentication, it is the “X-API-KEY” header which is normally used by AWS and has become very popular. What our code does in this case is to determine if there is an X-API-KEY header and store the token received in the $requestData array with the “token” key.

At this point we are already capturing all the necessary information to authenticate the request and process the data that is sent.

To authorize or not the operations based on the token, we are going to implement a new closure and add it to the list of existing endpoints. This is just to save us the implementation in a different way given the scope of the example.

The new closure (endpoint) would look like this:

/**
* checks if the token is valid, and prevents the execution of
* the requested endpoint.
* @param array $requestData contains the parameters sent in the request,
* for this endpoint is required an item with
* key "token" that contains the token
* received to authenticate and authorize
* the request.
* @return void
*/
$endpoints["checktoken"] = function ($requestData): void {

//you can create secure tokens with this line, but that is a discussion for another post..
//$token = str_replace("=", "", base64_encode(random_bytes(160 / 8)));

//authorized tokens
$tokens = array(
"fa3b2c9c-a96d-48a8-82ad-0cb775dd3e5d" => ""
);

if (!isset($requestData["token"])) {
echo json_encode("No token was received to authorize the operation. Verify the information sent");
exit;
}

if (!isset($tokens[$requestData["token"]])) {
echo json_encode("The token " . $requestData["token"] . " does not exists or is not authorized to perform this operation.");
exit;
}
};

This function contains the list of authorized tokens in the $tokens array and will validate if the received token exists or not on it to determine if the requested operation is executed or not.

Finally, we will apply a change to the call of our endpoints so that every time an attempt is made to execute one, the validation endpoint of our token is executed first, like this:

//we define the response encoding, by default we will use json
header("Content-Type: application/json; charset=UTF-8");

if (isset($endpoints[$endpointName])) {
// Received token validation.
$endpoints["checktoken"]($requestData);
// Endpoint execution.
$endpoints[$endpointName]($requestData);
} else {
$endpoints["404"](array("endpointName" => $endpointName));
}

And that would be the end of our script, below is the full version of the final files:

.htaccess

<Files "*.php">
Require ip 127.0.0.1
</Files>

<Files "index.php">
Require all granted
</Files>

RewriteEngine On
#Activate to force https.
#RewriteCond %{HTTPS} off
#RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]

RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]

RewriteBase /simple-api/
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]

index.php

<?php

$BASE_URI = "/simple-api/";
$endpoints = array();
$requestData = array();

//collect incoming parameters
switch ($_SERVER['REQUEST_METHOD']) {
case 'POST':
$requestData = $_POST;
break;
case 'GET':
$requestData = $_GET;
break;
case 'DELETE':
$requestData = $_DELETE;
break;
case 'PUT':
case 'PATCH':
parse_str(file_get_contents('php://input'), $requestData);

//if the information received cannot be interpreted as an arrangement it is ignored.
if (!is_array($requestData)) {
$requestData = array();
}

break;
default:
//TODO: implement here any other type of request method that may arise.
break;
}

//If the token is sent in a header X-API-KEY
if (isset($_SERVER["HTTP_X_API_KEY"])) {
$requestData["token"] = $_SERVER["HTTP_X_API_KEY"];
}

$parsedURI = parse_url($_SERVER["REQUEST_URI"]);
$endpointName = str_replace($BASE_URI, "", $parsedURI["path"]);

if (empty($endpointName)) {
$endpointName = "/";
}

// closures to define each endpoint logic,
// I know, this can be improved with some OOP but this is a basic example,
// don't do this at home, well, or if you want to do it, don't feel judged.

/**
* prints a default message if the API base path is queried.
* @param array $requestData contains the parameters sent in the request, for this endpoint they are ignored.
* @return void
*/
$endpoints["/"] = function (array $requestData): void {

echo json_encode("Welcome to my API!");
};

/**
* prints a greeting message with the name specified in the $requestData["name"] item.
* if the variable is empty a default name is used.
* @param array $requestData this array must contain an item with key "name"
* if you want to display a custom name in the greeting.
* @return void
*/
$endpoints["sayhello"] = function (array $requestData): void {

if (!isset($requestData["name"])) {
$requestData["name"] = "Misterious masked individual";
}

echo json_encode("hello! " . $requestData["name"]);
};

/**
* prints a default message if the endpoint path does not exist.
* @param array $requestData contains the parameters sent in the request,
* for this endpoint they are ignored.
* @return void
*/
$endpoints["404"] = function ($requestData): void {

echo json_encode("Endpoint " . $requestData["endpointName"] . " not found.");
};

/**
* checks if the token is valid, and prevents the execution of
* the requested endpoint.
* @param array $requestData contains the parameters sent in the request,
* for this endpoint is required an item with
* key "token" that contains the token
* received to authenticate and authorize
* the request.
* @return void
*/
$endpoints["checktoken"] = function ($requestData): void {

//you can create secure tokens with this line, but that is a discussion for another post..
//but i am using UUIDv4 instead.
//$token = str_replace("=", "", base64_encode(random_bytes(160 / 8)));

//authorized tokens
$tokens = array(
"fa3b2c9c-a96d-48a8-82ad-0cb775dd3e5d" => ""
);

if (!isset($requestData["token"])) {
echo json_encode("No token was received to authorize the operation. Verify the information sent");

exit;
}

if (!isset($tokens[$requestData["token"]])) {
echo json_encode("The token " . $requestData["token"] .
" does not exists or is not authorized to perform this operation.");

exit;
}
};

//we define the response encoding, by default we will use json
header("Content-Type: application/json; charset=UTF-8");

if (isset($endpoints[$endpointName])) {
$endpoints["checktoken"]($requestData);
$endpoints[$endpointName]($requestData);
} else {
$endpoints["404"](array("endpointName" => $endpointName));
}

Testing

To test the script, you can simply perform GET requests from your browser, in a local environment you can do it by entering a URL like this one:

http://localhost/simple-api/sayhello?token=fa3b2c9c-a96d-48a8-82ad-0cb775dd3e5d&name=john

If you want to test using POST you can use curl like this:

curl -d "token=fa3b2c9c-a96d-48a8-82ad-0cb775dd3e5d&name=john" -X POST http://localhost/simple-api/sayhello

Using the X-API-KEY header:

curl -H 'x-api-key: fa3b2c9c-a96d-48a8-82ad-0cb775dd3e5d' -d "name=john" -X POST http://localhost/simple-api/sayhello

That’s it. Easy, isn’t it?

Now you have a general idea about how an API works and how you can implement it with willpower, courage and a couple of scripts.

At Winkhosting.co we are much more than hosting. Don’t forget to visit us.

--

--