getgrav/grav · error · RuntimeException
Passwords did not match.
Error message
Passwords did not match.
What it means
The DataUser (non-flex, YAML file) save() path in system/src/Grav/Common/User/DataUser/User.php:153 performs the same password confirmation as the Flex implementation: a non-empty 'password'/'password1' that is not a string, or that differs from a non-empty 'password2', throws before the hash is written with Authentication::create(). Empty password2 skips the comparison, so the error always involves a present-but-mismatched confirmation (or a non-string password).
Source
Thrown at system/src/Grav/Common/User/DataUser/User.php:153
if (!$file->filename()) {
$locator = Grav::instance()['locator'];
// Check if a user with this username already exists (prevent overwriting)
$existingFile = $locator->findResource('account://' . $username . YAML_EXT);
if ($existingFile) {
throw new \RuntimeException('User account with this username already exists');
}
$file->filename($locator->findResource('account://' . $username . YAML_EXT, true, true));
}
// if plain text password, hash it and remove plain text
$password = $this->get('password') ?? $this->get('password1');
if (null !== $password && '' !== $password) {
$password2 = $this->get('password2');
if (!\is_string($password) || ($password2 && $password !== $password2)) {
throw new \RuntimeException('Passwords did not match.');
}
$this->set('hashed_password', Authentication::create($password));
}
$this->undef('password');
$this->undef('password1');
$this->undef('password2');
$data = $this->items;
if ($username === $data['username']) {
unset($data['username']);
}
unset($data['authenticated'], $data['authorized']);
$file->save($data);
// We need to signal Flex Users about the change.
/** @var Flex|null $flex */View on GitHub (pinned to 6040efed04)
Solutions
- Ensure password and password2 are identical non-empty strings before save(), or leave password2 unset to skip the check.
- Reject array/non-scalar password values at the boundary before they reach the account object.
- Catch RuntimeException around save() and re-present the form with a 'passwords did not match' error.
Example fix
// before
$user->set('password', $data['password']);
$user->set('password2', $data['password2']);
$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->set('password', $data['password']);
$user->save(); // password2 not set — comparison skipped Defensive patterns
Strategy: validation
Validate before calling
$pass = $data['password'] ?? $data['password1'] ?? null;
$confirm = $data['password2'] ?? null;
if (null !== $pass && '' !== $pass) {
if (!\is_string($pass) || ($confirm !== null && $confirm !== '' && $confirm !== $pass)) {
// reject before save()
}
} 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-show the password form
}
throw $e;
} Prevention
- Validate password/password2 pairs in your form layer before touching the account object.
- Compare, then undef password2 so no stale confirmation reaches save().
- Reject non-scalar password inputs at the request boundary.
When it happens
Trigger: Calling save() on a DataUser account after set('password', ...) and set('password2', ...) with different non-empty values; posting a form whose password field arrives as an array; copying password1 into the object while leaving a stale password2 in the same payload.
Common situations: Legacy sites using data accounts (system.accounts not set to flex) with custom registration forms; profile-save endpoints that forward whole request bodies; double submission making the confirmation field diverge from the password field.
Related errors
- Passwords did not match.
- Invalid username: contains invalid characters or sequences
- User account with this username already exists
- Theme name not provided.
- Failed to save file '%s': %s
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/fd90c24ada0457f9.
Report an issue: GitHub.