tymondesigns/jwt-auth · error · Tymon\JWTAuth\Exceptions\JWTException

Could not create token: {exception message}

Error message

Could not create token: {exception message}

What it means

Thrown by the Lcobucci provider's encode() when the underlying lcobucci/jwt library raises any exception while signing or serializing a new token; the original message is appended and the original exception is chained as previous. It is a generic wrapper, so the real cause (key material, passphrase, claim values) must be read from the chained exception. It signals that token creation failed before any string was ever returned.

Source

Thrown at src/Providers/JWT/Lcobucci.php:95

    /**
     * Create a JSON Web Token.
     *
     * @param  array  $payload
     * @return string
     *
     * @throws \Tymon\JWTAuth\Exceptions\JWTException
     */
    public function encode(array $payload)
    {
        $builder = $this->getBuilderFromClaims($payload);

        try {
            return $builder
                ->getToken($this->config->signer(), $this->config->signingKey())
                ->toString();
        } catch (Exception $e) {
            throw new JWTException('Could not create token: '.$e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * Decode a JSON Web Token.
     *
     * @param  string  $token
     * @return array
     *
     * @throws \Tymon\JWTAuth\Exceptions\JWTException
     */
    public function decode($token)
    {
        try {
            /** @var \Lcobucci\JWT\Token\Plain */
            $token = $this->config->parser()->parse($token);
        } catch (Exception $e) {
            throw new TokenInvalidException('Could not decode token: '.$e->getMessage(), $e->getCode(), $e);

View on GitHub (pinned to 6c70930a92)

Solutions

  1. Catch JWTException and inspect $e->getPrevious()->getMessage() to get the underlying signing error before changing anything
  2. If using RS*/ES*, validate the key parses with the exact passphrase: openssl pkey -in private.pem -check -passin pass:$JWT_PASSPHRASE, and make sure JWT_PRIVATE_KEY contains the full PEM including BEGIN/END lines and that JWT_PASSPHRASE matches how the key was generated
  3. Ensure iat/nbf/exp in custom payloads are numeric unix timestamps, not date strings or Carbon objects
  4. If using HS*, confirm JWT_SECRET is non-empty and shared across all issuing/verifying services
  5. Regenerate the key pair and update both JWT_PUBLIC_KEY and JWT_PRIVATE_KEY if the PEM is unrecoverable, then php artisan config:clear

Example fix

// before - date string claim breaks DateTimeImmutable::createFromFormat('U', ...)
$payload = ['sub' => $user->id, 'exp' => $user->expires_at->format('Y-m-d H:i:s')];
$token = JWTAuth::encode($payload); // JWTException: Could not create token: ...

// after - registered time claims must be unix timestamps
$payload = ['sub' => $user->id, 'exp' => $user->expires_at->getTimestamp()];
$token = JWTAuth::encode($payload);
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate time claims before encoding - the builder requires unix timestamps
use Illuminate\Support\Arr;

function assertEncodablePayload(array $payload): void
{
    foreach (['iat', 'nbf', 'exp'] as $claim) {
        $value = Arr::get($payload, $claim);
        if ($value !== null && (!is_int($value) && !ctype_digit((string) $value))) {
            throw new InvalidArgumentException("Claim '{$claim}' must be a unix timestamp integer.");
        }
    }
}

Type guard

null

Try / catch

use Tymon\JWTAuth\Exceptions\JWTException;

try {
    $token = auth('api')->login($user);
} catch (JWTException $e) {
    // the wrapper chains the real lcobucci/jwt error
    Log::error('JWT signing failed', ['cause' => $e->getPrevious()?->getMessage() ?? $e->getMessage()]);
    return response()->json(['error' => 'could_not_create_token'], 500);
}

Prevention

When it happens

Trigger: Calling JWTAuth::fromUser() / fromSubject(), auth('api')->login($user), or the JWT provider's encode($payload) when: the RSA/ECDSA private key PEM is invalid or truncated; JWT_PASSPHRASE is wrong or missing for an encrypted key; exp/iat/nbf claim values are not numeric unix timestamps (DateTimeImmutable::createFromFormat('U', $value) in getBuilderFromClaims() returns false and the builder rejects it); or a claim value cannot be serialized by lcobucci/jwt.

Common situations: A multiline PEM pasted into .env with newlines stripped so the key no longer parses; generating a key with a passphrase but leaving JWT_PASSPHRASE empty (or vice versa); passing Carbon date strings like '2026-08-20 10:00' instead of ->getTimestamp() in custom claims; switching from HS256 to RS256 while keeping stale key config; openssl extension not enabled.

Related errors


AI-assisted analysis of tymondesigns/jwt-auth@6c70930a92 (2026-08-21). Data as JSON: /api/errors/3e60dff4b875ca81. Report an issue: GitHub.