David Jones

Slim authentication

In the last post we looked at setting up a basic RESTful API. We created a database of users and a GET route to access them. Now we are looking into how we can secure our API and only let the applications we decide have access to it.

How to authenticate our applications

We need to create a new database table that will hold each application that will have a client ID. The application will pass through an authentication endpoint that will return a token to be used with each future request. This will provide us a stateless RESTful API that doesn't rely on the use of cookies or sessions. This will be secure enough providing we use SSL, which every API should.

Creating our client table

Here is the SQL to create our APIClients table and insert one row for our testApp.

-- UP
DROP TABLE IF EXISTS `APIClients`;
CREATE TABLE `APIClients` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(255) NOT NULL,
    `client_id` varchar(255) NOT NULL,
    `created_at` timestamp NOT NULL,
    `updated_at` timestamp NOT NULL,
    PRIMARY KEY (`id`)
);

-- Seeds
INSERT INTO `APIClients` (`name`, `client_id`, `created_at`, `updated_at`) VALUES
    ('testApp', 'password', NOW(), NOW());

Adding a new route

This will look similar to the user get route that we defined in the previous post. We are calling the get method with a URL string that contains two required parameters and a closure function. At this stage it will just echo the two parameters that were fed in or add an error to our response.

// Authenticate GET
$app->get('/api/v1/authenticate/:name/:client_id', function ($name, $client_id) {
    echo('Name: '.$name);
    echo('Client ID: '.$client_id);
});

Notice I added api/v1 to the beginning of the route. This is so we can version our API. This gives us better management over API change. We can prevent requests to older routes that may have been deprecated and easily facilitate a complete version increment without having to change route paths or our naming convention of the routes themselves.

Verifying the credentials

First thing we need to do is set up a way for us to access our new APIClients table. We will set up a model much like the User model we created in the previous post.

use Illuminate\Database\Eloquent\Model;

class APIClient extends Model {

    public    $timestamps = true;
    protected $table   = 'APIClients';
}

We can now use this in our authenticate route to verify the name and client_id passed belong to an application we want to allow to access our API.

Here is a run down of what were are going to do:

Before we begin to build this logic this we need to make sure we have the APITokens table, a model and a relationship between the APIClient and the APIToken.

Run this SQL.

-- UP
DROP TABLE IF EXISTS `APITokens`;
CREATE TABLE `APITokens` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `client_id` int(11) NOT NULL,
    `token` varchar(255) NOT NULL,
    `created_at` timestamp NOT NULL,
    `updated_at` timestamp NOT NULL,
    PRIMARY KEY (`id`)
);

Create a new model.

use Illuminate\Database\Eloquent\Model;

class APIToken extends Model {

    public    $timestamps = true;
    protected $table   = 'APITokens';
}

We will need to add a method to our APIClient model to allow us to associated our APIClient to a row in the APIToken table. Lucky Eloquent has a built in method for this. We can simply call the method hasOne passing in the class name of our APIToken model and the name of the foreign key column.

Our APIClient model should look like this.

use Illuminate\Database\Eloquent\Model;

class APIClient extends Model {

    public    $timestamps = true;
    protected $table   = 'APIClients';

    public function token()
    {
        return $this->hasOne('APIToken', 'client_id');
    }
}

Lets construct our authenticate route as we described it earlier. It should look something like this.

// Authenticate GET
$app->get('/api/v1/authenticate/:name/:client_id', function ($name, $client_id) {
    $response = [];
    $api_client = APIClient::where('name', $name)
                           ->where('client_id', $client_id)
                           ->first();

    if (!$api_client) {
        $response['error'] = 'authenticate.no-client-found';
        $app->halt(400, json_encode($response));
    } else {
        // Check if the user already has a token                
        $api_token = $api_client->token;
        if (!$api_token) {
            $api_token = new APIToken();
            $api_token->token = bin2hex(openssl_random_pseudo_bytes(16));
            $api_token->client_id = $api_client->id;
            if (!$api_token->save()) {
                $response['error'] = 'authenticate.token-not-saved';
                $app->halt(500, json_encode($response));
            }
        }

        if ($api_token) {
            $response['success'] = 'authenticate.success';
            $response['data']    = $api_token;
        }
    }

    echo(json_encode($response));
});

The response will hold the authentication token that the client will need to add to the Auth-Token http header. Lets say for example we are trying to access this API using an application built with AngularJS. We would use this so every successive http request to the API would be recognised as authenticated.

http.defaults.headers.common['Auth-Token'] = 'token';

Checking the request is authorised

We need to block clients that do no have a valid token from accessing any of our routes other than the authenticate route. We can do this by adding a hook called slim.before.dispatch. This is a default hook provided by Slim that is invoked before the target route is dispatched. This enables us to grab the Auth-Token from the header and check it exists in the database before the actual route logic is executed. This allows us to return a 401 error if the client's token does not exist.

Lets look at the code.

// Before filter to check the Auth Token is in the header
$app->hook('slim.before.dispatch', function() use ($app) {
    $publicRoutes = ['/api/v1/authenticate/:name/:client_id'];
    $currentRoute = $app->router()->getCurrentRoute()->getPattern();

    if (!in_array($currentRoute, $publicRoutes)) {
        $token = $charset = $app->request->headers->get('Auth-Token');

        if (!APIToken::where('token', $token)->first()) {
            $app->halt(401, json_encode(['Token not recognised.']));
        }
    }
});

We are getting the current route pattern and checking if it is in the array of routes we want to be accessible to unautherised clients. If it isn't then we need to get and check the token is in the APITokens table of our database. If the token isn't found we halt the script and respond with a 401 error and an error message.

The halt method is built into Slim and is designed to halt the application as soon as it is called and return a status code and an optional message.

Cross-domain resource sharing (CORS)

I am not going to go into too much detail with this but if we do not specify some extra headers client side libraries making AJAX calls will fail. The headers we need to set are the following.

$app->response->headers->set('Access-Control-Allow-Origin', '*');
$app->response->headers->set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
$app->response->headers->set('Access-Control-Allow-Headers', 'X-Requested-With');

Building a test AngularJS application

To give this a proper test lets create a simple little AngularJS application to autherise the client then call the users route and see what we get in return.

Lets first try and call the users route before setting the Auth-Token header using the response from our authenticate route.

app.factory('UserService', UserService);

app.controller('TestCtrl', TestCtrl);

TestCtrl.$inject = ['$http', 'Auth', 'UserService'];
function TestCtrl ($http, Auth, UserService) {
    UserService.getAll().then(function (data) {
        console.log(data.data);
    });
}

UserService.$inject = ['$http'];
function UserService ($http) {
    return {
        getAll: getAll
    }
    function getAll () {
        return $http.get('http://slim.api.local/api/v1/user').
            success(function (data) {
                return data;
            });
    }
}

If we run this code and inspect the response we will find we get a status of 401 which is expected. Now lets modify this and authenticate the client and set the Auth-Token before we call the user route.

app.factory('Auth', Auth);

// Existing code

TestCtrl.$inject = ['$http', 'Auth', 'UserService'];
function TestCtrl ($http, Auth, UserService) {
    Auth.getToken().then(function (data) {
        var token = data.data.data.token;
        $http.defaults.headers.common['Auth-Token'] = token;

        UserService.getAll().then(function (data) {
            console.log(data.data);
        });
    });
}

Auth.$inject = ['$http'];
function Auth ($http) {
    return {
        getToken: getToken
    }
    function getToken () {
        return $http.get('http://slim.api.local/api/v1/authenticate/testApp/password')
            .success(function (data) {
                return data;
            });
    }
}

// Existing code

If we now run the application we will see we get a response with a 200 status code and an object containing the three users from our database.

We have now authenticated our API making it more secure.

Make sure you check out the next post where we will learn what flaws are in this example has and other ways we can secure our RESTful API.

You can view this code by visiting this repository.