Skip to content

feat(mfa): MFA behavior redesign SDK update#46

Open
lakhansamani wants to merge 8 commits into
mainfrom
feat/mfa-behavior-redesign
Open

feat(mfa): MFA behavior redesign SDK update#46
lakhansamani wants to merge 8 commits into
mainfrom
feat/mfa-behavior-redesign

Conversation

@lakhansamani

Copy link
Copy Markdown
Contributor

Summary

Brings authorizer-js up to date with authorizer#686 (MFA behavior redesign — withheld-token first-time setup, WebAuthn/Email/SMS as MFA factors, lockout + admin recovery, OAuth login gated the same way as password login). Supersedes #45 (closed as superseded — targeted the older, now-merged #685 contract).

  • Extended Meta/User/AuthResponse/UpdateUserRequest with the new fields (is_mfa_enforced, has_skipped_mfa_setup_at, mfa_locked_at, should_offer_webauthn_mfa_verify/_setup, should_offer_email_otp_mfa_setup, should_offer_sms_otp_mfa_setup, reset_mfa), plus 3 new request types.
  • Added 4 new methods on Authorizer: skipMfaSetup, lockMfa, emailOtpMfaSetup, smsOtpMfaSetup — all GraphQL+REST dual-protocol, matching verifyOtp's existing pattern (the backend now has full REST/gRPC parity for these mutations too).
  • Added parseMfaRedirectParams(url), a standalone helper for the mfa_required/mfa_methods query params the backend's OAuth callback now appends instead of the normal state/code on a redirect that requires MFA.
  • Rebuilt __test__/querySync.test.ts: a regression guard asserting every field on AuthResponse/Meta/User has a matching token in its hand-written GraphQL query/fragment string, and vice versa — this exact drift class (field added to a type, forgotten in the query string) has shipped twice in this project's history already.

Design doc: docs/superpowers/specs/2026-07-15-mfa-behavior-redesign-js-sdk-design.md (local only, gitignored in this repo).

Process

3-task plan, each implemented and independently reviewed (task-scoped spec + quality gate); a final whole-branch review across all commits (opus) found no Critical/Important issues — every new field/method spot-checked directly against the backend's schema.graphqls and confirmed to match exactly, including nullability.

Known gap (out of scope, tracked separately)

Self-service deletion of a single non-passkey MFA method (remove just TOTP, keep Email-OTP) doesn't exist on the backend yet — only the admin's all-or-nothing reset_mfa. Deferred by explicit decision, not part of this PR.

Test plan

  • npx tsc --noEmit clean
  • All unit tests passing (73/73 — querySync, mfaMethods, mfaRedirect, plus every pre-existing suite)
  • __test__/index.test.ts's testcontainers integration suite needs AUTHORIZER_IMAGE pointed at a PR-#686 build to exercise the new schema — currently fails against the default 2.3.0 image (reproduces identically at the base commit, not a regression from this PR)
  • authorizer-react's corresponding UI update — separate PR, not started yet

The AuthResponse test claimed to check 'vice versa' but only verified
that interface fields appeared in the fragment. Fixed by adding reverse
check that each fragment token maps to an interface field, correctly
extracting only top-level tokens from authTokenFragment to handle the
nested user { userFragment } sub-selection.

Added new User <-> userFragment test with full bidirectional coverage,
matching the existing Meta test pattern. Both reverse-direction checks
now catch drift where fragments contain fields not in the interface.
JSDoc on parseMfaRedirectParams noting it needs an absolute URL
(window.location.href), not .search/.pathname. querySync's
AuthResponse check now uses exact top-level token membership on both
sides, matching the Meta/User checks instead of a looser substring
check on the forward direction.
3.3.0-rc.1 is already published to npm without skipMfaSetup,
emailOtpMfaSetup, smsOtpMfaSetup, or parseMfaRedirectParams -
reusing that version string left every clean npm install of
authorizer-react silently resolving a stale SDK.
email/smsOtpMfaSetup's TOTP twin - settings-mode TOTP enrollment for
an already-authenticated caller, backed by the backend's new
totp_mfa_setup mutation. Returns the enrollment payload
(authenticator_scanner_image/secret/recovery_codes) directly rather
than just a message, since nothing is sent anywhere for TOTP - the
caller scans/enters it, then completes enrollment via
verifyOtp({ is_totp: true }).
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