Why Use JWT Authentication in Laravel 11? (And When to Choose It Over Sanctum)
With Laravel's official packages like Sanctum and Passport available, you might wonder: why bother with JWT in 2026? The answer lies in specific use cases where stateless, self-contained tokens provide clear advantages.
JWT authentication excels in scenarios requiring true statelessness—no database lookups or session storage needed on every request. The token itself contains signed claims (user ID, expiration, roles, etc.), allowing any server with the secret key (or public key in asymmetric setups) to validate it independently. This makes JWT ideal for:
- Microservices architectures — Services can authenticate requests across domains without shared sessions or centralized auth servers.
- Cross-origin / third-party integrations — Mobile apps, desktop clients, or external APIs can securely consume your Laravel endpoints without CSRF worries or cookie limitations.
- High-scale, distributed systems — Horizontal scaling becomes seamless since no server needs to query a session store or Redis for every API call, reducing latency and infrastructure costs.
- Decoupled frontends — When your Laravel API serves completely separate clients (React Native, Flutter, Angular, etc.) that demand full token control.
Compared to Laravel Sanctum (the go-to for most first-party SPAs and simple token auth in 2026), Sanctum is simpler, officially supported, and handles both stateful (cookie-based) and stateless (API token) modes with built-in features like token abilities and easy revocation. Sanctum shines for Laravel + SPA/mobile combos where you want tight integration and less boilerplate.
However, Sanctum tokens are opaque (not self-contained JWTs), often requiring database checks for validation and revocation. JWTs trade that for full statelessness and standardization—perfect when you need pure REST compliance, interoperability with non-Laravel systems, or maximum performance in load-balanced environments. Many developers still prefer the php-open-source-saver/jwt-auth fork for its maturity, refresh token support, and blacklisting capabilities when stateless revocation is required.
In short: choose Sanctum for speed and simplicity in most Laravel projects; opt for JWT when statelessness, scalability across services, or strict JWT compliance matters most. This tutorial focuses on the JWT path so you can master a timeless, flexible auth strategy that powers countless production APIs today.

Table Of Content
1 Prerequisites
- PHP 8.2+
- Composer
- MySQL or compatible database
- Basic knowledge of Laravel routing, controllers, and Eloquent
2 Introduction
In the fast-evolving world of web development, building secure and scalable RESTful APIs is essential for modern applications—whether you're powering mobile apps, single-page applications (SPAs), microservices, or third-party integrations. Laravel 11 continues to shine as one of the most developer-friendly PHP frameworks, but when it comes to API authentication, choosing the right method can make or break your project's performance and maintainability.
JSON Web Tokens (JWT) provide a powerful, stateless authentication solution that has stood the test of time. Unlike traditional session-based auth, JWTs allow your Laravel backend to verify user identity without storing session data on the server. This makes scaling horizontally effortless across multiple servers or cloud instances. In this comprehensive Laravel 11 JWT authentication tutorial, we'll walk you through every step to create a production-ready RESTful API complete with user registration, login, token generation, refresh mechanisms, protected endpoints, and secure logout. We'll use the actively maintained php-open-source-saver/jwt-auth package to ensure compatibility and reliability in 2026.
By the end, you'll have a fully functional API ready for real-world use, plus insights into best practices for security, token management, and performance.
3 Create / Install a Laravel Project
3.1 Install Laravel Project
composer create-project laravel/laravel laravel11-jwt-app
Navigate to your project directory:
cd laravel11-jwt-app
3.2 Configure MySql Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_jwt
DB_USERNAME=root
DB_PASSWORD=
4 Install API Scaffold
php artisan install:api
If the user is not authenticated, an exception will be triggered. Update the bootstrap/app.php file to handle this:
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
$exceptions->renderable(function (Illuminate\Auth\AuthenticationException $e, $request) {
if ($request->is('api/*')) {
return response()->json(['message' => 'Unauthenticated.'], 401);
}
});
})->create();
5 Install and Configure JWT Package
Install the package:
composer require php-open-source-saver/jwt-auth
Publish the JWT configuration file:
php artisan vendor:publish --provider="PHPOpenSourceSaver\JWTAuth\Providers\LaravelServiceProvider"
Generate a secret key to sign the JWT:
php artisan jwt:secret
This will update the .env file with:
JWT_SECRET=xxxxxxxx
Configure the config/auth.php file to use the JWT guard:
return [
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
'hash' => false,
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],
'password_timeout' => 10800,
];
Update config/jwt.php (optional, for TTL as integer):
'ttl' => (int) env('JWT_TTL', 60),
6 Modify the User model
<?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 Laravel\Sanctum\HasApiTokens;
use PHPOpenSourceSaver\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
}
7 Create Controller - AuthController
Use the following artisan command to Create Controller.
php artisan make:controller AuthController
Add the following methods: register, login, logout, me, and refresh.
Key Methods in JWTAuthController:
register(): Handles user registration by validating the input, creating a user, and generating a JWT token for the newly created user.
login(): Authenticates a user using email and password, and returns a JWT token if the credentials are valid.
me(): Retrieves the currently authenticated user by parsing the JWT token from the request.
logout(): Invalidates the JWT token, effectively logging the user out.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\Models\User;
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
use PHPOpenSourceSaver\JWTAuth\Exceptions\JWTException;
class AuthController extends Controller
{
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password),
]);
$token = JWTAuth::attempt(['email' => $request->email, 'password' => $request->password]);
return response()->json([
'message' => 'User registered successfully',
'user' => $user,
'token' => $token,
], 201);
}
public function login(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|string|email',
'password' => 'required|string|min:6',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
if (!$token = JWTAuth::attempt($request->only('email', 'password'))) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
public function me()
{
return response()->json(auth()->user());
}
public function logout()
{
try {
JWTAuth::invalidate(JWTAuth::getToken());
return response()->json(['message' => 'Successfully logged out']);
} catch (JWTException $e) {
return response()->json(['error' => 'Failed to logout, please try again'], 500);
}
}
public function refresh()
{
try {
$newToken = JWTAuth::refresh(JWTAuth::getToken());
return $this->respondWithToken($newToken);
} catch (JWTException $e) {
return response()->json(['error' => 'Failed to refresh token'], 500);
}
}
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => config('jwt.ttl') * 60, // in seconds
]);
}
}
?>
8 Define API Endpoints in routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
Route::prefix('auth')->group(function () {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware('auth:api')->group(function () {
Route::get('me', [AuthController::class, 'me']);
Route::post('logout', [AuthController::class, 'logout']);
Route::post('refresh', [AuthController::class, 'refresh']);
});
});
// Example protected route
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
9 Folder Structure
10 Run Laravel Server to Test the App
To test the Laravel REST API Authentication Using JWT, set the request header to Accept: application/json and include the JWT token as a Bearer token for authenticated routes. Start the server using:
Use the following artisan command to Test the App.
php artisan serve
Test with Postman/cURL:
1.Register:- POST http://127.0.0.1:8000/api/auth/register
- Body: {"name": "Test User", "email": "test@example.com", "password": "password", "password_confirmation": "password"}
- POST http://127.0.0.1:8000/api/auth/login
- Body: {"email": "test@example.com", "password": "password"}
- Response: Includes access_token
- GET http://127.0.0.1:8000/api/auth/me
- Headers: Authorization: Bearer {access_token}
- POST http://127.0.0.1:8000/api/auth/refresh
- Headers: Authorization: Bearer {access_token}
- POST http://127.0.0.1:8000/api/auth/logout
- Headers: Authorization: Bearer {access_token}
11 Conclusion
Written by Revathi M
PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel
Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.
Frequently Asked Questions
The tutorial uses php-open-source-saver/jwt-auth, an actively maintained fork of the deprecated tymondesign/jwt-auth package.
Run composer require php-open-source-saver/jwt-auth, then php artisan vendor:publish to publish the config, and php artisan jwt:secret to generate JWT_SECRET in .env.
In config/auth.php, set the 'api' guard driver to 'jwt' and provider to 'users'.
Implement the JWTSubject interface by adding getJWTIdentifier() (returns $this->getKey()) and getJWTCustomClaims() (returns empty array) methods.
The register method validates name, email (unique), and password, creates the user with bcrypt, logs them in with auth()->login($user), and returns the user with a JWT token.
Use the 'auth:api' middleware on routes in routes/api.php. The JWT guard automatically validates the Bearer token in the Authorization header.
POST /auth/register, POST /auth/login, POST /auth/logout, POST /auth/me (protected), and POST /auth/refresh (for token refresh).
Call the refresh endpoint, which uses auth()->refresh() to generate and return a new token.
In bootstrap/app.php, handle AuthenticationException to return a JSON response with 401 status for API requests.
Yes, it uses signed tokens with JWT_SECRET, bcrypt passwords, validation, and server-side logout. Use HTTPS in production and consider token blacklisting for enhanced security.
