Laravel 12 API Authentication using JWT: Tutorial with Example

Learn how to implement secure JWT authentication in Laravel 12 APIs step-by-step tutorial. Perfect for developers building scalable, stateless authentication systems.
Laravel 12 API Authentication using JWT: Tutorial with Example Image

In modern web development, securing your APIs is crucial. JSON Web Tokens (JWT) have become a popular method for handling authentication in APIs due to their stateless nature and scalability. In this blog post, we’ll walk you through how to implement JWT-based authentication in a Laravel 12 API.

What is JWT?

JSON Web Token (JWT) authentication is a popular method for securing APIs in Laravel applications. In this tutorial, we will walk through implementing JWT authentication in Laravel 12.

JWT (JSON Web Token) is a compact, URL-safe means of representing claims to be transferred between two parties. It is commonly used for authentication and authorization in web applications. A JWT consists of three parts:

  1. Header: Contains metadata about the token, such as the signing algorithm.
  2. Payload: Contains the claims, which are statements about an entity (typically, the user) and additional data.
  3. Signature: Used to verify that the token was not tampered with.

Prerequisites

Before we begin, ensure you have the following installed:

  1. Laravel 12 installed on your local machine.
  2. A database configured in your .env file.
  3. Basic knowledge of Laravel (routes, controllers, migrations, etc.).

Step 1: Install JWT Authentication Package

To handle JWT authentication, we’ll use the tymon/jwt-auth package, which is a popular JWT implementation for Laravel.

Install the package via Composer:

composer require tymon/jwt-auth

Step 2: Configure JWT

After installing the package, publish the JWT configuration file:

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

This will create a config/jwt.php file where you can customize JWT settings.

Next, generate a secret key for signing the tokens:

php artisan jwt:secret

This will update your .env file with a JWT_SECRET key.

Step 3: Update the User Model

To use JWT, your User model must implement the Tymon\JWTAuth\Contracts\JWTSubject interface. Update your User model as follows:

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var list<string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var list<string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }
}

Step 4: Configure Auth Guard

Open the config/auth.php file and update the guards section to use JWT for the api guard:

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

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

Step 5: Define API Routes

Next, let’s create routes for user registration, login, and logout. Open the routes/api.php file and add the following routes:

<?php

use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\ProductController;
use Illuminate\Support\Facades\Route;

Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);

Route::middleware('auth:api')->group(function () {
    Route::post('logout', [AuthController::class, 'logout']);
    Route::post('refresh', [AuthController::class, 'refresh']);
    Route::get('user', [AuthController::class, 'user']);

    Route::prefix('a_products')->group(function () {
        Route::get('/', [ProductController::class, 'index']);
        Route::post('save', [ProductController::class, 'save']);
        Route::get('{id}/show', [ProductController::class, 'show']);
        Route::patch('{id}/update', [ProductController::class, 'update']);
        Route::delete('{id}/delete', [ProductController::class, 'delete']);
    });
});

Here’s what each route does:

  • /register: Handles user registration.
  • /login: Handles user login and returns an API token.
  • /logout: Logs the user out and revokes the token.
  • /user: Returns the authenticated user’s details.
  • /a_products: Returns a list of all products.
  • /a_products/save: Creates a new product with the provided details.
  • /a_products/{id}/show: Returns the details of a specific product based on the provided id.
  • /a_products/{id}/update: Updates the details of a specific product based on the provided id.
  • /a_products/{id}/delete: Deletes a specific product based on the provided id.

Step 6: Create the UserResource File

Next, create a UserResource to format the JSON response for your API. Run the following Artisan command:

php artisan make:resource UserResource

This command will generate a new file at app/Http/Resources/UserResource.php. Open the newly created UserResource.php file located in app/Http/Resources. Update the toArray method to customize the JSON response for the User model. For example:

public function toArray(Request $request): array
{
	return [
		'id' => $this->id,
		'name' => $this->name,
		'email' => $this->email,
		'email_verified_at' => $this->email_verified_at,
	];
}

Step 7: Create the AuthController

Next, create a controller to handle authentication logic. Run the following command:

php artisan make:controller Api\AuthController

Now, open the app/Http/Controllers/Api/AuthController.php file and add the following methods:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\CreateUserRequest;
use App\Http\Requests\Api\LoginRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Tymon\JWTAuth\Facades\JWTAuth;

class AuthController extends Controller
{
    public function register(CreateUserRequest $request)
    {
        $data = $request->validated();
        $data['password'] = Hash::make($data['password']);

        User::query()->create($data);

        return response()->json(['message' => 'User register successfully'], 201);
    }

    public function login(LoginRequest $request)
    {
        $data = $request->validated();

        if (! $token = JWTAuth::attempt($data)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

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

    public function user()
    {
        return new UserResource(auth()->user());
    }

    public function logout()
    {
        auth()->logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    public function refresh()
    {
        return response()->json([
            'token' => auth()->refresh(),
        ], 200);
    }
}

Next, Create the Form request file to handle the incoming request. Run following command to create file

php artisan make:request Api\CreateUserRequest
php artisan make:request Api\LoginRequest

Update app/Http/Request/Api/CreateUserRequest.php File:

public function authorize(): bool
{
	return true;
}

/**
 * Get the validation rules that apply to the request.
 *
 * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
 */
public function rules(): array
{
	$rules = [
		'name' => ['required', 'string', 'max:255'],
		'email' => ['required', 'email', 'unique:users', 'max:255'],
		'password' => ['required', 'string', 'min:8', 'max:255', 'confirmed'],
	];

	return $rules;
}

Update app/Http/Request/Api/LoginRequest.php File:

public function authorize(): bool
{
	return true;
}

/**
 * Get the validation rules that apply to the request.
 *
 * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
 */
public function rules(): array
{
	$rules = [
		'email' => ['required', 'email', 'max:255'],
		'password' => ['required', 'min:8'],
	];

	return $rules;
}

Step 8: Create the Product Migration File

Now for handle the product CRUD create the migration file. Run the following command:

php artisan make:migration create_products_table

Update Migration File:

public function up(): void
{
	Schema::create('products', function (Blueprint $table) {
		$table->id();
		$table->string('name');
		$table->text('description');
		$table->decimal('price');
		$table->string('image');
		$table->timestamps();
	});
}

Note: Don't forgot to run migration for create product table.

Step 9: Create the Product Model File:

Run following Command for create the Product model file:

php artisan make:model Product

Open the Product model at app/Models/Product.php and update it with the following code:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $guarded = [];
}

$guarded empty array allow all filed fillable

Step 10: Create the ProductResource File

Next, create a ProductResource to format the JSON response for your API. Run the following Artisan command:

php artisan make:resource ProductResource

This command will generate a new file at app/Http/Resources/ProductResource.php. Open the newly created ProductResource.php file located in app/Http/Resources. Update the toArray method to customize the JSON response for the Product model. For example:

public function toArray(Request $request): array
{
	return [
		'name' => $this->name,
		'description' => $this->description,
		'price' => $this->price,
		'image' => asset('storage/product/'.$this->image),
	];
}

Step 11: Create the ProductController

Next, create a controller to handle Product CRUD logic. Run the following command:

php artisan make:controller Api\ProductController

Now, open the app/Http/Controllers/Api/ProductController.php file and add the following methods:

<?php

namespace App\Http\Controllers\Api;

use App\Helper\CommonHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\CreateProductRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use Illuminate\Http\JsonResponse;

class ProductController extends Controller
{
    public function index()
    {
        $products = Product::query()->paginate(5);

        return ProductResource::collection($products);
    }

    public function save(CreateProductRequest $request): JsonResponse
    {
        $data = $request->validated();
        $data['image'] = CommonHelper::uploadFile($request->file('image'), 'product');
        Product::query()->create($data);

        return response()->json(['message' => 'Product create successfuly'], 200);
    }

    public function show($id): JsonResponse|ProductResource
    {
        $product = Product::query()->find($id);
        if (! empty($product)) {
            return new ProductResource($product);
        }

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

    public function update($id, CreateProductRequest $request): JsonResponse
    {
        $data = $request->validated();

        $product = Product::query()->where('id', $id)->first();
        if (! empty($product)) {
            if ($request->hasFile('image')) {
                $data['image'] = CommonHelper::uploadFile($request->file('image'), 'product', $product->image);
            }
            $product->update($data);

            return response()->json(['message' => 'Product update successfully'], 200);
        }

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

    public function delete($id): JsonResponse
    {
        $product = Product::where('id', $id)->first();

        if (! empty($product)) {
            CommonHelper::removeOldFile('public/product/'.$product->image);
            $product->delete();

            return response()->json(['message' => 'Product delete successfully'], 200);
        }

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

Next, Create the Form request file to handle the incoming request. Run following command to create file

php artisan make:request Api\CreateProductRequest

Update app/Http/Request/Api/CreateProductRequest.php File:

public function authorize(): bool
{
	return true;
}

/**
 * Get the validation rules that apply to the request.
 *
 * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
 */
public function rules(): array
{
	$rules = [
		'name' => ['required', 'string', 'max:255'],
		'description' => ['required', 'string'],
		'price' => ['required', 'numeric'],
		'image' => ['required', File::types(['jpeg', 'jpg', 'png', 'webp'])->max(1 * 500)],
	];

	if (in_array($this->method(), ['PUT', 'PATCH'])) {
		$rules['name'][0] = 'sometimes';
		$rules['description'][0] = 'sometimes';
		$rules['price'][0] = 'sometimes';
		$rules['image'] = array_merge(['sometimes'], CommonHelper::getFileValidationRule('image', ['jpeg', 'jpg', 'png', 	'webp'], (1 * 500)));
	}

	return $rules;
}

Step 12: Create CommonHelper File

Create CommonHelper.php file in app\Helper directory.

update the app\Helper\CommonHelper.php file:

<?php

namespace App\Helper;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\File;

class CommonHelper
{
    public static function uploadFile(UploadedFile $file, $path, $oldFile = ''): string
    {
        if (! empty($file)) {
            // Remove Old file
            if (! empty($oldFile)) {
                Storage::delete('public/'.$path.'/'.$oldFile);      // Delete file from local
            }

            // Upload image
            $path = $file->store('public/'.$path);
            $parts = explode('/', $path);

            return end($parts);
        }

        return '';
    }

    public static function removeOldFile($oldFile): void
    {
        // Remove Old file
        if (! empty($oldFile)) {
            // Storage::disk('s3')->delete($oldFile);    // Delete file from s3
            Storage::delete($oldFile);                      // Delete file from local
        }
    }

    public static function getFileValidationRule(string $key, $types, $size = (1 * 500)): array
    {
        if (request()->hasFile($key)) {
            return [File::types($types)->max($size)];
        }

        return ['string'];
    }
}

Step 13: Change File storing path private to public

Change the value of key drive in 'local' array in disk array storage_path('app/private') to storage_path('app').

'local' => [
	'driver' => 'local',
	'root' => storage_path('app'),
	'serve' => true,
	'throw' => false,
	'report' => false,
],

now link storage to public directory.

php artisan storage:link

Step 14: Test Your API

Now that your API is set up, it’s time to test it! You can use tools like Postman or cURL to send requests to your API endpoints and verify their functionality.

  1. Register a User
    • Endpoint: POST /api/register
    • Body:

      {
        "name": "Test User",
        "email": "test@gmail.com",
        "password": "password",
        "password_confirmation": "password"
      }
  2. Login
    • Endpoint: POST /api/login
    • Body:

      {
        "email": "test@gmail.com",
        "password": "password"
      }
  3. Access Authenticated Route
    • Endpoint: GET /api/user
    • Header:

      Authorization: Bearer your-api-token
  4. Logout
    • Endpoint: POST /api/logout
    • Header:

      Authorization: Bearer your-api-token

Product CRUD Operations

  1. List All Products
    • Endpoint: GET /api/a_products
    • Header:

      Authorization: Bearer your-api-token
  2. Create a New Product
    • Endpoint: POST /api/a_products/save
    • Header:

      Authorization: Bearer your-api-token
    • Body:

      {
        "name": "Test Product",
        "description": "Description of the product without any length limitation.",
        "price": 200,
        "image": "anyimage.jpg"
      }
  3. Show Product Details
    • Endpoint: GET /api/a_products/{id}/show
    • Header:

      Authorization: Bearer your-api-token
  4. Update a Product
    • Endpoint: PATCH /api/a_products/{id}/update
    • Header:

      Authorization: Bearer your-api-token
    • Body:

      {
        "name": "Updated Product Name",
        "description": "Updated description of the product.",
        "price": 250,
        "image": "updatedimage.jpg"
      }
    • Note: If the PATCH method doesn’t work in your API call, you can use POST with a hidden input _method='PATCH'.
  5. Delete a Product
    • Endpoint: DELETE /api/a_products/{id}/delete
    • Header:

      Authorization: Bearer your-api-token

Testing Tips

  • Use Postman or any API testing tool to send requests to the endpoints.
  • Ensure you include the Authorization header with the Bearer token for authenticated routes.
  • Verify the responses to confirm that your API is working as expected.

By following these steps, you can thoroughly test your API and ensure that all endpoints are functioning correctly. This will help you identify and fix any issues before deploying your application.

Conclusion

Congratulations! You’ve successfully implemented JWT-based authentication in your Laravel 12 API. This setup provides a secure and scalable way to handle user authentication for your application. You can further enhance this by adding features like token refreshing, role-based access control, and more.

authentication.

Follow us for more Laravel tutorials and tips! Happy coding! 🚀

Do you Like?