Posted by Neil Young on February 04, 2016
Recently 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.