Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)",
"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 @@ -1587,6 +1587,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"
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,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"
);
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 @@ -1689,7 +1689,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 @@ -1700,29 +1700,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 @@ -2658,20 +2657,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 @@ -2688,22 +2701,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
Loading