getgrav/grav · error · RuntimeException

Passwords did not match.

Error message

Passwords did not match.

What it means

Thrown from the Flex user object's save pipeline (UserObject.php:625). When a non-empty plain-text 'password' (or 'password1') property is set, Grav requires it to be a string and to equal a non-empty 'password2' confirmation property; otherwise it throws before hashing via Authentication::create(). The check is skipped when password2 is empty/null, so the error means a confirmation value was present and disagreed (or the password itself was not a scalar).

Source

Thrown at system/src/Grav/Common/Flex/Types/Users/UserObject.php:625

        if ($isNewUser) {
            $newKey = $this->getKey();

            // Prevent overwriting an existing account when a low-privileged user
            // creates a new user with an already-taken username (GHSA-rr73-568v-28f8).
            // Applies to every storage implementation, not just FileStorage.
            $storage = $this->getFlexDirectory()->getStorage();
            if ($storage->hasKey($newKey)) {
                throw new RuntimeException('User account with this username already exists');
            }

            $this->setStorageKey($newKey);
        }

        $password = $this->getProperty('password') ?? $this->getProperty('password1');
        if (null !== $password && '' !== $password) {
            $password2 = $this->getProperty('password2');
            if (!\is_string($password) || ($password2 && $password !== $password2)) {
                throw new \RuntimeException('Passwords did not match.');
            }

            $this->setProperty('hashed_password', Authentication::create($password));
        }
        $this->unsetProperty('password');
        $this->unsetProperty('password1');
        $this->unsetProperty('password2');

        // Backwards compatibility with older plugins.
        $fireEvents = $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
        $grav = $this->getContainer();
        if ($fireEvents) {
            $self = $this;
            $grav->fireEvent('onAdminSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => &$self]));
            if ($self !== $this) {
                throw new RuntimeException('Switching Flex User object during onAdminSave event is not supported! Please update plugin.');
            }
        }

View on GitHub (pinned to 6040efed04)

Solutions

  1. Make the two submitted values identical before saving, or unset password2 entirely — an empty/falsy confirmation skips the comparison entirely.
  2. Ensure 'password' reaches the object as a scalar string: reject array values at the form/API boundary (e.g. expect scalar in your validation) before assigning the property.
  3. Catch RuntimeException around save() in your controller and re-render the form with a 'passwords did not match' message instead of a 500 error.

Example fix

// before
$user->setProperty('password', $data['password']);
$user->setProperty('password2', $data['password_confirm']);
$user->save(); // RuntimeException('Passwords did not match.')

// after
if (!\is_string($data['password']) || ($data['password2'] ?? '') !== $data['password']) {
    throw new \InvalidArgumentException('Passwords did not match.');
}
$user->setProperty('password', $data['password']);
$user->save(); // password2 left unset — comparison skipped
Defensive patterns

Strategy: validation

Validate before calling

$pass  = $form->getValue('password') ?? $form->getValue('password1');
$confirm = $form->getValue('password2');
if (null !== $pass && '' !== $pass) {
    if (!\is_string($pass) || ($confirm !== null && $confirm !== '' && $confirm !== $pass)) {
        // reject before save(): show 'passwords did not match'
    }
}

Type guard

function passwordsAgree(mixed $pass, mixed $confirm): bool
{
    return null === $pass || '' === $pass
        || (\is_string($pass) && (!$confirm || $confirm === $pass));
}

Try / catch

try {
    $user->save();
} catch (\RuntimeException $e) {
    if (str_contains($e->getMessage(), 'Passwords did not match')) {
        // re-render form with confirmation error; do not retry unchanged
    }
    throw $e;
}

Prevention

When it happens

Trigger: Calling $user->save() after setProperty('password', ...) and setProperty('password2', ...) with two different non-empty values; posting a form where 'password' arrives as an array (e.g. password[]) so !is_string($password) fires; API/headless user creation that forwards a raw request body containing a stale password2 alongside a new password.

Common situations: Registration or profile-edit forms whose client-side confirmation was bypassed; test fixtures that set password but forget to clear password2; double form submission or browser autofill making the two fields diverge; admin plugins that copy password1 into the object while keeping the original password2 from the same payload.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/6c7155cc2dd9cf48. Report an issue: GitHub.