@metamask/profile-sync-controller 32.3.1 → 34.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +46 -2
  2. package/README.md +26 -11
  3. package/dist/controllers/authentication/AuthenticationController-method-action-types.d.ts +36 -22
  4. package/dist/controllers/authentication/AuthenticationController-method-action-types.d.ts.map +1 -1
  5. package/dist/controllers/authentication/AuthenticationController-method-action-types.js.map +1 -1
  6. package/dist/controllers/authentication/AuthenticationController.d.ts +33 -23
  7. package/dist/controllers/authentication/AuthenticationController.d.ts.map +1 -1
  8. package/dist/controllers/authentication/AuthenticationController.js +156 -80
  9. package/dist/controllers/authentication/AuthenticationController.js.map +1 -1
  10. package/dist/controllers/authentication/index.d.ts +1 -1
  11. package/dist/controllers/authentication/index.d.ts.map +1 -1
  12. package/dist/controllers/authentication/index.js.map +1 -1
  13. package/dist/controllers/user-storage/contact-syncing/controller-integration.js +5 -5
  14. package/dist/controllers/user-storage/contact-syncing/controller-integration.js.map +1 -1
  15. package/dist/sdk/authentication-jwt-bearer/flow-srp.d.ts +15 -12
  16. package/dist/sdk/authentication-jwt-bearer/flow-srp.d.ts.map +1 -1
  17. package/dist/sdk/authentication-jwt-bearer/flow-srp.js +11 -8
  18. package/dist/sdk/authentication-jwt-bearer/flow-srp.js.map +1 -1
  19. package/dist/sdk/authentication-jwt-bearer/mfa/schemas.d.ts +21 -16
  20. package/dist/sdk/authentication-jwt-bearer/mfa/schemas.d.ts.map +1 -1
  21. package/dist/sdk/authentication-jwt-bearer/mfa/schemas.js +24 -20
  22. package/dist/sdk/authentication-jwt-bearer/mfa/schemas.js.map +1 -1
  23. package/dist/sdk/authentication-jwt-bearer/mfa/services.d.ts +9 -7
  24. package/dist/sdk/authentication-jwt-bearer/mfa/services.d.ts.map +1 -1
  25. package/dist/sdk/authentication-jwt-bearer/mfa/services.js +11 -6
  26. package/dist/sdk/authentication-jwt-bearer/mfa/services.js.map +1 -1
  27. package/dist/sdk/authentication-jwt-bearer/mfa/types.d.ts +25 -11
  28. package/dist/sdk/authentication-jwt-bearer/mfa/types.d.ts.map +1 -1
  29. package/dist/sdk/authentication-jwt-bearer/mfa/types.js.map +1 -1
  30. package/dist/sdk/authentication.d.ts +5 -5
  31. package/dist/sdk/authentication.d.ts.map +1 -1
  32. package/dist/sdk/authentication.js +1 -1
  33. package/dist/sdk/authentication.js.map +1 -1
  34. package/dist/sdk/errors.d.ts +4 -4
  35. package/dist/sdk/errors.d.ts.map +1 -1
  36. package/dist/sdk/errors.js +6 -6
  37. package/dist/sdk/errors.js.map +1 -1
  38. package/dist/sdk/index.d.ts +2 -2
  39. package/dist/sdk/index.d.ts.map +1 -1
  40. package/dist/sdk/index.js.map +1 -1
  41. package/dist/sdk/mocks/auth.d.ts +12 -5
  42. package/dist/sdk/mocks/auth.d.ts.map +1 -1
  43. package/dist/sdk/mocks/auth.js +4 -6
  44. package/dist/sdk/mocks/auth.js.map +1 -1
  45. package/dist/shared/encryption/cache.d.ts +4 -4
  46. package/dist/shared/encryption/cache.d.ts.map +1 -1
  47. package/dist/shared/encryption/cache.js.map +1 -1
  48. package/dist/shared/encryption/constants.d.ts +0 -1
  49. package/dist/shared/encryption/constants.d.ts.map +1 -1
  50. package/dist/shared/encryption/constants.js +0 -2
  51. package/dist/shared/encryption/constants.js.map +1 -1
  52. package/dist/shared/encryption/encryption.d.ts.map +1 -1
  53. package/dist/shared/encryption/encryption.js +23 -25
  54. package/dist/shared/encryption/encryption.js.map +1 -1
  55. package/dist/shared/storage-schema.js.map +1 -1
  56. package/dist/shared/types/encryption.d.ts +1 -1
  57. package/dist/shared/types/encryption.d.ts.map +1 -1
  58. package/dist/shared/types/encryption.js.map +1 -1
  59. package/dist/shared/utils/message-signing.d.ts.map +1 -1
  60. package/dist/shared/utils/message-signing.js +2 -14
  61. package/dist/shared/utils/message-signing.js.map +1 -1
  62. package/package.json +14 -15
@@ -1,7 +1,7 @@
1
1
  import { BaseController } from '@metamask/base-controller';
2
2
  import { selectHdKeyringEntropySourceIds } from '@metamask/keyring-controller';
3
- import { BeginEnrollmentRequestStruct, CompleteEnrollmentRequestStruct, assertValidMfaRequest, BeginStepUpRequestStruct, CompleteStepUpRequestStruct, GetElevatedTokenRequestStruct, parseElevatedTokenClaims, } from '../../sdk/authentication-jwt-bearer/mfa/schemas.js';
4
- import { assertMessageStartsWithMetamask, AuthType, Env, JwtBearerAuth, PairConflictError, getMfaErrorCode, ElevatedTokenInvalidError, } from '../../sdk/index.js';
3
+ import { BeginEnrollmentRequestStruct, CompleteEnrollmentRequestStruct, assertValidMfaRequest, BeginVerificationRequestStruct, CompleteVerificationRequestStruct, GetVerificationTokenRequestStruct, parseVerificationTokenClaims, } from '../../sdk/authentication-jwt-bearer/mfa/schemas.js';
4
+ import { assertMessageStartsWithMetamask, AuthType, Env, JwtBearerAuth, PairConflictError, getMfaErrorCode, VerificationTokenInvalidError, } from '../../sdk/index.js';
5
5
  import { decodeJwtPayload } from '../../sdk/utils/jwt.js';
6
6
  import { toErrorMessage } from '../../sdk/utils/to-error-message.js';
7
7
  import { getPrimaryHdKeyringEntropySourceId } from '../../shared/utils/entropy-source.js';
@@ -70,19 +70,19 @@ const metadata = {
70
70
  includeInDebugSnapshot: false,
71
71
  usedInUi: true,
72
72
  },
73
- stepUpSessionExpiresAt: {
74
- includeInStateLogs: false,
75
- persist: false,
76
- includeInDebugSnapshot: false,
77
- usedInUi: true,
78
- },
79
73
  };
80
74
  /**
81
- * Lifetime of an elevated session. Deliberately shorter than the
82
- * elevated token's own `exp` so a fresh ceremony is required per sensitive
83
- * action window, per the MFA phase-1 specification.
75
+ * Default maximum age of a verification session that may authorize enrolling a
76
+ * credential. A session opened for an unrelated operation must not be able to
77
+ * add a factor long after the fact, while a session proven moments ago (or
78
+ * during the same setup flow, via `maxSessionAgeMs`) may.
84
79
  */
85
- export const STEP_UP_SESSION_TTL_MS = 60_000;
80
+ export const ENROLLMENT_MAX_SESSION_AGE_MS = 2 * 60_000;
81
+ /**
82
+ * Key of the only verification session held today. Sessions are keyed by token
83
+ * audience so audience-scoped tokens can be added without reshaping storage.
84
+ */
85
+ const DEFAULT_AUDIENCE = '';
86
86
  const MESSENGER_EXPOSED_METHODS = [
87
87
  'performSignIn',
88
88
  'performSignOut',
@@ -98,10 +98,10 @@ const MESSENGER_EXPOSED_METHODS = [
98
98
  'refreshEnrolledCredentials',
99
99
  'beginCredentialEnrollment',
100
100
  'completeCredentialEnrollment',
101
- 'beginStepUp',
102
- 'completeStepUp',
103
- 'getElevatedProfileToken',
104
- 'clearStepUpSession',
101
+ 'beginCredentialVerification',
102
+ 'completeCredentialVerification',
103
+ 'getVerificationToken',
104
+ 'clearVerificationSession',
105
105
  ];
106
106
  /**
107
107
  * Controller that enables authentication for restricted endpoints.
@@ -121,8 +121,12 @@ export class AuthenticationController extends BaseController {
121
121
  * ceremony started under the previous session cannot apply its result.
122
122
  */
123
123
  #authSessionEpoch = 0;
124
- #stepUpSession = null;
125
- #stepUpTimer;
124
+ /**
125
+ * Verification sessions keyed by token audience. At most one session per
126
+ * audience; only `DEFAULT_AUDIENCE` is used until audience-scoped tokens
127
+ * exist.
128
+ */
129
+ #verificationSessions = new Map();
126
130
  /**
127
131
  * Sequence number of the most recently started credentials refresh. Only
128
132
  * that refresh may write the cache, so a slower, earlier request cannot
@@ -135,6 +139,10 @@ export class AuthenticationController extends BaseController {
135
139
  * `performSignIn` can't clear `needsProfilePairing` afterwards.
136
140
  */
137
141
  #profilePairingRequestEpoch = 0;
142
+ /**
143
+ * Sign-in currently running. Overlapping callers share it.
144
+ */
145
+ #ongoingSignIn;
138
146
  #keyringController = {
139
147
  setupLockedStateSubscriptions: () => {
140
148
  const { isUnlocked } = this.messenger.call('KeyringController:getState');
@@ -145,7 +153,7 @@ export class AuthenticationController extends BaseController {
145
153
  this.messenger.subscribe('KeyringController:lock', () => {
146
154
  this.#authSessionEpoch += 1;
147
155
  this.#isUnlocked = false;
148
- this.clearStepUpSession();
156
+ this.clearVerificationSession();
149
157
  });
150
158
  },
151
159
  };
@@ -337,6 +345,18 @@ export class AuthenticationController extends BaseController {
337
345
  }
338
346
  async performSignIn() {
339
347
  this.#assertIsUnlocked('performSignIn');
348
+ if (this.#ongoingSignIn) {
349
+ return await this.#ongoingSignIn;
350
+ }
351
+ const signIn = this.#runPerformSignIn().finally(() => {
352
+ if (this.#ongoingSignIn === signIn) {
353
+ this.#ongoingSignIn = undefined;
354
+ }
355
+ });
356
+ this.#ongoingSignIn = signIn;
357
+ return await signIn;
358
+ }
359
+ async #runPerformSignIn() {
340
360
  const epochAtStart = this.#profilePairingRequestEpoch;
341
361
  const entropySourceIds = this.#getHdKeyringEntropySourceIds();
342
362
  const accessTokens = [];
@@ -604,8 +624,9 @@ export class AuthenticationController extends BaseController {
604
624
  this.#setTraceAttribute(context, 'mfaErrorCode', mfaCode);
605
625
  if (mfaCode === 'authentication_required' && this.#isUnlocked) {
606
626
  this.#invalidateSrpSession(this.#getPrimaryEntropySourceId());
607
- // An elevated session must not outlive a rejected base session.
608
- this.clearStepUpSession();
627
+ // A verification session must not outlive a rejected base
628
+ // session.
629
+ this.clearVerificationSession();
609
630
  }
610
631
  }
611
632
  throw error;
@@ -664,9 +685,13 @@ export class AuthenticationController extends BaseController {
664
685
  const sessionEpoch = this.#authSessionEpoch;
665
686
  assertValidMfaRequest(request, BeginEnrollmentRequestStruct);
666
687
  const primaryEntropySourceId = this.#getPrimaryEntropySourceId();
667
- // Enrolling beyond the first credential requires AAL2: send the elevated
668
- // token while a session is live and let the server decide.
669
- const accessToken = this.getElevatedProfileToken()?.accessToken;
688
+ // The server requires AAL2 to add a credential once one that proves AAL2
689
+ // exists: send the verification token only while a recent enough session
690
+ // is live and let the server decide. An older session stays open for
691
+ // other consumers.
692
+ const accessToken = this.getVerificationToken({
693
+ maxSessionAgeMs: request.maxSessionAgeMs ?? ENROLLMENT_MAX_SESSION_AGE_MS,
694
+ })?.accessToken;
670
695
  const challenge = await this.#runMfaRequest('MFA Enroll Begin', request.reason.operation, request.type, async () => await this.#auth.beginMfaEnrollment(request.type, {
671
696
  email: request.email,
672
697
  entropySourceId: primaryEntropySourceId,
@@ -678,6 +703,11 @@ export class AuthenticationController extends BaseController {
678
703
  /**
679
704
  * Completes credential enrollment and refreshes the credential cache.
680
705
  *
706
+ * The server returns an assertion for the new credential, which opens a
707
+ * verification session like `completeCredentialVerification`, replacing
708
+ * any earlier one. If that exchange fails, the earlier session is kept:
709
+ * the credential is enrolled either way.
710
+ *
681
711
  * A cache-refresh failure does not undo successful enrollment. Email
682
712
  * enrollment invalidates the primary SRP session *after* refresh so the
683
713
  * credentials call can reuse the still-valid access token; the next token
@@ -694,8 +724,22 @@ export class AuthenticationController extends BaseController {
694
724
  const sessionEpoch = this.#authSessionEpoch;
695
725
  assertValidMfaRequest(request, CompleteEnrollmentRequestStruct);
696
726
  const { type } = request.proof;
727
+ const { operation } = request.reason;
697
728
  const primaryEntropySourceId = this.#getPrimaryEntropySourceId();
698
- await this.#runMfaRequest('MFA Enroll Complete', request.reason.operation, type, async () => await this.#auth.completeMfaEnrollment(request.flowId, request.proof, primaryEntropySourceId));
729
+ const assertion = await this.#runMfaRequest('MFA Enroll Complete', operation, type, async () => await this.#auth.completeMfaEnrollment(request.flowId, request.proof, primaryEntropySourceId));
730
+ try {
731
+ await this.#openVerificationSessionFromAssertion(assertion, {
732
+ operation,
733
+ type,
734
+ sessionEpoch,
735
+ methodName: 'completeCredentialEnrollment',
736
+ });
737
+ }
738
+ catch {
739
+ // Callers see no session and verify the credential instead.
740
+ }
741
+ // Also catches a session that ended during the enrollment request; the
742
+ // exchange above then ran for nothing, and its session was never opened.
699
743
  try {
700
744
  this.#assertAuthSessionEpoch(sessionEpoch, 'completeCredentialEnrollment');
701
745
  }
@@ -705,8 +749,6 @@ export class AuthenticationController extends BaseController {
705
749
  }
706
750
  throw error;
707
751
  }
708
- // The factor set changed: require a fresh ceremony against the new set.
709
- this.clearStepUpSession();
710
752
  try {
711
753
  return await this.refreshEnrolledCredentials();
712
754
  }
@@ -720,74 +762,103 @@ export class AuthenticationController extends BaseController {
720
762
  }
721
763
  }
722
764
  /**
723
- * Begins step-up verification with an enrolled credential.
765
+ * Begins verification with an enrolled credential.
724
766
  *
725
767
  * @param request - Credential type and trace reason.
726
768
  * @returns A challenge for the client-owned ceremony.
727
769
  */
728
- async beginStepUp(request) {
729
- this.#assertIsUnlocked('beginStepUp');
770
+ async beginCredentialVerification(request) {
771
+ this.#assertIsUnlocked('beginCredentialVerification');
730
772
  const sessionEpoch = this.#authSessionEpoch;
731
- assertValidMfaRequest(request, BeginStepUpRequestStruct);
773
+ assertValidMfaRequest(request, BeginVerificationRequestStruct);
732
774
  const primaryEntropySourceId = this.#getPrimaryEntropySourceId();
733
- const challenge = await this.#runMfaRequest('MFA Step-Up Begin', request.reason.operation, request.type, async () => await this.#auth.beginMfaVerification(request.type, primaryEntropySourceId));
734
- this.#assertAuthSessionEpoch(sessionEpoch, 'beginStepUp');
775
+ const challenge = await this.#runMfaRequest('MFA Verification Begin', request.reason.operation, request.type, async () => await this.#auth.beginMfaVerification(request.type, primaryEntropySourceId));
776
+ this.#assertAuthSessionEpoch(sessionEpoch, 'beginCredentialVerification');
735
777
  return challenge;
736
778
  }
737
779
  /**
738
- * Completes step-up verification and opens a short-lived elevated session.
780
+ * Completes verification and opens a short-lived verification session.
739
781
  *
740
- * The AAL2 assertion returned by the MFA service is exchanged at Hydra for
741
- * an elevated access token, whose claims are checked before the session
742
- * opens. The token itself never enters controller state.
782
+ * The assertion returned by the MFA service is exchanged at Hydra for an
783
+ * access token. Its assurance level is not checked: the services receiving
784
+ * the token enforce their own requirements. The token itself never enters
785
+ * controller state.
743
786
  *
744
787
  * @param request - Flow identifier, platform or email proof, and trace reason.
745
- * @returns The elevated profile access token.
788
+ * @returns The verification token.
746
789
  */
747
- async completeStepUp(request) {
748
- this.#assertIsUnlocked('completeStepUp');
790
+ async completeCredentialVerification(request) {
791
+ this.#assertIsUnlocked('completeCredentialVerification');
749
792
  const sessionEpoch = this.#authSessionEpoch;
750
- assertValidMfaRequest(request, CompleteStepUpRequestStruct);
793
+ assertValidMfaRequest(request, CompleteVerificationRequestStruct);
751
794
  const { type } = request.proof;
752
795
  const { operation } = request.reason;
753
796
  const primaryEntropySourceId = this.#getPrimaryEntropySourceId();
754
- const assertion = await this.#runMfaRequest('MFA Step-Up Complete', operation, type, async () => await this.#auth.completeMfaVerification(request.flowId, request.proof, primaryEntropySourceId));
755
- this.#assertAuthSessionEpoch(sessionEpoch, 'completeStepUp');
797
+ const assertion = await this.#runMfaRequest('MFA Verification Complete', operation, type, async () => await this.#auth.completeMfaVerification(request.flowId, request.proof, primaryEntropySourceId));
798
+ this.#assertAuthSessionEpoch(sessionEpoch, 'completeCredentialVerification');
799
+ return await this.#openVerificationSessionFromAssertion(assertion, {
800
+ operation,
801
+ type,
802
+ sessionEpoch,
803
+ methodName: 'completeCredentialVerification',
804
+ });
805
+ }
806
+ /**
807
+ * Exchanges an MFA assertion at Hydra and opens the verification session
808
+ * with the resulting token.
809
+ *
810
+ * @param assertion - Assertion returned by a verify or enroll completion.
811
+ * @param context - Trace tags and the session the call started in.
812
+ * @param context.operation - Feature operation, for tracing.
813
+ * @param context.type - Credential type the assertion proves, for tracing.
814
+ * @param context.sessionEpoch - Authenticated session the call started in.
815
+ * @param context.methodName - Public method name, for error messages.
816
+ * @returns The verification token.
817
+ */
818
+ async #openVerificationSessionFromAssertion(assertion, { operation, type, sessionEpoch, methodName, }) {
756
819
  const accessToken = await this.#runMfaRequest('MFA Token Exchange', operation, type, async () => await this.#auth.exchangeMfaAssertion(assertion.token));
757
- this.#assertAuthSessionEpoch(sessionEpoch, 'completeStepUp');
820
+ this.#assertAuthSessionEpoch(sessionEpoch, methodName);
758
821
  let decodedClaims;
759
822
  try {
760
823
  decodedClaims = decodeJwtPayload(accessToken.accessToken);
761
824
  }
762
825
  catch (error) {
763
- throw new ElevatedTokenInvalidError(toErrorMessage(error));
826
+ throw new VerificationTokenInvalidError(toErrorMessage(error));
764
827
  }
765
- const claims = parseElevatedTokenClaims(decodedClaims);
766
- if (claims.exp * 1000 <= Date.now()) {
767
- throw new ElevatedTokenInvalidError('Elevated token is expired');
828
+ const claims = parseVerificationTokenClaims(decodedClaims);
829
+ if (!Number.isFinite(accessToken.expiresIn) || accessToken.expiresIn <= 0) {
830
+ throw new VerificationTokenInvalidError('Verification token has no remaining lifetime');
768
831
  }
769
832
  const token = { ...accessToken, claims };
770
- this.#openStepUpSession(token);
833
+ this.#openVerificationSession(token);
771
834
  return token;
772
835
  }
773
836
  /**
774
- * Returns the active elevated token when it meets the requested freshness.
837
+ * Returns the active verification token when it meets the requested
838
+ * freshness.
839
+ *
840
+ * Low-level: features should go through the client MFA kit
841
+ * (`verifyOrEnroll`), which reuses a matching session without showing any
842
+ * screen and checks which method proved it. Read the token directly only
843
+ * from code that cannot show UI, and treat `null` as "let the UI layer
844
+ * ask".
775
845
  *
776
846
  * @param request - Optional maximum session age in milliseconds, measured
777
847
  * from when the token was obtained. Zero always requires a new ceremony.
778
- * @returns A live elevated token, or null when no reusable session exists.
848
+ * @returns A live verification token, or null when no reusable session
849
+ * exists.
779
850
  */
780
- getElevatedProfileToken(request = {}) {
781
- this.#assertIsUnlocked('getElevatedProfileToken');
782
- assertValidMfaRequest(request, GetElevatedTokenRequestStruct);
783
- const session = this.#stepUpSession;
851
+ getVerificationToken(request = {}) {
852
+ this.#assertIsUnlocked('getVerificationToken');
853
+ assertValidMfaRequest(request, GetVerificationTokenRequestStruct);
854
+ const session = this.#verificationSessions.get(DEFAULT_AUDIENCE);
784
855
  if (!session) {
785
856
  return null;
786
857
  }
787
858
  const now = Date.now();
788
859
  if (now >= session.expiresAt) {
789
860
  // The hard-expiry timer has not fired yet (e.g. a suspended tab).
790
- this.clearStepUpSession();
861
+ this.#dropVerificationSession(DEFAULT_AUDIENCE);
791
862
  return null;
792
863
  }
793
864
  // `>=` so a zero max age always forces a fresh ceremony. Not cleared: the
@@ -799,36 +870,41 @@ export class AuthenticationController extends BaseController {
799
870
  return session.token;
800
871
  }
801
872
  /**
802
- * Opens the elevated session. Its lifetime is the session TTL clamped to
803
- * the token's own `exp`, so the session never outlives the token.
873
+ * Opens the verification session for the lifetime the server gave the token.
874
+ * It is measured from `expires_in` on the local clock rather than read from
875
+ * `exp`, so a skewed device clock cannot shorten or extend it.
804
876
  *
805
- * @param token - The freshly exchanged elevated token.
877
+ * @param token - The freshly exchanged verification token.
806
878
  */
807
- #openStepUpSession(token) {
808
- this.clearStepUpSession();
809
- const expiresAt = Math.min(token.obtainedAt + STEP_UP_SESSION_TTL_MS, token.claims.exp * 1000);
810
- this.#stepUpSession = { token, expiresAt };
811
- this.update((state) => {
812
- state.stepUpSessionExpiresAt = expiresAt;
813
- });
814
- this.#stepUpTimer = setTimeout(() => this.clearStepUpSession(), Math.max(0, expiresAt - Date.now()));
879
+ #openVerificationSession(token) {
880
+ const audience = DEFAULT_AUDIENCE;
881
+ this.#dropVerificationSession(audience);
882
+ const expiresAt = token.obtainedAt + token.expiresIn * 1000;
883
+ const timer = setTimeout(() => this.#dropVerificationSession(audience), Math.max(0, expiresAt - Date.now()));
815
884
  // Never keep a Node process alive for the expiry timer (tests, tooling).
816
- this.#stepUpTimer.unref?.();
885
+ timer.unref?.();
886
+ this.#verificationSessions.set(audience, { token, expiresAt, timer });
817
887
  }
818
888
  /**
819
- * Clears the in-memory elevated session and its expiration timer.
889
+ * Drops one audience's verification session and its expiration timer.
890
+ *
891
+ * @param audience - Audience whose session to drop.
820
892
  */
821
- clearStepUpSession() {
822
- if (this.#stepUpTimer !== undefined) {
823
- clearTimeout(this.#stepUpTimer);
824
- this.#stepUpTimer = undefined;
893
+ #dropVerificationSession(audience) {
894
+ const session = this.#verificationSessions.get(audience);
895
+ if (session) {
896
+ clearTimeout(session.timer);
897
+ this.#verificationSessions.delete(audience);
825
898
  }
826
- this.#stepUpSession = null;
827
- if (this.state.stepUpSessionExpiresAt !== undefined) {
828
- this.update((state) => {
829
- state.stepUpSessionExpiresAt = undefined;
830
- });
899
+ }
900
+ /**
901
+ * Clears every in-memory verification session and its expiration timer.
902
+ */
903
+ clearVerificationSession() {
904
+ for (const { timer } of this.#verificationSessions.values()) {
905
+ clearTimeout(timer);
831
906
  }
907
+ this.#verificationSessions.clear();
832
908
  }
833
909
  /**
834
910
  * Drops the cached credential list. Callers that wipe profile state must go
@@ -848,7 +924,7 @@ export class AuthenticationController extends BaseController {
848
924
  }
849
925
  performSignOut() {
850
926
  this.#authSessionEpoch += 1;
851
- this.clearStepUpSession();
927
+ this.clearVerificationSession();
852
928
  this.#clearEnrolledCredentials();
853
929
  this.update((state) => {
854
930
  state.isSignedIn = false;
@@ -862,7 +938,7 @@ export class AuthenticationController extends BaseController {
862
938
  clearState() {
863
939
  this.#profilePairingRequestEpoch += 1;
864
940
  this.#authSessionEpoch += 1;
865
- this.clearStepUpSession();
941
+ this.clearVerificationSession();
866
942
  this.#clearEnrolledCredentials();
867
943
  this.update(() => ({ ...defaultState }));
868
944
  }