Skip to content

feat(mfa): add idp:reset-2fa command - #171

Open
romanetar wants to merge 2 commits into
feat/mfa-disable-two-factor-servicefrom
feat/mfa-reset-2fa-command
Open

romanetar wants to merge 2 commits into
feat/mfa-disable-two-factor-servicefrom
feat/mfa-reset-2fa-command

Conversation

@romanetar

@romanetar romanetar commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

ref: https://app.clickup.com/t/86ba2zp7r

Stacked on #169 (base: feat/mfa-disable-two-factor-service), which is stacked on #166 and #125. Merge bottom-up: #125, #166, #169, then this one.

Summary

Second half of ClickUp ticket 12 (Admin Enforcement and Reset Commands): idp:reset-2fa {email} --reason="...", for operators with server access who need to recover a user that lost their second factor and recovery codes.

  • --reason is required: without it (or blank) the command exits non-zero before touching anything.
  • Fails with a clear message if the email does not exist.
  • Calls IRecoveryCodeService::disableTwoFactor($user, null, null) from feat(mfa): add IRecoveryCodeService::disableTwoFactor #169, which clears two_factor_enabled and two_factor_enforced_at, deletes the recovery codes and revokes the trusted devices in one transaction. The command does not reimplement any of that.
  • Emits the settings_changed audit event with {"reason": "<--reason>", "actor": "console", "operator": "<os user>@<hostname>"} and ip_address 127.0.0.1.
  • Prints a summary: unused recovery codes deleted and active trusted devices revoked (counted before the reset).
  • Group-enforced users stay challenged at the next login, since enforcement is derived from group membership. The output says so explicitly, and tells the operator the user has no codes and must regenerate them from the profile Security section after logging in with the email OTP.

Things to know

  • If the audit event cannot be recorded, the reset is already committed, so the command reports it and exits with a failure instead of hiding the missing trail.
  • "The next password login of a group-enforced admin still lands on the 2FA challenge; a self-enrolled user's does not" is checked through User::shouldRequire2FA() on the reloaded user, which is what the login uses, not through an HTTP login.
  • Together with feat(mfa): add idp:enforce-admin-2fa command #166 this covers the ticket's two commands. Ticket 14 can reuse disableTwoFactor as is.

Test plan

  • tests/unit/Console/Reset2FACommandTest.php: 6 tests (missing and blank reason, unknown user, success with audit metadata, enforced-group warning, audit failure).
  • tests/Reset2FACommandIntegrationTest.php: 3 tests against the DB (no reason changes nothing; self-enrolled user fully cleared with 1 settings_changed row; group-enforced admin still requires 2FA).
  • The four new test files of this stack run together: 25 tests, 135 assertions, 0 failures.
  • Full phpunit suite not run. tests/RecoveryCodeRegenerationTest.php already fails on the base branch (Class "Strategies\MFA\MFAChallengeStrategyFactory" not found), unrelated to this change.

@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Explain Change →

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

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 53ec627f-069a-43fd-9bee-5232c1de3c86

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
Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · 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

@romanetar
romanetar added this pull request to stack #170 October 7, 2026 17:51
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

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

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

@smarcet
smarcet force-pushed the feat/mfa-reset-2fa-command branch from d79aef8 to fe2914a Compare November 4, 2026 13:21
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

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

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

1 similar comment
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

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

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Using get_current_user() attributes 2FA resets to the PHP script owner rather than the executing operator, undermining audit-trail accountability.

1 open issue
What changed in this PR

Adds idp:reset-2fa for operators recovering users who have lost their second factor and recovery codes, reusing the shared reset service from #169.

Changes:

  • Requires a nonblank reason, resolves the user by email, and delegates the reset to disableTwoFactor.
  • Records audit metadata, reports reset counts and audit failures, and warns when group enforcement still applies.
  • Adds command registration and tests for validation, reset effects, auditing, and continued group enforcement.
File Description
tests/​unit/​Console/​Reset2FACommandTest.php Tests validation, service calls, audit metadata, warnings, and audit failure handling.
tests/​Reset2FACommandIntegrationTest.php Checks persisted reset effects and continued group enforcement.
app/​Console/​Kernel.php Registers the reset command with Artisan.
app/​Console/​Commands/​Reset2FACommand.php Implements reset orchestration, auditing, and operator output.

Review effort: Balanced

// the service owns the side effects (flags, codes, devices) in one transaction
$recovery_code_service->disableTwoFactor($user, null, null);

$operator = sprintf('%s@%s', get_current_user(), gethostname());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romanetar Confirmed: get_current_user() returns the owner of the artisan file, not the effective user of the process, so the operator in the settings_changed audit row can name the deploy account instead of whoever ran the command. Suggested fix: posix_getpwuid(posix_geteuid())['name'], falling back to get_current_user() when posix is unavailable. Note that inside a k8s pod (kubectl exec) neither identifies the person, so --reason should name the operator or the ticket, as in the SDS §4.11 example.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b727811. The operator now comes from posix_getpwuid(posix_geteuid()), falling back to get_current_user() when posix is unavailable (see processUser() in Reset2FACommand). Agreed that inside a k8s pod neither identifies the person, so --reason is the accountability anchor and should name the operator or ticket.

@smarcet
smarcet force-pushed the feat/mfa-reset-2fa-command branch from fe2914a to aeeb94f Compare November 4, 2026 17:42
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

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

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

1 similar comment
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

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

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

@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

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

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

@romanetar
romanetar force-pushed the feat/mfa-reset-2fa-command branch from 967399c to ad297f6 Compare November 10, 2026 16:55
@romanetar
romanetar requested a review from smarcet November 10, 2026 16:56
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

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

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

1 similar comment
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

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

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

@romanetar
romanetar force-pushed the feat/mfa-reset-2fa-command branch from 0c69b29 to 0231417 Compare November 10, 2026 19:01
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

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

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

Resets 2FA for one user, for operators with server access: requires
--reason (aborts without it), finds the user by email and calls
IRecoveryCodeService::disableTwoFactor, which clears the enrollment, deletes
the recovery codes and revokes the trusted devices in one transaction.

Emits a settings_changed audit event with {reason, actor: "console",
operator} and prints a summary with the number of codes deleted and devices
revoked. Group-enforced users stay challenged at login because enforcement is
derived from group membership; the output says so explicitly.
get_current_user() returns the owner of the artisan script, not the OS
user running the command, so the audit operator was always the checkout
owner.
@romanetar
romanetar force-pushed the feat/mfa-reset-2fa-command branch from 1129da2 to b727811 Compare November 10, 2026 19:13
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

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

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

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.

3 participants