lcobucci/jwt · error · ConstraintViolation
You should pass a plain token
Error message
You should pass a plain token
What it means
StrictValidAt requires an UnencryptedToken because it must read the plain (unencrypted) claim set to check iat, nbf, and exp. When a Token (e.g. an encrypted JWE) is passed instead of an UnencryptedToken (a decrypted JWS), it throws ConstraintViolation 'You should pass a plain token'. You must decrypt first, then validate time claims on the decrypted result.
Solutions
- Decrypt the token first with the Encryption/decrypt API, then validate the resulting UnencryptedToken
- Ensure you construct constraints for the JWS (decrypted) token, not the JWE container
- Check that your token parsing produces a PlainToken/SignedToken (UnencryptedToken) before validation
Example fix
// before $validator->assert($encryptedToken, new StrictValidAt($clock)); // after $decrypted = $encryption->decrypt($encryptedToken); $validator->assert($decrypted, new StrictValidAt($clock));
Defensive patterns
Strategy: type-guard
Validate before calling
if (! $token instanceof UnencryptedToken) { throw new InvalidArgumentException('Decrypt token before strict time validation'); } Type guard
function isPlainToken(Token $token): bool { return $token instanceof UnencryptedToken; } Try / catch
try {
$validator->assert($token, new StrictValidAt($clock));
} catch (ConstraintViolation $e) {
if (str_contains($e->getMessage(), 'plain token')) { $token = $encryption->decrypt($token); }
} Prevention
- Always decrypt JWE tokens before running claim-based constraints
- Type-hint UnencryptedToken in validation entry points so violations surface early
- Separate your validation pipeline into decrypt-then-validate stages
When it happens
Trigger: Passing an encrypted token object (Token) rather than UnencryptedToken to Validator::assert() with a StrictValidAt constraint — typically when using the JWE support (encrypt/decrypt API) and validating the encrypted container instead of the decrypted payload.
Common situations: Token stored/transmitted as JWE and fed directly into a validator configured with time constraints; refactored code that switched from signed to encrypted tokens without adding a decryption step; confusion between NonEncryptedToken types after loading via token string parsing.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- You should pass a plain token
- You should pass a plain token
- No constraint given.
- Encryption is not supported yet
- The claim " " is a registered claim, another constraint…
AI-assisted analysis of lcobucci/jwt@375813049c (2026-09-14).
Data as JSON: /api/errors/c0b7fb3b1e6d9926.
Report an issue: GitHub.
Appendix: source
Thrown at src/Validation/Constraint/StrictValidAt.php:39
}
private function guardLeeway(?DateInterval $leeway): DateInterval
{
if ($leeway === null) {
return new DateInterval('PT0S');
}
if ($leeway->invert === 1) {
throw LeewayCannotBeNegative::create();
}
return $leeway;
}
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);View on GitHub (pinned to 375813049c)