Skip to content

feat(profile-sync): add MFA step-up sessions - #10267

Merged
mathieuartu merged 7 commits into
mainfrom
mfa/controller-stepup
Sep 22, 2026
Merged

mathieuartu merged 7 commits into
mainfrom
mfa/controller-stepup

Conversation

@mathieuartu

@mathieuartu mathieuartu commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Explanation

Adds MFA step-up verification and a memory-only elevated AAL2 session on AuthenticationController.

  • beginStepUp, completeStepUp, getElevatedProfileToken, clearStepUpSession.
  • In-memory elevated token with TTL; AuthenticationController:stepUpSession on open/close.
  • Session is torn down on lock, sign-out, wallet reset, successful enrollment, and when getElevatedProfileToken observes expiry.
  • Completing enrollment or step-up re-asserts the wallet is still unlocked after network calls so a lock during the request cannot reopen an AAL2 session.
  • MFA network steps are traced with operation / credentialType tags and span attributes for outcome / mfaCode.

References

Depends on #10266. Stack: stack #10268.
Related to: https://consensyssoftware.atlassian.net/browse/MUL-2263

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

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 / completeStepUp run passkey or email OTP ceremonies, exchange the MFA assertion for an AAL2 elevated profile token, and expose it via getElevatedProfileToken while a short-lived session is open. The token stays memory-only; persisted/UI state is limited to non-secret stepUpSessionExpiresAt (60s TTL clamped to JWT exp).

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 in getElevatedProfileToken). Callers can require fresher proof with maxSessionAgeMs without 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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@mathieuartu
mathieuartu force-pushed the mfa/controller-stepup branch 4 times, most recently from 7826fcb to e84159d Compare September 16, 2026 21:03
@mathieuartu
mathieuartu force-pushed the mfa/controller-stepup branch 2 times, most recently from 9c60aec to 26775e8 Compare September 17, 2026 07:46
@mathieuartu
mathieuartu force-pushed the mfa/controller-stepup branch 2 times, most recently from b04a6c3 to 89d2052 Compare September 18, 2026 13:47
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
mathieuartu force-pushed the mfa/controller-stepup branch 2 times, most recently from ed5705e to 07823f4 Compare September 18, 2026 21:10
Base automatically changed from mfa/controller-enrollment to main September 21, 2026 15:37
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>
ccharly
ccharly previously approved these changes Sep 22, 2026
expect(() =>
controller.getElevatedProfileToken({ maxSessionAgeMs: -1 }),
).toThrow(/MFA\[invalid_request\]/u);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@mathieuartu
mathieuartu added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit 5bbe76c Sep 22, 2026
136 checks passed
@mathieuartu
mathieuartu deleted the mfa/controller-stepup branch September 22, 2026 11:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants