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

  1. Decrypt the token first with the Encryption/decrypt API, then validate the resulting UnencryptedToken
  2. Ensure you construct constraints for the JWS (decrypted) token, not the JWE container
  3. 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

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


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)