Stateless Authentication for your Laravel API using JSON Web Tokens

JWT Authentication with LaravelRecently I had to put together an API call which needed to be authenticated by an application that we are building inside of an Electron Packager. Instead of using a session based implementation, I looked into using JSON Web Token so we could make the calls without having to open up a unnecessary session on the server. I did look at using Basic Auth inside of Laravel but I felt that JWT seemed to offer a bit more security as the calling application would need to have a valid token in order to make subsequent calls to the server.

JWT allows for a stateless authentication exchange between the application and the server by taking a username or email and a password and then passing back to the application a token generated by a key and with a date of expiry. Because our API was built on Laravel we were able to do this easily using JWT-Auth.

Firstly, add the JWT-Auth package to your composer.json file:

{
    "name": "laravel/laravel",
    "description": "The Laravel Framework.",
    "keywords": ["framework", "laravel"],
    "license": "MIT",
    "type": "project",
    "require": {
        "php": ">=5.5.9",
        "laravel/framework": "5.2.*"
    },
    "require-dev": {
        "fzaninotto/faker": "~1.4",
        "mockery/mockery": "0.9.*",
        "phpunit/phpunit": "~4.0",
        "symfony/css-selector": "2.8.*|3.0.*",
        "symfony/dom-crawler": "2.8.*|3.0.*",
        "tymon/jwt-auth": "0.5.*"
    },
....

Then install the package by running:

composer update

This will add the package to your Laravel installation

Now we need to add the provider to our providers array in config/app.php

Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class

Whilst we are in this file we will add the facades for the package in the aliases array:

'JWTAuth'   => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class

Now we have added our package provider and facades. We can publish the vendor file using artisan from the command line:

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"

We now need a secret key that will be used to generate the token for the API calls:

php artisan jwt:generate

Following this you will see that the key value has changed in the config/jwt.php file

We will now setup a user in our users table in Laravel. If you have not migrated the users table (this comes bundled with Laravel) then run:

php artisan migrate

This should create a users table in your database

Now we will make a Users seeder to add the user to the database

php artisan make:seeder UsersTableSeeder

This will create a file in database/seeds called UsersTableSeeder.php. We are now going to create a user that will be used to get our token:

<?php
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
use App\User;
class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Model::unguard();
        //create a new user object
        $user = new User;
        $user->name = "Test User";
        $user->email = "test@test.com";
        $user->password = bcrypt("secret");
        $user->save();
        Model::reguard();
    }
}
?>

Add the following to your DatabaseSeeder in the same directory so it looks like this:

<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $this->call(UsersTableSeeder::class);
    }
}
?>

Now run the following:

php artisan db:seed --UsersTableSeeder

This will add the user to the database and encrypt the password.

First we will need to create our api controller so the user can authentication and get access to the endpoints.

php artisan make:controller ApiController

This will create the ApiController class in your Controllers directory

Now we need to create a login method that will allow the user to get a token:

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\User;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use JWTAuth; //JWtAuth module
use Tymon\JWTAuth\Exceptions\JWTException; //JWTException handler
class ApiController extends Controller
{
    //
    /**
     * Instantiate the authentication controller
     */
    public function __construct()
    {
           
        //add middleware to only allow the user access to the 
        //authenticate function. If we want to allow access to other functions
        //WITHOUT authentication then we would add them in the except array below
        $this->middleware('jwt.auth', ['except' => ['login']]);
       
    } 
    /**
     * Get a user with an id
     *
     * @param  int     $id
     * @return App\User $user
     */
     public function getUser($id)
     {
        // retrieve the admin user (you) that we created earlier
        $user = User::find( $id );
        //return the user
        return $user;   
    } 
    /**
     * Authenticate the given email and password
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function login(Request $request)
    {
        //assign the users email and password to a credentials object
        $credentials = $request->only('email', 'password');
        //now attempt to login the user
        try 
        {
            
            //attempt to get a token
            $token = JWTAuth::attempt($credentials);
            //if the token is invalid
            if (!$token) 
            {
                
                //return that the credentials are invalid
                return response()->json(['error' => 'invalid_credentials'], 401);
            
            }
        } 
        catch (JWTException $e) 
        {
            
            // something went wrong when trying to create a token
            return response()->json(['error' => 'could_not_create_token'], 500);
        }
        //we have success and a token was generated
        return response()->json(compact('token'));
    }
}

Now we have a way to login to our API we need to create the routing to handle the login. Add the following to your routes.php page:

//create a routing group that has a prefix of api
Route::group(['prefix' => 'api'], function () {
    //create a resource group that will require a token for authentication
    //the only array will store the functions that require authentication
    Route::resource('token', 'ApiController', ['only' => ['getUser']]);
    //post method to handle retrieval of the token using the login method
    //in the ApiController. This will need to take a the email and password
    Route::post('token', 'ApiController@login');
    //route that will allow access to a user with a valid token
    Route::resource('user/{id}', 'ApiController@getUser');
});

Now we can test this by using a HTTP sniffer. I would recommend the excellent Chrome extension Postman as this enables you to test any http requests and has bundles of features!

First, I test posting the following to login:

http://localhost/api/token?password=secret&email=test@test.com

This will then give me a token that I can make subsequent calls with:

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6XC9cLzE5Mi4xNjguOTkuMTAwXC9hcGlcL2dldC10b2tlbiIsImlhdCI6MTQ1NDU3OTU1MSwiZXhwIjoxNDU0NTgzMTUxLCJuYmYiOjE0NTQ1Nzk1NTEsImp0aSI6IjUxMGU5M2Q2YmUwOWZjZjliZDhlM2M5ZGYwNTNmOWZmIn0.QMCtxF7j2zBNf6eFzqzxEgyg7Hmk9ydqDN4-7Uic-FY"
}

Try leaving out the email and password and you should see the following:

{
  "error": "invalid_credentials"
}

With the token that is generated, we can now make a call to our user route:

http://192.168.99.100/api/user/1?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6XC9cLzE5Mi4xNjguOTkuMTAwXC9hcGlcL2dldC10b2tlbiIsImlhdCI6MTQ1NDU3OTU1MSwiZXhwIjoxNDU0NTgzMTUxLCJuYmYiOjE0NTQ1Nzk1NTEsImp0aSI6IjUxMGU5M2Q2YmUwOWZjZjliZDhlM2M5ZGYwNTNmOWZmIn0.QMCtxF7j2zBNf6eFzqzxEgyg7Hmk9ydqDN4-7Uic-FY

This should allow access and return the user object:

{
  "id": 1,
  "name": "Test User",
  "email": "test@test.com",
  "created_at": "2016-02-04 09:29:31",
  "updated_at": "2016-02-04 09:29:31"
}

Any call to this route without the token would result in the following:

{
  "error": "token_not_provided"
}

Because we are good developers we would want to unit test our code! Lets setup a unit test case!

Create the following file in your tests directory called JwtTest.php

<?php
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
use Illuminate\Http\Response as HttpResponse;
class JwtTest extends TestCase 
{
    //store credentials
    protected $credentials;
    /**
     * Constructor
     *
     * @return void
     */
    public function setUp()
    {    
        //parent setup
        parent::setUp();
        //setup the jwt crdentials
        $this->credentials = JWTAuth::attempt(['email' => "test@test.com", 'password' => "secret"]);
    }
    /**
     * Test for unauthorised access
     */
    public function testUnauthenticated()
    {
        //try to call the user api call with a token
        $response = $this->call('GET', '/api/user/1');
        //check that we get json back
        $this->seeJson([ 'error' => 'token_not_provided' ]);
        //call should be blocked
        $this->assertEquals(HttpResponse::HTTP_BAD_REQUEST, $response->status());
    }
    /**
     * Test for authorised access
     */
    public function testAuthenticated()
    {
        // as a user, try to access the entry endpoint 
        $response = $this->call(
            'POST',
            '/api/user/1',
            [], //parameters
            [], //cookies
            [], //files
            ['HTTP_Authorization' => 'Bearer ' . $this->credentials], // server
            []
        );
        //call should be allowed
        $this->assertEquals(HttpResponse::HTTP_OK, $response->status());
    }
}
?>

There are times when building an API that you will not need to create a session for the user. JWT authentication offers a way for your to make sure users 'handshake' with your server and are only allowed to make calls to your protected routes with a valid token.