Reader Stacks

JWT Authentication in Laravel: Setup and Protected Routes

JWT auth trades Sanctum's simpler cookie/token model for a fully stateless token the server never stores — useful specifically for a mobile app or third-party API consumer, not typical for a first-party web app.

JWT Authentication in Laravel: Setup and Protected Routes

JWT (JSON Web Token) authentication issues a self-contained, stateless token that the server never needs to store — a genuinely different model from Sanctum's session/token-database approach, and one that specifically suits a mobile app or third-party API consumer.

Installing the tymon/jwt-auth package

composer require tymon/jwt-auth
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret

jwt:secret generates the signing key Laravel uses to create and verify tokens — this key must stay private and consistent, since rotating it invalidates every previously issued token immediately.

Configuring the User model

use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

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

Setting the guard to jwt

// config/auth.php
'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

The login endpoint, issuing a token

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

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

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

Protecting routes with the JWT middleware

Route::middleware('auth:api')->group(function () {
    Route::get('/user', function (Request $request) {
        return auth('api')->user();
    });
});

Sending the token from the client

fetch('/api/user', {
    headers: {
        'Authorization': `Bearer ${token}`,
    }
});

Refreshing an expiring token

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

Since a JWT is stateless and self-verifying (rather than checked against a database record), the server can't simply "extend" an existing token's expiry — refresh() instead issues a genuinely new token, typically called by the client shortly before the current one expires.

Logging out (invalidating the token)

public function logout()
{
    auth('api')->logout();
    return response()->json(['message' => 'Logged out']);
}

JWT vs. Sanctum: when each one actually fits

Sanctum (covered elsewhere on this site) suits a first-party SPA or simple mobile app talking to its own backend, with simpler setup and revocable tokens stored in the database — JWT suits a scenario needing a genuinely stateless, self-contained token, such as authenticating across multiple independent services that shouldn't all need direct database access to verify a token's validity.