Skip to content

feat(profile-sync): add MFA authentication services - #10265

Merged
mathieuartu merged 8 commits into
mainfrom
mfa/sdk-services
Sep 18, 2026
Merged

mathieuartu merged 8 commits into
mainfrom
mfa/sdk-services

Conversation

@mathieuartu

@mathieuartu mathieuartu commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

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 #10264. Follow-ups: #10266, #10267. Stack: stack #10268.
Related to: https://consensyssoftware.atlassian.net/browse/MUL-2261

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
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.

Reviewed by Cursor Bugbot for commit 56b3671. 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.

@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/sdk-services branch 2 times, most recently from 3b1793f to ccdc7a1 Compare September 17, 2026 06:53
Base automatically changed from mfa/sdk-foundation to main September 17, 2026 13:39
@mathieuartu
mathieuartu force-pushed the mfa/sdk-services branch 2 times, most recently from 7625f86 to 55ed40f Compare September 17, 2026 13:41
pull Bot pushed a commit to Reality2byte/core that referenced this pull request Sep 17, 2026
## 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>

@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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

mathieuartu and others added 7 commits September 18, 2026 09:23
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>

@gantunesr gantunesr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

lets wait for @ccharly to approve it too before merging

@ccharly ccharly left a comment

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.

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({

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.

Maybe we could put the entire token as sensitive too? Just in case

(can be done in a follow-up too)

@mathieuartu
mathieuartu added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit 8762cfa Sep 18, 2026
136 checks passed
@mathieuartu
mathieuartu deleted the mfa/sdk-services branch September 18, 2026 12:13
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.

3 participants