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 Guard Laravel Package

irazasyed/jwt-auth-guard

A JWT authentication guard for Laravel and Lumen that issues, verifies, and refreshes JSON Web Tokens as a drop-in replacement for session-based guards, with support for custom claims and multiple auth drivers.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you're installing first. irazasyed/jwt-auth-guard is a thin auth-guard adapter on top of tymon/jwt-auth (0.5.*). It was last released in April 2016 targeting Laravel 5.1/5.2 and PHP 5.5.9+. Pin laravel/framework and tymon/jwt-auth explicitly in composer.json before running composer require irazasyed/jwt-auth-guard, because Composer will otherwise pull an incompatible tymon release and the guard will break at boot.

Minimal setup, in order:

  1. composer require irazasyed/jwt-auth-guard (this pulls tymon in as a dependency).
  2. Register Irazasyed\JwtAuthGuard\JwtAuthGuardServiceProvider in config/app.php — there is no package auto-discovery on these Laravel versions.
  3. Publish the underlying JWT config: php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider" and generate a secret with php artisan jwt:secret (writes JWT_SECRET into .env). An empty secret produces TokenInvalidException on every request — this is the #1 first-run failure.
  4. Add the guard in config/auth.php:
'guards' => [
    'api' => [
        'driver'   => 'jwt',
        'provider' => 'users',
    ],
],
  1. Make your User model implement the tymon contract so tokens can be minted:
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    public function getJWTIdentifier() { return $this->getKey(); }
    public function getJWTCustomClaims() { return []; }
}

First use case — a login endpoint that hands back a token. The guard's attempt() returns the JWT string on success and false on failure:

Route::post('api/login', function () {
    $credentials = request()->only('email', 'password');

    if (! $token = Auth::guard('api')->attempt($credentials)) {
        return response()->json(['error' => 'invalid_credentials'], 401);
    }

    return response()->json(compact('token'));
});

Then protect a route with auth:api and send Authorization: Bearer <token>. Auth::guard('api')->user() resolves the user from the token's sub claim via the users provider — no session, no cookies.

Implementation Patterns

Set the API guard as default for an API-only app. If every request is token-authenticated, set 'defaults' => ['guard' => 'api'] in config/auth.php. Then auth()->user(), auth()->attempt() and the plain auth middleware all hit the JWT guard, and you avoid sprinkling guard('api') everywhere. In mixed apps, keep web as default and be explicit — Auth::attempt() routes to the default guard, not the API one.

Protect route groups, not individual routes:

Route::group(['prefix' => 'api', 'middleware' => ['api', 'auth:api']], function () {
    Route::get('me', fn () => response()->json(auth()->user()));
    Route::post('logout', function () {
        auth()->logout(); // blacklists the current token
        return response()->json(['ok' => true]);
    });
});

Refresh flow. refresh() invalidates the presented token (blacklisted) and returns a new one; store it client-side and discard the old one. The window is refresh_ttl (14 days by default) while ttl is 60 minutes — a client should hit /refresh proactively on a 401 with token_expired, not reactively on every failure.

Route::post('api/refresh', function () {
    try {
        return response()->json(['token' => Auth::guard('api')->refresh()]);
    } catch (Tymon\JWTAuth\Exceptions\JWTException $e) {
        return response()->json(['error' => 'token_blacklisted'], 401);
    }
});

Custom claims. Don't try to pass claims through the guard's attempt() — go to the underlying service, which is reachable either via the JWTAuth facade or by __call proxying on the guard:

$token = JWTAuth::customClaims(['role' => $user->role, 'tenant' => $user->tenant_id])
                 ->fromUser($user);

Read them back with JWTAuth::getPayload($token)->get('role'). Keep custom claims small — every claim is re-sent on every request.

Middleware / exception integration. Because the guard plugs into Laravel's native auth, auth:api works as-is, but the stock Authenticate middleware only returns JSON when the request "expects JSON." Make your API clients send Accept: application/json, or override the middleware for API routes and always return the 401 envelope.

Testing. Mint a real token in the test rather than faking the guard:

$token = JWTAuth::fromUser($user);
$this->call('GET', '/api/me', [], [], [], [
    'HTTP_AUTHORIZATION' => 'Bearer '.$token,
    'HTTP_ACCEPT'        => 'application/json',
]);

This exercises the actual guard parsing, which is where most bugs live.

Gotchas and Tips

  • The package is unmaintained (2016). If you're on Laravel 5.5+ with package auto-discovery, Illuminate\Contracts\Auth\Guard no longer declares attempt(), and tymon 1.x renamed namespaces (Tymon\JWTAuth\Facades\JWTAuth, JWTAuth::parseToken() → JWTAuth::getToken()). You cannot upgrade tymon independently — the guard's internals call the 0.5 API. Treat the versions as a fixed unit or migrate to tymon 2.x / Sanctum / Passport.
  • Expired or malformed tokens surface as HTTP 500, not 401. The guard's user() lets TokenExpiredException / TokenInvalidException propagate; Illuminate\Auth\AuthenticationException is the only thing Authenticate catches. Register handlers for Tymon\JWTAuth\Exceptions\JWTException (and the TokenExpiredException subclass) in app/Exceptions/Handler.php and return 401 with a machine-readable code, so clients can distinguish token_expired from token_invalid and decide whether to refresh.
  • Authorization header may disappear. Apache with mod_php/CGI and some PHP-FPM setups strip it. Add SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 (Apache) or fastcgi_param HTTP_AUTHORIZATION $http_authorization (nginx) before you spend an afternoon debugging a guard that "works in Postman."
  • Guard user caching. After attempt(), the guard caches $this->user. If you call auth()->user() immediately in the same request and get null, either call auth()->setUser($user) yourself or re-resolve through the guard. Don't assume attempt() populates the user instance.
  • Blacklist needs a shared cache. Logout/invalidate depends on blacklist_enabled plus a persistent cache store. With the array driver (the default in phpunit.xml) each test request boots a fresh cache, so a "logged out" token still authenticates. Use database/redis/file in tests that assert invalidation.
  • blacklist_grace_period (default 0) keeps a token valid for N seconds after logout to tolerate concurrent in-flight requests. Set it explicitly and document it — it confuses QA when a "revoked" token still works.
  • Do not override reserved claims. customClaims() merges into the payload; setting sub, iat, exp, nbf or jti corrupts validation or blacklisting. Use namespaced names like app_role.
  • customClaims() is sticky for the request. The setter mutates the JWTAuth instance; if you mint two tokens in one request (e.g., in a test loop or a queue job), reset with JWTAuth::customClaims([]) or re-resolve the service.
  • Config quirks: ttl and refresh_ttl are in minutes, not seconds. jwt.user and jwt.identifier in config/jwt.php are only used by JWTAuth::attempt()/fromUser() paths — when you go through the guard, the provider in config/auth.php wins, and the two can drift silently. Keep them pointed at the same model.
  • The guard forwards unknown methods to tymon via __call. That's convenient (Auth::guard('api')->getPayload()) but means typos fail at runtime, not at static analysis. If your IDE can't see a method, that's why — reach for the JWTAuth facade in anything non-trivial for clarity.
  • Extension point: if you need claim injection on every login (tenant, scopes), wrap the guard rather than editing it — bind your own Auth::extend('jwt', ...) closure that decorates the package's guard and calls customClaims() inside attempt(). That keeps you upgradeable (within 0.5.x) and testable.
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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core