Skip to content

feat(mfa): enforce the plugin's MFA policy at sign-in - #88

Open
bordoni wants to merge 1 commit into
feat/mfa-corefrom
feat/mfa-plugin-enforcement
Open

bordoni wants to merge 1 commit into
feat/mfa-corefrom
feat/mfa-plugin-enforcement

Conversation

@bordoni

@bordoni bordoni commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

The plugin's MFA policy now runs at sign-in, so "Require" means something. Stacked on #86. Part of #28.

Changes

  • Every AuthKit sign-in asks the policy before it sets a session. That covers password, magic code, OAuth and the hosted-AuthKit callback.
    • A user with a factor answers a code.
    • A user who must use MFA but has no factor enrolls an authenticator app on the spot (QR code and setup key).
    • A profile that can't satisfy the user's MFA refuses with 403 workos_mfa_blocked (filter workos_mfa_block_message, action workos_mfa_blocked).
  • Nothing sensitive waits in the browser. The WorkOS response, tokens included, stays server-side in Mfa\PendingLogin; the browser holds only an mfa_token. The token is bound to the Login Profile, single use, capped at 5 wrong codes, and expires after 10 minutes (filter workos_mfa_pending_ttl).
  • Routes.
    • New POST /auth/mfa/enroll.
    • /auth/mfa/challenge and /auth/mfa/verify accept mfa_token. With it, they only accept factors and challenges that belong to this sign-in.
  • WorkOS's own mfa_enrollment now enrolls in the same step instead of being refused.
  • enforce: always asks a user without a factor to enroll instead of refusing them.
  • Hosted AuthKit. When the policy wants a code after the callback, the sign-in finishes on /workos/login/{profile}/?workos_mfa=…. That page renders only the MFA step, even for redirect-mode profiles.
  • Password-only sign-ins (?fallback=1, wp_signon(), headless mode, the login button) refuse users who have or need MFA. Application passwords are exempt. Filter workos_mfa_allow_password_fallback.
  • React shell: a new MfaEnroll step, MfaChallenge sends mfa_token, and the shell can resume from data-mfa.

Screenshots

Verified

  • New MfaPluginEnforcementTest (14 tests), run through the real REST routes with WorkOS mocked at the HTTP layer:
    • The MFA step instead of a session, then challenge and verify finishing the sign-in.
    • Foreign factors and forged challenges refused; wrong codes capped; the token bound to its profile.
    • Enrollment during sign-in, in both plugin mode and WorkOS mode.
    • Blocking, an unchanged sign-in for users without MFA, the password guard, and the resume markup.
  • The tests whose behavior intentionally changed (always, mfa_enrollment) now pin both modes.
  • Local WPUnit: 844 tests. The only failures are the 11 environment-only ones that also fail on clean main.
  • PHPCS, PHPStan, tsc and bun run build pass.

In plugin mode every AuthKit sign-in asks Mfa\Policy before it sets a
session. A user with a factor answers a code; a user who must use MFA and
has none enrolls an authenticator on the spot; a profile that can't
satisfy the user's MFA refuses with 403 workos_mfa_blocked.

- Mfa\PendingLogin parks the WorkOS response server-side; the browser
  only holds an mfa_token (profile-bound, single use, 5 wrong codes,
  10 minutes, filter workos_mfa_pending_ttl)
- POST /auth/mfa/enroll; /challenge and /verify accept mfa_token and
  only the sign-in's own factors and challenges
- WorkOS's mfa_enrollment now enrolls in the same step
- LoginCompleter::finish() is the shared last step
- hosted-AuthKit sign-ins finish on /workos/login/{profile}/?workos_mfa=
- Mfa\PasswordGuard: password-only sign-ins refuse users with MFA
  (application passwords exempt; workos_mfa_allow_password_fallback)
- React shell: MfaEnroll step (QR + setup key), MfaChallenge takes
  mfa_token, resume from data-mfa
@bordoni
bordoni force-pushed the feat/mfa-plugin-enforcement branch from d862ddd to 5fb5e4e Compare October 18, 2026 17:24
@bordoni
bordoni marked this pull request as ready for review October 19, 2026 05:28
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