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

  1. Ensure password and password2 are identical non-empty strings before save(), or leave password2 unset to skip the check.
  2. Reject array/non-scalar password values at the boundary before they reach the account object.
  3. 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

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


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