Laravel JWT: Stateless Authentication for Your APIs
Sanctum covers most SPA and mobile auth needs. But when your API must be verified independently across services without a shared session, JWT is the right tool. Here is how to implement it properly.
Sanctum handles the majority of Laravel auth needs cleanly,SPA tokens, mobile tokens, simple API tokens. JWT solves a narrower, specific problem: verifying a token without hitting a shared database or session store. If you do not have that problem, JWT adds complexity for nothing.
JWT vs Sanctum: when to actually reach for JWT
A Sanctum token is a database row,every request does a lookup to confirm it is still valid, which also makes revocation instant and simple. A JWT is self-contained: its signature alone proves validity, so any service holding the public key (or shared secret) can verify it without touching your database. That matters when multiple independent services need to verify the same token.
If you have one Laravel app talking to a single frontend, Sanctum is simpler and safer. Reach for JWT only when multiple independent services need to verify tokens without a shared session store.
Anatomy of a JWT
A JWT has three parts: a header (signing algorithm), a payload (claims,subject, expiration, custom data), and a signature. The signature prevents tampering, but the payload is only base64-encoded, not encrypted. Never put secrets or sensitive data in JWT claims,anyone can decode and read them.
Implementing it with tymon/jwt-auth
// config/auth.php
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
// AuthController.php
public function login(LoginRequest $request)
{
if (! $token = auth('api')->attempt($request->validated())) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return response()->json([
'access_token' => $token,
'expires_in' => auth('api')->factory()->getTTL() * 60,
]);
}Refresh tokens: the part people get wrong
Keep access tokens short-lived (15 minutes to 1 hour) and pair them with a longer-lived refresh token. Rotate the refresh token on every use,issuing a new one and invalidating the old,so a stolen refresh token can only be used once before the legitimate user's next refresh reveals the theft.
public function refresh()
{
$newToken = auth('api')->refresh();
return response()->json([
'access_token' => $newToken,
'expires_in' => auth('api')->factory()->getTTL() * 60,
]);
}Security pitfalls to avoid
- ✓Algorithm confusion attacks: always pin the expected algorithm server-side, never trust the alg header from the token itself.
- ✓Revocation is hard: a stolen JWT stays valid until it expires. Keep access token lifetimes short and maintain a blocklist for refresh tokens only.
- ✓Never store JWTs in localStorage for browser apps,use httpOnly cookies to avoid XSS token theft.
- ✓Always validate exp, iss, and aud claims, not just the signature.
JWT solves one specific problem: stateless verification across independent services. If that is not your architecture, Sanctum's simplicity,and easy revocation,wins every time.
Need help with this topic? REST API Design
Discover this service →