Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Jwt Auth Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. 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.

  2. Publish config and set the secret.

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret   # writes JWT_SECRET to .env
  1. Wire the guard. In config/auth.php, add a JWT driver guard and make it the API default:
'guards' => [
    'api' => ['driver' => 'jwt', 'provider' => 'users'],
],
  1. Implement the contract on your User model — this is the one thing people miss first:
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    public function getJWTIdentifier() { return $this->getKey(); }
    public function getJWTCustomClaims() { return []; }
}
  1. First use case — stateless login → protected route. This is the 15-minute path to a working API:
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.

Implementation Patterns

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:

  • Static per-user: return them from getJWTCustomClaims() — they're baked into every token for that user.
  • Per-request: 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.

Gotchas and Tips

  • 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.
  • Clients can't tell "expired" from "invalid" by status code alone. 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.
  • Blacklist needs a shared, persistent cache. The default 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.
  • Empty result from 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.
  • Long-lived workers and Octane. The facade wraps a singleton that caches the parsed token. In queued jobs, Octane workers, or tests reusing the container, call JWTAuth::unsetToken()/parseToken() explicitly instead of relying on ambient state — otherwise a token from request N can leak into request N+1.
  • Token bloat. Custom claims inflate the token; header size limits (and some proxies at 8 KB) bite fast if you stuff permissions into claims. Store ids and look up the rest.
  • CORS for the refresh middleware. If you use the refresh-on-response-header pattern, the browser needs Access-Control-Expose-Headers: Authorization, or JS silently never sees the new token.
  • Extension points worth knowing: bind your own 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).
  • Lumen specifics: register the service provider in bootstrap/app.php, copy config/jwt.php manually, and add the auth middleware + facade alias yourself — nothing is auto-discovered.
  • Consider whether you need it. If your clients are first-party SPAs or mobile apps you also control, Sanctum's cookie/stateful flow is less surface area. This package earns its keep for stateless, multi-service, cross-domain APIs where the token is the contract.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle