DEV Community

Zeeshan Ahmd
Zeeshan Ahmd

Posted on

JWT authentication for Lumen 5.4

Recently I have been tinkering with Angular-4 to get a taste of it and I decided to create a quick project to get my hands dirty. I decided to create a blog with authentication etc. My main focus was on the frontend so I decided to quickly bootstrap an application in Lumen because of its simplicity and almost zero-configuration development. For the authentication, I decided to go with JWT and this post is going to be a quick write-up on how I integrated that and how anyone can integrate JWT authentication in their APIs. Firstly if you are not aware of what JWT Authentication is, I would suggest you to go through this nice little article to get the idea.

I hope you are excited so lets get started.

First things first, let's create an empty lumen project. Open up your terminal and run the following command to create a fresh copy of lumen project on your desktop:

composer create-project --prefer-dist laravel/lumen lumen-jwt
Enter fullscreen mode Exit fullscreen mode

Now we need to create the configuration file i.e. .env at the root of directory. Create it by simply copying the content of .env.example to .env:

cd lumen-jwt
cp .env.example .env
Enter fullscreen mode Exit fullscreen mode

Now lets set a few configuration values in our .env file. Open this lumen project in your favorite code editor or IDE and put the below in the .env ile:

APP_ENV=local
APP_DEBUG=true
APP_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=

CACHE_DRIVER=file

JWT_SECRET=JhbGciOiJIUzI1N0eXAiOiJKV1QiLC
Enter fullscreen mode Exit fullscreen mode

Note: Remember to set the APP_KEY and JWT_SECRET to your own.

Create a migration file for the users table:

php artisan make:migration create_users_table
Enter fullscreen mode Exit fullscreen mode

Modify the migration file created inside the database/migrations directory to have name, email and password fields. For me the file is database/migrations/2017_09_05_115448_create_users_table.php:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('users');
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let's define some factory methods that will help us in populating some seed data in the database. Open the file database/factories/ModelFactory.php and modify it to look like below:

<?php

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| Here you may define all of your model factories. Model factories give
| you a convenient way to create models for testing and seeding your
| database. Just tell the factory how a default model should look.
|
*/

$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->email,
        'password' => bcrypt('12345'),
    ];
});
Enter fullscreen mode Exit fullscreen mode

Now let's create the seeder to populate database with some users. Mmodify database/seeds/UsersTableSeeder.php to look like:

<?php

use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // create 10 users using the user factory
        factory(App\User::class, 10)->create();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let's register this user seeder in our database seeders. Modify database/seeds/DatabaseSeeder.php to look like this:

<?php

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Model::unguard();

        // Register the user seeder
        $this->call(UsersTableSeeder::class);

        Model::reguard();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now create the configured database in MySQL and run the following commands inside your terminal to create the users table and add some dummy data respectively:

php artisan migrate
php artisan db:seed
Enter fullscreen mode Exit fullscreen mode

If you run into any errors, run the below to make sure that composer knows about these newly created classes

composer dump-autoload
Enter fullscreen mode Exit fullscreen mode

Lumen does not have facades and eloquent enabled by default, let's enable them first by opening the bootstrap/app.php file and uncommenting the following lines:

$app->withFacades();
$app->withEloquent();
Enter fullscreen mode Exit fullscreen mode

Now let's create the endpoint to generate JWT token. There are tons of libraries out there that will help you with it we will use the one called firebase/php-jwt. Open up your terminal and run the following command to pull it in using composer:

composer require firebase/php-jwt
Enter fullscreen mode Exit fullscreen mode

Now let's add the endpoint POST /auth/login that will accept the credentials and return a token for us. Let's register the route first by adding the following route inside routes/web.php file:

<?php
$app->post('auth/login', ['uses' => 'AuthController@authenticate']);
Enter fullscreen mode Exit fullscreen mode

Now we need the controller AuthController with method authenticate. Inside app/Http/Controllers folder create a new AuthController.php file and put follwoing content inside it:

In a production ready application you will probably have models and helper methods to achieve this but for the sake of brevity let's put everything inside the controller.

<?php

namespace App\Http\Controllers;

use Validator;
use App\User;
use Firebase\JWT\JWT;
use Illuminate\Http\Request;
use Firebase\JWT\ExpiredException;
use Illuminate\Support\Facades\Hash;
use Laravel\Lumen\Routing\Controller as BaseController;

class AuthController extends BaseController 
{
    /**
     * The request instance.
     *
     * @var \Illuminate\Http\Request
     */
    private $request;

    /**
     * Create a new controller instance.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     */
    public function __construct(Request $request) {
        $this->request = $request;
    }

    /**
     * Create a new token.
     * 
     * @param  \App\User   $user
     * @return string
     */
    protected function jwt(User $user) {
        $payload = [
            'iss' => "lumen-jwt", // Issuer of the token
            'sub' => $user->id, // Subject of the token
            'iat' => time(), // Time when JWT was issued. 
            'exp' => time() + 60*60 // Expiration time
        ];

        // As you can see we are passing `JWT_SECRET` as the second parameter that will 
        // be used to decode the token in the future.
        return JWT::encode($payload, env('JWT_SECRET'));
    } 

    /**
     * Authenticate a user and return the token if the provided credentials are correct.
     * 
     * @param  \App\User   $user 
     * @return mixed
     */
    public function authenticate(User $user) {
        $this->validate($this->request, [
            'email'     => 'required|email',
            'password'  => 'required'
        ]);

        // Find the user by email
        $user = User::where('email', $this->request->input('email'))->first();

        if (!$user) {
            // You wil probably have some sort of helpers or whatever
            // to make sure that you have the same response format for
            // differents kind of responses. But let's return the 
            // below respose for now.
            return response()->json([
                'error' => 'Email does not exist.'
            ], 400);
        }

        // Verify the password and generate the token
        if (Hash::check($this->request->input('password'), $user->password)) {
            return response()->json([
                'token' => $this->jwt($user)
            ], 200);
        }

        // Bad Request response
        return response()->json([
            'error' => 'Email or password is wrong.'
        ], 400);
    }
}
Enter fullscreen mode Exit fullscreen mode

Lets open up your terminal and test our authenticate route by sending a post request and check if we are getting token in response.

curl -X POST -F 'email=ziishaned@gmail.com' -F 'password=admin' http://localhost:8080/auth/login
Enter fullscreen mode Exit fullscreen mode

After hitting the route you will get something like following result in response:

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJibG9nLmRldiIsInN1YiI6IjU5YWEyYTQ3ZjlkYzQxMTU3ODAwNjM0NiIsImlhdCI6MTUwNDYwNTY1MiwiZXhwIjoxNTA0NjA5MjUyfQ.F-7q5gR7TnLFaHxKhOiacgPlOzAYGgQ1lu5mZ_WWnqI"
}
Enter fullscreen mode Exit fullscreen mode

Now we need a middleware to protect our routes. Lets create a middleware JwtMiddleware inside app/Http/Middleware folder that will validate provided token and put the following content inside it.

<?php

namespace App\Http\Middleware;

use Closure;
use Exception;
use App\User;
use Firebase\JWT\JWT;
use Firebase\JWT\ExpiredException;

class JwtMiddleware
{
    public function handle($request, Closure $next, $guard = null)
    {
        $token = $request->get('token');

        if(!$token) {
            // Unauthorized response if token not there
            return response()->json([
                'error' => 'Token not provided.'
            ], 401);
        }

        try {
            $credentials = JWT::decode($token, env('JWT_SECRET'), ['HS256']);
        } catch(ExpiredException $e) {
            return response()->json([
                'error' => 'Provided token is expired.'
            ], 400);
        } catch(Exception $e) {
            return response()->json([
                'error' => 'An error while decoding token.'
            ], 400);
        }

        $user = User::find($credentials->sub);

        // Now let's put the user in the request class so that you can grab it from there
        $request->auth = $user;

        return $next($request);
    }
}
Enter fullscreen mode Exit fullscreen mode

Open the bootstrap/app.php file and register our JwtMiddleware and alias it with jwt.auth or whatever you like.

$app->routeMiddleware([
    'jwt.auth' => App\Http\Middleware\JwtMiddleware::class,
]);
Enter fullscreen mode Exit fullscreen mode

Now let's protect some of our routes. Open the routes file i.e. routes/web.php and put the following routes inside it:

$app->group(['middleware' => 'jwt.auth'], function() use ($app) {
    $app->get('users', function() {
        $users = \App\User:all();
        return response()->json($users);
    });
});
Enter fullscreen mode Exit fullscreen mode

Lets open up your terminal and test if our request succeeds by hitting users route. Run the following command inside your terminal:

curl -X GET http://localhost:8080/users
Enter fullscreen mode Exit fullscreen mode

Output:

{
  "error": "Token not provided."
}
Enter fullscreen mode Exit fullscreen mode

Now lets make another request to users route on which we implemented jwt.auth middleware and this time lets put the token that you will get by hitting authenticate route. Run the following commands inside your terminal:

curl -X POST -F 'email=ziishaned@gmail.com' -F 'password=admin' http://localhost:8080/auth/login
curl -X GET 'http://localhost:8080/api/v1/users?token=tokenFromFirstRequest'
Enter fullscreen mode Exit fullscreen mode

Output:

[
    {
        "id": 1,
        "email": "johndoe@gmail.com",
        "updated_at": "2017-09-02 03:49:27",
        "created_at": "2017-09-02 03:49:27"
    },
    {
        "id": 2,
        "email": "example@example.com",
        "updated_at": "2017-09-02 03:49:27",
        "created_at": "2017-09-02 03:49:27"
    }
]
Enter fullscreen mode Exit fullscreen mode

And that about wraps it up. If you have any questions feel free to leave your comments below.

Top comments (5)

Collapse
 
kodnificent profile image
Victor Mbamara 🇳🇬

Thanks for this lovely post.
What's happens in the case you want to access the endpoint
api/v1/posts from your Frontend app to display posts to anyone who visits your web app. Should this route be protected. What's the best way to do this?

Collapse
 
loizenai profile image
loizenai

So nice ! Thanks you

I have a post for sharing!

Angular SpringBoot Jwt Authentication Example:
loizenai.com/angular-10-spring-boo...

Please check it for feedback! Thanks

Collapse
 
israelbazan76 profile image
israel bazan

great article thanks for sharing!

just a little correction, in AuthController.php
function jwt the return value must be
return JWT::encode($payload, env('JWT_SECRET'),'HS256');
in otherwise,when we try to access to a protected route will fail

Collapse
 
peppeg85 profile image
peppeg85

hello, thanks for your great post, jus one question, in the jwtmiddleware, when you
return $next($request); then how can i retrieve the $request (so the logged user data) in the next controller?

Collapse
 
dorenta profile image
Shashank

Thank you for the excellent post, this was what I was exactly looking for!!