From bc73dfd9b051ba2e8616e78ce3369774cb362a76 Mon Sep 17 00:00:00 2001 From: sameerag Date: Thu, 10 Jul 2025 12:09:15 -0700 Subject: [PATCH 1/8] Remove prompt code and update Platform broker sample to have prompt buttons added --- .../src/controllers/StandardController.ts | 17 ----- lib/msal-browser/src/error/NativeAuthError.ts | 3 +- .../PlatformAuthInteractionClient.ts | 2 + .../test/error/NativeAuthError.spec.ts | 4 +- .../VanillaJSTestApp2.0/app/wamBroker/auth.js | 71 +++++++++++++++++ .../app/wamBroker/authConfig.js | 3 +- .../app/wamBroker/graph.js | 76 +++++++++++++++++++ .../app/wamBroker/index.html | 8 ++ .../VanillaJSTestApp2.0/server.js | 4 +- 9 files changed, 164 insertions(+), 24 deletions(-) diff --git a/lib/msal-browser/src/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index 8cbda4d184..4a6c393949 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -1647,23 +1647,6 @@ export class StandardController implements IController { return false; } - if (request.prompt) { - switch (request.prompt) { - case PromptValue.NONE: - case PromptValue.CONSENT: - case PromptValue.LOGIN: - this.logger.trace( - "canUsePlatformBroker: prompt is compatible with platform broker flow" - ); - break; - default: - this.logger.trace( - `canUsePlatformBroker: prompt = ${request.prompt} is not compatible with platform broker flow, returning false` - ); - return false; - } - } - if (!accountId && !this.getNativeAccountId(request)) { this.logger.trace( "canUsePlatformBroker: nativeAccountId is not available, returning false" diff --git a/lib/msal-browser/src/error/NativeAuthError.ts b/lib/msal-browser/src/error/NativeAuthError.ts index 637bcac2e4..707bcd6ade 100644 --- a/lib/msal-browser/src/error/NativeAuthError.ts +++ b/lib/msal-browser/src/error/NativeAuthError.ts @@ -52,8 +52,7 @@ export function isFatalNativeAuthError(error: NativeAuthError): boolean { if ( error.ext && error.ext.status && - (error.ext.status === NativeStatusCodes.PERSISTENT_ERROR || - error.ext.status === NativeStatusCodes.DISABLED) + error.ext.status === NativeStatusCodes.DISABLED ) { return true; } diff --git a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts index 6546f6a6ff..c1c7f77f76 100644 --- a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts @@ -1012,6 +1012,8 @@ export class PlatformAuthInteractionClient extends BaseInteractionClient { case PromptValue.NONE: case PromptValue.CONSENT: case PromptValue.LOGIN: + case PromptValue.CREATE: + case PromptValue.SELECT_ACCOUNT: this.logger.trace( "initializeNativeRequest: prompt is compatible with native flow" ); diff --git a/lib/msal-browser/test/error/NativeAuthError.spec.ts b/lib/msal-browser/test/error/NativeAuthError.spec.ts index 4513c887c4..f21103229d 100644 --- a/lib/msal-browser/test/error/NativeAuthError.spec.ts +++ b/lib/msal-browser/test/error/NativeAuthError.spec.ts @@ -18,7 +18,7 @@ import * as NativeStatusCode from "../../src/broker/nativeBroker/NativeStatusCod describe("NativeAuthError Unit Tests", () => { describe("NativeAuthError", () => { describe("isFatal tests", () => { - it("should return true for isFatal when WAM status is PERSISTENT_ERROR", () => { + it("should return false for isFatal when WAM status is PERSISTENT_ERROR", () => { const error = new NativeAuthError( "testError", "testErrorDescription", @@ -29,7 +29,7 @@ describe("NativeAuthError Unit Tests", () => { status: NativeStatusCode.PERSISTENT_ERROR, } ); - expect(isFatalNativeAuthError(error)).toBe(true); + expect(isFatalNativeAuthError(error)).toBe(false); }); it("should return true for isFatal when WAM status is DISABLED", () => { diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/auth.js index cea2113516..aa97affb6a 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/auth.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/auth.js @@ -76,3 +76,74 @@ async function getTokenPopup(request, account) { } }); } + +async function getTokenPopupCreate(request, account) { + request.prompt = "create"; + const startTime = Date.now(); + + // Acquire token using popup directly + console.log("acquiring token using popup with prompt: create"); + return await myMSALObj.acquireTokenPopup(request).then((response) => { + console.log(`Token acquisition time elapsed: ${Date.now() - startTime}ms`); + console.log(response); + return response; + }).catch(error => { + console.error(error); + }); +} + +async function getTokenPopupNone(request, account) { + request.prompt = "none"; + const startTime = Date.now(); + + console.log("acquiring token silently with prompt: none"); + return await myMSALObj.acquireTokenSilent(request).then((response) => { + console.log(`Token acquisition time elapsed: ${Date.now() - startTime}ms`); + console.log(response); + return response; + }).catch(error => { + console.error("Silent token acquisition failed:", error); + }); +} + +async function getTokenPopupLogin(request, account) { + request.prompt = "login"; + const startTime = Date.now(); + + console.log("acquiring token using popup with prompt: login"); + return await myMSALObj.acquireTokenPopup(request).then((response) => { + console.log(`Token acquisition time elapsed: ${Date.now() - startTime}ms`); + console.log(response); + return response; + }).catch(error => { + console.error(error); + }); +} + +async function getTokenPopupConsent(request, account) { + request.prompt = "consent"; + const startTime = Date.now(); + + console.log("acquiring token using popup with prompt: consent"); + return await myMSALObj.acquireTokenPopup(request).then((response) => { + console.log(`Token acquisition time elapsed: ${Date.now() - startTime}ms`); + console.log(response); + return response; + }).catch(error => { + console.error(error); + }); +} + +async function getTokenPopupSelectAccount(request, account) { + request.prompt = "select_account"; + const startTime = Date.now(); + + console.log("acquiring token using popup with prompt: select_account"); + return await myMSALObj.acquireTokenPopup(request).then((response) => { + console.log(`Token acquisition time elapsed: ${Date.now() - startTime}ms`); + console.log(response); + return response; + }).catch(error => { + console.error(error); + }); +} diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/authConfig.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/authConfig.js index 602c16b197..972b792ad8 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/authConfig.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/authConfig.js @@ -10,7 +10,8 @@ const isPlatformBrokerAvailable = msal.isPlatformBrokerAvailable().then((isAvail const msalConfig = { auth: { clientId: "591ddbcc-105b-42c5-89e6-c7638c4124d4", - authority: "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca" + //authority: "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca" + authority: "https://login.microsoftonline.com/common" }, cache: { cacheLocation: "sessionStorage", // This configures where your cache will be stored diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/graph.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/graph.js index a9c3f8474b..94239197aa 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/graph.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/graph.js @@ -28,3 +28,79 @@ async function seeProfile() { callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI); } } + +// Test functions for different prompt values +async function testTokenPromptCreate() { + const currentAcc = myMSALObj.getAccountByHomeId(accountId); + if (currentAcc) { + console.log("Testing token acquisition with prompt: create"); + const response = await getTokenPopupCreate(loginRequest, currentAcc).catch(error => { + console.log("Error with prompt: create", error); + }); + if (response) { + console.log("Successfully acquired token with prompt: create", response); + } + } else { + console.log("No account found. Please sign in first."); + } +} + +async function testTokenPromptNone() { + const currentAcc = myMSALObj.getAccountByHomeId(accountId); + if (currentAcc) { + console.log("Testing token acquisition with prompt: none"); + const response = await getTokenPopupNone(loginRequest, currentAcc).catch(error => { + console.log("Error with prompt: none", error); + }); + if (response) { + console.log("Successfully acquired token with prompt: none", response); + } + } else { + console.log("No account found. Please sign in first."); + } +} + +async function testTokenPromptLogin() { + const currentAcc = myMSALObj.getAccountByHomeId(accountId); + if (currentAcc) { + console.log("Testing token acquisition with prompt: login"); + const response = await getTokenPopupLogin(loginRequest, currentAcc).catch(error => { + console.log("Error with prompt: login", error); + }); + if (response) { + console.log("Successfully acquired token with prompt: login", response); + } + } else { + console.log("No account found. Please sign in first."); + } +} + +async function testTokenPromptConsent() { + const currentAcc = myMSALObj.getAccountByHomeId(accountId); + if (currentAcc) { + console.log("Testing token acquisition with prompt: consent"); + const response = await getTokenPopupConsent(loginRequest, currentAcc).catch(error => { + console.log("Error with prompt: consent", error); + }); + if (response) { + console.log("Successfully acquired token with prompt: consent", response); + } + } else { + console.log("No account found. Please sign in first."); + } +} + +async function testTokenPromptSelectAccount() { + const currentAcc = myMSALObj.getAccountByHomeId(accountId); + if (currentAcc) { + console.log("Testing token acquisition with prompt: select_account"); + const response = await getTokenPopupSelectAccount(loginRequest, currentAcc).catch(error => { + console.log("Error with prompt: select_account", error); + }); + if (response) { + console.log("Successfully acquired token with prompt: select_account", response); + } + } else { + console.log("No account found. Please sign in first."); + } +} diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/index.html b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/index.html index 99526f1bfa..7470096fce 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/index.html +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/index.html @@ -40,6 +40,14 @@
Please sign-in to see your profile an

+
+
+
Token Acquisition with Different Prompts:
+ + + + + diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/server.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/server.js index a3c69d8246..2c81fc696f 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/server.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/server.js @@ -92,8 +92,8 @@ if (argv.https) { * Please see "Certificates and Secrets" (https://learn.microsoft.com/azure/active-directory/develop/security-best-practices-for-app-registration#certificates-and-secrets) * for more information. */ - const privateKey = fs.readFileSync('/certs/key.pem', 'utf8'); - const certificate = fs.readFileSync('/certs/cert.pem', 'utf8'); + const privateKey = fs.readFileSync('./certs/key.pem', 'utf8'); + const certificate = fs.readFileSync('./certs/cert.pem', 'utf8'); const credentials = { key: privateKey, cert: certificate }; const httpsServer = https.createServer(credentials, app); httpsServer.listen(port); From 7addccb97aa2f78897860cfcb0077bedf7d4a0a9 Mon Sep 17 00:00:00 2001 From: sameerag Date: Mon, 22 Sep 2025 22:35:42 -0700 Subject: [PATCH 2/8] Add platform broker telemetry fixes for handleRedirectPromise and test updates --- .../src/controllers/StandardController.ts | 17 ----------------- .../test/app/PublicClientApplication.spec.ts | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/lib/msal-browser/src/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index 01b15781b1..22dbfe72d8 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -1704,23 +1704,6 @@ export class StandardController implements IController { return false; } - if (request.prompt) { - switch (request.prompt) { - case PromptValue.NONE: - case PromptValue.CONSENT: - case PromptValue.LOGIN: - this.logger.trace( - "canUsePlatformBroker: prompt is compatible with platform broker flow" - ); - break; - default: - this.logger.trace( - `canUsePlatformBroker: prompt = ${request.prompt} is not compatible with platform broker flow, returning false` - ); - return false; - } - } - if (!accountId && !this.getNativeAccountId(request)) { this.logger.trace( "canUsePlatformBroker: nativeAccountId is not available, returning false" diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 0cf06e8b00..fbeca4bdcb 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -2735,7 +2735,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(popupSpy).toHaveBeenCalledTimes(0); }); - it("falls back to web flow if prompt is select_account and emits platform telemetry", 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, From 93be5f011997329dcd410b58dc54ec6000058e63 Mon Sep 17 00:00:00 2001 From: sameerag Date: Mon, 22 Sep 2025 22:50:01 -0700 Subject: [PATCH 3/8] Remove prompt related special handling for native flows --- .../src/error/BrowserAuthError.ts | 8 -------- .../src/error/BrowserAuthErrorCodes.ts | 1 - .../PlatformAuthInteractionClient.ts | 20 ------------------- 3 files changed, 29 deletions(-) diff --git a/lib/msal-browser/src/error/BrowserAuthError.ts b/lib/msal-browser/src/error/BrowserAuthError.ts index 3c5849dc9b..e93bfecae3 100644 --- a/lib/msal-browser/src/error/BrowserAuthError.ts +++ b/lib/msal-browser/src/error/BrowserAuthError.ts @@ -88,8 +88,6 @@ export const BrowserAuthErrorMessages = { "Native extension is not installed. If you think this is a mistake call the initialize function.", [BrowserAuthErrorCodes.nativeConnectionNotEstablished]: `Connection to native platform has not been established. Please install a compatible browser extension and run initialize(). ${ErrorLink}`, [BrowserAuthErrorCodes.uninitializedPublicClientApplication]: `You must call and await the initialize function before attempting to call any other MSAL API. ${ErrorLink}`, - [BrowserAuthErrorCodes.nativePromptNotSupported]: - "The provided prompt is not supported by the native platform. This request should be routed to the web based flow.", [BrowserAuthErrorCodes.invalidBase64String]: "Invalid base64 encoded string.", [BrowserAuthErrorCodes.invalidPopTokenRequest]: @@ -326,12 +324,6 @@ export const BrowserAuthErrorMessage = { BrowserAuthErrorCodes.uninitializedPublicClientApplication ], }, - nativePromptNotSupported: { - code: BrowserAuthErrorCodes.nativePromptNotSupported, - desc: BrowserAuthErrorMessages[ - BrowserAuthErrorCodes.nativePromptNotSupported - ], - }, invalidBase64StringError: { code: BrowserAuthErrorCodes.invalidBase64String, desc: BrowserAuthErrorMessages[ diff --git a/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts b/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts index 31ee9fc9e5..9c65e4d401 100644 --- a/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts +++ b/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts @@ -54,7 +54,6 @@ export const nativeConnectionNotEstablished = "native_connection_not_established"; export const uninitializedPublicClientApplication = "uninitialized_public_client_application"; -export const nativePromptNotSupported = "native_prompt_not_supported"; export const invalidBase64String = "invalid_base64_string"; export const invalidPopTokenRequest = "invalid_pop_token_request"; export const failedToBuildHeaders = "failed_to_build_headers"; diff --git a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts index f5f97ea5f7..18044269f2 100644 --- a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts @@ -1020,26 +1020,6 @@ export class PlatformAuthInteractionClient extends BaseInteractionClient { ); return undefined; } - - // If request is interactive, check if prompt provided is allowed to go directly to native broker - switch (prompt) { - case PromptValue.NONE: - case PromptValue.CONSENT: - case PromptValue.LOGIN: - case PromptValue.CREATE: - case PromptValue.SELECT_ACCOUNT: - this.logger.trace( - "initializeNativeRequest: prompt is compatible with native flow" - ); - return prompt; - default: - this.logger.trace( - `initializeNativeRequest: prompt = ${prompt} is not compatible with native flow` - ); - throw createBrowserAuthError( - BrowserAuthErrorCodes.nativePromptNotSupported - ); - } } /** From 9ef9e09f127341c93a51c779befbdc2acbf81b03 Mon Sep 17 00:00:00 2001 From: sameerag Date: Tue, 23 Sep 2025 21:16:56 -0700 Subject: [PATCH 4/8] Hold off on prompt=create for native flows --- .../src/error/BrowserAuthError.ts | 8 ++ .../src/error/BrowserAuthErrorCodes.ts | 1 + .../PlatformAuthInteractionClient.ts | 21 ++++ .../test/app/PublicClientApplication.spec.ts | 118 +++--------------- .../PlatformAuthInteractionClient.spec.ts | 84 ++++++++----- 5 files changed, 94 insertions(+), 138 deletions(-) diff --git a/lib/msal-browser/src/error/BrowserAuthError.ts b/lib/msal-browser/src/error/BrowserAuthError.ts index e93bfecae3..3c5849dc9b 100644 --- a/lib/msal-browser/src/error/BrowserAuthError.ts +++ b/lib/msal-browser/src/error/BrowserAuthError.ts @@ -88,6 +88,8 @@ export const BrowserAuthErrorMessages = { "Native extension is not installed. If you think this is a mistake call the initialize function.", [BrowserAuthErrorCodes.nativeConnectionNotEstablished]: `Connection to native platform has not been established. Please install a compatible browser extension and run initialize(). ${ErrorLink}`, [BrowserAuthErrorCodes.uninitializedPublicClientApplication]: `You must call and await the initialize function before attempting to call any other MSAL API. ${ErrorLink}`, + [BrowserAuthErrorCodes.nativePromptNotSupported]: + "The provided prompt is not supported by the native platform. This request should be routed to the web based flow.", [BrowserAuthErrorCodes.invalidBase64String]: "Invalid base64 encoded string.", [BrowserAuthErrorCodes.invalidPopTokenRequest]: @@ -324,6 +326,12 @@ export const BrowserAuthErrorMessage = { BrowserAuthErrorCodes.uninitializedPublicClientApplication ], }, + nativePromptNotSupported: { + code: BrowserAuthErrorCodes.nativePromptNotSupported, + desc: BrowserAuthErrorMessages[ + BrowserAuthErrorCodes.nativePromptNotSupported + ], + }, invalidBase64StringError: { code: BrowserAuthErrorCodes.invalidBase64String, desc: BrowserAuthErrorMessages[ diff --git a/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts b/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts index 9c65e4d401..31ee9fc9e5 100644 --- a/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts +++ b/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts @@ -54,6 +54,7 @@ export const nativeConnectionNotEstablished = "native_connection_not_established"; export const uninitializedPublicClientApplication = "uninitialized_public_client_application"; +export const nativePromptNotSupported = "native_prompt_not_supported"; export const invalidBase64String = "invalid_base64_string"; export const invalidPopTokenRequest = "invalid_pop_token_request"; export const failedToBuildHeaders = "failed_to_build_headers"; diff --git a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts index 18044269f2..3b2039df01 100644 --- a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts @@ -1020,6 +1020,27 @@ export class PlatformAuthInteractionClient extends BaseInteractionClient { ); return undefined; } + + // If request is interactive, check if prompt provided is allowed to go directly to native broker + switch (prompt) { + case PromptValue.NONE: + case PromptValue.CONSENT: + case PromptValue.LOGIN: + case PromptValue.SELECT_ACCOUNT: + this.logger.trace( + "initializeNativeRequest: prompt is compatible with native flow" + ); + return prompt; + default: + this.logger.trace( + `initializeNativeRequest: prompt = ${prompt} is not compatible with native flow` + ); + throw createBrowserAuthError( + BrowserAuthErrorCodes.nativePromptNotSupported + ); + } + + return undefined; } /** diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index fbeca4bdcb..22086665a9 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -15,6 +15,7 @@ import { TEST_SSH_VALUES, TEST_STATE_VALUES, TEST_TOKEN_LIFETIMES, + TEST_TOKEN_RESPONSE, TEST_TOKENS, TEST_URIS, testLogoutUrl, @@ -119,6 +120,7 @@ import { PlatformAuthDOMHandler } from "../../src/broker/nativeBroker/PlatformAu import * as CacheKeys from "../../src/cache/CacheKeys.js"; import { getAccountKeysCacheKey } from "../../src/cache/CacheKeys.js"; import exp from "constants"; +import { TestTokenResponse } from "../custom_auth/test_resources/TestConstants.js"; const cacheConfig = { temporaryCacheLocation: BrowserCacheLocation.SessionStorage, @@ -1769,7 +1771,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, @@ -1780,29 +1782,27 @@ 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 platform broker call fails due to fatal error and emits platform telemetry", async () => { @@ -2230,8 +2230,8 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { correlationId: RANDOM_TEST_GUID, scopes: ["User.Read"], account: testAccount, - prompt: "select_account", }).catch((e) => {}); + done(); }); }); @@ -2735,96 +2735,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(popupSpy).toHaveBeenCalledTimes(0); }); - 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, - }, - 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, - uniqueId: testAccount.localAccountId, - tenantId: testAccount.tenantId, - scopes: TEST_CONFIG.DEFAULT_SCOPES, - idToken: "test-idToken", - idTokenClaims: {}, - accessToken: "test-accessToken", - fromCache: false, - correlationId: RANDOM_TEST_GUID, - expiresOn: TestTimeUtils.nowDateWithOffset(3600), - account: testAccount, - tokenType: AuthenticationScheme.BEARER, - }; - - jest.spyOn(BrowserCrypto, "createNewGuid").mockReturnValue( - RANDOM_TEST_GUID - ); - - const nativeAcquireTokenSpy: jest.SpyInstance = jest.spyOn( - PlatformAuthInteractionClient.prototype, - "acquireToken" - ); - - const popupSpy: jest.SpyInstance = jest - .spyOn(PopupClient.prototype, "acquireToken") - .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(undefined); - expect(events[0].isPlatformAuthorizeRequest).toBe(true); - expect(events[0].isPlatformBrokerRequest).toBe(undefined); - pca.removePerformanceCallback(callbackId); - }); - - const response = await pca.acquireTokenPopup({ - scopes: ["User.Read"], - account: testAccount, - prompt: "select_account", - }); - - expect(response).toBe(testTokenResponse); - expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(0); - expect(popupSpy).toHaveBeenCalledTimes(1); - }); - it("falls back to web flow if platform broker call fails due to fatal error and emits platform telemetry", async () => { const config = { auth: { diff --git a/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts index 3cd1a32772..0ce8a942f6 100644 --- a/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts @@ -459,40 +459,6 @@ describe("PlatformAuthInteractionClient Tests", () => { expect(response.expiresOn).toBeDefined(); }); - it("throws if prompt: select_account", (done) => { - platformAuthInteractionClient - .acquireToken({ - scopes: ["User.Read"], - prompt: PromptValue.SELECT_ACCOUNT, - }) - .catch((e) => { - expect(e.errorCode).toBe( - BrowserAuthErrorMessage.nativePromptNotSupported.code - ); - expect(e.errorMessage).toBe( - BrowserAuthErrorMessage.nativePromptNotSupported.desc - ); - done(); - }); - }); - - it("throws if prompt: create", (done) => { - platformAuthInteractionClient - .acquireToken({ - scopes: ["User.Read"], - prompt: PromptValue.CREATE, - }) - .catch((e) => { - expect(e.errorCode).toBe( - BrowserAuthErrorMessage.nativePromptNotSupported.code - ); - expect(e.errorMessage).toBe( - BrowserAuthErrorMessage.nativePromptNotSupported.desc - ); - done(); - }); - }); - it("prompt: none succeeds", async () => { jest.spyOn( PlatformAuthExtensionHandler.prototype, @@ -568,6 +534,56 @@ describe("PlatformAuthInteractionClient Tests", () => { expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); + 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: 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(AuthenticationScheme.BEARER); + }); + + it("prompt: create succeeds", async () => { + jest.spyOn( + PlatformAuthExtensionHandler.prototype, + "sendMessage" + ).mockImplementation((): Promise => { + return Promise.resolve(MOCK_WAM_RESPONSE); + }); + const response = await platformAuthInteractionClient.acquireToken({ + scopes: ["User.Read"], + prompt: PromptValue.CREATE, + }); + 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(AuthenticationScheme.BEARER); + }); + it("does not throw account switch error when homeaccountid is same", (done) => { const raw_client_info = "eyJ1aWQiOiAiMDAwMDAwMDAtMDAwMC0wMDAwLTY2ZjMtMzMzMmVjYTdlYTgxIiwgInV0aWQiOiIzMzM4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQifQ=="; From 1c549153146c165534986574fe9c0e00c444daed Mon Sep 17 00:00:00 2001 From: sameerag Date: Tue, 23 Sep 2025 21:41:58 -0700 Subject: [PATCH 5/8] Remove prompt=create support for WAM --- .../src/controllers/StandardController.ts | 18 +++ .../test/app/PublicClientApplication.spec.ts | 111 +++++++++++++++++- .../PlatformAuthInteractionClient.spec.ts | 38 +++--- 3 files changed, 141 insertions(+), 26 deletions(-) diff --git a/lib/msal-browser/src/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index 22dbfe72d8..ffb3af974d 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -1704,6 +1704,24 @@ export class StandardController implements IController { return false; } + if (request.prompt) { + switch (request.prompt) { + case PromptValue.NONE: + case PromptValue.CONSENT: + case PromptValue.LOGIN: + case PromptValue.SELECT_ACCOUNT: + this.logger.trace( + "canUsePlatformBroker: prompt is compatible with platform broker flow" + ); + break; + default: + this.logger.trace( + `canUsePlatformBroker: prompt = ${request.prompt} is not compatible with platform broker flow, returning false` + ); + return false; + } + } + if (!accountId && !this.getNativeAccountId(request)) { this.logger.trace( "canUsePlatformBroker: nativeAccountId is not available, returning false" diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 22086665a9..7155a3d12e 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -119,9 +119,6 @@ import { EndSessionRequest } from "../../src/request/EndSessionRequest.js"; import { PlatformAuthDOMHandler } from "../../src/broker/nativeBroker/PlatformAuthDOMHandler.js"; import * as CacheKeys from "../../src/cache/CacheKeys.js"; import { getAccountKeysCacheKey } from "../../src/cache/CacheKeys.js"; -import exp from "constants"; -import { TestTokenResponse } from "../custom_auth/test_resources/TestConstants.js"; - const cacheConfig = { temporaryCacheLocation: BrowserCacheLocation.SessionStorage, cacheLocation: BrowserCacheLocation.SessionStorage, @@ -1805,6 +1802,114 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(1); }); + 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, + }, + 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, + uniqueId: testAccount.localAccountId, + tenantId: testAccount.tenantId, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + idToken: "test-idToken", + idTokenClaims: {}, + accessToken: "test-accessToken", + fromCache: false, + correlationId: RANDOM_TEST_GUID, + expiresOn: TestTimeUtils.nowDateWithOffset(3600), + account: testAccount, + tokenType: AuthenticationScheme.BEARER, + }; + + 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") + .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, + prompt: "select_account", + }); + + expect(response).toBe(testTokenResponse); + expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(1); + expect(popupSpy).toHaveBeenCalledTimes(0); + }); + it("falls back to web flow if platform broker call fails due to fatal error and emits platform telemetry", async () => { const config = { auth: { diff --git a/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts index 0ce8a942f6..c95a2b7cc0 100644 --- a/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts @@ -559,29 +559,21 @@ describe("PlatformAuthInteractionClient Tests", () => { expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); - it("prompt: create succeeds", async () => { - jest.spyOn( - PlatformAuthExtensionHandler.prototype, - "sendMessage" - ).mockImplementation((): Promise => { - return Promise.resolve(MOCK_WAM_RESPONSE); - }); - const response = await platformAuthInteractionClient.acquireToken({ - scopes: ["User.Read"], - prompt: PromptValue.CREATE, - }); - 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(AuthenticationScheme.BEARER); + it("throws if prompt: create", (done) => { + platformAuthInteractionClient + .acquireToken({ + scopes: ["User.Read"], + prompt: PromptValue.CREATE, + }) + .catch((e) => { + expect(e.errorCode).toBe( + BrowserAuthErrorMessage.nativePromptNotSupported.code + ); + expect(e.errorMessage).toBe( + BrowserAuthErrorMessage.nativePromptNotSupported.desc + ); + done(); + }); }); it("does not throw account switch error when homeaccountid is same", (done) => { From 88ee1254806ac8e577ad535a1f3aefa2d43cca84 Mon Sep 17 00:00:00 2001 From: sameerag Date: Tue, 23 Sep 2025 21:57:14 -0700 Subject: [PATCH 6/8] Change files --- ...-msal-browser-0e275f35-88b3-4bb4-8699-815b7091bf1b.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@azure-msal-browser-0e275f35-88b3-4bb4-8699-815b7091bf1b.json diff --git a/change/@azure-msal-browser-0e275f35-88b3-4bb4-8699-815b7091bf1b.json b/change/@azure-msal-browser-0e275f35-88b3-4bb4-8699-815b7091bf1b.json new file mode 100644 index 0000000000..446b515234 --- /dev/null +++ b/change/@azure-msal-browser-0e275f35-88b3-4bb4-8699-815b7091bf1b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Add prompt=select_account for native flows (#8062)", + "packageName": "@azure/msal-browser", + "email": "sameera.gajjarapu@microsoft.com", + "dependentChangeType": "patch" +} From 4128cdc23ff83b6909193e63ef71fe2654e336e7 Mon Sep 17 00:00:00 2001 From: sameerag Date: Tue, 23 Sep 2025 22:01:02 -0700 Subject: [PATCH 7/8] Remove typo --- .../src/interaction_client/PlatformAuthInteractionClient.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts index 3b2039df01..637bef40e4 100644 --- a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts @@ -1039,8 +1039,6 @@ export class PlatformAuthInteractionClient extends BaseInteractionClient { BrowserAuthErrorCodes.nativePromptNotSupported ); } - - return undefined; } /** From 335b046cf67c464bcdfb60ed0bfb51a52571b9e4 Mon Sep 17 00:00:00 2001 From: sameerag Date: Tue, 23 Sep 2025 22:05:43 -0700 Subject: [PATCH 8/8] Update sample code with copilot feedback --- .../VanillaJSTestApp2.0/app/wamBroker/auth.js | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/auth.js index aa97affb6a..f9f84e21a8 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/auth.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/auth.js @@ -78,12 +78,12 @@ async function getTokenPopup(request, account) { } async function getTokenPopupCreate(request, account) { - request.prompt = "create"; + const requestWithPromptCreate = {... request, prompt: "create"}; const startTime = Date.now(); // Acquire token using popup directly console.log("acquiring token using popup with prompt: create"); - return await myMSALObj.acquireTokenPopup(request).then((response) => { + return await myMSALObj.acquireTokenPopup(requestWithPromptCreate).then((response) => { console.log(`Token acquisition time elapsed: ${Date.now() - startTime}ms`); console.log(response); return response; @@ -93,11 +93,12 @@ async function getTokenPopupCreate(request, account) { } async function getTokenPopupNone(request, account) { - request.prompt = "none"; + + const requestWithPromptNone = {... request, prompt: "none"}; const startTime = Date.now(); console.log("acquiring token silently with prompt: none"); - return await myMSALObj.acquireTokenSilent(request).then((response) => { + return await myMSALObj.acquireTokenSilent(requestWithPromptNone).then((response) => { console.log(`Token acquisition time elapsed: ${Date.now() - startTime}ms`); console.log(response); return response; @@ -107,11 +108,11 @@ async function getTokenPopupNone(request, account) { } async function getTokenPopupLogin(request, account) { - request.prompt = "login"; + const requestWithPromptLogin = {... request, prompt: "login"}; const startTime = Date.now(); console.log("acquiring token using popup with prompt: login"); - return await myMSALObj.acquireTokenPopup(request).then((response) => { + return await myMSALObj.acquireTokenPopup(requestWithPromptLogin).then((response) => { console.log(`Token acquisition time elapsed: ${Date.now() - startTime}ms`); console.log(response); return response; @@ -121,11 +122,11 @@ async function getTokenPopupLogin(request, account) { } async function getTokenPopupConsent(request, account) { - request.prompt = "consent"; + const requestWithPromptConsent = {... request, prompt: "consent"}; const startTime = Date.now(); console.log("acquiring token using popup with prompt: consent"); - return await myMSALObj.acquireTokenPopup(request).then((response) => { + return await myMSALObj.acquireTokenPopup(requestWithPromptConsent).then((response) => { console.log(`Token acquisition time elapsed: ${Date.now() - startTime}ms`); console.log(response); return response; @@ -135,11 +136,11 @@ async function getTokenPopupConsent(request, account) { } async function getTokenPopupSelectAccount(request, account) { - request.prompt = "select_account"; + const requestWithPromptSelectAccount = {... request, prompt: "select_account"}; const startTime = Date.now(); console.log("acquiring token using popup with prompt: select_account"); - return await myMSALObj.acquireTokenPopup(request).then((response) => { + return await myMSALObj.acquireTokenPopup(requestWithPromptSelectAccount).then((response) => { console.log(`Token acquisition time elapsed: ${Date.now() - startTime}ms`); console.log(response); return response;