Skip to content

fix(mfa): read WorkOS MFA errors, call the real MFA endpoints, stop the password fallback skipping MFA - #85

Open
bordoni wants to merge 3 commits into
release/1.1.0from
fix/mfa-login-errors
Open

bordoni wants to merge 3 commits into
release/1.1.0from
fix/mfa-login-errors

Conversation

@bordoni

@bordoni bordoni commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

MFA on main can't work against real WorkOS. This fixes the sign-in side, so the rest of the 1.1.0 MFA work (#28) builds on calls that actually reach WorkOS.

Changes

  • WorkOS MFA errors are read. When MFA is required, WorkOS answers authenticate with an mfa_challenge or mfa_enrollment error, not a success carrying a pending token. LoginCompleter only looked at successes, so users saw WorkOS's raw error. The new Auth\AuthKit\MfaStep reads the step off the error: mfa_challenge becomes the existing code step (the profile's factor allowlist still applies), and mfa_enrollment returns 403 workos_authkit_mfa_enrollment_required until the sign-in form can enroll a factor.
  • The real MFA endpoints. Challenge, verify and factor delete called /user_management/auth_factors/… and /user_management/auth_challenges/…, which WorkOS doesn't serve. Factors enrolled through User Management are MFA API factors, so the calls now go to /auth/factors/{id}/challenge, /auth/challenges/{id}/verify and /auth/factors/{id} (the same paths as workos-php's MultiFactorAuth service). POST /auth/mfa/totp/enroll read the factor from the wrong level of the response (WorkOS nests it under authentication_factor), so the QR code and secret came back empty. It now reads them correctly and also returns the challenge_id WorkOS creates with the factor.
  • enforce = always accepts a correct code. The response to a verified code has no pending token, and the always check read that as "no factor". POST /auth/mfa/verify now tells complete() the second factor was satisfied.
  • The WordPress password can't skip MFA. On headless wp-login.php, an MFA error was treated like a wrong WorkOS password, and core's own password check at priority 20 (which ignores an earlier WP_Error) then signed the user in with no second factor. The filter now refuses with 403 workos_mfa_step_unsupported and re-applies the refusal after core's handlers. The login button's form gives the same message, and the AuthKit password route no longer re-syncs the password to WorkOS when only the second factor is missing.

Verified

  • New MfaLoginErrorsTest (10 tests) feeds the error shape the API client really produces: challenge and enrollment errors, the factor allowlist, no token leak on enrollment, always with a satisfied factor, and headless wp_authenticate() refusing an MFA step while still falling back for a wrong WorkOS password.
  • New cases in AuthKitRestMfaTest (always + verify succeeds, a wrong code still fails) and AuthKitRestPasswordTest (mfa_challenge surfaces and skips the WP fallback). ApiClientAuthKitTest asserts the /auth/... paths.
  • Local WPUnit: every suite these changes touch passes. The full run has the same 11 environment-only failures as clean main on the local runner (constants and "plugin disabled" cases). CI is authoritative.
  • PHPCS passes, and PHPStan reports nothing in the changed files.

Not in this PR

  • SMS enrollment still goes through /user_management/users/{id}/auth_factors, which WorkOS documents as TOTP-only. MFA in 1.1.0 is TOTP.

Part of #28

…skipping them

WorkOS answers authenticate with mfa_challenge / mfa_enrollment errors,
not a pending token in a success body. LoginCompleter now reads the step
from the error (MfaStep), so the sign-in form shows the code prompt and
the profile's factor allowlist applies. mfa_enrollment is refused with a
clear 403 until the form can enroll a factor.

- enforce=always no longer rejects the response from a correct code
- headless wp-login.php refuses an MFA step past WP's own password
  handlers, which ignored the earlier WP_Error and signed the user in
- the login button and the AuthKit password route stop treating an MFA
  step as a wrong password
…onse

Challenge, verify and factor delete went to /user_management/... paths
WorkOS doesn't serve. Factors enrolled through User Management are MFA API
factors, so they go to /auth/factors/{id}/challenge,
/auth/challenges/{id}/verify and /auth/factors/{id} (matches workos-php's
MultiFactorAuth service). TOTP enroll now reads the factor from
authentication_factor and returns the challenge WorkOS creates with it.
@bordoni
bordoni added this pull request to stack #87 October 18, 2026 16:10
@bordoni bordoni added this to the 1.1.0 milestone Oct 18, 2026
@bordoni bordoni changed the title fix/mfa login errors fix(mfa): read WorkOS MFA errors, call the real MFA endpoints, stop the password fallback skipping MFA Oct 18, 2026
wp_json_encode( [] ) is [], and WorkOS answers 422 "Validation failed"
to it. A TOTP challenge (POST /auth/factors/{id}/challenge, no SMS
template) always had an empty body, so the code step could never start.
Found on a live run against WorkOS staging; empty bodies are now {}.
@bordoni
bordoni marked this pull request as ready for review October 19, 2026 05:27
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