Use PHP to set up a RESTful API with simple authentication

Hello!

It is sometimes necessary to bridge different web applications together in order to transmit or receive information and process it. One scenario could be a mobile application that connects to a web service to pull or push data in order to update the app or provide “real time” services with centralized data.

One of the PHP frameworks we have been working with lately is the aptly named Slim. The Slim framework is a very lean PHP framework that allows for a straight forward path to creating the commonly needed building blocks to produce dynamic web services.

First, before we do anything, it would be ideal to set up a web site to host the Slim based API service. In the interest of security it is a good idea to force SSL for all requests. That means you will have to purchase an SSL certificate for your website. This will encrypt all requests and ensure all the data transmitted or received is protected.

We use NGINX as a web service, so we have created a configuration that hosts the site and forces non-HTTPS requests to be redirected to HTTPS :

server {
        listen 80;
        server_name your-domain.com;
        rewrite        ^ https://$server_name$request_uri? permanent;
        }
}

The above nginx configuration directive takes whatever the request URI and rewrites it to force HTTPS. Now we need to start a new Slim application. You can read the readme documentation for Slim to get an idea of how you can start your application, which is a good idea to get to know what is needed to get started. Since our application will be using a MySQL database to insert and read data, we will include a MySQL library at the beginning of our application where we initialize everything :

require 'vendor/autoload.php';
require_once 'lib/mysql.php';

// initialize the app
$app = new \Slim\Slim();

// Queries
$app->get('/', 'getHome');
$app->get('/data', 'getData');
$app->post('/data', 'postData');

If you read the Slim getting started documentation, the above code should be pretty straightforward. We initialize the app, load MySQL connection directives, and define 3 query types : Home (“/”), Reading Data (GET “/data”), Writing Data (POST “/data”).

Before we do anything, we want the Slim framework to define a requirement. We want to check for a specific custom host header that contains an API Key. The key could be anything, and in this case we will define it as a random hash. You can define the keys in the database (if you want to have multiple API “Users” for example). In our example, we hard code the key in the logic to make things simpler. If you are forcing HTTPS at this point, the host header submitted and received should be encrypted. There are many more secure ways to complete this validation but again we opted to keep things simple in our examples.

So how do you do this check in Slim? You need to define the API checking logic as a hook that is called before “dispatch”. This means that you are defining the check to happen before any request is processed.

$app->hook('slim.before.dispatch', function () use ($app){

        $headers = request_headers();
        $response = array();
        $app = \Slim\Slim::getInstance();

        $api_key = $headers['X-API-KEY'];

        // this could be a MYSQL query that parses an API Key table, for example
        if($api_key == '612e648bf9594adb50844cad6895f2cf') {
                $authorized = true;
        } else if ($api_key == NULL) {
                $response["error"] = true;
                $response["message"] = '{"error":{"text": "api key not sent" }}';
                $app->response->headers['X-Authenticated'] = 'False';
                $authorized = false;
                $app->halt(401, $response['message']);
        } else {
                $response["error"] = true;
                $response["message"] = '{"error":{"text": "api key invalid" }}';
                $app->response->headers['X-Authenticated'] = 'False';
                $authorized = false;
        }

        if(!$authorized){ //key is false
                // dont return 403 if you request the home page
                $req = $_SERVER['REQUEST_URI'];
                if ($req != "/") {
                $app->halt('403', $response['message']); // or redirect, or other something
                }
        }

});

$app->run();

Whats happening here? Well we defined the hook “slim.before.dispatch”. In that hook, we are checking for the host header sent in any request coming to Slim. The host header we are checking for is “X-API-KEY”, but it could be anything obviously. It could be an “Authorization” header as well as the X-API-KEY header. In the header, we are checking for the value. If that value matches what we have defined internally (the random hash), then we will complete subsequent requests in Slim. If the key doesnt match, we block the requests and return a 403 HTTP response with an accompanying JSON response.

The JSON response can be processed by the app and used for debugging or used to present a user-friendly error message. Before going further, I should note that at the top of the code snippet above, you see a function “request_headers()” being called. This actually retrieves all headers submitted in the request. This function is useful for this purpose, as well as for other purposes. You may find it useful here as well as in other applications :

function request_headers() {
        $arh = array();
        $rx_http = '/\AHTTP_/';
        foreach($_SERVER as $key => $val) {
                if( preg_match($rx_http, $key) ) {
                        $arh_key = preg_replace($rx_http, '', $key);
                        $rx_matches = array();
                        // do string manipulations to restore the original letter case
                        $rx_matches = explode('_', $arh_key);
                        if( count($rx_matches) > 0 and strlen($arh_key) > 2 ) {
                                foreach($rx_matches as $ak_key => $ak_val) $rx_matches[$ak_key] = ucfirst($ak_val);
                                $arh_key = implode('-', $rx_matches);
                        }
                        $arh[$arh_key] = $val;
                }
        }
        return( $arh );
}

Now that we have the rudimentary API authentication out of the way, we can get to the good stuff! We want our API to spit out requests in JSON. In case you don’t know, JSON is a very lightweight data interchange format. It is commonly used in these scenarios when different applications need to talk to each other. It helps standardize the “discussion” and makes it very easy to share and process data. You can read more about JSON here.

What we’ll do here is show you how we want to process a “read” or “GET” request. The POST request is a bit more complicated and we will cover that in a separate post perhaps.

What we want to do is process the “GET” requests for the URI “/data” , do a simple database query to retrieve the data and return it as a JSON response for the 3rd party application to receive and process.

function getData() {
        $sql = "SELECT * from data order by created_at DESC;";

        try {
                $db = connect_db();
                $result = $db->query($sql);
                while ( $row = $result->fetch_array(MYSQLI_ASSOC) ) {
                        $data[] = $row;
                }
                $db = NULL;
                echo json_encode($data);
        } catch(PDOException $e) {
                echo '{"error":{"text":'. $e->getMessage() .'}}';
        }
}

If you look at the above code snippet, you’ll see its very straightforward. This is because all of the prep-work and mechanics behind the reception and processing of requests is handled by slim up until this point. This will allow you to focus on the actual core logic of your code and spend less time building the blocks that quite frankly other people have done (and have done better).

The function in the above code snippet simply defines a MySQL query where we are selecting data from a table and storing the results. The while-loop iterates through each result and stores it in the array “$data[]”.

We then use the PHP function “json_encode” to encode the data and echo it as an output. Any errors with the MySQL query are caught by the PDOException catch and the respective error message is returned in JSON format as well.

This is very straightforward and simple. There are additional security and stability best practices that one would need to consider. This is especially true if you are developing an application that needs to scale. Additional checks, logic and caching mechanisms such as memcache could be considered. Since those problems are unique to the scope of each individual application, its not necessary to go into that here.

Hopefully this guide will help you with your web application!