diff --git a/change/@azure-msal-browser-prompt-select-account-msal-v5.json b/change/@azure-msal-browser-prompt-select-account-msal-v5.json new file mode 100644 index 0000000000..cf5b56b0f1 --- /dev/null +++ b/change/@azure-msal-browser-prompt-select-account-msal-v5.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Add prompt=select_account support for native flows in msal-v5 (#8062)", + "packageName": "@azure/msal-browser", + "email": "github-copilot@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/lib/msal-browser/src/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index 1d2a36e6fd..a31296efcb 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -1581,6 +1581,7 @@ export class StandardController implements IController { case Constants.PromptValue.NONE: case Constants.PromptValue.CONSENT: case Constants.PromptValue.LOGIN: + case Constants.PromptValue.SELECT_ACCOUNT: this.logger.trace( "canUsePlatformBroker: prompt is compatible with platform broker flow", correlationId diff --git a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts index 887dc70140..6eae456737 100644 --- a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts @@ -1076,6 +1076,7 @@ export class PlatformAuthInteractionClient extends BaseInteractionClient { case Constants.PromptValue.NONE: case Constants.PromptValue.CONSENT: case Constants.PromptValue.LOGIN: + case Constants.PromptValue.SELECT_ACCOUNT: this.logger.trace( "initializeNativeRequest: prompt is compatible with native flow", this.correlationId diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 9a083d5ad8..e2f26b1f6e 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -1663,7 +1663,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { pca.initialize(); }); - it("falls back to web flow if prompt is select_account", async () => { + it("Does not fall back to web flow if prompt is select_account", async () => { const config = { auth: { clientId: TEST_CONFIG.MSAL_CLIENT_ID, @@ -1674,29 +1674,28 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - await pca.initialize(); stubExtensionProvider(config); + await pca.initialize(); - //Implementation of PCA was moved to controller. + // Implementation of PCA was moved to controller. + // eslint-disable-next-line @typescript-eslint/no-explicit-any pca = (pca as any).controller; const testAccount = BASIC_NATIVE_TEST_ACCOUNT_INFO; - const nativeAcquireTokenSpy = jest.spyOn( - PlatformAuthInteractionClient.prototype, - "acquireTokenRedirect" - ); - const redirectSpy: jest.SpyInstance = jest - .spyOn(RedirectClient.prototype, "acquireToken") - .mockImplementation(); + const nativeAcquireTokenSpy: jest.SpyInstance = jest + .spyOn( + PlatformAuthInteractionClient.prototype, + "acquireTokenRedirect" + ) + .mockResolvedValue(); await pca.acquireTokenRedirect({ scopes: ["User.Read"], account: testAccount, prompt: "select_account", }); - expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(0); - expect(redirectSpy).toHaveBeenCalledTimes(1); + expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(1); }); it("falls back to web flow if native broker call fails due to fatal error", async () => { @@ -2608,7 +2607,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(popupSpy).toHaveBeenCalledTimes(0); }); - it("falls back to web flow if prompt is select_account", async () => { + it("Does not fall back to web flow if prompt is select_account and emits platform telemetry", async () => { const config = { auth: { clientId: TEST_CONFIG.MSAL_CLIENT_ID, @@ -2616,12 +2615,26 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { system: { allowPlatformBroker: true, }, + telemetry: { + client: new BrowserPerformanceClient({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + }), + application: { + appName: TEST_CONFIG.applicationName, + appVersion: TEST_CONFIG.applicationVersion, + }, + }, }; pca = new PublicClientApplication(config); stubExtensionProvider(config); await pca.initialize(); + //Implementation of PCA was moved to controller. + pca = (pca as any).controller; + const testAccount = BASIC_NATIVE_TEST_ACCOUNT_INFO; const testTokenResponse: AuthenticationResult = { authority: TEST_CONFIG.validAuthority, @@ -2638,13 +2651,59 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { tokenType: Constants.AuthenticationScheme.BEARER, }; - const nativeAcquireTokenSpy: jest.SpyInstance = jest.spyOn( - PlatformAuthInteractionClient.prototype, - "acquireToken" + jest.spyOn(BrowserCrypto, "createNewGuid").mockReturnValue( + RANDOM_TEST_GUID ); + + const nativeAcquireTokenSpy: jest.SpyInstance = jest + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") + .mockImplementation(async function ( + this: PlatformAuthInteractionClient, + request + ) { + expect(request.correlationId).toBe(RANDOM_TEST_GUID); + + // Add isNativeBroker to the measurement that was started by StandardController + // This simulates what the real PlatformAuthInteractionClient does + if ( + this.performanceClient && + (this.performanceClient as any).eventsByCorrelationId + ) { + const eventMap = (this.performanceClient as any) + .eventsByCorrelationId; + const existingEvent = eventMap.get(this.correlationId); + if (existingEvent) { + existingEvent.isNativeBroker = true; + } + } + + return testTokenResponse; + }); + const popupSpy: jest.SpyInstance = jest .spyOn(PopupClient.prototype, "acquireToken") - .mockResolvedValue(testTokenResponse); + .mockImplementation(function ( + this: PopupClient, + request: PopupRequest + ): Promise { + const eventMap = (this.performanceClient as any) + .eventsByCorrelationId; + const existingEvent = eventMap.get( + request.correlationId || RANDOM_TEST_GUID + ); + if (existingEvent) { + existingEvent.isPlatformAuthorizeRequest = true; + } + + return Promise.resolve(testTokenResponse); + }); + + // Add performance callback + const callbackId = pca.addPerformanceCallback((events) => { + expect(events[0].isNativeBroker).toBe(true); + pca.removePerformanceCallback(callbackId); + }); + const response = await pca.acquireTokenPopup({ scopes: ["User.Read"], account: testAccount, @@ -2652,8 +2711,8 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); expect(response).toBe(testTokenResponse); - expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(0); - expect(popupSpy).toHaveBeenCalledTimes(1); + expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(1); + expect(popupSpy).toHaveBeenCalledTimes(0); }); it("falls back to web flow if native broker call fails due to fatal error", async () => { diff --git a/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts index a296408819..b9d5f07571 100644 --- a/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts @@ -617,23 +617,31 @@ describe("PlatformAuthInteractionClient Tests", () => { expect(response.expiresOn).toBeDefined(); }); - it("throws if prompt: select_account", (done) => { - platformAuthInteractionClient - .acquireToken({ - scopes: ["User.Read"], - prompt: Constants.PromptValue.SELECT_ACCOUNT, - }) - .catch((e) => { - expect(e.errorCode).toBe( - BrowserAuthErrorCodes.nativePromptNotSupported - ); - expect(e.errorMessage).toBe( - getDefaultErrorMessage( - BrowserAuthErrorCodes.nativePromptNotSupported - ) - ); - done(); - }); + it("prompt: select_account succeeds", async () => { + jest.spyOn( + PlatformAuthExtensionHandler.prototype, + "sendMessage" + ).mockImplementation((): Promise => { + return Promise.resolve(MOCK_WAM_RESPONSE); + }); + const response = await platformAuthInteractionClient.acquireToken({ + scopes: ["User.Read"], + prompt: Constants.PromptValue.SELECT_ACCOUNT, + }); + expect(response.accessToken).toEqual( + MOCK_WAM_RESPONSE.access_token + ); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); + expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); + expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); + expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); + expect(response.authority).toEqual(TEST_CONFIG.validAuthority); + expect(response.scopes).toContain(MOCK_WAM_RESPONSE.scope); + expect(response.correlationId).toEqual(RANDOM_TEST_GUID); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); + expect(response.tokenType).toEqual( + Constants.AuthenticationScheme.BEARER + ); }); it("throws if prompt: create", (done) => {