feat(profile-sync): add MFA authentication services - #10265
Conversation
906f800 to
4ff3dc2
Compare
7872ea7 to
47c2854
Compare
3b1793f to
ccdc7a1
Compare
ccdc7a1 to
ac84c3f
Compare
7625f86 to
55ed40f
Compare
## Explanation Adds the MFA validation layer in `@metamask/profile-sync-controller` that later PRs in this stack consume. - Superstruct schemas and inferred types for auth-service MFA responses, WebAuthn ceremony payloads, and controller-boundary requests (passkey and email OTP only). - `MfaError` family with a stable enumerable `mfaCode`, plus helpers that survive JSON-RPC serialization (`getMfaErrorCode`, `isMfaError`, `getMfaRetryAfterMs`). - Shared `decodeJwtPayload` (no signature verification) and slightly stricter login JWT `exp` handling. - Expanded `HTTP_STATUS_CODES` for later MFA error mapping. This PR does not call MFA HTTP endpoints or change controller behavior. ## References Stacked under [stack #10268](https://github.com/MetaMask/core/pull/10268). Follow-ups: MetaMask#10265, MetaMask#10266, MetaMask#10267. Related to: https://consensyssoftware.atlassian.net/browse/MUL-2260 ## 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] > **Medium Risk** > Touches authentication-adjacent validation, JWT claim handling, and structured MFA errors, though runtime sign-in/MFA flows are unchanged until follow-up PRs. > > **Overview** > Introduces the **MFA validation layer** in `@metamask/profile-sync-controller` for later stack PRs—no MFA HTTP calls or controller behavior changes in this diff. > > Adds Superstruct schemas and inferred types under `authentication-jwt-bearer/mfa` for passkey and email OTP flows: auth-service responses, WebAuthn ceremony payloads, enrollment/step-up requests, and **AAL2 elevated-token** claim parsing (including Hydra `ext` nesting). `assertValidMfaRequest` / `assertValidMfaResponse` map validation failures to `MfaError` with path details. > > Expands **`MfaError`** with typed subclasses, stable enumerable **`mfaCode`**, and helpers (`getMfaErrorCode`, `isMfaError`, `getMfaRetryAfterMs`) that work after JSON-RPC serialization. Adds shared **`decodeJwtPayload`** and refactors login JWT expiry checks to use it with stricter `exp` typing. **`HTTP_STATUS_CODES`** gains `401` and `502` for upcoming error mapping. > > Dependencies: **`@metamask/superstruct`** (runtime), **`@metamask/rpc-errors`** (tests). Changelog updated. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d6d147b. 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>
55ed40f to
32a03ff
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 32a03ff. Configure here.
ef981db to
9b772b3
Compare
Co-authored-by: Cursor <cursoragent@cursor.com>
- Recognise 401 before requiring a JSON error body - Map code-less 429s to rate_limited instead of otp_resend_cooldown - Stop parsing the unstable error message for a retry delay - Return only the assertion from verify/complete; drop profile mapping - Tolerate unknown credential statuses and missing email.verified - Export domain types only from the SDK entrypoint Co-authored-by: Cursor <cursoragent@cursor.com>
9b772b3 to
32dce23
Compare
ccharly
left a comment
There was a problem hiding this comment.
LGTM. I left 1 note about a potential missing sensitive (can be done in the next PR).
Also, as discussed internally, we could potentially enforce typing in some of the input type, so the compiler can enforce it, e.g.: using type: 'passkey' and enforce the field passkey_attestation to be required.
But this can come later too. Might be relevant if we add more credential type in the future!
| @@ -241,7 +247,7 @@ export const GetElevatedTokenRequestStruct = object({ | |||
| }); | |||
|
|
|||
| export const ElevatedTokenClaimsStruct = type({ | |||
There was a problem hiding this comment.
Maybe we could put the entire token as sensitive too? Just in case
(can be done in a follow-up too)
## 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>

Explanation
Wires the MFA HTTP client into the SRP JWT auth SDK on top of the validation foundation.
mfa/servicescalls/api/v2/mfa/*(enroll, enroll complete, verify, verify complete, credentials), validates request/response bodies, maps server codes toMfaErrorsubclasses, and handles OTP cooldown /Retry-After.SRPJwtBearerAuthexposes begin/complete enrollment and verification, credential listing, and assertion-to-token exchange.JwtBearerAuthforwards those methods and rejects non-SRP auth types.Clients still go through
AuthenticationControllerin later PRs; this layer is not UI-facing.References
Depends on #10264. Follow-ups: #10266, #10267. Stack: stack #10268.
Related to: https://consensyssoftware.atlassian.net/browse/MUL-2261
Checklist
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 toMfaErrorsubclasses, and handles OTP cooldown plusRetry-After.SRPJwtBearerAuthand publicJwtBearerAuthexpose enrollment/step-up flows, credential listing, andexchangeMfaAssertion(AAL2 JWT → elevated access token via existing OIDC). MFA is SRP-only; email enrollment uses a separateemailoption fromentropySourceId.Schemas mark challenges, OTP codes, passkey payloads, and tokens with
sensitive(). Tests, nock fixtures, changelog, and SDK exports for MFA types are included.Reviewed by Cursor Bugbot for commit 56b3671. Bugbot is set up for automated code reviews on this repo. Configure here.