tymondesigns/jwt-auth · error · Tymon\JWTAuth\Exceptions\TokenInvalidException
Token Signature could not be verified.
Error message
Token Signature could not be verified.
What it means
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.
Source
Thrown at src/Providers/JWT/Lcobucci.php:117
/**
* Decode a JSON Web Token.
*
* @param string $token
* @return array
*
* @throws \Tymon\JWTAuth\Exceptions\JWTException
*/
public function decode($token)
{
try {
/** @var \Lcobucci\JWT\Token\Plain */
$token = $this->config->parser()->parse($token);
} catch (Exception $e) {
throw new TokenInvalidException('Could not decode token: '.$e->getMessage(), $e->getCode(), $e);
}
if (! $this->config->validator()->validate($token, ...$this->config->validationConstraints())) {
throw new TokenInvalidException('Token Signature could not be verified.');
}
return Collection::wrap($token->claims()->all())
->map(function ($claim) {
if ($claim instanceof DateTimeInterface) {
return $claim->getTimestamp();
}
return is_object($claim) && method_exists($claim, 'getValue')
? $claim->getValue()
: $claim;
})
->toArray();
}
/**
* Create an instance of the builder with all of the claims applied.
*View on GitHub (pinned to 6c70930a92)
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
Example fix
# before - verifier configured with a stale cached secret php artisan config:cache # cached with empty/old JWT_SECRET # after - set the env value, then rebuild the cache echo "JWT_SECRET=shared-secret" >> .env php artisan config:clear && php artisan config:cache
Defensive patterns
Strategy: try-catch
Validate before calling
// You cannot verify a signature yourself without the key, but you can verify your OWN config is sane at boot:
// (catches wrong/missing key material before users do)
if (config('jwt.algo') === 'HS256') {
abort_if(empty(config('jwt.secret')), 500, 'jwt.secret not configured');
} Type guard
null
Try / catch
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
try {
$payload = JWTAuth::parseToken()->checkOrFail(); // runs decode + signature constraint
} catch (TokenInvalidException $e) {
// message is intentionally generic: wrong key, wrong algo, or tampering all land here
return response()->json(['error' => 'token_signature_invalid'], 401);
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Could not create token: {exception message}
- The given algorithm could not be found
- Private key is not set.
- Secret is not set.
- Public key is not set.
AI-assisted analysis of tymondesigns/jwt-auth@6c70930a92 (2026-08-21).
Data as JSON: /api/errors/32f49155d385ccee.
Report an issue: GitHub.