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

  1. 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)
  2. Run php artisan config:clear (and re-run config:cache) after any .env change - cached config silently keeps the old secret
  3. 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
  4. If the secret was rotated intentionally, treat outstanding tokens as invalid: return 401 and force the client through refresh/login
  5. 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

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


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