Skip to content

Commit 721c033

Browse files
authored
Allow for prompt=select_account in native flows (#8062)
Native flows currently are never called directly if prompt=select_account. Add support for the same. PS: prompt=create is still in discussion, will add a new PR if we need to account for the same or *if we can remove the prompt related code altogether.
1 parent eb0997d commit 721c033

File tree

10 files changed

+329
-140
lines changed

10 files changed

+329
-140
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Add prompt=select_account for native flows (#8062)",
4+
"packageName": "@azure/msal-browser",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-browser/src/controllers/StandardController.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,6 +1709,7 @@ export class StandardController implements IController {
17091709
case PromptValue.NONE:
17101710
case PromptValue.CONSENT:
17111711
case PromptValue.LOGIN:
1712+
case PromptValue.SELECT_ACCOUNT:
17121713
this.logger.trace(
17131714
"canUsePlatformBroker: prompt is compatible with platform broker flow"
17141715
);

lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,7 @@ export class PlatformAuthInteractionClient extends BaseInteractionClient {
10261026
case PromptValue.NONE:
10271027
case PromptValue.CONSENT:
10281028
case PromptValue.LOGIN:
1029+
case PromptValue.SELECT_ACCOUNT:
10291030
this.logger.trace(
10301031
"initializeNativeRequest: prompt is compatible with native flow"
10311032
);

lib/msal-browser/test/app/PublicClientApplication.spec.ts

Lines changed: 118 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
TEST_SSH_VALUES,
1616
TEST_STATE_VALUES,
1717
TEST_TOKEN_LIFETIMES,
18+
TEST_TOKEN_RESPONSE,
1819
TEST_TOKENS,
1920
TEST_URIS,
2021
testLogoutUrl,
@@ -118,8 +119,6 @@ import { EndSessionRequest } from "../../src/request/EndSessionRequest.js";
118119
import { PlatformAuthDOMHandler } from "../../src/broker/nativeBroker/PlatformAuthDOMHandler.js";
119120
import * as CacheKeys from "../../src/cache/CacheKeys.js";
120121
import { getAccountKeysCacheKey } from "../../src/cache/CacheKeys.js";
121-
import exp from "constants";
122-
123122
const cacheConfig = {
124123
temporaryCacheLocation: BrowserCacheLocation.SessionStorage,
125124
cacheLocation: BrowserCacheLocation.SessionStorage,
@@ -1769,7 +1768,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
17691768
pca.initialize();
17701769
});
17711770

1772-
it("falls back to web flow if prompt is select_account", async () => {
1771+
it("Does not fall back to web flow if prompt is select_account", async () => {
17731772
const config = {
17741773
auth: {
17751774
clientId: TEST_CONFIG.MSAL_CLIENT_ID,
@@ -1780,29 +1779,135 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
17801779
};
17811780
pca = new PublicClientApplication(config);
17821781

1782+
stubExtensionProvider(config);
17831783
await pca.initialize();
1784+
1785+
// Implementation of PCA was moved to controller.
1786+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1787+
pca = (pca as any).controller;
1788+
1789+
const testAccount = BASIC_NATIVE_TEST_ACCOUNT_INFO;
1790+
1791+
const nativeAcquireTokenSpy: jest.SpyInstance = jest
1792+
.spyOn(
1793+
PlatformAuthInteractionClient.prototype,
1794+
"acquireTokenRedirect"
1795+
)
1796+
.mockResolvedValue();
1797+
await pca.acquireTokenRedirect({
1798+
scopes: ["User.Read"],
1799+
account: testAccount,
1800+
});
1801+
1802+
expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(1);
1803+
});
1804+
1805+
it("Does not fall back to web flow if prompt is select_account and emits platform telemetry", async () => {
1806+
const config = {
1807+
auth: {
1808+
clientId: TEST_CONFIG.MSAL_CLIENT_ID,
1809+
},
1810+
system: {
1811+
allowPlatformBroker: true,
1812+
},
1813+
telemetry: {
1814+
client: new BrowserPerformanceClient({
1815+
auth: {
1816+
clientId: TEST_CONFIG.MSAL_CLIENT_ID,
1817+
},
1818+
}),
1819+
application: {
1820+
appName: TEST_CONFIG.applicationName,
1821+
appVersion: TEST_CONFIG.applicationVersion,
1822+
},
1823+
},
1824+
};
1825+
pca = new PublicClientApplication(config);
1826+
17841827
stubExtensionProvider(config);
1828+
await pca.initialize();
17851829

17861830
//Implementation of PCA was moved to controller.
17871831
pca = (pca as any).controller;
17881832

17891833
const testAccount = BASIC_NATIVE_TEST_ACCOUNT_INFO;
1834+
const testTokenResponse: AuthenticationResult = {
1835+
authority: TEST_CONFIG.validAuthority,
1836+
uniqueId: testAccount.localAccountId,
1837+
tenantId: testAccount.tenantId,
1838+
scopes: TEST_CONFIG.DEFAULT_SCOPES,
1839+
idToken: "test-idToken",
1840+
idTokenClaims: {},
1841+
accessToken: "test-accessToken",
1842+
fromCache: false,
1843+
correlationId: RANDOM_TEST_GUID,
1844+
expiresOn: TestTimeUtils.nowDateWithOffset(3600),
1845+
account: testAccount,
1846+
tokenType: AuthenticationScheme.BEARER,
1847+
};
17901848

1791-
const nativeAcquireTokenSpy = jest.spyOn(
1792-
PlatformAuthInteractionClient.prototype,
1793-
"acquireTokenRedirect"
1849+
jest.spyOn(BrowserCrypto, "createNewGuid").mockReturnValue(
1850+
RANDOM_TEST_GUID
17941851
);
1795-
const redirectSpy: jest.SpyInstance = jest
1796-
.spyOn(RedirectClient.prototype, "acquireToken")
1797-
.mockImplementation();
1798-
await pca.acquireTokenRedirect({
1852+
1853+
const nativeAcquireTokenSpy: jest.SpyInstance = jest
1854+
.spyOn(PlatformAuthInteractionClient.prototype, "acquireToken")
1855+
.mockImplementation(async function (
1856+
this: PlatformAuthInteractionClient,
1857+
request
1858+
) {
1859+
expect(request.correlationId).toBe(RANDOM_TEST_GUID);
1860+
1861+
// Add isNativeBroker to the measurement that was started by StandardController
1862+
// This simulates what the real PlatformAuthInteractionClient does
1863+
if (
1864+
this.performanceClient &&
1865+
(this.performanceClient as any).eventsByCorrelationId
1866+
) {
1867+
const eventMap = (this.performanceClient as any)
1868+
.eventsByCorrelationId;
1869+
const existingEvent = eventMap.get(this.correlationId);
1870+
if (existingEvent) {
1871+
existingEvent.isNativeBroker = true;
1872+
}
1873+
}
1874+
1875+
return testTokenResponse;
1876+
});
1877+
1878+
const popupSpy: jest.SpyInstance = jest
1879+
.spyOn(PopupClient.prototype, "acquireToken")
1880+
.mockImplementation(function (
1881+
this: PopupClient,
1882+
request: PopupRequest
1883+
): Promise<AuthenticationResult> {
1884+
const eventMap = (this.performanceClient as any)
1885+
.eventsByCorrelationId;
1886+
const existingEvent = eventMap.get(
1887+
request.correlationId || RANDOM_TEST_GUID
1888+
);
1889+
if (existingEvent) {
1890+
existingEvent.isPlatformAuthorizeRequest = true;
1891+
}
1892+
1893+
return Promise.resolve(testTokenResponse);
1894+
});
1895+
1896+
// Add performance callback
1897+
const callbackId = pca.addPerformanceCallback((events) => {
1898+
expect(events[0].isNativeBroker).toBe(true);
1899+
pca.removePerformanceCallback(callbackId);
1900+
});
1901+
1902+
const response = await pca.acquireTokenPopup({
17991903
scopes: ["User.Read"],
18001904
account: testAccount,
18011905
prompt: "select_account",
18021906
});
18031907

1804-
expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(0);
1805-
expect(redirectSpy).toHaveBeenCalledTimes(1);
1908+
expect(response).toBe(testTokenResponse);
1909+
expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(1);
1910+
expect(popupSpy).toHaveBeenCalledTimes(0);
18061911
});
18071912

18081913
it("falls back to web flow if platform broker call fails due to fatal error and emits platform telemetry", async () => {
@@ -2230,8 +2335,8 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
22302335
correlationId: RANDOM_TEST_GUID,
22312336
scopes: ["User.Read"],
22322337
account: testAccount,
2233-
prompt: "select_account",
22342338
}).catch((e) => {});
2339+
done();
22352340
});
22362341
});
22372342

@@ -2735,96 +2840,6 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
27352840
expect(popupSpy).toHaveBeenCalledTimes(0);
27362841
});
27372842

2738-
it("falls back to web flow if prompt is select_account and emits platform telemetry", async () => {
2739-
const config = {
2740-
auth: {
2741-
clientId: TEST_CONFIG.MSAL_CLIENT_ID,
2742-
},
2743-
system: {
2744-
allowPlatformBroker: true,
2745-
},
2746-
telemetry: {
2747-
client: new BrowserPerformanceClient({
2748-
auth: {
2749-
clientId: TEST_CONFIG.MSAL_CLIENT_ID,
2750-
},
2751-
}),
2752-
application: {
2753-
appName: TEST_CONFIG.applicationName,
2754-
appVersion: TEST_CONFIG.applicationVersion,
2755-
},
2756-
},
2757-
};
2758-
pca = new PublicClientApplication(config);
2759-
2760-
stubExtensionProvider(config);
2761-
await pca.initialize();
2762-
2763-
//Implementation of PCA was moved to controller.
2764-
pca = (pca as any).controller;
2765-
2766-
const testAccount = BASIC_NATIVE_TEST_ACCOUNT_INFO;
2767-
const testTokenResponse: AuthenticationResult = {
2768-
authority: TEST_CONFIG.validAuthority,
2769-
uniqueId: testAccount.localAccountId,
2770-
tenantId: testAccount.tenantId,
2771-
scopes: TEST_CONFIG.DEFAULT_SCOPES,
2772-
idToken: "test-idToken",
2773-
idTokenClaims: {},
2774-
accessToken: "test-accessToken",
2775-
fromCache: false,
2776-
correlationId: RANDOM_TEST_GUID,
2777-
expiresOn: TestTimeUtils.nowDateWithOffset(3600),
2778-
account: testAccount,
2779-
tokenType: AuthenticationScheme.BEARER,
2780-
};
2781-
2782-
jest.spyOn(BrowserCrypto, "createNewGuid").mockReturnValue(
2783-
RANDOM_TEST_GUID
2784-
);
2785-
2786-
const nativeAcquireTokenSpy: jest.SpyInstance = jest.spyOn(
2787-
PlatformAuthInteractionClient.prototype,
2788-
"acquireToken"
2789-
);
2790-
2791-
const popupSpy: jest.SpyInstance = jest
2792-
.spyOn(PopupClient.prototype, "acquireToken")
2793-
.mockImplementation(function (
2794-
this: PopupClient,
2795-
request: PopupRequest
2796-
): Promise<AuthenticationResult> {
2797-
const eventMap = (this.performanceClient as any)
2798-
.eventsByCorrelationId;
2799-
const existingEvent = eventMap.get(
2800-
request.correlationId || RANDOM_TEST_GUID
2801-
);
2802-
if (existingEvent) {
2803-
existingEvent.isPlatformAuthorizeRequest = true;
2804-
}
2805-
2806-
return Promise.resolve(testTokenResponse);
2807-
});
2808-
2809-
// Add performance callback
2810-
const callbackId = pca.addPerformanceCallback((events) => {
2811-
expect(events[0].isNativeBroker).toBe(undefined);
2812-
expect(events[0].isPlatformAuthorizeRequest).toBe(true);
2813-
expect(events[0].isPlatformBrokerRequest).toBe(undefined);
2814-
pca.removePerformanceCallback(callbackId);
2815-
});
2816-
2817-
const response = await pca.acquireTokenPopup({
2818-
scopes: ["User.Read"],
2819-
account: testAccount,
2820-
prompt: "select_account",
2821-
});
2822-
2823-
expect(response).toBe(testTokenResponse);
2824-
expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(0);
2825-
expect(popupSpy).toHaveBeenCalledTimes(1);
2826-
});
2827-
28282843
it("falls back to web flow if platform broker call fails due to fatal error and emits platform telemetry", async () => {
28292844
const config = {
28302845
auth: {

lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -459,40 +459,6 @@ describe("PlatformAuthInteractionClient Tests", () => {
459459
expect(response.expiresOn).toBeDefined();
460460
});
461461

462-
it("throws if prompt: select_account", (done) => {
463-
platformAuthInteractionClient
464-
.acquireToken({
465-
scopes: ["User.Read"],
466-
prompt: PromptValue.SELECT_ACCOUNT,
467-
})
468-
.catch((e) => {
469-
expect(e.errorCode).toBe(
470-
BrowserAuthErrorMessage.nativePromptNotSupported.code
471-
);
472-
expect(e.errorMessage).toBe(
473-
BrowserAuthErrorMessage.nativePromptNotSupported.desc
474-
);
475-
done();
476-
});
477-
});
478-
479-
it("throws if prompt: create", (done) => {
480-
platformAuthInteractionClient
481-
.acquireToken({
482-
scopes: ["User.Read"],
483-
prompt: PromptValue.CREATE,
484-
})
485-
.catch((e) => {
486-
expect(e.errorCode).toBe(
487-
BrowserAuthErrorMessage.nativePromptNotSupported.code
488-
);
489-
expect(e.errorMessage).toBe(
490-
BrowserAuthErrorMessage.nativePromptNotSupported.desc
491-
);
492-
done();
493-
});
494-
});
495-
496462
it("prompt: none succeeds", async () => {
497463
jest.spyOn(
498464
PlatformAuthExtensionHandler.prototype,
@@ -568,6 +534,48 @@ describe("PlatformAuthInteractionClient Tests", () => {
568534
expect(response.tokenType).toEqual(AuthenticationScheme.BEARER);
569535
});
570536

537+
it("prompt: select_account succeeds", async () => {
538+
jest.spyOn(
539+
PlatformAuthExtensionHandler.prototype,
540+
"sendMessage"
541+
).mockImplementation((): Promise<PlatformAuthResponse> => {
542+
return Promise.resolve(MOCK_WAM_RESPONSE);
543+
});
544+
const response = await platformAuthInteractionClient.acquireToken({
545+
scopes: ["User.Read"],
546+
prompt: PromptValue.SELECT_ACCOUNT,
547+
});
548+
expect(response.accessToken).toEqual(
549+
MOCK_WAM_RESPONSE.access_token
550+
);
551+
expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token);
552+
expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid);
553+
expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid);
554+
expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS);
555+
expect(response.authority).toEqual(TEST_CONFIG.validAuthority);
556+
expect(response.scopes).toContain(MOCK_WAM_RESPONSE.scope);
557+
expect(response.correlationId).toEqual(RANDOM_TEST_GUID);
558+
expect(response.account).toEqual(TEST_ACCOUNT_INFO);
559+
expect(response.tokenType).toEqual(AuthenticationScheme.BEARER);
560+
});
561+
562+
it("throws if prompt: create", (done) => {
563+
platformAuthInteractionClient
564+
.acquireToken({
565+
scopes: ["User.Read"],
566+
prompt: PromptValue.CREATE,
567+
})
568+
.catch((e) => {
569+
expect(e.errorCode).toBe(
570+
BrowserAuthErrorMessage.nativePromptNotSupported.code
571+
);
572+
expect(e.errorMessage).toBe(
573+
BrowserAuthErrorMessage.nativePromptNotSupported.desc
574+
);
575+
done();
576+
});
577+
});
578+
571579
it("does not throw account switch error when homeaccountid is same", (done) => {
572580
const raw_client_info =
573581
"eyJ1aWQiOiAiMDAwMDAwMDAtMDAwMC0wMDAwLTY2ZjMtMzMzMmVjYTdlYTgxIiwgInV0aWQiOiIzMzM4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQifQ==";

0 commit comments

Comments
 (0)