Skip to content

feat(mfa): add IRecoveryCodeService::disableTwoFactor - #169

Open
romanetar wants to merge 1 commit into
feat/mfa-admin-enforcement-and-reset-commandsfrom
feat/mfa-disable-two-factor-service
Open

romanetar wants to merge 1 commit into
feat/mfa-admin-enforcement-and-reset-commandsfrom
feat/mfa-disable-two-factor-service

Conversation

@romanetar

Copy link
Copy Markdown
Contributor

Stacked on #166 (base: feat/mfa-admin-enforcement-and-reset-commands), which is stacked on #125. Merge bottom-up: #125, #166, then this one.

Summary

Provisional implementation of the shared disable/reset service method that ClickUp ticket 12 (idp:reset-2fa) needs and ticket 14 (profile toggle and admin CRUD) will reuse. The ticket allows implementing it in ticket 12 when ticket 14 has not landed, with this exact contract:

IRecoveryCodeService::disableTwoFactor(User $user, ?string $currentPassword, ?User $actor): void

  • In a single transaction: disable2FA() (clears two_factor_enabled and two_factor_enforced_at), deletes every recovery code, and revokes every trusted device (is_revoked = 1, rows are kept).
  • Afterwards records a device_revoked audit event, best-effort: a failure there does not fail the operation.
  • $actor decides the password rule:
    • null (console, e.g. idp:reset-2fa): no password.
    • another user (an administrator): no password. Authorizing the administrator is the caller's job.
    • the same user (self-service): $currentPassword must match, otherwise ValidationException and nothing changes.
  • It does not emit settings_changed: the caller knows the context (reason, actor, operator) and emits it, so there is exactly one row.
  • Group-enforced users are still challenged at login afterwards, since enforcement is derived from group membership (User::shouldRequire2FA()).

Things to know

  • It does not call DeviceTrustService::removeTrustedDevices as the ticket suggests: that method's audit call opens its own transaction, and nesting transactions can close the entity manager if the inner one fails (the existing code in RecoveryCodeService warns about this). It uses IUserTrustedDeviceRepository::revokeAllForUser inside the same transaction instead, which is what removeTrustedDevices does for the DB part.
  • RecoveryCodeService gets a new constructor dependency (IUserTrustedDeviceRepository). Nothing instantiates it with new; the container resolves it.
  • If ticket 14 lands its own disableTwoFactor, the two need to be reconciled; ticket 14 can adopt this one directly.

Test plan

  • tests/unit/RecoveryCodeServiceDisableTwoFactorTest.php: 6 tests (console, admin, self-service with correct / wrong / missing password, audit failure).
  • tests/DisableTwoFactorIntegrationTest.php: 2 tests against the DB (everything cleared, 2 devices revoked, 1 device_revoked row and no settings_changed; wrong password leaves state untouched).
  • Full phpunit suite not run. tests/RecoveryCodeRegenerationTest.php already fails on the base branch (Class "Strategies\MFA\MFAChallengeStrategyFactory" not found), unrelated to this change.

Shared service method that disables 2FA for a user: clears
two_factor_enabled and two_factor_enforced_at, deletes every recovery code
and revokes every trusted device in a single transaction, then records a
device_revoked audit event best-effort.

The actor decides the password rule: null (console) and another user (admin)
need no password, the user acting on their own account must provide the
current one. The settings_changed audit event is left to the caller, which
knows the context (reason, actor, operator).

Group-enforced users are still challenged at login afterwards, as
enforcement is derived from group membership.
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5f947953-e3ba-4280-b3ed-173a2afa41e1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/openstackid/openapi/pr-169/

This page is automatically updated on each push to this PR.

@romanetar
romanetar added this pull request to stack #170 October 7, 2026 17:46
@romanetar romanetar mentioned this pull request Oct 7, 2026
3 of 4 tasks

This branch has not been deployed

No deployments
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.

1 participant