Skip to content

fix(user:reset-password): generate policy-compliant random passwords#33

Merged
kojiromike merged 3 commits into
mainfrom
fix/random-password-policy
May 12, 2026
Merged

fix(user:reset-password): generate policy-compliant random passwords#33
kojiromike merged 3 commits into
mainfrom
fix/random-password-policy

Conversation

@kojiromike
Copy link
Copy Markdown
Contributor

@kojiromike kojiromike commented May 12, 2026

Summary

  • --random previously emitted bin2hex(random_bytes(12)) — hex-only, no uppercase or symbols. Installs with secure_password enabled rejected the password at next login, leaving a "successful" reset that produced an unusable account.
  • Adds Service\PasswordPolicy that mirrors AuthUtils' private validators against the install's runtime $GLOBALS (gbl_minimum_password_length, gbl_maximum_password_length, secure_password).
  • Reworks the generator to produce a 20-char password covering all four character classes (CSPRNG-shuffled), then validates through PasswordPolicy with a 32-attempt retry cap before failing loudly.

Why duplicate the policy?

OpenEMR's checks are private and the only public path (AuthUtils::updatePassword()) couples validation with persistence and die(). There is no side-effect-free validator to call. The class docblock acknowledges this and points at the upstream lines; openemr/openemr#12127 proposes the upstream fix so Service\PasswordPolicy can later be deleted in favor of a thin wrapper. Until our minimum-supported version moves past that change, the workaround stays.

Test plan

  • composer phpunit — 51 tests, 189 assertions
  • composer check (php-lint, phpcs, phpstan level 9, rector) clean
  • task smoke against the dev container — passed (Generated password: W4trECTuLP_seM=fmXgS → 20 chars, all four classes present)

Closes #12

The previous --random implementation produced bin2hex(random_bytes(12)) —
hex-only, no uppercase or symbols. Installs with secure_password enabled
rejected the password at next login, leaving a "successful" reset that
produced an unusable account.

Introduce Service\PasswordPolicy that mirrors AuthUtils' private
validators against the install's runtime $GLOBALS, and rework the
generator to produce a 20-char password covering all four character
classes, validated through PasswordPolicy with a retry cap.

The duplication is acknowledged in the class docblock with a pointer to
the upstream lines; openemr/openemr#12127 tracks the path to deletion
once a public, side-effect-free validator lands in OpenEMR.

Closes #12

Assisted-by: Claude Code
… type

Self-review: the empty-alphabet LogicException in pick() was unreachable
defensive code added only to satisfy PHPStan's random_int argument
analysis. A non-empty-string param type expresses the same invariant
statically and lets the body stay one line.

Assisted-by: Claude Code
Self-review: PasswordPolicy had no extension hook, so making it final
expresses intent and prevents accidental subclassing. PHPUnit's
createMock cannot double final classes, so callers (the command, tests)
now depend on PasswordPolicyInterface and the concrete is sealed.

Assisted-by: Claude Code
@kojiromike kojiromike merged commit 23a082a into main May 12, 2026
10 checks passed
@kojiromike kojiromike deleted the fix/random-password-policy branch May 12, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--random password may fail OpenEMR password policy

1 participant