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.