Home / Blog / JWT Authentication in Laravel: Step-by-Step Guide with Code
Web Development

JWT Authentication in Laravel: Step-by-Step Guide with Code

AJAjish Stephen August 7, 2026 9 min read
JWT Authentication in Laravel: Step-by-Step Guide with Code

JWT (JSON Web Token) authentication is one of the most common ways to secure a Laravel API — especially when the API needs to be consumed by a separate frontend, a mobile app, or a third-party service. This guide walks through the full setup using tymon/jwt-auth, the most widely used JWT package for Laravel, with working code at every step.

Step 1: Install the package

Pull in the package via Composer:

composerrequire tymon/jwt-auth

Step 2: Publish config and generate the secret

Publish the package config, then generate a signing secret:

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

This adds a JWT_SECRET key to your .env file — this is what signs and verifies every token, so it should never be shared or committed.

Step 3: Update the User model

Implement JWTSubject on your User model so JWT knows how to identify the authenticated user:

namespaceApp\Models;

useIlluminate\Foundation\Auth\User as Authenticatable;
useTymon\JWTAuth\Contracts\JWTSubject;

classUserextendsAuthenticatableimplementsJWTSubject
{
  public functiongetJWTIdentifier()
  {
    return$this->getKey();
  }

  public functiongetJWTCustomClaims()
  {
    return[];
  }
}

Step 4: Set the JWT guard

In config/auth.php, point the API guard at JWT:

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

Step 5: Build the auth controller

This handles register, login, logout, refresh, and the current-user endpoint:

namespaceApp\Http\Controllers;

useApp\Models\User;
useIlluminate\Http\Request;
useIlluminate\Support\Facades\Auth;
useIlluminate\Support\Facades\Hash;

classAuthControllerextendsController
{
  public functionregister(Request$request)
  {
    $validated=$request->validate([
      'name'=>'required|string|max:255',
      'email'=>'required|email|unique:users',
      'password'=>'required|min:8',
    ]);

    $user= User::create([
      'name'=>$validated['name'],
      'email'=>$validated['email'],
      'password'=>Hash::make($validated['password']),
    ]);

    $token= Auth::guard('api')->login($user);

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

  public functionlogin(Request$request)
  {
    $credentials=$request->only('email','password');

    if(!$token= Auth::guard('api')->attempt($credentials)) {
      returnresponse()->json(['error'=>'Unauthorized'], 401);
    }

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

  public functionme()
  {
    returnresponse()->json(Auth::guard('api')->user());
  }

  public functionlogout()
  {
    Auth::guard('api')->logout();
    returnresponse()->json(['message'=>'Successfully logged out']);
  }

  public functionrefresh()
  {
    return$this->respondWithToken(Auth::guard('api')->refresh());
  }

  protected functionrespondWithToken($token)
  {
    returnresponse()->json([
      'access_token'=>$token,
      'token_type'=>'bearer',
      'expires_in'=>Auth::guard('api')->factory()->getTTL() * 60,
    ]);
  }
}

Step 6: Add the routes

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']);
});

Add these inside routes/api.php, not routes/web.php — remember the lesson from earlier in this project: routes in api.php get the api middleware group automatically, which JWT relies on for stateless authentication without session/CSRF handling.

Testing it

Test the login endpoint with curl:

curl-X POST http://localhost:8000/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password"}'

A successful response returns an access_token — attach it to future requests as Authorization: Bearer <token>.

Building this into a larger project, or need help structuring an API that will actually scale? This is exactly the kind of work I do as part of web development engagements.

Common questions

Should I use JWT or Laravel Sanctum for API authentication?
Sanctum is the better default for most Laravel apps — it's simpler and built for first-party SPAs and mobile apps talking to your own backend. JWT makes more sense when multiple separate services need to verify a token independently without querying your database, or when you're integrating with a non-Laravel backend that expects standard JWT.
Where should I store the JWT on the client side?
For web apps, an httpOnly cookie is safer than localStorage since it isn't accessible to JavaScript and reduces XSS risk. For mobile apps, secure device storage (Keychain on iOS, Keystore on Android) is the standard approach. Avoid plain localStorage if you can help it.
How long should a JWT token live before expiring?
Short-lived access tokens (15–60 minutes) paired with a longer-lived refresh token is the standard pattern. This limits the damage window if a token is compromised, while the refresh token lets the user stay logged in without re-entering credentials constantly.
Do I need to invalidate JWTs on logout?
Yes, if you want logout to be immediate and secure. JWTs are stateless by design, so a token remains technically valid until it expires unless you explicitly blacklist it. tymon/jwt-auth includes a blacklist feature for exactly this — call it on logout so the token can't be reused even if it's intercepted.
Building an API that needs real authentication?
I build secure, well-structured Laravel APIs for real projects, not just tutorials.
Explore Web Development →
© Copyright 2024 Ajish Stephen