DEV Community

Ndifreke Friday
Ndifreke Friday

Posted on • Edited on • Originally published at voidnerd.com

Build a JWT Authenticated API with Lumen(v5.8)

In this tutorial, we will be using lumen; a super-fast micro-framework by laravel to build a simple and secure REST API. At the end of this tutorial, you should be able to build production-ready APIs. Let's get started!

Prerequisite

Make sure you have the essentials, I beg of you.

  • PHP >= 7.1.3
  • OpenSSL PHP Extension
  • PDO PHP Extension
  • Mbstring PHP Extension
  • Mysql >= 5.7
  • Composer (Dependency Manager for PHP)
  • Postman (To test your endpoints)

Installation

First things first, you need to get lumen's cli.

$ composer global require "laravel/lumen-installer"
Enter fullscreen mode Exit fullscreen mode

If your download was successful, run below command to confirm you have lumen installed.

$ lumen
Enter fullscreen mode Exit fullscreen mode

If you encounter an error like -bash: lumen: command not found, you need to add composer's vender bin to your path.

If all is in order, you should see something like so.

Lumen

Now run this command to create the lumen project

$ lumen new auth-app
Enter fullscreen mode Exit fullscreen mode

Enter the project folder

$ cd auth-app
Enter fullscreen mode Exit fullscreen mode

Run the app

$ php -S localhost:8000 -t public
Enter fullscreen mode Exit fullscreen mode

Load localhost:8000 on your browsers address bar and it should render a result as shown below.

Lumen response

Open up the project (auth-app) in your preferred editor.

Create a .env file, copy all contents in .env.example into the .env file and add your database configurations.

In boostrap/app.php uncomment the facades and eloquent method

//before

// $app->withFacades();

// $app->withEloquent();

//after

$app->withFacades();

$app->withEloquent();

Enter fullscreen mode Exit fullscreen mode

Turning on withFacades inject the application IoC to Illuminate\Support\Facades\Facade. Without doing so even if you're importing Illuminate\Support\Facades\File it wouldn't work. Credit

The $app->withEloquent() method actually enables the query builder too. It's registering the DatabaseServiceProvider, which is required to use the query builder. Credit.

Create a user

Make user's database migration

$ php artisan make:migration create_users_table --create=users
Enter fullscreen mode Exit fullscreen mode

Locate the migration file database/migrations/*_create_users_table.php and add neede table columns(name, email, password); see code below:

...
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique()->notNullable();
            $table->string('password');
            $table->timestamps();
        });
    }
...
Enter fullscreen mode Exit fullscreen mode

Migrate your database

$ php artisan migrate

Enter fullscreen mode Exit fullscreen mode

Add register route which as the name implies; register users. Locate routes/web.php and insert the needed code as seen below

// API route group
$router->group(['prefix' => 'api'], function () use ($router) {
   // Matches "/api/register
   $router->post('register', 'AuthController@register');

});
Enter fullscreen mode Exit fullscreen mode

Since we are going to prefix api in all our endpoint, to reduce repetition we will use route grouping to do just that.

This method ($router->post($uri, $callback); takes in a $url and a $callback parameter. In the $callback, AuthController is our controller class (we will create this class in a bit) and register is a method in said class.

Let's create our AuthControler.

Create file app/Http/Controllers/AuthController.php and populate it with code as seen below.


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use  App\User;

class AuthController extends Controller
{
    /**
     * Store a new user.
     *
     * @param  Request  $request
     * @return Response
     */
    public function register(Request $request)
    {
        //validate incoming request 
        $this->validate($request, [
            'name' => 'required|string',
            'email' => 'required|email|unique:users',
            'password' => 'required|confirmed',
        ]);

        try {

            $user = new User;
            $user->name = $request->input('name');
            $user->email = $request->input('email');
            $plainPassword = $request->input('password');
            $user->password = app('hash')->make($plainPassword);

            $user->save();

            //return successful response
            return response()->json(['user' => $user, 'message' => 'CREATED'], 201);

        } catch (\Exception $e) {
            //return error message
            return response()->json(['message' => 'User Registration Failed!'], 409);
        }

    }


}
Enter fullscreen mode Exit fullscreen mode

Register a user(use POSTMAN) with route localhost:8000/api/register and you should get a successful response like so

Lumen register example

User sign in

Pull in the JWT authentication package.

$ composer require tymon/jwt-auth:dev-develop
Enter fullscreen mode Exit fullscreen mode

Generate your API secret

$ php artisan jwt:secret
Enter fullscreen mode Exit fullscreen mode

create file config/auth.php with below config

//config.auth.php

<?php

return [
    'defaults' => [
        'guard' => 'api',
        'passwords' => 'users',
    ],

    'guards' => [
        'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => \App\User::class
        ]
    ]
];

Enter fullscreen mode Exit fullscreen mode

Make some changes to your User model(app/User.php) to fit tymon/jwt-auth's requirements. Keep your eye out for everything that includes "JWT".

<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Laravel\Lumen\Auth\Authorizable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;


use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Model implements AuthenticatableContract, AuthorizableContract, JWTSubject
{
    use Authenticatable, Authorizable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email'
    ];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = [
        'password',
    ];




    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

Enter fullscreen mode Exit fullscreen mode

Make some changes to bootstrap/app.php

//before
// $app->routeMiddleware([
//     'auth' => App\Http\Middleware\Authenticate::class,
// ]);

//After
$app->routeMiddleware([
    'auth' => App\Http\Middleware\Authenticate::class,
]);
Enter fullscreen mode Exit fullscreen mode
//before
 // $app->register(App\Providers\AppServiceProvider::class);
 // $app->register(App\Providers\AuthServiceProvider::class);
 // $app->register(App\Providers\EventServiceProvider::class);

//After
 // $app->register(App\Providers\AppServiceProvider::class);
 $app->register(App\Providers\AuthServiceProvider::class);
 // $app->register(App\Providers\EventServiceProvider::class);

 // Add this line
 $app->register(Tymon\JWTAuth\Providers\LumenServiceProvider::class);
Enter fullscreen mode Exit fullscreen mode

Add login route in routes/web.php

// API route group
$router->group(['prefix' => 'api'], function () use ($router) {
     // Matches "/api/register
    $router->post('register', 'AuthController@register');

      // Matches "/api/login
     $router->post('login', 'AuthController@login');
});
Enter fullscreen mode Exit fullscreen mode

Add a global respondWithToken method to Controller class in app/Http/Controllers/Controller.php. This is so we could access it from any other controller.

   ...
  //import auth facades
  use Illuminate\Support\Facades\Auth;


  //Add this method to the Controller class
  protected function respondWithToken($token)
    {
        return response()->json([
            'token' => $token,
            'token_type' => 'bearer',
            'expires_in' => Auth::factory()->getTTL() * 60
        ], 200);
    }

Enter fullscreen mode Exit fullscreen mode

Add a login method to your AuthController class in app/Http/Controllers/AuthController.php

   ...

   //import auth facades
   use Illuminate\Support\Facades\Auth;

   ...

     /**
     * Get a JWT via given credentials.
     *
     * @param  Request  $request
     * @return Response
     */
    public function login(Request $request)
    {
          //validate incoming request 
        $this->validate($request, [
            'email' => 'required|string',
            'password' => 'required|string',
        ]);

        $credentials = $request->only(['email', 'password']);

        if (! $token = Auth::attempt($credentials)) {
            return response()->json(['message' => 'Unauthorized'], 401);
        }

        return $this->respondWithToken($token);
    }

Enter fullscreen mode Exit fullscreen mode

Login a user using route localhost:8000/api/login and you should get a successful response like so:

Lumen login example

Authenticated routes

For our grand finale, we are going to make some authenticated routes.

Add a couple of routes to routes/web.php

...
// API route group
$router->group(['prefix' => 'api'], function () use ($router) {
    // Matches "/api/register
   $router->post('register', 'AuthController@register');
     // Matches "/api/login
    $router->post('login', 'AuthController@login');

    // Matches "/api/profile
    $router->get('profile', 'UserController@profile');

    // Matches "/api/users/1 
    //get one user by id
    $router->get('users/{id}', 'UserController@singleUser');

    // Matches "/api/users
    $router->get('users', 'UserController@allUsers');
});

...

Enter fullscreen mode Exit fullscreen mode

Create a file app/Http/Controllers/UserController.php and populate it with this elegant looking code.

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;
use  App\User;

class UserController extends Controller
{
     /**
     * Instantiate a new UserController instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Get the authenticated User.
     *
     * @return Response
     */
    public function profile()
    {
        return response()->json(['user' => Auth::user()], 200);
    }

    /**
     * Get all User.
     *
     * @return Response
     */
    public function allUsers()
    {
         return response()->json(['users' =>  User::all()], 200);
    }

    /**
     * Get one user.
     *
     * @return Response
     */
    public function singleUser($id)
    {
        try {
            $user = User::findOrFail($id);

            return response()->json(['user' => $user], 200);

        } catch (\Exception $e) {

            return response()->json(['message' => 'user not found!'], 404);
        }

    }

}


Enter fullscreen mode Exit fullscreen mode

Below is an example call to one of the three newly added endpoints

Lumen users example

Here's a link to the full code on github.

The end-ish!

I hope this article has helped you in some way, and that you build upon this knowledge to deploy awesome APIs in the nearest future. I would like to see your contributions down in the comments too.

Hasta la vista.

Top comments (51)

Collapse
 
dhavaldignizant profile image
dhaval-dignizant

getting this error after install jwt

php artisan jwt:secret

There are no commands defined in the "jwt" namespace.

Collapse
 
b6t3m6n profile image
B6T3M6N

Had the same issue.

following the docs here jwt-auth.readthedocs.io/en/develop...
especially the chapter Bootstrap file changes fixed the issue

Add the following snippet to the bootstrap/app.php file under the providers section as follows:

// Uncomment this line
$app->register(App\Providers\AuthServiceProvider::class);

// Add this line
$app->register(Tymon\JWTAuth\Providers\LumenServiceProvider::class);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ndiecodes profile image
Ndifreke Friday

Please check to make sure tymon/jwt-auth installed successfully

Also type php artisan to see available commands.

Keep me posted on your progress.

Collapse
 
ndiecodes profile image
Ndifreke Friday

Side note: the command jwt:secret updates your .env file with something like JWT_SECRET=secretkeystring.

"It is the key that will be used to sign your tokens. How that happens exactly will depend on the algorithm that you choose to use."

Thread Thread
 
dhavaldignizant profile image
dhaval-dignizant

now it's working. Thank you.

Thread Thread
 
ndiecodes profile image
Ndifreke Friday

You are welcome

Thread Thread
 
dhavaldignizant profile image
dhaval-dignizant

Hi Ndifreke,
can you explain how to publish jwt library also need to token expire time and refresh token. thank you in advance.

Thread Thread
 
ndiecodes profile image
Ndifreke Friday

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

The above command publishes the JWTAuth Package. Here's this thing, the vendor:publish command is not available in lumen.
You can try using third party packages to pull this in ( github.com/laravelista/lumen-vendo... )

For managing expiration time and refreshing tokens, this link should help out; jwt-auth.readthedocs.io/en/develop...

Thread Thread
 
dhavaldignizant profile image
dhaval-dignizant

Hi Ndifreke,
Thanks for reply. one more thing I need if you help me. you have any example for lumen with vue also include in jwt token use.

Collapse
 
cleathley profile image
Chris Leathley • Edited

you might want to also add

use Illuminate\Support\Facades\Auth;

when you add respondWithToken to app/Http/Controllers/Controller.php (Lumin 6+)

Collapse
 
kbzone profile image
Cabello, Hector Guillermo

This is key.

I was followed step by step your tutorial, writing the code by myself (i mean, without clone your github project) and without this line (thanks @cleathley ) an error appears.

I think you should edit your post and add that line after you explain this:
«Add a global respondWithToken method to Controller class»

Anyway, great tutorial. It help me a lot!!

Collapse
 
ndiecodes profile image
Ndifreke Friday • Edited

I'm glad it helped 👍

I will update the post, so new readers don't run into this same error.

Collapse
 
sayajin101 profile image
Syns

This saved my life ty

Collapse
 
ndiecodes profile image
Ndifreke Friday

Thanks For this, I will update the tutorial to help people not encounter this error in the future.

Collapse
 
wmazed profile image
Walid Yacine MAZED • Edited

First of all great tutorial, thanks a lot for all your efforts dude, I've a question, how can I set a remember me system with JWT token ? And also, how to increase expiration duration from the config file (config/auth.php) ?

Collapse
 
ndiecodes profile image
Ndifreke Friday • Edited

Would there be a need for remember me? As long as the client has the token; they have access to the system.

You could extend token's time to live for those type of users, that could work...I think.

Override the token ttl
$token = auth()->setTTL(7200)->attempt($credentials);

Further Reading
stackoverflow.com/questions/236038...

Collapse
 
wmazed profile image
Walid Yacine MAZED • Edited

Thanks dude, I've another question please, if the token is stocked localy, then yo access protected area I've check the user token for that, if Auth::user() did the job, why we stock the token localy ?

Collapse
 
wmazed profile image
Walid Yacine MAZED

For the second part of the question, the answer should be:
github.com/tymondesigns/jwt-auth/b...

Collapse
 
debabratakarfa profile image
Debabrata Karfa • Edited

Getting this error,

In Connection.php line 665:

SQLSTATE[HY000] [2002] Connection refused (SQL: create table migrations (id int unsigned not null auto_increment primary key, migration varchar(255) not null, batch int not
null) default character set utf8mb4 collate 'utf8mb4_unicode_ci')

In Connector.php line 70:

SQLSTATE[HY000] [2002] Connection refused

Using Homestead development environment.

Collapse
 
ndiecodes profile image
Ndifreke Friday • Edited

This error is due to PHP not being able to connect to MySQL

"Create a .env file, copy all contents in .env.example into the .env file and add your database configurations."

Collapse
 
racedaemon profile image
racedaemon

This was of great help. The only problem I encountered was when running php artisan jwt:secret.
$app->register(Tymon\JWTAuth\Providers\LumenServiceProvider::class); needs to be added to app.php before the command is executed.

Collapse
 
jaiminhothi profile image
jaimin-hothi • Edited

Hi,
Thanks for your great tutorial.
But I want a custom field (email and password) both so could you make it an example for that.

My field name = USER_NAME, PASSWORD (Both are capital)

I try but token always false.

If you do then its great help for me.

Thank you

Collapse
 
bosz profile image
Fongoh Martin T.

Nice tutorial, very clear and straight to the point. My worry is why the dev-develope branch? The main branch, does it have an issue?

Collapse
 
ndiecodes profile image
Ndifreke Friday

For some reason the dev-develop branch has always worked for new versions of lumen, the next best thing would be version @2.0-dev

Collapse
 
bosz profile image
Fongoh Martin T.

Ok thanks. I tried using the default and it keeps failing and when i use dev-develop, it worked well.

Thanks again for the tutorial, nice, easy and straight to the point.

I come again with one question. If i log into different devices, eg 4 different clients and have 4 different tokens, will all 4 be valid simultaneously? If not, what will you suggest to be done to ensure multiple tokens are valid across different devices.

:)

Thread Thread
 
ndiecodes profile image
Ndifreke Friday

Every Token Generated that is yet to expired is valid; So yes, a user can have multiple tokens across multiple devices.

Collapse
 
executions12 profile image
executions12

everything except php artisan jwt::secret is work...
so, i write jwt key in .env by myself..

thank you, nice tutorial...

Collapse
 
yunchurlee profile image
yunchurlee

I've just reviewed and coded through. It worked wonderfully~~~

Collapse
 
isrortega profile image
isrortega

Thank you, very good tutorial