gabmaxs/jwt-auth
JWT authentication for Laravel and Lumen. Provides token generation, validation, refresh, and invalidation with middleware integration, configurable claims, and support for multiple guards. Simplifies securing APIs with stateless, standards-compliant JSON Web Tokens.
Install and confirm you have the right package. The repo link points at tymondesigns/jwt-auth while the package name is gabmaxs/jwt-auth — check the fork's composer.json, then composer require gabmaxs/jwt-auth (or tymon/jwt-auth if you are on upstream). Laravel 5.5+ auto-discovers Tymon\JWTAuth\Providers\LaravelServiceProvider.
Publish config and set the secret.
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret # writes JWT_SECRET to .env
config/auth.php, add a JWT driver guard and make it the API default:'guards' => [
'api' => ['driver' => 'jwt', 'provider' => 'users'],
],
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
public function getJWTIdentifier() { return $this->getKey(); }
public function getJWTCustomClaims() { return []; }
}
Route::post('/login', fn (Request $r) => response()->json([
'token' => auth('api')->attempt($r->only('email', 'password')),
]));
Route::middleware('auth:api')->get('/me', fn (Request $r) => $r->user());
If attempt() returns false, credentials are wrong; if it throws, you're usually missing JWT_SECRET or the contract. Start by reading config/jwt.php (ttl, refresh_ttl, blacklist_enabled, providers, parsers) — nearly every "why doesn't it work" question resolves there.
Full auth controller shape. Keep token issuance in one place so refresh/logout semantics stay consistent:
public function login(Request $request) {
$token = auth('api')->attempt($request->only('email','password'));
if (! $token) return response()->json(['error' => 'invalid_credentials'], 401);
return $this->respondWithToken($token);
}
public function refresh() { return $this->respondWithToken(auth('api')->refresh()); }
public function logout() { auth('api')->logout(); return response()->json(['ok' => true]); }
protected function respondWithToken($token) {
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60,
]);
}
Route middleware. auth:api for protected routes. For refresh-on-the-fly, add the package's refresh middleware (Tymon\JWTAuth\Http\Middleware\RefreshToken, historically aliased jwt.refresh) — it re-issues a token and returns it in the Authorization response header, so clients must read it rather than the body. Middleware aliases have changed across major versions, so verify against the changelog for your installed version rather than copying blog snippets.
Custom claims for tenancy/roles. Two options, pick one and be consistent:
getJWTCustomClaims() — they're baked into every token for that user.JWTAuth::claims(['tenant_id' => $id])->attempt($creds). Cheap, but the claim is only as trustworthy as your validation on read: always re-check authorization server-side (JWTAuth::getPayload()->get('tenant_id')), never trust it as the only gate.Multi-guard / multi-audience. Define separate guards (api, admin) with different providers, and add an aud-style claim via a custom getJWTCustomClaims(). Reject cross-audience tokens in a small middleware rather than relying on guard separation alone — the same secret signs both.
Revocation and "logout everywhere". With blacklist_enabled => true, logout()/invalidate() writes the token's jti to cache until refresh_ttl elapses. For per-device revocation you need your own store (device id claim + a revoked_devices table checked in middleware) — the blacklist only knows individual tokens.
Testing. $this->actingAs($user, 'api') works because the JWT guard uses Laravel's GuardHelpers. When you need to exercise real token parsing, mint one: $token = JWTAuth::fromUser($user); then $this->withHeader('Authorization', "Bearer $token"). Avoid withoutMiddleware() — it hides the exact failures you want to catch.
Integration tips. Put auth:api on the route group, not the controller constructor (Laravel 8+ no longer supports controller middleware). For SPAs, prefer returning the token in a body/HttpOnly cookie over URL parameters, and disable the input/query parsers so tokens can't leak into access logs.
JWT_SECRET + config:cache. jwt:secret writes .env only. If config is cached, the new secret isn't picked up until php artisan config:cache runs again. Deploy scripts that cache config before secret generation are a classic outage.ttl ≠ refresh_ttl. ttl (default 60 min) is access-token lifetime; refresh_ttl (default 2 weeks) is how long a token may be exchanged. An expired-but-unblacklisted token can still be refreshed inside the refresh window — but only once, because refresh() blacklists the old one. Concurrent refresh calls from two tabs will race and one will fail with 401.auth:api returns a flat 401. Catch TokenExpiredException vs TokenInvalidException vs TokenBlacklistedException in app/Exceptions/Handler.php and return a machine-readable code so the client knows whether to refresh or re-login.Illuminate storage uses your cache driver; array (and per-node file) breaks revocation across requests/instances. Use Redis or a database cache in any multi-node or load-balanced setup.auth('api')->user() on a valid token almost always means getJWTIdentifier() doesn't return a value present in the provider's table, or lock_subject/pruning deleted the user. The token is fine; the subject lookup isn't.Auth::login() returns nothing useful here. The JWT guard is stateless — use auth('api')->login($user) or JWTAuth::fromUser($user) to get a token. Similarly, $request->user() resolves the default guard; on API routes use auth('api')->user() or set auth.defaults.guard.JWTAuth::unsetToken()/parseToken() explicitly instead of relying on ambient state — otherwise a token from request N can leak into request N+1.Access-Control-Expose-Headers: Authorization, or JS silently never sees the new token.Tymon\JWTAuth\Providers\Storage\Storage implementation for a custom revocation store; swap Tymon\JWTAuth\Providers\User\UserInterface to resolve subjects from something other than Eloquent; extend Tymon\JWTAuth\Http\Parser\Parser to add a custom token source; use JWTFactory::customClaims([...])->make() when you need a token-shaped payload without a user (service-to-service, tests).bootstrap/app.php, copy config/jwt.php manually, and add the auth middleware + facade alias yourself — nothing is auto-discovered.How can I help you explore Laravel packages today?