lcobucci/jwt · error · ConstraintViolation

"Expiration Time" claim missing

Error message

"Expiration Time" claim missing

What it means

StrictValidAt enforces that exp, nbf, and iat are ALL present, unlike lenient validators that skip missing claims. If the token has no 'exp' (Expiration Time) registered claim, assertExpiration throws '"Expiration Time" claim missing' before even checking expiry. This guards against never-expiring tokens being accepted.

Solutions

  1. Fix the issuer to always call ->expiresAt(new DateTimeImmutable(...)) (or equivalent) when building tokens
  2. If strictness is undesired, use a non-strict constraint (e.g. ValidAt with optional claims behavior / LooseValidAt if available) that tolerates missing exp
  3. Add pre-validation check: $token->claims()->has('exp') and reject such tokens at the boundary with a clearer error
  4. Contact the token provider to require exp per your token profile (RFC 8725 recommends exp)

Example fix

// before
$builder->issuedBy('me')->withClaim('iat', $now);
// after
$builder->issuedBy('me')->issuedAt($now)->expiresAt($now->modify('+1 hour'));
Defensive patterns

Strategy: try-catch

Validate before calling

if (! $token->claims()->has('exp')) {
    throw new InvalidArgumentException('Token must carry exp claim for StrictValidAt');
}

Type guard

function hasExpirationClaim(UnencryptedToken $t): bool { return $t->claims()->has('exp'); }

Try / catch

try {
    $validator->assert($token, new StrictValidAt($clock));
} catch (ConstraintViolation $e) {
    if (str_contains($e->getMessage(), 'claim missing')) { /* reject token; require exp */ }
}

Prevention

When it happens

Trigger: Validating with StrictValidAt a token whose claim set lacks the RegisteredClaims::EXPIRATION_TIME ('exp') — e.g. tokens issued by builder code that only called withClaim('iat', ...) and never expiresAt().

Common situations: Issuer library configured without mandatory exp; handcrafted or third-party JWTs that omit exp; token builder refactored and the expiresAt() call dropped; accepting tokens from an external service that does not set exp.

Related errors


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

Appendix: source

Thrown at src/Validation/Constraint/StrictValidAt.php:53

    public function assert(Token $token): void
    {
        if (! $token instanceof UnencryptedToken) {
            throw ConstraintViolation::error('You should pass a plain token', $this);
        }

        $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(UnencryptedToken $token, DateTimeInterface $now): void
    {
        if (! $token->claims()->has(Token\RegisteredClaims::EXPIRATION_TIME)) {
            throw ConstraintViolation::error('"Expiration Time" claim missing', $this);
        }

        if ($token->isExpired($now)) {
            throw ConstraintViolation::error('The token is expired', $this);
        }
    }

    /** @throws ConstraintViolation */
    private function assertMinimumTime(UnencryptedToken $token, DateTimeInterface $now): void
    {
        if (! $token->claims()->has(Token\RegisteredClaims::NOT_BEFORE)) {
            throw ConstraintViolation::error('"Not Before" claim missing', $this);
        }

        if (! $token->isMinimumTimeBefore($now)) {
            throw ConstraintViolation::error('The token cannot be used yet', $this);
        }
    }

View on GitHub (pinned to 375813049c)