Repository navigation
Conversation
- WorkOS\Mfa\Settings: per-environment mfa_mode (plugin | workos | off) and mfa_required_roles; filter workos_mfa_mode - WorkOS\Mfa\Policy: profile enforce + enrolled + required -> none, challenge, enroll or block; filters workos_mfa_required_for_user and workos_mfa_decision - WorkOS\Mfa\FactorService: the one path for status, list (reconciles the mirrored flag with WorkOS), enroll + confirm with a first code, delete (ownership-checked), reset and the require flag; actions and mfa.* activity-log events - workos_mfa_* PHP helpers, docs/mfa.md, README pointers
bordoni
force-pushed
the
feat/mfa-core
branch
from
October 18, 2026 17:24
2e058a2 to
40ebdeb
Compare
bordoni
marked this pull request as ready for review
October 19, 2026 05:27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The MFA building blocks for #28: who decides that a sign-in needs a second factor, and one service for a user's factors. Stacked on #85. Sign-in enforcement, self-service and the admin screens come in the next PRs of this stack.
Why a mode setting
WorkOS's own MFA is one switch per environment: once it's set to Required, every non-SSO user must enroll at their next sign-in. Sites that want MFA for some users only (a role, a flagged account, one login page) need WordPress to decide. So
mfa_mode(per environment, filterworkos_mfa_mode) is:plugin(default): the plugin decides per user, role and Login Profile.workos: WorkOS enforces MFA for everyone, and the plugin runs the steps WorkOS asks for.off: the plugin challenges nobody.Changes
WorkOS\Mfa\Policy::decide( $user_id, $profile )returnsnone,challenge,enrollorblockfrom the profile'smfa.enforceand factor list, whether the user has a factor, and whether they must use MFA (admin flag,mfa_required_roles, filterworkos_mfa_required_for_user). Filterworkos_mfa_decisionhas the last word. A profile that turns MFA off blocks a user who has it, instead of quietly downgrading them.WorkOS\Mfa\FactorServiceis the one path for status, listing, enrolling (the factor stays unconfirmed until the first code checks out, and an unfinished one is deleted on restart or after a day), deleting (ownership-checked, 404 otherwise), resetting and the require flag. It keeps the mirrored_workos_mfa_enabledflag in step with WorkOS, since factors can change outside WordPress.workos_mfa_get_status(),workos_mfa_is_enabled(),workos_mfa_is_required(),workos_mfa_set_required(),workos_mfa_list_factors(),workos_mfa_reset()andworkos_mfa_decision(). Like the service, they don't check capabilities.workos_mfa_mode,workos_mfa_required_for_user,workos_mfa_decisionandworkos_mfa_totp_issuer; actionsworkos_mfa_enrolled,workos_mfa_factor_removed,workos_mfa_resetandworkos_mfa_required_changed. Activity Log eventsmfa.enrolled,mfa.removed,mfa.reset,mfa.required_setandmfa.required_cleared.docs/mfa.md(modes, decision table, service, helpers, errors, hooks, events). The README's MFA line no longer claims SMS and WebAuthn.Verified
MfaPolicyTest(18 tests): every row of the decision table, a profile without TOTP, theworkosandoffmodes, the mode filter, required roles and both filters.MfaFactorServiceTest(21 tests) against WorkOS-shaped responses: enroll, restart, unlinked users, confirm (valid code,valid: false, 4xx, malformed, wrong factor), the pending factor hidden from lists, drift in both directions, a stale pending factor, delete (own and someone else's), reset, the require flag and the helpers.mainon the local runner. PHPCS passes, and PHPStan reports nothing in the changed files.Part of #28