A Detailed Guide to Implementing JWT Authentication in Laravel

When it comes to implementing stateless authentication in Laravel, Laravel developers usually pick one of the official packages, such as Laravel Passport, Laravel Sanctum, or the very popular jwt-auth package.

Now all these packages are excellent but in my opinion, learning how to implement a stateless authentication system using only the php-jwt is worth learning. This is an open-source project maintained by firebase (yes, that firebase) and it’s also enlisted in the jwt.io website.

In this tutorial, you’ll learn about JWT authentication, how to create new custom guards, issue and verify JWT tokens manually, and a few other stuff. Without further ado, let’s jump in.

Prerequisites

In order to follow along, you’ll need to have PHP and Composer installed on your computer. You can either bootstrap a new Laravel project or you can clone this repository from my GitHub account.

Once you have a new project ready, cd into that project’s root and execute the following command to install the php-jwt package:

composer require firebase/php-jwt

Once it’s installed, open the project using your favorite code editor or IDE and you’re ready to go forward. So, how does JWT authentication work?

Creating a New Guard

Guards in Laravel are ways to supply the logic that is used to identify authenticated users. By default, Laravel comes with a default web guard. You can find all the guards inside the config/auth.php file on your project.

Although every new Laravel project comes with a single guard, you can actually create custom guards as you see fit in your project. The Laravel documentation has detailed instructions for adding custom guards to your projects.

Now, there are two routes that you can take for creating your guard. The first one is using the Auth::extend() method along with a new class. The second route, which is also the simplest, is using a closure to describe your authentication logic.

In this tutorial, we’ll follow the second route. To do so, open your app/Providers/AuthServiceProvider.php file and add the following lines of code inside the boot method:

Auth::viaRequest('jwt', function (Request $request) {
    try{
        $tokenPayload = JWT::decode($request->bearerToken(), new Key(config('jwt.key'), 'HS256'));

      return \App\Models\User::find($tokenPayload)->first();
    } catch(\Exception $th){
        Log::error($th);
        return null;
    }
});

The Auth::viaRequest() method takes a guard driver’s name as the first parameter and closure as the second. Inside the closure, you’ll write the logic for retrieving a user from the database and returning an instance of the App\Models\User class. If no user is retrieved from the database, the guard should return null.

If you know how stateless authentication works you may already know that the client usually sends the bearer token in the header. The bearerToken() method inside the $request contains that token if sent by the client. This is how you begin the JWT token authentication.

Once the token is acquired, you then decode the token using the JWT::decode() method. The method takes the bearer token sent by the user as the first argument and an instance of the Firebase\JWT\Key class as the second. A key in this context is the collection of your JWT_KEY and an encryption algorithm.

Decoding a token lets you check whether it’s a valid token or not and also gives you the payload of the token. A payload in this case is a piece of data that can be used to identify a user. In our JWT authentication example, it’s the user’s id.

If the given token is valid, you’ll use that id to find the corresponding user from the database and return that. If it’s not valid then you’ll log the failure and return null.

Also, don’t forget to add the following use statements at the top of the file:

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;

Notice the call to the config('jwt.key') helper function. This indicates that you’ll need to add a new configuration file. Create a new file config/jwt.php and put the following lines of code in there:

<?php

return [
    'key' => env('JWT_KEY', 'secret'),
];

This will set the default JWT_KEY to the string secret and if you want to override it, just add your JWT_KEY in the .env file.

Now that you’ve defined a new guard, open the config/auth.php file and update the guards array as follows:

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

Now you have a new guard called api and its driver is jwt, the one you just defined inside the app/Providers/AuthServiceProvider.php file. You can also set it as the default guard by updating the defaults array within the config/auth.php file. Your guard is now ready. The next step is creating the controllers.

Writing The Controller

Create a new controller called ApiAuthController by executing the following command within your project’s root:

php artisan make:controller ApiAuthController

The first method that you’ll write is the registration method for your web api JWT authentication. But, you’ll need a form request to go with it. To make one, execute the following command in the project’s root to create a new request:

php artisan make:request UserRegistrationRequest

Open the app/Http/Requests/UserRegistrationRequest.php file and within the authorize() method, change the return false statement to return true. Then update the rules() method as follows:

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
    return [
        'name' => 'required',
        'email' => 'required|email|unique:App\Models\User,email',
        'password' => 'required|confirmed|min:8',
    ];
}

Now go back to the app/Http/Controllers/ApiAuthController.php file and add the following code for the register() method:

public function register(UserRegistrationRequest $request)
{
    DB::beginTransaction();

    try {
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        DB::commit();

        return response()->json([
            'success' => true,
            'message' => 'User Registered Succesfully!',
            'data' => [
                'accessToken' => JWT::encode($user->id, config('jwt.key')),
            ],
        ], 200);
    } catch (\Throwable $th) {
        Log::error($th->getMessage());
        DB::rollback();

        return response()->json([
            'success' => false,
            'message' => 'User Registration Failed!'
        ], 500);
    }
}

Nothing fancy here. You just create a new user from the data sent by the client. Once the new user is created, you generate a new access token by executing the JWT::encode() method and sending it back. Much like the JWT::decode() method you worked with before, the JWT::encode() method takes the newly created user’s id as the payload and your JWT_KEY as the key (duh!).

Don’t forget to add the following use statements at the top of the file:

use App\Models\User;
use Firebase\JWT\JWT;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\UserRegistrationRequest;

Now open the routes/api.php file and add the following line in there:

Route::prefix('auth')->group(function() {
    Route::post('register', [ApiAuthController::class, 'register']);
});

Again don’t forget to add the following use statement at the top of the file:

use App\Http\Controllers\ApiAuthController;

At this point, you can test out the API. But for that, you’ll need a working database connection. I’m assuming that you have a working database connection and that you have migrated the database. If so, start your application by executing the php artisan serve command.

Open up Postman or whatever REST client you fancy and make a POST request to http://localhost:8000/api/auth/register with the following JSON body:

{
    "name" : "Sherlock Holmes",
    "email" : "[email protected]",
    "password" : "12345678",
    "password_confirmation" : "12345678"
}

If you did everything right, you should receive a response as follows:

{
    "success": true,
    "message": "User Registered Succesfully!",
    "data": {
        "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.MQ.AbcSR3DWum91KOgfKxUHm78rLs_DrrZ1CrDgpUFFzls"
    }
}

Also, make sure to send Accept in headers with the value of application/json, or else you may error responses as HTML.

You can use this access token to make authenticated requests to the server. Go back to your app/Http/Controllers/ApiAuthController.php file and add the following code for the authenticatedUserDetails() method:

public function authenticatedUserDetails()
{
    return response()->json([
        'success' => true,
        'message' => 'Authenticated User Details.',
        'data' => [
            'user' => Auth::guard('api')->user(),
        ],
    ], 200);
}

If you’ve been working with Laravel JWT authentication methods for some time now you should be familiar with the Auth::user() method for JWT authentication. It returns the currently authenticated user using the default guard. Considering you’re using a non-default guard, you’ll have to use the Auth::guard() method to specify its name. However, if you’ve configured the api guard as default, you can skip the call to the Auth::guard() method and write Auth::user() instead.

Open up the routes/api.php file once again and update the auth group as follows:

Route::prefix('auth')->group(function() {
    Route::post('register', [ApiAuthController::class, 'register']);
    Route::middleware('auth:api')->get('authenticated-user-details', [ApiAuthController::class, 'authenticatedUserDetails']);
});

Now open your favorite REST client once again and make a GET request to http://localhost:8000/api/auth/authenticated-user-details by sending the access token you received previously as the bearer token. If everything goes fine, you’ll receive a response as follows:

{
    "success": true,
    "message": "Authenticated User Details.",
    "data": {
        "user": {
            "id": 1,
            "name": "Sherlock Holmes",
            "email": "[email protected]",
            "email_verified_at": null,
            "created_at": "2021-12-21T07:57:44.000000Z",
            "updated_at": "2021-12-21T07:57:44.000000Z"
        }
    }
}

This means that the register and user details functionalities are working great. Let’s create an endpoint for registered users to log in. Before that, you’ll need another form request to go along with it. To do so, execute the following command on the project’s root:

php artisan make:request UserLoginRequest

Open the app/Http/Requests/UserLoginRequest.php file and within the authorize() method, change the return false statement to return true. Then update the rules() method as follows:

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
    return [
        'email' => 'required|email',
        'password' => 'required',
    ];
}

Now open up the app/Http/Controllers/ApiAuthController.php file and add the following code for the login() method:

public function login(UserLoginRequest $request)
{
    if (User::where('email', '=', $request->email)->exists()) {
        $user = User::where('email', '=', $request->email)->first();

        if (Hash::check($request->password, $user->password)) {
            return response()->json([
                'success' => true,
                'message' => 'User Logged In Succesfully!',
                'data' => [
                    'accessToken' => JWT::encode($user->id, config('jwt.key')),
                ],
            ], 200);
        }

        return response()->json([
            'success' => true,
            'message' => 'Wrong User Credential!',
            'data' => null,
        ], 400);
    }

    return response()->json([
        'success' => false,
        'message' => 'No User With That Email Address!',
        'data' => null,
    ], 404);
}

There’s nothing fancy in this one either. You’re checking whether a user exists with the client given email or not. If yes, then you check whether the given password is correct or not. If yes, then you generate a new access token and send that back.

Open the routes/api.php file and update the auth group as follows:

Route::prefix('auth')->group(function() {
    Route::post('register', [ApiAuthController::class, 'register']);
    Route::post('login', [ApiAuthController::class, 'login']);
    Route::middleware('auth:api')->get('authenticated-user-details', [ApiAuthController::class, 'authenticatedUserDetails']);
});

Open up your favorite REST client once again and make a POST request to http://localhost:8000/api/auth/login with the following JSON body:

{
    "email" : "[email protected]",
    "password" : "12345678"
}

If everything goes fine, you’ll receive a response as follows:

{
    "success": true,
    "message": "User Logged In Succesfully!",
    "data": {
        "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.MQ.AbcSR3DWum91KOgfKxUHm78rLs_DrrZ1CrDgpUFFzls"
    }
}

This is a new access token that you can use for authorizing your user just like the old one. These tokens we’re generating have no expiry date. But in a real-life scenario, you’ll have to set a sane expiration date and implement token refresh functionality. But that’s out of the scope of this article. Maybe in another one.

Moreover, if you are interested in learning about the possibilities with Laravel, you can read the articles about deploying Laravel API on AWS Lambda, compare Laravel vs Symfony, how to use Laravel with MongoDB, or how to deploy laravel applications on virtual private servers. You can also learn more about the security in Laravel.

Conclusion

I would like to thank you for the time you've spent reading this article. I hope you've enjoyed it and have learned some valuable stuff from this article. If you're confused about any part of this article, feel free to browse the reference repository. I would also suggest you read through the official Laravel docs on custom guards to learn more. If you have any questions, feel free to reach out to me. I'm available on Twitter and LinkedIn and always happy to help. Until the next one, stay safe and keep on learning.

FAQs

Q: How to refresh JWT tokens in Laravel?
To refresh JWT tokens in Laravel, use the refresh method provided by the JWT auth package. This method generates a new token while invalidating the old one, allowing continuous authentication without requiring the user to log in again. 
Q: What's the impact of JWT on Laravel app performance?
JWT can slightly increase Laravel app performance due to stateless authentication, reducing the need for database queries to validate each request. However, complex token encryption and validation processes may introduce minimal overhead.
Q: How to integrate JWT with Laravel's Gates and Policies?
Integrate JWT with Laravel's Gates and Policies by extracting user roles or permissions from the JWT payload and using them within Gates and Policies to authorize actions. Define Gates and Policies as usual, then pass the user extracted from the JWT token to these authorization mechanisms to determine if the action is allowed.
Farhan Hasin Chowdhury
Farhan Hasin Chowdhury
Senior Software Engineer

Farhan is a passionate full-stack developer and author. He's a huge fan of the open-source mindset and loves sharing his knowledge with the community.

Expertise
  • Laravel
  • MySQL
  • Vue.js
  • Node.js
  • AWS
  • DigitalOcean
  • Kubernetes
  • AWS RDS
  • MongoDB
  • Python
  • Elastic Beanstalk
  • AWS S3
  • AWS CloudFront
  • Redis
  • Express.js
  • Amazon EC2
  • PostgreSQL
  • FastAPI
  • GitLab CI/CD
  • JavaScript
  • PHP
  • +16

Ready to start?

Get in touch or schedule a call.