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\JWTExceptionView on GitHub (pinned to 6c70930a92)
Solutions
- 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
- 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
- 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)
- Run php artisan config:clear so cached config is rebuilt with the new values
- 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
- Generate and store the key pair (private + public + passphrase) as a unit in your secret manager; never deploy one side without the other
- Add a deploy smoke test that resolves auth('api') and mints a throwaway token, catching missing keys before traffic does
- Keep the PEM intact: quote env values and preserve newlines; verify locally with openssl pkey -check
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
- Public key is not set.
- Could not create token: {exception message}
- Token Signature could not be verified.
- The given algorithm could not be found
- Secret is not set.
AI-assisted analysis of tymondesigns/jwt-auth@6c70930a92 (2026-08-21).
Data as JSON: /api/errors/e03bcd8b2dc68113.
Report an issue: GitHub.