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

Laravel Jwt Laravel Package

stechstudio/laravel-jwt

JWT authentication for Laravel: issue and validate JSON Web Tokens, authenticate API requests with middleware, and configure custom claims and token lifetimes. A lightweight package for securing Laravel APIs without sessions.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements (3.0): PHP 8.2+, Laravel 11+ (through 13). If you are still on Laravel 8–10 or PHP 8.1, pin to the 2.x line.

Minimal setup:

composer require <vendor>/<package>   # resolves to ^3.0
php artisan vendor:publish --tag=jwt-config
php artisan jwt:secret                # or set JWT_SECRET manually

Set JWT_SECRET (and JWT_SIGNER if you are not using HS256) in .env, then php artisan config:clear.

Where to look first:

  1. config/jwt.php — signer, secret/keys, TTLs, and the guard/claims mapping.
  2. The JWT facade — token issuing, parsing, and invalidation.
  3. ParsedToken — the object you get back from parsing; validation and claim access live here.
  4. The middleware alias and the request macros — these are your two lowest-friction integration points.
  5. The exception classes — they extend Laravel's auth exceptions, so they render as 401s out of the box.

First use case: issue a token on login and protect a route with it.

Route::post('/login', function (Request $request) {
    $user = User::where('email', $request->email)->firstOrFail();

    if (! Hash::check($request->password, $user->password)) {
        abort(401);
    }

    return ['token' => JWT::fromSubject($user)->issue()];
});

Route::get('/me', fn (Request $request) => $request->user())
    ->middleware('jwt.auth');

That is the whole loop: issue, send as Authorization: Bearer <token>, let the middleware resolve the subject.

Implementation Patterns

Issue from a subject, not from raw claims. Prefer the subject-based helper over hand-building payloads — it keeps sub, iat, nbf, and exp consistent and makes the guard's user resolution work without extra code.

Parse → validate → act. Treat ParsedToken as a value object, not a service:

$token = JWT::parse($request->bearerToken());

if (! $token->validate()) {
    // expired, bad signature, or nbf/iat violation
}

$userId = $token->claims()->get('sub');

Keep validate() calls at the boundary (middleware, a form request, a small TokenGuard), and pass plain claims inward. Don't let ParsedToken leak into your domain layer.

Configure the signer once, and mean it. In 3.0, ParsedToken::validate() uses config('jwt.signer') instead of hard-coding HS256. That means asymmetric setups are now first-class:

// config/jwt.php
'signer' => env('JWT_SIGNER', 'HS256'),   // or RS256 / ES256 / EdDSA
'secret' => env('JWT_SECRET'),            // HMAC only
'keys'   => [
    'public'  => env('JWT_PUBLIC_KEY'),
    'private' => env('JWT_PRIVATE_KEY'),
],

The practical win: a service can hold only the public key and still validate RS256 tokens — previously it could not, and teams worked around it by disabling validation or hand-rolling a verifier. If you have such a workaround, delete it in this upgrade. It is now redundant and probably masks bugs.

Multi-guard / multi-issuer. If you issue tokens for users, admins, and machine clients, put the audience or issuer in the claims and add a thin wrapper that asserts it after validate(). validate() checks cryptographic validity and time bounds — it does not know your tenancy model.

Refresh without a second round trip. Keep access tokens short (5–15 min) and put refresh behind its own endpoint with rotation and a rate limiter. In Laravel 11+ the limiter goes in a service provider, not RouteServiceProvider:

RateLimiter::for('jwt-refresh', fn (Request $r) => Limit::perMinute(10)->by($r->ip()));

Middleware registration is Laravel 11+ style. Register the alias in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->alias(['jwt.auth' => \Vendor\Jwt\Http\Middleware\Authenticate::class]);
})

Request macros for the fast path. The macro is the cheapest way to reach a claim from a controller without importing anything. Use it for sub; for anything else, parse explicitly — macros that return mixed hide failures.

Exception handling. The package's exceptions extend Laravel's auth exceptions, so they become 401s. Only add explicit handling in bootstrap/app.php if you need a JSON envelope or want to distinguish "expired" from "invalid" so clients know when to refresh:

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (TokenExpiredException $e) {
        return response()->json(['error' => 'token_expired'], 401);
    });
})

Testing. Use Carbon::setTestNow() to exercise exp/nbf boundaries, and assert on a real token round-trip rather than on a mock of ParsedToken — the value here is exactly the parse/validate path. If your suite is on PHPUnit 11, check your own data providers for the static requirement, since the package's internal upgrade often nudges teams to bump the suite at the same time.

Gotchas and Tips

The upgrade itself:

  • lcobucci/jwt v4 → v5 is the only change with teeth. The package's own API is unchanged, but v5 reworked how Configuration, Signer, and Key objects are constructed. If you only touch the facade, ParsedToken, the middleware, the macros, and the exceptions — you are done after bumping PHP and Laravel. If you import Lcobucci\JWT\* anywhere (custom signers, a Configuration you build yourself, a bespoke validation path), read v5's upgrade guide first. Grep for Lcobucci\JWT before you start.
  • lcobucci/clock is now an explicit dependency. Normally transparent — but if you were relying on it arriving transitively and have a Clock implementation of your own, confirm both resolve to the same instance rather than two classes with the same name.
  • Bump the CI matrix to PHP 8.2/8.3/8.4 and Laravel 11–13 in the same PR as the composer bump, or you will not see the failures until deploy.

Behavioural trap: after this release, the signer in config is authoritative. A token signed with RS256 while config('jwt.signer') still says HS256 will now fail to validate — loudly, which is correct, but it can look like a regression if your config was silently wrong for months. If validation suddenly fails on an existing deployment, print the token header's alg and compare it to config('jwt.signer') before looking anywhere else.

Key formatting. RSA keys pasted into .env lose their newlines and fail at parse time with an opaque Lcobucci error. Either base64 the key and decode it in a service provider, or use \n-escaped strings with str_replace('\n', "\n", ...). Trim trailing whitespace either way.

Config caching. config('jwt.signer') is now load-bearing for validation. Any config change requires php artisan config:cache (or config:clear) — a stale cached config after deploying new keys is the single most common cause of "it works locally."

Clock skew. iat, nbf, and exp are checked against the injected clock. In multi-node deployments with drift, one node will reject tokens the others accept. Add a few seconds of leeway rather than widening the TTL, and never set leeway large enough to mask an expiry bug. In tests, Carbon::setTestNow() covers the package's clock usage; do not fake time at the system level.

Token size. Asymmetric tokens with many claims get long. If you ship them in cookies or headers, check your proxy's header-size limit — this bites at exactly the point you add roles and permissions to the payload.

Don't put mutable state in the token. Permissions, feature flags, and subscription tier change while the token is still valid. Keep the payload to identity (sub, aud, jti) and look up the rest. jti also gives you a cheap revocation key.

Revocation is not built in. Tokens are stateless; the middleware validates a signature, not a blocklist. If you need immediate logout-everywhere, keep short TTLs and maintain your own jti denylist in cache — do not assume the facade's invalidation touches already-issued tokens.

Debugging checklist, in order: decode the payload without verifying (any JWT debugger) → confirm alg matches config → confirm exp/nbf are sane → confirm the signature verifies with the public key → confirm the middleware alias is actually registered. The first four catch nearly everything, and the fifth catches the rest.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core