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
- Catch JWTException and inspect $e->getPrevious()->getMessage() to get the underlying signing error before changing anything
- 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
- Ensure iat/nbf/exp in custom payloads are numeric unix timestamps, not date strings or Carbon objects
- If using HS*, confirm JWT_SECRET is non-empty and shared across all issuing/verifying services
- 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
- Run openssl pkey -check on your private key in CI before deploy so malformed keys never reach production
- Keep time claims (iat/nbf/exp) as integers end-to-end; convert Carbon instances with ->getTimestamp()
- Quote PEM env values so newlines survive; add a config assertion in a smoke test after deployment
- Never let token-issuing endpoints return raw exception text - log the chained getPrevious() instead
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
- Could not decode token: {exception message}
- Token Signature could not be verified.
- The given algorithm could not be found
- Private key is not set.
- Secret is not set.
AI-assisted analysis of tymondesigns/jwt-auth@6c70930a92 (2026-08-21).
Data as JSON: /api/errors/3e60dff4b875ca81.
Report an issue: GitHub.