---
title: "Guards — JWT for Laravel | Roundly"
description: "Wire the jwt and service-jwt guard drivers — claims mode or Eloquent provider mode, the request pipeline and token-version checks."
url: https://roundly-consulting.com/open-source/docs/jwt-for-laravel/guards
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[JWT for Laravel](https://roundly-consulting.com/open-source/docs/jwt-for-laravel.md)

# Guards

Declare the guards in config/auth.php — the package provides the drivers:

```php
'guards' => [
    // Claims mode: build a TokenUser straight from the token (no DB).
    'api' => ['driver' => 'jwt'],

    // Provider mode: resolve a real Eloquent user via `sub`.
    // 'api' => ['driver' => 'jwt', 'provider' => 'users'],

    'service' => ['driver' => 'service-jwt'],
],
```

Then protect routes as usual:

```php
Route::middleware('auth:api')->get('/me', fn () => ['id' => auth()->id()]);
Route::middleware('auth:service')->post('/internal/sync', SyncController::class);
```

In claims mode request()->user() is a TokenUser — not an Eloquent model, so return the fields you need (getAuthIdentifier(), claims()) rather than the object itself, which Laravel can’t turn into a response.

## Claims mode and provider mode

- Claims mode — no provider: the guard builds guard.identity (default TokenUser) straight from the verified claims. No database query.
- Provider mode — with a provider: the guard calls retrieveById() with the sub claim, so $request->user() is your Eloquent model.

```php
$user = $request->user();            // TokenUser in claims mode
$user->getAuthIdentifier();          // the 'sub' claim
$user->claims();                     // the full Claims bag
$user->hasPermission('posts.edit');  // membership in the 'permissions' claim
```

## The request pipeline

On every request the jwt guard runs: bearer token → verify (RS256, pinned iss, the guard’s own audience) → required scope (default access) → denylist → identity → optional token-version check. Any failure yields a null user — a 401. A missing or invalid RSA key (KeyLoadFailed) is rethrown instead, so an operator error surfaces as a 500. The guard never dispatches TokenVerificationFailed; that event belongs to explicit Jwt::verify() calls.

## Reading the current claims

```php
Jwt::guard('api')->claims();       // ?Claims — that guard's verified claims, null without a user
Jwt::guard('clients')->claims();   // never another guard's claims

auth()->guard('api')->payload();   // the same claims, straight from the guard
```

Jwt::guard($name)->claims() names the guard it reads, so one guard’s claims never answer for another. Claims resolve through the auth manager’s per-request guards, never static state, so long-lived workers (Octane, queues) can’t leak one request’s claims into the next. Both guards honour setUser(): a user set on them is kept until forgetUser(), so Laravel’s actingAs($user, $guard) works (see Testing). validate() runs the same full pipeline:

```php
// Runs the full pipeline: signature, pins, scope, denylist, identity, token version.
Auth::guard('api')->validate(['token' => $jwt]);   // bool
```

## Token-version freshness

In provider mode, set guard.token\_version so a bumped version invalidates old tokens. Prefer an invokable class — it survives php artisan config:cache, a closure does not:

```php
namespace App\Auth;

use Illuminate\Contracts\Auth\Authenticatable;

final class TokenVersion
{
    public function __invoke(Authenticatable $user): int
    {
        return (int) $user->token_version;
    }
}
```

```php
// config/jwt.php
'guard' => [
    'token_version' => \App\Auth\TokenVersion::class,   // __invoke(Authenticatable $user): int
],
```

The guard compares the returned int with the token’s tv claim; a mismatch or an absent tv rejects the token. Bump the stored version to log a user out everywhere — after a password change, for example:

```php
// Mint with the user's current version…
Jwt::mintAccessToken(
    AccessTokenRequest::for($user->id)->tokenVersion($user->token_version)
);

// …then "log out everywhere": every token carrying the old tv is rejected.
$user->increment('token_version');
```

A value the guard cannot call — a missing class, a class without \_\_invoke — throws JwtMisconfigured rather than silently skipping the check.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/jwt-for-laravel.md)
