codecasts/laravel-jwt
JWT authentication for Laravel: generate and validate JSON Web Tokens, protect routes with auth middleware, and add stateless token-based authentication to APIs and apps. A lightweight integration for securing Laravel endpoints with configurable JWT guards.
composer require codecasts/laravel-jwt
config/app.php if your Laravel version doesn't auto-discover it:
// providers
Codecasts\JWT\Providers\LaravelServiceProvider::class,
// aliases
'JWTAuth' => Codecasts\JWT\Facades\JWTAuth::class,
php artisan vendor:publish --provider="Codecasts\JWT\Providers\LaravelServiceProvider"
# .env
JWT_SECRET=base64:... # long, random, same value across app servers
config/auth.php:
'guards' => [
'api' => ['driver' => 'jwt', 'provider' => 'users'],
],
app/Http/Kernel.php (jwt.auth, jwt.refresh are the names to look for first).Route::post('auth/login', function (Request $request) {
$token = JWTAuth::attempt($request->only('email', 'password'));
if (! $token) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
return response()->json(compact('token'));
});
Route::get('me', fn () => auth('api')->user())->middleware('jwt.auth');
If auth('api')->user() returns the model from the Bearer header, the integration is sound — everything else is layering.Dedicated auth controller, not closures. Keep token issuance in a small controller (LoginController, RefreshController, LogoutController) so you can unit-test issuance and swap the transport later.
class AuthController extends Controller
{
public function login(Request $request)
{
$token = JWTAuth::attempt($request->only('email', 'password'));
return $token
? response()->json(['token' => $token, 'token_type' => 'bearer'])
: response()->json(['error' => 'invalid_credentials'], 401);
}
}
Read the user through the guard, not through the token. The idiomatic path is auth('api')->user() inside controllers and policies; it resolves the token from the request, validates it, and loads the User provider. Use JWTAuth::parseToken()->authenticate() only when you need the token object itself (e.g., to invalidate).
Stateless login/logout endpoints. Login returns a token, logout invalidates the presented token:
public function logout()
{
JWTAuth::invalidate(JWTAuth::getToken());
return response()->noContent();
}
Because the guard is stateless there is no session to clear — logout is purely a token operation.
Refresh with explicit endpoints, not jwt.refresh middleware everywhere. The jwt.refresh middleware swaps the old token for a new one on each request and (typically) returns it in a response header. That's convenient for internal SPAs but produces a token stream that's hard to debug and painful for mobile clients. Prefer a POST /auth/refresh that returns {token: ...} and let clients call it near expiry.
Protect groups of routes with the middleware, then lean on policies. Don't sprinkle token checks in controllers:
Route::middleware('jwt.auth')->group(function () {
Route::apiResource('posts', PostController::class);
Route::get('me', fn () => auth('api')->user());
});
Authorize against $request->user() in policies exactly as you would with session auth.
Multi-tenancy / multiple identities. Add a second jwt guard with a different provider (e.g., admins) when you need separate token audiences; the guard driver is generic over the provider, so this is cheaper than building a parallel auth system.
Testing. In PHPUnit, bypass the HTTP login flow and mint tokens directly:
$token = JWTAuth::fromUser(User::factory()->create());
$this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/me')
->assertOk();
This keeps feature tests fast and independent of password hashing.
Integration tip — token transport. Clients send Authorization: Bearer <token>. If you also want cookie support for browsers, set the header from a cookie in a tiny middleware rather than teaching the guard about cookies.
tymon/jwt-auth, Laravel Sanctum, or Passport instead.secret is required and must be identical across nodes. A missing or per-node JWT_SECRET produces tokens that validate locally and fail behind a load balancer — the classic "works on my machine" auth bug. Set it in .env and never commit a fallback.ttl is in minutes in this package's config. Read the published config file rather than assuming seconds; a 60 there means one hour, and clock skew between issuer and verifier can cause borderline 401s.invalidate() rejects the presented token from being reused by your code, but there's no blacklist: a stolen unexpired token remains valid until exp. If you need immediate revocation (password change, logout-everywhere), keep short TTLs and add a jti/version column check in the guard, or move to a package with a blacklist.Authorization header must be well-formed. Bearer with the wrong case, an empty value, or a proxy stripping the header yields "token not provided" rather than "invalid token" — differentiate those errors in your exception handler so client debugging isn't a guessing game.app/Exceptions/Handler.php and return 401 JSON consistently instead of an HTML error page to an API client.config/jwt.php, run php artisan config:clear in dev; a cached config in production will silently ignore your .env change until config:cache is re-run.OPTIONS requests won't carry the Authorization header; exclude them from jwt.auth or the browser SPA will see failed preflights as auth failures.jwt.refresh middleware, ensure the refreshed token is returned to the client in a documented header/form — otherwise clients keep replaying the old token until it hard-expires.How can I help you explore Laravel packages today?