{"record":{"id":"e03bcd8b2dc68113","repo":"tymondesigns/jwt-auth","slug":"private-key-is-not-set","errorCode":null,"errorMessage":"Private key is not set.","messagePattern":"Private key is not set\\.","errorType":"exception","errorClass":"Tymon\\JWTAuth\\Exceptions\\JWTException","httpStatus":null,"severity":"critical","filePath":"src/Providers/JWT/Lcobucci.php","lineNumber":231,"sourceCode":"     */\n    protected function isAsymmetric()\n    {\n        return is_subclass_of($this->signer, Rsa::class)\n            || is_subclass_of($this->signer, Ecdsa::class);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @return \\Lcobucci\\JWT\\Signer\\Key\n     *\n     * @throws \\Tymon\\JWTAuth\\Exceptions\\JWTException\n     */\n    protected function getSigningKey()\n    {\n        if ($this->isAsymmetric()) {\n            if (! $privateKey = $this->getPrivateKey()) {\n                throw new JWTException('Private key is not set.');\n            }\n\n            return $this->getKey($privateKey, $this->getPassphrase() ?? '');\n        }\n\n        if (! $secret = $this->getSecret()) {\n            throw new JWTException('Secret is not set.');\n        }\n\n        return $this->getKey($secret);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @return \\Lcobucci\\JWT\\Signer\\Key\n     *\n     * @throws \\Tymon\\JWTAuth\\Exceptions\\JWTException","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/tymondesigns/jwt-auth/blob/6c70930a92710d97e8e52b182fca2176097f33be/src/Providers/JWT/Lcobucci.php#L213-L249","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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"],"exampleFix":"# before - asymmetric algo, no key material\nJWT_ALGO=RS256\n# JWT_PRIVATE_KEY unset -> JWTException: Private key is not set.\n\n# after\nJWT_ALGO=RS256\nJWT_PUBLIC_KEY=\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBg...\\n-----END PUBLIC KEY-----\\n\"\nJWT_PRIVATE_KEY=\"-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADAN...\\n-----END PRIVATE KEY-----\\n\"\nJWT_PASSPHRASE=my-passphrase","handlingStrategy":"validation","validationCode":"// Fail fast at boot when asymmetric signing lacks key material\npublic function boot(): void\n{\n    $asymmetric = in_array(config('jwt.algo'), ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'], true);\n\n    if ($asymmetric) {\n        foreach (['private', 'public'] as $side) {\n            if (empty(config(\"jwt.keys.{$side}\"))) {\n                throw new RuntimeException(\"jwt.keys.{$side} must be set for algorithm \".config('jwt.algo'));\n            }\n        }\n        openssl_pkey_get_private(config('jwt.keys.private'), config('jwt.keys.passphrase') ?? '')\n            || throw new RuntimeException('jwt.keys.private is not a loadable PEM (or passphrase is wrong).');\n    }\n}","typeGuard":"function hasCompleteAsymmetricKeys(array $keysConfig): bool\n{\n    return !empty($keysConfig['private'])\n        && !empty($keysConfig['public'])\n        && openssl_pkey_get_private($keysConfig['private'], $keysConfig['passphrase'] ?? '') !== false;\n}","tryCatchPattern":"use Tymon\\JWTAuth\\Exceptions\\JWTException;\n\ntry {\n    return auth('api')->login($user);\n} catch (JWTException $e) {\n    // 'Private key is not set.' means the service could not even be built - config, not user, error\n    report($e);\n    abort(500, 'token service unavailable');\n}","preventionTips":["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"],"tags":["jwt","php","laravel","configuration","rsa","ecdsa","missing-key"],"backgroundTag":"jwt-missing-signing-key","analyzedSha":"6c70930a92710d97e8e52b182fca2176097f33be","analyzedAt":"2026-08-21T02:16:37.040Z","schemaVersion":2},"datasetVersion":"2026-08-21T03:17:12.404Z"}