Laravel 12 API Authentication using JWT: Tutorial with Example

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:
- Header: Contains metadata about the token, such as the signing algorithm.
- Payload: Contains the claims, which are statements about an entity (typically, the user) and additional data.
- Signature: Used to verify that the token was not tampered with.
Prerequisites
Before we begin, ensure you have the following installed:
- Laravel 12 installed on your local machine.
- A database configured in your
.env
file. - 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.
- Register a User
- Endpoint: POST /api/register
Body:
{ "name": "Test User", "email": "test@gmail.com", "password": "password", "password_confirmation": "password" }
- Login
- Endpoint: POST /api/login
Body:
{ "email": "test@gmail.com", "password": "password" }
- Access Authenticated Route
- Endpoint: GET /api/user
Header:
Authorization: Bearer your-api-token
- Logout
- Endpoint: POST /api/logout
Header:
Authorization: Bearer your-api-token
Product CRUD Operations
- List All Products
- Endpoint: GET /api/a_products
Header:
Authorization: Bearer your-api-token
- 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" }
- Show Product Details
- Endpoint: GET /api/a_products/{id}/show
Header:
Authorization: Bearer your-api-token
- 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'.
- 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! 🚀