lcobucci/jwt · error · ConstraintViolation

The token is expired

Error message

The token is expired

What it means

This ConstraintViolation is thrown by the LooseValidAt constraint when the token's `exp` (expiration) claim is present and in the past relative to the current time (adjusted by the configured leeway). It is the library's way of enforcing that tokens are only accepted within their validity window.

Solutions

  1. Obtain a fresh token by re-authenticating/refreshing before validating again.
  2. Add leeway to absorb clock skew: new LooseValidAt($clock, DateInterval::createFromDateString('60 seconds')).
  3. Check the token's `exp` claim ($token->claims()->get('exp')) against your server time to confirm it really is past, not a clock issue.
  4. If clocks are the problem, synchronize both systems with NTP; extend the token lifetime at issuance if sessions legitimately need to be longer.

Example fix

// before: strict validation rejects skewed clocks
$validator->assert($token, [new LooseValidAt($clock)]); // expired thrown

// after: allow 1 minute of leeway
$validator->assert($token, [
    new LooseValidAt($clock, DateInterval::createFromDateString('1 minute')),
]);
Defensive patterns

Strategy: try-catch

Validate before calling

$exp = $token->claims()->get('exp');
if ($exp instanceof DateTimeInterface && $exp->getTimestamp() < time()) {
    // refresh token before validating
}

Type guard

function isUsableLifetime(Lcobucci\JWT\Token $token, int $leewaySeconds = 0): bool
{
    $exp = $token->claims()->get('exp');
    return ! $exp instanceof DateTimeInterface
        || $exp->getTimestamp() >= time() - $leewaySeconds;
}

Try / catch

try {
    $validator->assert($token, $constraints);
} catch (Lcobucci\JWT\Validation\ConstraintViolation $e) {
    if ($e->getConstraint() instanceof Lcobucci\JWT\Validation\Constraint\LooseValidAt) {
        // expired (or not-yet-valid): trigger token refresh / re-login
    }
}

Prevention

When it happens

Trigger: LooseValidAt::assert($token) calls assertExpiration(), which throws when Token::isExpired($now) returns true — i.e. the token carries an `exp` claim earlier than ($now - leeway).

Common situations: A user presenting a cached/stored token after its lifetime ended; long-running jobs replaying a token issued at start; server clock skewed far forward (or the issuer's clock far behind) so tokens appear expired; missing leeway configuration between systems with unsynchronized clocks.

Understand the failure class

Related errors


AI-assisted analysis of lcobucci/jwt@375813049c (2026-09-14). Data as JSON: /api/errors/ec4ca85539efa3ae. Report an issue: GitHub.

Appendix: source

Thrown at src/Validation/Constraint/LooseValidAt.php:48

        }

        return $leeway;
    }

    public function assert(Token $token): void
    {
        $now = $this->clock->now();

        $this->assertIssueTime($token, $now->add($this->leeway));
        $this->assertMinimumTime($token, $now->add($this->leeway));
        $this->assertExpiration($token, $now->sub($this->leeway));
    }

    /** @throws ConstraintViolation */
    private function assertExpiration(Token $token, DateTimeInterface $now): void
    {
        if ($token->isExpired($now)) {
            throw ConstraintViolation::error('The token is expired', $this);
        }
    }

    /** @throws ConstraintViolation */
    private function assertMinimumTime(Token $token, DateTimeInterface $now): void
    {
        if (! $token->isMinimumTimeBefore($now)) {
            throw ConstraintViolation::error('The token cannot be used yet', $this);
        }
    }

    /** @throws ConstraintViolation */
    private function assertIssueTime(Token $token, DateTimeInterface $now): void
    {
        if (! $token->hasBeenIssuedBefore($now)) {
            throw ConstraintViolation::error('The token was issued in the future', $this);
        }
    }

View on GitHub (pinned to 375813049c)