{"record":{"id":"3e60dff4b875ca81","repo":"tymondesigns/jwt-auth","slug":"could-not-create-token-exception-message","errorCode":null,"errorMessage":"Could not create token: {exception message}","messagePattern":"Could not create token: (.+?)","errorType":"exception","errorClass":"Tymon\\JWTAuth\\Exceptions\\JWTException","httpStatus":null,"severity":"error","filePath":"src/Providers/JWT/Lcobucci.php","lineNumber":95,"sourceCode":"\n    /**\n     * Create a JSON Web Token.\n     *\n     * @param  array  $payload\n     * @return string\n     *\n     * @throws \\Tymon\\JWTAuth\\Exceptions\\JWTException\n     */\n    public function encode(array $payload)\n    {\n        $builder = $this->getBuilderFromClaims($payload);\n\n        try {\n            return $builder\n                ->getToken($this->config->signer(), $this->config->signingKey())\n                ->toString();\n        } catch (Exception $e) {\n            throw new JWTException('Could not create token: '.$e->getMessage(), $e->getCode(), $e);\n        }\n    }\n\n    /**\n     * Decode a JSON Web Token.\n     *\n     * @param  string  $token\n     * @return array\n     *\n     * @throws \\Tymon\\JWTAuth\\Exceptions\\JWTException\n     */\n    public function decode($token)\n    {\n        try {\n            /** @var \\Lcobucci\\JWT\\Token\\Plain */\n            $token = $this->config->parser()->parse($token);\n        } catch (Exception $e) {\n            throw new TokenInvalidException('Could not decode token: '.$e->getMessage(), $e->getCode(), $e);","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/tymondesigns/jwt-auth/blob/6c70930a92710d97e8e52b182fca2176097f33be/src/Providers/JWT/Lcobucci.php#L77-L113","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before - date string claim breaks DateTimeImmutable::createFromFormat('U', ...)\n$payload = ['sub' => $user->id, 'exp' => $user->expires_at->format('Y-m-d H:i:s')];\n$token = JWTAuth::encode($payload); // JWTException: Could not create token: ...\n\n// after - registered time claims must be unix timestamps\n$payload = ['sub' => $user->id, 'exp' => $user->expires_at->getTimestamp()];\n$token = JWTAuth::encode($payload);","handlingStrategy":"try-catch","validationCode":"// Validate time claims before encoding - the builder requires unix timestamps\nuse Illuminate\\Support\\Arr;\n\nfunction assertEncodablePayload(array $payload): void\n{\n    foreach (['iat', 'nbf', 'exp'] as $claim) {\n        $value = Arr::get($payload, $claim);\n        if ($value !== null && (!is_int($value) && !ctype_digit((string) $value))) {\n            throw new InvalidArgumentException(\"Claim '{$claim}' must be a unix timestamp integer.\");\n        }\n    }\n}","typeGuard":"null","tryCatchPattern":"use Tymon\\JWTAuth\\Exceptions\\JWTException;\n\ntry {\n    $token = auth('api')->login($user);\n} catch (JWTException $e) {\n    // the wrapper chains the real lcobucci/jwt error\n    Log::error('JWT signing failed', ['cause' => $e->getPrevious()?->getMessage() ?? $e->getMessage()]);\n    return response()->json(['error' => 'could_not_create_token'], 500);\n}","preventionTips":["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"],"tags":["jwt","php","laravel","token-signing","configuration","lcobucci"],"backgroundTag":"jwt-signing-failed","analyzedSha":"6c70930a92710d97e8e52b182fca2176097f33be","analyzedAt":"2026-08-21T02:16:37.040Z","schemaVersion":2},"datasetVersion":"2026-08-21T03:17:12.404Z"}