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
- Fix the issuer to always call ->expiresAt(new DateTimeImmutable(...)) (or equivalent) when building tokens
- If strictness is undesired, use a non-strict constraint (e.g. ValidAt with optional claims behavior / LooseValidAt if available) that tolerates missing exp
- Add pre-validation check: $token->claims()->has('exp') and reject such tokens at the boundary with a clearer error
- 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
- Enforce exp at issuance in the token builder; make it mandatory in your issuer wrapper
- Reject exp-less tokens at ingestion from third parties
- Document your token profile (RFC 8725) with exp as required
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
- "Not Before" claim missing
- The token does not have the claim
- The token does not have the claim
- "Issued At" claim missing
- No constraint given.
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)