Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Add prompt=select_account for native flows (#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 @@ -1709,6 +1709,7 @@ export class StandardController implements IController {
case PromptValue.NONE:
case PromptValue.CONSENT:
case PromptValue.LOGIN:
case 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 @@ -1026,6 +1026,7 @@ export class PlatformAuthInteractionClient extends BaseInteractionClient {
case PromptValue.NONE:
case PromptValue.CONSENT:
case PromptValue.LOGIN:
case PromptValue.SELECT_ACCOUNT:
this.logger.trace(
"initializeNativeRequest: prompt is compatible with native flow"
);
Expand Down
221 changes: 118 additions & 103 deletions lib/msal-browser/test/app/PublicClientApplication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
TEST_SSH_VALUES,
TEST_STATE_VALUES,
TEST_TOKEN_LIFETIMES,
TEST_TOKEN_RESPONSE,
TEST_TOKENS,
TEST_URIS,
testLogoutUrl,
Expand Down Expand Up @@ -118,8 +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";

const cacheConfig = {
temporaryCacheLocation: BrowserCacheLocation.SessionStorage,
cacheLocation: BrowserCacheLocation.SessionStorage,
Expand Down Expand Up @@ -1769,7 +1768,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 @@ -1780,29 +1779,135 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
};
pca = new PublicClientApplication(config);

stubExtensionProvider(config);
await pca.initialize();

// 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.SpyInstance = jest
.spyOn(
PlatformAuthInteractionClient.prototype,
"acquireTokenRedirect"
)
.mockResolvedValue();
await pca.acquireTokenRedirect({
scopes: ["User.Read"],
account: testAccount,
});

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,
};

const nativeAcquireTokenSpy = jest.spyOn(
PlatformAuthInteractionClient.prototype,
"acquireTokenRedirect"
jest.spyOn(BrowserCrypto, "createNewGuid").mockReturnValue(
RANDOM_TEST_GUID
);
const redirectSpy: jest.SpyInstance = jest
.spyOn(RedirectClient.prototype, "acquireToken")
.mockImplementation();
await pca.acquireTokenRedirect({

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<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(nativeAcquireTokenSpy).toHaveBeenCalledTimes(0);
expect(redirectSpy).toHaveBeenCalledTimes(1);
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 () => {
Expand Down Expand Up @@ -2230,8 +2335,8 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
correlationId: RANDOM_TEST_GUID,
scopes: ["User.Read"],
account: testAccount,
prompt: "select_account",
}).catch((e) => {});
done();
});
});

Expand Down Expand Up @@ -2735,96 +2840,6 @@ 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 () => {
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<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(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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -568,6 +534,48 @@ describe("PlatformAuthInteractionClient Tests", () => {
expect(response.tokenType).toEqual(AuthenticationScheme.BEARER);
});

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: 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("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) => {
const raw_client_info =
"eyJ1aWQiOiAiMDAwMDAwMDAtMDAwMC0wMDAwLTY2ZjMtMzMzMmVjYTdlYTgxIiwgInV0aWQiOiIzMzM4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQifQ==";
Expand Down
Loading