diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index e371bbd9083..d66c90c8213 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Coalesce overlapping `performSignIn` calls so only one sign-in runs at a time ([#10646](https://github.com/MetaMask/core/pull/10646)) - Rely on `@metamask/key-tree` crypto implementation for HMAC-SHA-512 instead of hardcoded `noble` implementation ([#10424](https://github.com/MetaMask/core/pull/10424)) - `@metamask/key-tree` uses WebCrypto API if available and fallback to `noble` otherwise. - One note, we expect the platform to provide a fully-compliant WebCrypto (`crypto.subtle`) implementation for this to work (`@metamask/key-tree` detection is global and not "per crypto functions"). diff --git a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts index cee2884326d..8632da513a1 100644 --- a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts +++ b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts @@ -596,6 +596,26 @@ describe('AuthenticationController', () => { expect(controller.state.needsProfilePairing).toBe(false); }); + it('shares one in-flight performSignIn across overlapping callers', async () => { + const metametrics = createMockAuthMetaMetrics(); + arrangeAuthAPIs({ mockPairProfilesDelayMs: 50 }); + const { messenger } = createMockAuthenticationMessenger(); + + const controller = new AuthenticationController({ + messenger, + metametrics, + }); + + const first = controller.performSignIn(); + const second = controller.performSignIn(); + const [firstTokens, secondTokens] = await Promise.all([first, second]); + + expect(secondTokens).toBe(firstTokens); + + const thirdTokens = await controller.performSignIn(); + expect(thirdTokens).not.toBe(firstTokens); + }); + it('does NOT throw out of performSignIn when pairProfiles fails, leaves needsProfilePairing=true', async () => { const metametrics = createMockAuthMetaMetrics(); arrangeAuthAPIs({ diff --git a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts index d73cb4cdb4e..bce495dfd0e 100644 --- a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts +++ b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts @@ -333,6 +333,11 @@ export class AuthenticationController extends BaseController< */ #profilePairingRequestEpoch = 0; + /** + * Sign-in currently running. Overlapping callers share it. + */ + #ongoingSignIn?: Promise; + readonly #keyringController = { setupLockedStateSubscriptions: () => { const { isUnlocked } = this.messenger.call('KeyringController:getState'); @@ -596,7 +601,21 @@ export class AuthenticationController extends BaseController< public async performSignIn(): Promise { this.#assertIsUnlocked('performSignIn'); + if (this.#ongoingSignIn) { + return await this.#ongoingSignIn; + } + + const signIn = this.#runPerformSignIn().finally(() => { + if (this.#ongoingSignIn === signIn) { + this.#ongoingSignIn = undefined; + } + }); + + this.#ongoingSignIn = signIn; + return await signIn; + } + async #runPerformSignIn(): Promise { const epochAtStart = this.#profilePairingRequestEpoch; const entropySourceIds = this.#getHdKeyringEntropySourceIds(); const accessTokens: string[] = [];