feat(profile-sync): add MFA step-up sessions - #10267
Merged
Merged
Conversation
mathieuartu
added this pull request to stack #10268
September 16, 2026 13:42
This was referenced Sep 16, 2026
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
from
September 16, 2026 14:08
58aa78b to
796a725
Compare
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
from
September 16, 2026 20:13
5fe0fb9 to
b6feacb
Compare
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
from
September 16, 2026 20:31
b6feacb to
2c4687d
Compare
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
4 times, most recently
from
September 16, 2026 21:03
7826fcb to
e84159d
Compare
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
from
September 17, 2026 06:48
e84159d to
ab66eab
Compare
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
from
September 17, 2026 06:53
ab66eab to
15ab75a
Compare
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
2 times, most recently
from
September 17, 2026 07:46
9c60aec to
26775e8
Compare
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
from
September 17, 2026 08:12
26775e8 to
5478a59
Compare
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
from
September 18, 2026 12:13
98c36cc to
ca7b5d0
Compare
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
2 times, most recently
from
September 18, 2026 13:47
b04a6c3 to
89d2052
Compare
pull Bot
pushed a commit
to Reality2byte/core
that referenced
this pull request
Sep 18, 2026
## Explanation Wires the MFA HTTP client into the SRP JWT auth SDK on top of the validation foundation. - `mfa/services` calls `/api/v2/mfa/*` (enroll, enroll complete, verify, verify complete, credentials), validates request/response bodies, maps server codes to `MfaError` subclasses, and handles OTP cooldown / `Retry-After`. - `SRPJwtBearerAuth` exposes begin/complete enrollment and verification, credential listing, and assertion-to-token exchange. - Public `JwtBearerAuth` forwards those methods and rejects non-SRP auth types. - Nock fixtures and unit tests cover happy paths and error mapping. Clients still go through `AuthenticationController` in later PRs; this layer is not UI-facing. ## References Depends on MetaMask#10264. Follow-ups: MetaMask#10266, MetaMask#10267. Stack: [stack #10268](https://github.com/MetaMask/core/pull/10268). Related to: https://consensyssoftware.atlassian.net/browse/MUL-2261 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **High Risk** > Touches authentication, step-up tokens, and passkey/OTP handling; mistakes could weaken session elevation or mishandle credentials, though coverage is extensive. > > **Overview** > Adds **passkey and email OTP MFA** to the profile-sync JWT auth SDK: a new HTTP layer calls `/api/v2/mfa/*` (enroll, complete, verify, credentials), validates payloads, maps server codes to `MfaError` subclasses, and handles OTP cooldown plus `Retry-After`. > > **`SRPJwtBearerAuth`** and public **`JwtBearerAuth`** expose enrollment/step-up flows, credential listing, and **`exchangeMfaAssertion`** (AAL2 JWT → elevated access token via existing OIDC). MFA is **SRP-only**; email enrollment uses a separate `email` option from `entropySourceId`. > > Schemas mark challenges, OTP codes, passkey payloads, and tokens with **`sensitive()`**. Tests, nock fixtures, changelog, and SDK exports for MFA types are included. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 56b3671. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
2 times, most recently
from
September 18, 2026 21:10
ed5705e to
07823f4
Compare
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
from
September 21, 2026 14:07
07823f4 to
1eafaf2
Compare
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
from
September 21, 2026 15:37
1eafaf2 to
744dee0
Compare
pull Bot
pushed a commit
to Reality2byte/core
that referenced
this pull request
Sep 21, 2026
## Explanation Exposes UI-driven MFA enrollment on `AuthenticationController` (no ApprovalController). - In-memory `enrolledCredentials` cache (optional on the state type so partial-state selectors stay assignable) with redacted emails in state logs. - `refreshEnrolledCredentials`, `beginCredentialEnrollment`, `completeCredentialEnrollment`. - `AuthenticationController:credentialsChanged` when the cache actually changes. - Automatic credential refresh after sign-in and unlock when MFA is enabled; refresh failure does not fail sign-in. - Cache cleared (and the change event published) on lock, sign-out, and wallet reset. - Regenerated messenger action types; README and changelog updates. Enrollment is UI-orchestrated: the controller returns challenges; the client collects the passkey or OTP proof and calls complete. ## References Depends on MetaMask#10265. Follow-up: MetaMask#10267. Stack: [stack #10268](https://github.com/MetaMask/core/pull/10268). Related to: https://consensyssoftware.atlassian.net/browse/MUL-2262 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **High Risk** > Changes authentication session lifecycle and MFA enrollment, including SRP token invalidation after email enroll and race handling around lock/sign-out—security-sensitive behavior that clients must wire correctly. > > **Overview** > Adds **UI-orchestrated MFA enrollment** on `AuthenticationController`: `refreshEnrolledCredentials`, `beginCredentialEnrollment`, and `completeCredentialEnrollment`, wired to the existing JWT bearer MFA SDK and new messenger action types. > > Enrollment state lives in a **non-persisted** `enrolledCredentials` cache (UI-visible; state logs allow-list type/status/enrolledAt only). An optional **`trace`** constructor hook (`@metamask/controller-utils`) wraps MFA network steps with operation/credential tags and outcomes. > > **Session and concurrency guards** bump an auth-session epoch on lock, sign-out, and wallet reset so in-flight MFA work cannot apply after the session ends; credential refreshes are sequenced so slower responses cannot overwrite newer lists. **Email OTP completion** invalidates the primary SRP session after a post-enroll refresh (or even when the session ends mid-flow) so cached tokens are not reused without the new email claim; passkey enroll does not touch the session. Sign-out/`clearState` clear the credential cache. > > Also adds MFA nock fixtures and mock response helpers, depends on `@metamask/controller-utils`, and tightens `ElevatedTokenClaimsStruct` as a single sensitive struct. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d70d671. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
mathieuartu
force-pushed
the
mfa/controller-stepup
branch
from
September 22, 2026 08:05
744dee0 to
716bf2e
Compare
ccharly
reviewed
Sep 22, 2026
ccharly
previously approved these changes
Sep 22, 2026
| expect(() => | ||
| controller.getElevatedProfileToken({ maxSessionAgeMs: -1 }), | ||
| ).toThrow(/MFA\[invalid_request\]/u); | ||
| }); |
Contributor
There was a problem hiding this comment.
Nit: Should we have a test to also ensure the session is not clear when the request ends up as "invalid request"? e.g. we're never clearing the token in that case?
ccharly
approved these changes
Sep 22, 2026
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.
Explanation
Adds MFA step-up verification and a memory-only elevated AAL2 session on
AuthenticationController.beginStepUp,completeStepUp,getElevatedProfileToken,clearStepUpSession.AuthenticationController:stepUpSessionon open/close.getElevatedProfileTokenobserves expiry.operation/credentialTypetags and span attributes for outcome /mfaCode.References
Depends on #10266. Stack: stack #10268.
Related to: https://consensyssoftware.atlassian.net/browse/MUL-2263
Checklist
Note
High Risk
Changes authentication and elevated-token handling for sensitive operations; incorrect session TTL or teardown logic could block legitimate actions or briefly retain AAL2 access after lock/sign-out.
Overview
Adds MFA step-up verification on
AuthenticationController:beginStepUp/completeStepUprun passkey or email OTP ceremonies, exchange the MFA assertion for an AAL2 elevated profile token, and expose it viagetElevatedProfileTokenwhile a short-lived session is open. The token stays memory-only; persisted/UI state is limited to non-secretstepUpSessionExpiresAt(60s TTL clamped to JWTexp).Session lifecycle clears elevated access on wallet lock, sign-out, wallet reset, successful credential enrollment, base-session
authentication_required, and hard expiry (timer or lazy check ingetElevatedProfileToken). Callers can require fresher proof withmaxSessionAgeMswithout tearing down the session for others.Hardening reuses auth-session epoch checks so lock/sign-out during in-flight MFA cannot apply results; invalid exchanged tokens fail with
elevated_token_invalid. MFA network tracing now sets span attributes (outcome,mfaErrorCode) via the trace callback context instead of mutating shared trace data. A 401 on credential refresh also invalidates the SRP session and drops any step-up session.Docs/changelog and messenger action types are updated; tests cover step-up flows, TTL edge cases, and enrollment lock races.
Reviewed by Cursor Bugbot for commit 8f0582f. Bugbot is set up for automated code reviews on this repo. Configure here.