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

Php Jwt Laravel Package

firebase/php-jwt

A simple PHP library to encode and decode JSON Web Tokens (JWT), conforming to RFC 7519. Specify supported algorithms, verify tokens with Key objects, and set leeway for clock skew between servers.

View on GitHub
Deep Wiki
Context7

Getting Started

firebase/php-jwt is a low-level JWT primitive — it encodes and decodes tokens and verifies signatures, nothing more. In a Laravel app you'll typically pull it in as composer require firebase/php-jwt and then decide where it belongs: either as a standalone service (issuing/verifying your own tokens) or as a verifier for third-party tokens (Google IAP, OIDC providers, webhooks, microservices).

Minimal setup

composer require firebase/php-jwt

Because the library has no Laravel service provider, the fastest path is a single service class registered in the container (or a JwtService in app/Services). For anything involving asymmetric keys you'll also want the keys in config/ and .env, never hard-coded.

// config/jwt.php
return [
    'secret'    => env('JWT_SECRET'),
    'algo'      => env('JWT_ALGO', 'HS256'),
    'ttl'       => (int) env('JWT_TTL', 3600),
    'issuer'    => env('JWT_ISSUER', config('app.url')),
    'leeway'    => (int) env('JWT_LEEWAY', 30),
    'audience'  => env('JWT_AUDIENCE'),
];
// app/Services/JwtService.php
namespace App\Services;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class JwtService
{
    public function issue(array $claims, ?string $kid = null): string
    {
        $now = time();
        $payload = array_merge([
            'iss' => config('jwt.issuer'),
            'iat' => $now,
            'nbf' => $now,
            'exp' => $now + config('jwt.ttl'),
            'jti' => (string) \Str::uuid(),
        ], $claims);

        return JWT::encode($payload, config('jwt.secret'), config('jwt.algo'), $kid);
    }

    public function verify(string $token): \stdClass
    {
        JWT::$leeway = config('jwt.leeway');
        return JWT::decode($token, new Key(config('jwt.secret'), config('jwt.algo')));
    }
}

Where to look first

  • The README's exception-handling block — you will build your middleware around it.
  • Key and JWK::parseKeySet() — most real integrations are asymmetric/JWKS, not HS256.
  • CachedKeySet if you're validating tokens from a public JWKS endpoint (Google, Auth0, Okta).

First use case

The natural first use in Laravel is API authentication with a custom token (or as a supplement to Sanctum for stateless service-to-service calls). Issue a JWT on login, verify it in middleware, attach the resolved user to the request. Do not attempt to replace auth guards with raw JWT checks scattered through controllers — build one AuthenticateJwt middleware and reuse it.

Implementation Patterns

1. Middleware-based API auth (token issuance + verification)

Issue tokens in a login controller, verify in middleware, and short-circuit with a 401:

// app/Http/Middleware/AuthenticateJwt.php
public function handle($request, Closure $next)
{
    $token = $request->bearerToken();

    if (!$token) {
        return response()->json(['message' => 'Missing token'], 401);
    }

    try {
        $claims = app(JwtService::class)->verify($token);

        // Resolve your own user; the JWT is not a session
        $user = User::find($claims->sub);
        if (!$user) {
            return response()->json(['message' => 'Invalid subject'], 401);
        }

        $request->setUserResolver(fn () => $user);
        $request->attributes->set('jwt', $claims);

    } catch (ExpiredException $e) {
        return response()->json(['message' => 'Token expired'], 401);
    } catch (SignatureInvalidException $e) {
        return response()->json(['message' => 'Bad signature'], 401);
    } catch (UnexpectedValueException | InvalidArgumentException | DomainException $e) {
        return response()->json(['message' => 'Invalid token'], 401);
    }

    return $next($request);
}

Register it on specific route groups (routes/api.php) rather than globally — you don't want it interfering with session-based web routes.

2. Asymmetric keys with kid rotation

For multi-service architectures prefer RS256/ES256 and a kid in the header. This is the pattern that scales when keys rotate:

$token = JWT::encode($payload, $privateKey, 'RS256', 'kid-2026-09');

$keys = [
    'kid-2026-09' => new Key($publicKeyCurrent, 'RS256'),
    'kid-2026-03' => new Key($publicKeyOld, 'RS256'),
];

$claims = JWT::decode($token, $keys);

Always pass an array keyed by kid so verification survives rotation. Store private keys outside the webroot (storage/app/keys) and read via file_get_contents or an OpenSSL wrapper.

3. Validating third-party tokens with JWKS

For OIDC/SSO integrations (Google, Auth0, Azure AD, IAP), don't fetch keys on every request — use CachedKeySet with Laravel's cache driver wrapped as PSR-6, or cache the JWK::parseKeySet() result yourself:

$jwks = Cache::remember('jwks:google', now()->addHours(6), function () {
    return Http::get('https://www.gstatic.com/iap/verify/public_key-jwk')->json();
});

$keys = JWK::parseKeySet($jwks, 'RS256'); // pin the expected algo
$claims = JWT::decode($token, $keys);

Pinning the algorithm as the second argument to parseKeySet prevents "key confusion" attacks where an attacker swaps RS256 for HS256.

4. Custom claims as your authorization model

Use JWT claims for identity facts, not as a replacement for Gate/Policy. A common pattern: put sub, role, tenant_id, scopes in the token and hydrate an in-memory user in middleware, then let normal Laravel authorization continue:

Gate::define('access-tenant', fn (User $user, int $tenantId) =>
    $user->tenant_id === $tenantId
);

Remember: claims are only as trustworthy as the signature. Never accept a client-supplied role without verifying the signature first, and never mutate authorization state based on an unverified token.

5. Short-lived access + refresh tokens

The library has no refresh mechanism. Implement it yourself: issue a short access token (exp = 15 min) and a long refresh token stored hashed in the DB (with a jti). On refresh, verify the JWT and check the jti exists in your refresh table, then rotate it. This gives you revocation the JWT spec doesn't provide.

6. Testing

Because JWT::decode validates exp/nbf against time(), seed your tests with Carbon::setTestNow() and use JWT::$leeway = 0 in test setup (or set it high for clock-skew simulation). Generate a throwaway key in setUp() rather than committing fixtures:

protected function setUp(): void
{
    parent::setUp();
    config(['jwt.secret' => str_repeat('a', 64), 'jwt.algo' => 'HS256']);
    Carbon::setTestNow('2026-09-28 12:00:00');
}

Gotchas and Tips

Algorithm confusion is the #1 security bug

Never call JWT::decode($token, $key) where $key is a plain string and let the token dictate the algorithm — this is the classic alg: none / RS→HS downgrade. Always wrap keys in Key($secret, $algo) or pass an array of kid => Key. Pin the algorithm at verification time.

JWT::decode returns stdClass, not arrays

Cast with (array) $claims, or use json_decode(json_encode($claims), true). In Laravel this bites people who type-hint array $claims on a method and get a stdClass at runtime.

Exceptions are not all JWTException

The exception tree is broader than the README suggests at first glance:

  • InvalidArgumentException — empty/malformed key
  • DomainException — unsupported algorithm, bad key, missing libsodium/openssl
  • SignatureInvalidException — signature mismatch
  • BeforeValidException (extends UnexpectedValueException) — nbf/iat in the future
  • ExpiredException (extends UnexpectedValueException) — exp passed
  • UnexpectedValueException — malformed token, algo mismatch, bad kid

Catch UnexpectedValueException broadly, but catch ExpiredException/BeforeValidException separately if you want to return a distinguishable client error (401 vs 403, or a "refresh" hint).

Clock skew is real

If issuing and verifying across containers, set JWT::$leeway = 30; (seconds). The README recommends a few minutes max. Note it's a static property — set it once in a service provider, not per-request mid-flight, or you'll get surprising behavior in queue workers and Octane.

CachedKeySet and Octane/queue workers

CachedKeySet holds an internal rate limiter and relies on PSR-6 caching — under Laravel Octane the object persists across requests, so its internal state should be fine, but make sure the cache pool you inject is Octane-safe (array/redis, not file). Also note the 10 RPS limiter applies only when a key is not found, not on every fetch.

jti, sub, and aud are not validated for you

JWT::decode checks exp, nbf, iat, and signature — it does not enforce aud, iss, or single-use jti. If you need audience restriction, verify it yourself:

$claims = app(JwtService::class)->verify($token);
if (($claims->aud ?? null) !== config('jwt.audience')) {
    throw new \UnexpectedValueException('Invalid audience');
}

Key format friction

  • HS256 secrets must be long enough but the library won't enforce it — enforce it at the config layer.
  • RS256 private keys must be PEM (-----BEGIN RSA PRIVATE KEY-----); an OpenSSLAsymmetricKey from openssl_pkey_get_private() also works if you need passphrase support.
  • EdDSA keys are base64-encoded sodium raw keys, not PEM — a common source of confusion if you're copying from OpenSSL docs.
  • PS256 (RSA-PSS) requires the optional phpseclib/phpseclib:^3.0 dependency; missing it throws DomainException.

Base64url, not Base64

If you're debugging tokens, base64_decode($headerPart) will fail on - and _. Use base64_decode(strtr($part, '-_', '+/')) or JWT::urlsafeB64Decode($part). Never validate or trust decoded header/payload before signature verification — the README's header-decoding example is explicitly unsafe.

Token size limits

JWT claims are sent on every request. Avoid stuffing large permission arrays into the token — keep claims minimal (sub, tenant_id, scopes) and resolve details server-side. Cookies (if you set the token in one) have ~4KB limits; JWT-in-header doesn't, but proxies may reject oversized headers.

Revocation requires state

A stateless JWT is valid until exp. If you need "logout everywhere," either keep a jti deny-list in Redis with a TTL matching the token lifetime, or use short-lived access tokens plus DB-backed refresh tokens. Don't pretend the token is revocable when it isn't.

When not to use this package

  • If you want Laravel-native token auth with revocation and abilities, Sanctum is the better default; it handles guards, middleware, and revocation.
  • If you're integrating with Passport or an OAuth2 server, use that.
  • firebase/php-jwt shines for stateless inter-service tokens, verifying external IdP tokens, and self-issued tokens where you control both ends. Reach for it deliberately, not as a Sanctum replacement.

Extension points

  • Build a JwtService (or TokenIssuer) interface and bind it in a service provider so tests can swap a fake — but fakes rarely help since the real thing is deterministic; prefer setting Carbon::setTestNow().
  • Use a custom Key map for per-tenant secrets: Key objects are cheap, so loading them from Eloquent per request is viable if cached.
  • Wrap verification in a dedicated JwtGuard implementing Illuminate\Contracts\Auth\Guard if you want auth:jwt to feel native alongside auth:sanctum — worth doing once rather than sprinkling middleware checks across controllers.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin