Extending
Every moving part is bound behind a contract in JwtServiceProvider, so a host app can swap any piece by binding its own implementation:
| Contract | Default binding | Responsibility |
|---|---|---|
UserTokens\Contracts\UserTokenIssuer | NativeUserTokenIssuer | Mint RS256 user tokens. |
UserTokens\Contracts\UserTokenVerifier | NativeUserTokenVerifier | Verify RS256 user tokens — signature, time, pinned iss/aud. |
ServiceTokens\Contracts\ServiceTokenIssuer | NativeServiceTokenService | Issue HS256 service tokens. |
ServiceTokens\Contracts\ServiceTokenVerifier | NativeServiceTokenService | Verify inbound HS256 service tokens. |
Denylist\Contracts\Denylist | CacheDenylist | Store 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.audienceJwt::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 cryptoBy 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.