Skip to content
7 changes: 7 additions & 0 deletions change/@azure-msal-browser-prompt-select-account-msal-v5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Add prompt=select_account support for native flows in msal-v5 [#8062](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8063)",
"packageName": "@azure/msal-browser",
"email": "[email protected]",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions lib/msal-browser/src/controllers/StandardController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
97 changes: 78 additions & 19 deletions lib/msal-browser/test/app/PublicClientApplication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 () => {
Expand Down Expand Up @@ -2608,20 +2607,34 @@ 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,
},
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,
Expand All @@ -2638,22 +2651,68 @@ 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<AuthenticationResult> {
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(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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlatformAuthResponse> => {
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) => {
Expand Down