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

Jwt Laravel Package

f21/jwt

f21/jwt adds JSON Web Token authentication to Laravel applications, providing a JWT guard, token generation and validation, middleware for protecting routes, and helpers for stateless API authentication without relying on sessions. It integrates with Laravel’s auth system and supports API token work...

View on GitHub
Deep Wiki
Context7

Getting Started

f21/jwt is a small, dependency-light PHP JWT library (F21\Jwt\Jwt) that wraps encode/decode with HS256/HS384/HS512 (and RS* via OpenSSL) plus a handful of typed exceptions. It is archived, so treat it as legacy glue — install it, verify the source once, then wrap it behind your own interface so you can swap it out later.

Minimal path:

composer require f21/jwt
use F21\Jwt\Jwt;
use F21\Jwt\Exception\TokenExpiredException;
use F21\Jwt\Exception\SignatureInvalidException;

$jwt = new Jwt(getenv('JWT_SECRET'), 'HS256');

// issue
$token = $jwt->encode([
    'sub' => $user->id,
    'iat' => time(),
    'exp' => time() + 3600,
    'scope' => 'api',
]);

// verify
try {
    $payload = $jwt->decode($token);
} catch (TokenExpiredException $e) {
    // 401 with a distinct code so the client can refresh
} catch (SignatureInvalidException $e) {
    // 401, never leak details
}

Where to look first, in this order:

  1. src/Jwt.php — the constructor signature (secret, algo/algorithm) and whether decode() returns an array or stdClass. These two details differ between releases and are the source of most "why is my claim missing" bugs.
  2. src/Exception/ — the exact exception class names. Catch on these, not on \Exception, or you will silently swallow expiry.
  3. The README for the encode options (exp, nbf, leeway) it actually supports, if any.

First real use case: drop it into a stateless API auth middleware. Issue a short-lived access token on login, validate on each request, and return 401 with WWW-Authenticate: Bearer on failure. That is the 80% path and where the library is adequate.

Implementation Patterns

Wrap it, do not sprinkle it. Bind a single configured instance in the container and expose your own narrow interface. When you eventually migrate to firebase/php-jwt or lcobucci/jwt, the blast radius is one class.

// AppServiceProvider
$this->app->singleton(TokenService::class, fn () => new TokenService(
    new Jwt(config('jwt.secret'), config('jwt.algo', 'HS256'))
));

Middleware validation.

public function handle($request, Closure $next)
{
    $header = $request->header('Authorization', '');
    if (! str_starts_with($header, 'Bearer ')) {
        return response()->json(['error' => 'missing_token'], 401);
    }

    try {
        $claims = $this->tokens->parse(substr($header, 7));
    } catch (TokenExpiredException) {
        return response()->json(['error' => 'token_expired'], 401);
    } catch (\Throwable) {
        return response()->json(['error' => 'token_invalid'], 401);
    }

    $request->attributes->set('auth', [
        'id' => $claims['sub'],
        'scope' => $claims['scope'] ?? null,
    ]);

    return $next($request);
}

Access + refresh split. Keep the access token in the Authorization header and the refresh token in an HttpOnly, Secure, SameSite=Lax cookie. The library has no revocation, so give refresh tokens a jti claim and persist jti => [user_id, expires_at] in your DB/Redis; reject on refresh if the jti is not present. That table is your only logout/revocation mechanism — the JWT itself cannot be invalidated.

Claim discipline. Standard claims (iss, aud, sub, exp, nbf, jti) plus a namespaced app claim (https://yourapp.com/roles) rather than top-level admin: true, which is a common collision/confusion vector. Validate aud and iss yourself in TokenService::parse() — small libraries like this one frequently skip those checks, and that omission is a real security gap for multi-service tokens.

Key rotation via kid. The library gives you no key selection. Implement it above the library: read the header's kid (base64url-decode segment 0 yourself), look up the secret in config, then construct a Jwt with that key. This is the cleanest extension point and keeps you migration-ready.

Testing. Freeze time or inject a clock; exp comparisons against time() make tests flaky at the second boundary. Generate tokens in a factory and assert on the decoded array rather than on the raw string.

Signing vs. encryption. This is JWS only — payloads are base64url-encoded, not encrypted. Anyone holding the token can read the claims. Never put PII, roles you would not show the user, or anything sensitive in the payload.

Gotchas and Tips

  • Archived means no security patches. This is the headline risk. JWT handling is security-critical and CVEs in parsing/algorithm handling do happen. Acceptable for internal, low-value tokens behind a firewall; questionable for public-facing auth. If you must keep it, pin the exact version in composer.json and add a composer audit step plus a calendar reminder to reassess.
  • Algorithm confusion. Confirm from the source that decode() does not read alg from the token header and honour it. If it does, an attacker can present alg: none or an HS256 token signed with your RS256 public key. Always pass the algorithm explicitly to the constructor and, if the library lets the token decide, stop using it.
  • Secret strength. HS256 secrets must be ≥32 random bytes (bin2hex(random_bytes(32))), not env('APP_KEY') reuse and never a human-typed string. Rotating APP_KEY will silently invalidate every issued token — keep the JWT secret separate.
  • Base64url, not base64. Padding and +// characters are the classic hand-rolled-decoder pitfall. If you decode segments yourself (for kid, above), use rtrim(strtr(base64_encode($x), '+/', '-_'), '=') and pad on the way back in.
  • exp type mismatches. Some versions compare exp as an int, others coerce; passing a string exp or an ISO-8601 date can produce tokens that expire immediately or never. Always time() + $ttl as an integer.
  • Clock skew. Distributed systems drift. If the library has no leeway option, subtract 30–60s from exp when issuing to compensate. Without this you get intermittent, unreproducible 401s in production only.
  • Exception granularity. Catch TokenExpiredException and SignatureInvalidException separately — "expired" is actionable for the client (refresh), "invalid" is not. Broad catch (\Exception) hides malformed-config errors as auth failures and makes debugging miserable.
  • Debugging tokens. Decode the payload manually (json_decode(base64url_decode($parts[1]))) before blaming the library — 90% of "signature invalid" reports are a trailing newline in .env, a leading Bearer left on the string, or whitespace from a curl copy-paste.
  • Header size. Every claim is sent on every request. Keep the payload small; do not stuff permission lists in — put a sub plus a version/jti and look up the rest.
  • Return type. Check whether decode() returns an array or an object in your pinned version before writing $claims['sub'] everywhere. Mixed usage across the codebase is a subtle source of null ids.
  • Migration off-ramp. Keep TokenService accepting and emitting the same claim set you would use with firebase/php-jwt (which is a near-drop-in for encode/decode and has a matching exception style). That makes the eventual swap a single-file change rather than an audit of every controller.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource