NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Every moving part is bound behind a contract in JwtServiceProvider, so a host app can swap any piece by binding its own implementation:

ContractDefault bindingResponsibility
UserTokens\Contracts\UserTokenIssuerNativeUserTokenIssuerMint RS256 user tokens.
UserTokens\Contracts\UserTokenVerifierNativeUserTokenVerifierVerify RS256 user tokens — signature, time, pinned iss/aud.
ServiceTokens\Contracts\ServiceTokenIssuerNativeServiceTokenServiceIssue HS256 service tokens.
ServiceTokens\Contracts\ServiceTokenVerifierNativeServiceTokenServiceVerify inbound HS256 service tokens.
Denylist\Contracts\DenylistCacheDenylistStore and query revoked jtis.
use RoundlyConsulting\Jwt\Denylist\Contracts\Denylist;

// A service provider in the host app.
public function register(): void
{
    $this->app->singleton(Denylist::class, fn ($app) => new DatabaseDenylist(/* ... */));
}

Implement each contract’s methods exactly. The user-token contracts take an optional audience that a custom implementation must accept — the jwt guard always passes its own audience to verify():

// Denylist
public function has(string $jti): bool;
public function deny(string $jti, CarbonImmutable $until): void;
public function denyToken(IssuedToken $token): void;

// UserTokenIssuer / UserTokenVerifier take an optional audience
public function mint(string $subject, Scope|string $scope, int $ttl, array $extraClaims = [], ?string $audience = null): IssuedToken;
public function verify(string $jwt, ?string $audience = null): Claims;   // null ⇒ the configured jwt.audience

Jwt::services() resolves ServiceTokenIssuer and ServiceTokenVerifier from the container on every call, so rebinding the issuer changes what issue(), request() and authenticate() produce, and rebinding the verifier changes what verify() and the service-jwt guard accept.

A custom claims-mode identity

TokenUser is deliberately not final and builds itself with new static, so a subclass comes back from the guard as itself:

namespace App\Auth;

use RoundlyConsulting\Jwt\UserTokens\TokenUser;

class ApiActor extends TokenUser
{
    public function tenant(): string
    {
        return $this->claims()->has('tenant')
            ? $this->claims()->string('tenant')
            : 'none';
    }
}
// config/jwt.php — or per guard via auth.guards.<name>.identity
'guard' => [
    'identity' => \App\Auth\ApiActor::class,
],

Keep TokenUser’s constructor signature, or override fromClaims() too. To start from scratch, implement ClaimsAuthenticatable — fromClaims(Claims $claims): self on top of Laravel’s Authenticatable — and, for claim-based authorization, ChecksPermissions with hasPermission(string $ability): bool. An invalid identity class falls back to TokenUser.

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

By donating, you agree to our donation terms.

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.