ellite/Wallos · error · InvalidArgumentException

The counter must be at least 0.

Error message

The counter must be at least 0.

What it means

HOTP::verify throws InvalidArgumentException when the optional $counter argument is a negative int. Since verify() accepts null|0|positive-int, any negative value is rejected outright before verification proceeds.

Solutions

  1. Clamp the counter: $counter = max(0, $computedCounter); before calling verify().
  2. Omit the $counter argument (pass null) to verify at the object's current counter.
  3. Validate user-supplied counters: reject negatives at input boundary with a 4xx-style validation error.
  4. Check stored counter values in your database for corruption/negative values.

Example fix

// before
$ok = $hotp->verify($otp, $storedCounter - $window);
// after
$ok = $hotp->verify($otp, max(0, $storedCounter - $window));
Defensive patterns

Strategy: validation

Validate before calling

if ($counter !== null && (!is_int($counter) || $counter < 0)) {
    throw new DomainException('Counter must be a non-negative integer');
}

Type guard

function isValidCounter(mixed $counter): bool {
    return $counter === null || (is_int($counter) && $counter >= 0);
}

Try / catch

try {
    $valid = $hotp->verify($otp, $counter);
} catch (InvalidArgumentException $e) {
    log_warning('Negative counter passed to verify', ['counter' => $counter]);
    return false;
}

Prevention

When it happens

Trigger: Calling $hotp->verify($otp, -1) or passing a negative counter computed at runtime (e.g. $counter = $current - $window going below 0), or a negative value from user input/DB.

Common situations: Implementing look-ahead window verification where code computes counter minus window without clamping at 0; restoring state from a database with corrupt negative counters.

Related errors


AI-assisted analysis of ellite/Wallos@52820e87ca (2026-09-13). Data as JSON: /api/errors/080bee885237d1bd. Report an issue: GitHub.

Appendix: source

Thrown at libs/OTPHP/HOTP.php:74

        return $value;
    }

    public function getProvisioningUri(): string
    {
        return $this->generateURI('hotp', [
            'counter' => $this->getCounter(),
        ]);
    }

    /**
     * If the counter is not provided, the OTP is verified at the actual counter.
     *
     * @param null|0|positive-int $counter
     */
    public function verify(string $otp, null|int $counter = null, null|int $window = null): bool
    {
        $counter >= 0 || throw new InvalidArgumentException('The counter must be at least 0.');

        if ($counter === null) {
            $counter = $this->getCounter();
        } elseif ($counter < $this->getCounter()) {
            return false;
        }

        return $this->verifyOtpWithWindow($otp, $counter, $window);
    }

    public function setCounter(int $counter): void
    {
        $this->setParameter('counter', $counter);
    }

    /**
     * @return array<non-empty-string, callable>
     */

View on GitHub (pinned to 52820e87ca)