Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13 JWT API Authentication Tutorial: Login, Refresh and Logout

๐Ÿ› ๏ธ Laravel Tutorials5 min read

Laravel 13 JWT API Authentication Tutorial: Login, Refresh and Logout

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 10, 2026
Laravel 13 JWT API Authentication Tutorial: Login, Refresh and Logout

To add JWT authentication to a Laravel 13 API, install a maintained JWT package such as php-open-source-saver/jwt-auth, generate a secret with php artisan jwt:secret, make the User model implement JWTSubject, and set the api guard driver to jwt. Login then returns a signed token that clients send as a Bearer header.

This article targets Laravel 13 on PHP 8.3 to 8.5. Before installing, open the package page on Packagist and confirm the latest release lists support for your Laravel version. The code below was written for this article and not executed in a live project.

What is JWT authentication and when should you use it?

A JSON Web Token (JWT) is a signed string that carries claims such as the user id and an expiry time. The server verifies the signature on every request instead of looking up a session. JWT suits stateless APIs and mobile clients. If you only need simple first-party tokens, Laravel Sanctum is easier. If you need a full OAuth2 server, use Passport.

Prerequisites

  • PHP 8.3+, Composer, and a Laravel 13 project with a database.
  • API routes enabled. In a fresh Laravel 13 app run php artisan install:api to create routes/api.php. If it asks about Sanctum, you can answer no only if you do not need it.

Step 1: How do you install the package?

Code
composer require php-open-source-saver/jwt-auth
php artisan vendor:publish --provider="PHPOpenSourceSaver\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret

The publish step creates config/jwt.php. The jwt:secret command writes JWT_SECRET to your .env. Keep that value private and never commit it.

Step 2: How do you update the User model?

The model must implement JWTSubject, which tells the package what to store in the token.

JavaScript
namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use PHPOpenSourceSaver\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    protected $fillable = ['name', 'email', 'password'];
    protected $hidden = ['password', 'remember_token'];

    protected function casts(): array
    {
        return ['password' => 'hashed'];
    }

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

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

Step 3: How do you configure the auth guard?

In config/auth.php, add an api guard that uses the jwt driver. You can also make it the default guard if the app is API only.

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

Step 4: How do you create register, login and logout endpoints?

Generate the controller: php artisan make:controller Api/AuthController.

JavaScript
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class AuthController extends Controller
{
    public function register(Request $request): JsonResponse
    {
        $data = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);

        $user = User::create($data);
        $token = auth('api')->login($user);

        return $this->tokenResponse($token, 201);
    }

    public function login(Request $request): JsonResponse
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        if (! $token = auth('api')->attempt($credentials)) {
            return response()->json(['message' => 'Invalid credentials.'], 401);
        }

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

    public function me(): JsonResponse
    {
        return response()->json(auth('api')->user());
    }

    public function refresh(): JsonResponse
    {
        return $this->tokenResponse(auth('api')->refresh());
    }

    public function logout(): JsonResponse
    {
        auth('api')->logout();

        return response()->json(['message' => 'Logged out.']);
    }

    private function tokenResponse(string $token, int $status = 200): JsonResponse
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth('api')->factory()->getTTL() * 60,
        ], $status);
    }
}

attempt() checks the credentials and returns a token on success. getTTL() returns minutes, so multiplying by 60 gives seconds.

Step 5: How do you protect the routes?

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

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('/refresh', [AuthController::class, 'refresh']);
    Route::post('/logout', [AuthController::class, 'logout']);
});

How do you test it with curl?

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

curl http://localhost:8000/api/me \
  -H "Accept: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN"

How long does a token live?

The lifetime comes from JWT_TTL in minutes (the package default is 60). The refresh window is controlled by JWT_REFRESH_TTL. A short TTL with refresh is safer than a long-lived token, because a stolen token expires sooner. With the blacklist enabled, logout invalidates the token before it expires.

Common mistakes

  • Missing jwt:secret. Without a secret, every login throws a signing error.
  • Guard driver left as session. Then auth('api')->attempt() returns a boolean, not a token string.
  • Storing tokens in localStorage on a web front end. Any XSS bug can steal them; prefer secure, HTTP-only cookies for browser apps.
  • Putting secrets in custom claims. JWT payloads are encoded, not encrypted; anyone holding the token can read them.
  • No Accept header. Send Accept: application/json so errors come back as JSON.

How do you verify it works?

  1. Register a user and check for a 201 with an access_token.
  2. Call /api/me with the Bearer token and expect the user JSON.
  3. Call it with a modified token and expect 401.
  4. Call /api/logout, then reuse the token and expect 401.

FAQ

Which JWT package should I use?

Use an actively maintained fork such as php-open-source-saver/jwt-auth, and confirm it supports your Laravel version on Packagist.

JWT or Sanctum?

Sanctum for simple first-party apps, JWT when clients need self-contained signed tokens, Passport for OAuth2.

Can I add custom claims?

Yes. Return them from getJWTCustomClaims(), but keep them small and non-sensitive.

References

Article Index & Topic Tags
LaravelAuthenticationApiJwt

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.