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
- Make the two submitted values identical before saving, or unset password2 entirely — an empty/falsy confirmation skips the comparison entirely.
- 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.
- 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
- Render password and password2 from the same form object so they are validated together.
- Validate is_string($password) at the request boundary before assigning it to the object.
- In API endpoints, compare then unset password2 so a stale confirmation can never reach save().
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
- Passwords did not match.
- Invalid username: contains invalid characters or sequences
- 500
- %s: Relationship %s does not exist
- 400
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/6c7155cc2dd9cf48.
Report an issue: GitHub.