David Jones

Slim authentication improved

In our last visit to Slim we authenticated our basic RESTful API using the Auth-Token http header. This gave us some protection but we neglected to secure it the best we could.

Here is a list of improvements we should make.

Make sure the IP trying to access our API is recognised as safe

Although we are requiring our client to pass through their application name and password, which they should be keeping secure, it may fall into the wrong hands.

We can be more sure that the request has come from the correct user by storing their IP address in a column in the APIClients table. Then whenever we get a request we would check for a valid APIClients by both the token sent in the Auth-Token header and the IP address the request was sent from.

Using the same example from our other Slim posts lets implement this.

First lets alter the APIClients table with an ip_address column and add a new APIClient with the IP from my local environment.

ALTER TABLE `APIClients` ADD COLUMN `ip_address` VARCHAR(20) AFTER `client_id`;

Also add the new user.

INSERT INTO `APIClients` (`name`, `client_id`, `ip_address`, `created_at`, `updated_at`) VALUES
    ('test-app', 'password1234', '127.0.0.1',  NOW(), NOW());

The only edit we need to do to the code is to add an additional where clause to the get the APIClient in our authenticate route. The new version should look like this.

$api_client = APIClient::where('name', $name)
                           ->where('client_id', $client_id)
                           ->where('ip_address', $_SERVER['REMOTE_ADDR'])
                           ->first();

Relying on the IP address alone is not going to give you that much security, you should use it along side other methods.

Access our API routes from a middleware

In our previous example we accessed our API straight from the client. This means any user with a small amount of knowledge could inspect the http headers from their browser and notice that the Auth-Token is visible in the request headers. They could then get this and start making requests.

They could have also views the JavaScript code and noticed that we are setting the client name and password as plain text in the URI. This should never happen in a production environment.

To avoid this sort of vulnerability you should be accessing the API through some sort of server side middleware.

Ignore any request not using SSL

We should require anyone trying to access your API to use SSL. If they are not we should return an error. We should not automatically redirect them to the an SSL enabled version.

Lets get this check enabled on our API.

We can do this in our before filter. We can simply inspect the headers and if the user is not trying to access the API over https then halt the application and return a 404 because technically the user is trying to access a route that doesn't exist.

if (!$_SERVER['HTTPS']) {
    $app->halt(404, json_encode(['HTTPS connection required']));
}

Refresh the token after a certain period of time

Lets assume that someone who we do not accessing our API gets their hands on an access token and starts making requests. We do not want them to have uninhibited access to our API. We can set the autherisation token token to expire forcing the client to request a new access token by providing their credentials and a refresh token.

We can do this by modifying the check in our before filter which checks if the token provided by the client is in our APITokens table. We also need to make sure we provide the refresh token to our client when they first try and authorise themselves.

We need to modify our APITokens table by adding a new column for the refresh token. we could add another column that indicates when the token expires but for the purpose of this example we can compare the current time to the updated_at column and if it is more than a predetermined period we can recognise this token as expired.

ALTER TABLE `APITokens` ADD COLUMN `refresh_token` VARCHAR(255) AFTER `token`;

Lets edit our authenticate route to also save a refresh token.

$api_token                = new APIToken();
$api_token->token         = bin2hex(openssl_random_pseudo_bytes(16));
$api_token->refresh_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));
}

Inside our before filter we need to add another check that compares the updated_at column of our APIToken object to the current date minus one hour. If it is less than this value when we recognise the token as expire and we halt the application.

$apiToken = APIToken::where('token', $token)->first();

if (!$apiToken) {
    $app->halt(401, json_encode(['Token not recognised.']));
}

$now = new DateTime();
if ($apiToken->updated_at < $now->modify('-1 hour')) {
    $app->halt(401, json_encode(['Token has expired. You need to refresh your toke by calling the /api/vi/refresh route with your refresh token']));
}

We how need to add another route to allow the user to pass in their refresh token and client information to generate a new authenticated token if their current one has expired.

$app->put('api/v1/refresh/:refresh_token/:name/:client_id', function ($refresh_token, $name, $client_id) use ($app) {
    $response = [];
    $api_client = APIClient::where('name', $name)
                           ->where('client_id', $client_id)
                           ->first();

    if (!$api_client) {
        $response['error'] = 'refresh.no-client-found';
        $app->halt(400, json_encode($response));
    } else {
        $token = $api_client->token;

        if (!$token) {
            $app->halt(401, json_encode(['refresh.token-not-found']));
        } else if ($token->refresh_token != $refresh_token) {
            $app->halt(401, json_encode(['refresh.refresh-token-invalid']));
        } else {
            $token->token         = bin2hex(openssl_random_pseudo_bytes(16));
            $token->refresh_token = bin2hex(openssl_random_pseudo_bytes(16));
            if (!$token->save()) {
                $app->halt(500, json_encode(['refresh.']));
            }

            $response['success'] = 'refresh.token-updated-successfully';
            $response['data']    = $token;
        }
    }

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

This looks similar to our authenticate route but with a few differences. We have an additional parameter in the URI that is defined as the clients current refresh token. There is a check to evaluate if the refresh token we have in the database is the same as the refresh token that was passed in. If all our checks pass then we create a new authentication token and a new refresh token and pass the object back to the client.

The client will then be able to swap the authentication token in the header with the new one and continue making requests.