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

Private key is not set.

Error message

Private key is not set.

What it means

Thrown by getSigningKey() when an asymmetric algorithm (RS*/ES*) is configured but the keys.private config value is null or empty. buildConfig() calls getSigningKey() from the provider constructor, so this surfaces the moment the JWT provider is instantiated - any attempt to issue or verify a token fails with a 500, since the service cannot be constructed at all.

Source

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

     */
    protected function isAsymmetric()
    {
        return is_subclass_of($this->signer, Rsa::class)
            || is_subclass_of($this->signer, Ecdsa::class);
    }

    /**
     * {@inheritdoc}
     *
     * @return \Lcobucci\JWT\Signer\Key
     *
     * @throws \Tymon\JWTAuth\Exceptions\JWTException
     */
    protected function getSigningKey()
    {
        if ($this->isAsymmetric()) {
            if (! $privateKey = $this->getPrivateKey()) {
                throw new JWTException('Private key is not set.');
            }

            return $this->getKey($privateKey, $this->getPassphrase() ?? '');
        }

        if (! $secret = $this->getSecret()) {
            throw new JWTException('Secret is not set.');
        }

        return $this->getKey($secret);
    }

    /**
     * {@inheritdoc}
     *
     * @return \Lcobucci\JWT\Signer\Key
     *
     * @throws \Tymon\JWTAuth\Exceptions\JWTException

View on GitHub (pinned to 6c70930a92)

Solutions

  1. Generate a key pair if you do not have one: openssl genrsa -aes256 -out private.pem 4096 (passphrase optional) then openssl rsa -in private.pem -pubout -out public.pem
  2. Set JWT_PRIVATE_KEY, JWT_PUBLIC_KEY, and JWT_PASSPHRASE (empty string if none) in the environment of every app instance; the value must be the actual PEM contents including BEGIN/END lines
  3. Quote the PEM in .env with real newlines preserved: JWT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" (single-line with literal \n also works)
  4. Run php artisan config:clear so cached config is rebuilt with the new values
  5. Verify the key parses before redeploying: openssl pkey -in private.pem -check -passin env:JWT_PASSPHRASE

Example fix

# before - asymmetric algo, no key material
JWT_ALGO=RS256
# JWT_PRIVATE_KEY unset -> JWTException: Private key is not set.

# after
JWT_ALGO=RS256
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBg...\n-----END PUBLIC KEY-----\n"
JWT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADAN...\n-----END PRIVATE KEY-----\n"
JWT_PASSPHRASE=my-passphrase
Defensive patterns

Strategy: validation

Validate before calling

// Fail fast at boot when asymmetric signing lacks key material
public function boot(): void
{
    $asymmetric = in_array(config('jwt.algo'), ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'], true);

    if ($asymmetric) {
        foreach (['private', 'public'] as $side) {
            if (empty(config("jwt.keys.{$side}"))) {
                throw new RuntimeException("jwt.keys.{$side} must be set for algorithm ".config('jwt.algo'));
            }
        }
        openssl_pkey_get_private(config('jwt.keys.private'), config('jwt.keys.passphrase') ?? '')
            || throw new RuntimeException('jwt.keys.private is not a loadable PEM (or passphrase is wrong).');
    }
}

Type guard

function hasCompleteAsymmetricKeys(array $keysConfig): bool
{
    return !empty($keysConfig['private'])
        && !empty($keysConfig['public'])
        && openssl_pkey_get_private($keysConfig['private'], $keysConfig['passphrase'] ?? '') !== false;
}

Try / catch

use Tymon\JWTAuth\Exceptions\JWTException;

try {
    return auth('api')->login($user);
} catch (JWTException $e) {
    // 'Private key is not set.' means the service could not even be built - config, not user, error
    report($e);
    abort(500, 'token service unavailable');
}

Prevention

When it happens

Trigger: Setting JWT_ALGO to RS256/RS384/RS512/ES256/ES384/ES512 while JWT_PUBLIC_KEY/JWT_PRIVATE_KEY env vars are unset (config keys 'keys.private' resolves to null via Arr::get). The first call to auth('api')->login(), auth:api middleware, or resolving the 'tymon.jwt' service from the container triggers it.

Common situations: Switching from HS256 to asymmetric signing but only generating the key pair locally; keys present in the dev .env but missing from the production environment's secret store; CI pipeline without JWT_PRIVATE_KEY; config cached before the env value was added; a .env value written as JWT_PRIVATE_KEY= (empty).

Related errors


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