{"record":{"id":"32f49155d385ccee","repo":"tymondesigns/jwt-auth","slug":"token-signature-could-not-be-verified","errorCode":null,"errorMessage":"Token Signature could not be verified.","messagePattern":"Token Signature could not be verified\\.","errorType":"exception","errorClass":"Tymon\\JWTAuth\\Exceptions\\TokenInvalidException","httpStatus":null,"severity":"error","filePath":"src/Providers/JWT/Lcobucci.php","lineNumber":117,"sourceCode":"    /**\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);\n        }\n\n        if (! $this->config->validator()->validate($token, ...$this->config->validationConstraints())) {\n            throw new TokenInvalidException('Token Signature could not be verified.');\n        }\n\n        return Collection::wrap($token->claims()->all())\n            ->map(function ($claim) {\n                if ($claim instanceof DateTimeInterface) {\n                    return $claim->getTimestamp();\n                }\n\n                return is_object($claim) && method_exists($claim, 'getValue')\n                    ? $claim->getValue()\n                    : $claim;\n            })\n            ->toArray();\n    }\n\n    /**\n     * Create an instance of the builder with all of the claims applied.\n     *","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/tymondesigns/jwt-auth/blob/6c70930a92710d97e8e52b182fca2176097f33be/src/Providers/JWT/Lcobucci.php#L99-L135","documentation":"Thrown by the Lcobucci provider's decode() when the token parsed successfully but failed the SignedWith validation constraint: the signature over header.payload does not verify with the configured verification key using the configured algorithm. The message is deliberately generic - it does not distinguish a tampered token from a wrong key, wrong secret, or wrong algorithm, because revealing that would leak signing configuration.","triggerScenarios":"Presenting a token that was signed with a different secret (JWT_SECRET differs between issuer and verifier), a public key that does not match the issuing private key, or after changing JWT_ALGO (e.g. HS256 to RS256) while clients still hold old tokens; also any client-side modification of header or payload claims.","commonSituations":"JWT_SECRET set in one environment but not another (staging vs production), config cached with php artisan config:cache before the env value existed, multiple services sharing tokens without sharing the secret, a secret rotated without invalidating outstanding tokens, or a regenerated RSA key pair while old tokens are still in circulation.","solutions":["Verify the verifier uses exactly the same secret/keys as the issuer: compare JWT_SECRET values, and for RSA/ECDSA confirm the public key matches the private key (openssl x509 -noout -modulus vs openssl rsa -noout -modulus)","Run php artisan config:clear (and re-run config:cache) after any .env change - cached config silently keeps the old secret","Confirm JWT_ALGO matches how the outstanding tokens were actually signed; if you changed it deliberately, old tokens must expire or clients must re-authenticate","If the secret was rotated intentionally, treat outstanding tokens as invalid: return 401 and force the client through refresh/login","If only one client fails, inspect its token's alg header (decode it manually) - it may be signing with a different library default such as HS512"],"exampleFix":"# before - verifier configured with a stale cached secret\nphp artisan config:cache   # cached with empty/old JWT_SECRET\n\n# after - set the env value, then rebuild the cache\necho \"JWT_SECRET=shared-secret\" >> .env\nphp artisan config:clear && php artisan config:cache","handlingStrategy":"try-catch","validationCode":"// You cannot verify a signature yourself without the key, but you can verify your OWN config is sane at boot:\n// (catches wrong/missing key material before users do)\nif (config('jwt.algo') === 'HS256') {\n    abort_if(empty(config('jwt.secret')), 500, 'jwt.secret not configured');\n}","typeGuard":"null","tryCatchPattern":"use Tymon\\JWTAuth\\Exceptions\\TokenInvalidException;\n\ntry {\n    $payload = JWTAuth::parseToken()->checkOrFail(); // runs decode + signature constraint\n} catch (TokenInvalidException $e) {\n    // message is intentionally generic: wrong key, wrong algo, or tampering all land here\n    return response()->json(['error' => 'token_signature_invalid'], 401);\n}","preventionTips":["Provision the identical JWT_SECRET (or key pair) in every environment that issues or verifies tokens - use your platform's secret store, not copied .env files","Treat config changes as token-invalidating: after rotating secrets or algorithms, return 401 so clients re-authenticate","Include a signature round-trip test (issue then decode) in your test suite to catch mismatched key pairs before deploy","Run php artisan config:clear whenever .env changes in any environment that caches config"],"tags":["jwt","php","laravel","signature-verification","authentication","configuration"],"backgroundTag":"jwt-signature-verification-failed","analyzedSha":"6c70930a92710d97e8e52b182fca2176097f33be","analyzedAt":"2026-08-21T02:16:37.040Z","schemaVersion":2},"datasetVersion":"2026-08-21T03:17:12.404Z"}