diff --git a/change/@azure-msal-browser-1e3569d5-aa2c-4084-ada1-91a18950994c.json b/change/@azure-msal-browser-1e3569d5-aa2c-4084-ada1-91a18950994c.json new file mode 100644 index 0000000000..9e44fd7746 --- /dev/null +++ b/change/@azure-msal-browser-1e3569d5-aa2c-4084-ada1-91a18950994c.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Add storeInCache request parameter to control which tokens are persisted to the cache", + "packageName": "@azure/msal-browser", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-common-5fe8f135-689c-46e5-9922-27a0da506e7a.json b/change/@azure-msal-common-5fe8f135-689c-46e5-9922-27a0da506e7a.json new file mode 100644 index 0000000000..7a8047dbb9 --- /dev/null +++ b/change/@azure-msal-common-5fe8f135-689c-46e5-9922-27a0da506e7a.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Add storeInCache request parameter to control which tokens are persisted to the cache", + "packageName": "@azure/msal-common", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-node-44da9453-f869-4abb-8e69-0781beb4db9c.json b/change/@azure-msal-node-44da9453-f869-4abb-8e69-0781beb4db9c.json new file mode 100644 index 0000000000..e80b759a65 --- /dev/null +++ b/change/@azure-msal-node-44da9453-f869-4abb-8e69-0781beb4db9c.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Omit new storeInCache request parameter from public API surface", + "packageName": "@azure/msal-node", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-browser/docs/device-bound-tokens.md b/lib/msal-browser/docs/device-bound-tokens.md index 86d9920369..9bbd1e1dab 100644 --- a/lib/msal-browser/docs/device-bound-tokens.md +++ b/lib/msal-browser/docs/device-bound-tokens.md @@ -51,6 +51,6 @@ A working sample can be found [here](https://github.com/AzureAD/microsoft-authen There are a few things that may behave a little differently when acquiring tokens through WAM. -- The `forceRefresh` parameter for `acquireTokenSilent` calls is not supported by WAM. You may receive a cached token from WAM regardless of what this flag is set to. +- All cache related configuration applies only to MSAL's local cache. The native broker controls its own, more secure, cache which is used instead of browser storage and it does not support configuration of its cache behavior. This means you may receive a cached token regardless of the value of request parameters such as: `forceRefresh`, `cacheLookupPolicy` or `storeInCache`. In addition, tokens received from the native broker are _not_ stored in local or session storage regardless of what you have configured on PublicClientApplication. - If WAM needs to prompt the user for interaction a system prompt will be opened. This prompt looks a bit different from the browser popup windows you may be used to. - Switching your account in the WAM prompt is not supported and MSAL.js will throw an error (Error Code: user_switch) if this happens. It is your app's responsibility to catch this error and handle it in a way that makes sense for your scenarios (e.g. Show an error page, retry with the new account, retry with the original account, etc.) diff --git a/lib/msal-browser/src/broker/nativeBroker/NativeRequest.ts b/lib/msal-browser/src/broker/nativeBroker/NativeRequest.ts index a1ee1e2d2b..bdbbb819cf 100644 --- a/lib/msal-browser/src/broker/nativeBroker/NativeRequest.ts +++ b/lib/msal-browser/src/broker/nativeBroker/NativeRequest.ts @@ -4,7 +4,7 @@ */ import { NativeExtensionMethod } from "../../utils/BrowserConstants"; -import { StringDict } from "@azure/msal-common"; +import { StoreInCache, StringDict } from "@azure/msal-common"; /** * Token request which native broker will use to acquire tokens @@ -30,6 +30,7 @@ export type NativeTokenRequest = { resourceRequestUri?: string; extendedExpiryToken?: boolean; extraParameters?: StringDict; + storeInCache?: StoreInCache; // Object of booleans indicating whether to store tokens in the cache or not (default is true) }; /** diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index 524c7584b6..4f5cd792c5 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -664,7 +664,10 @@ export class NativeInteractionClient extends BaseInteractionClient { cachedAccessToken ); - this.nativeStorageManager.saveCacheRecord(nativeCacheRecord); + this.nativeStorageManager.saveCacheRecord( + nativeCacheRecord, + request.storeInCache + ); } protected addTelemetryFromNativeResponse( diff --git a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts index 197bc2bc43..c7241bdf8b 100644 --- a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts @@ -39,6 +39,7 @@ import { SilentCacheClient } from "../../src/interaction_client/SilentCacheClien import { NativeExtensionRequestBody } from "../../src/broker/nativeBroker/NativeRequest"; import { getDefaultPerformanceClient } from "../utils/TelemetryUtils"; import { CryptoOps } from "../../src/crypto/CryptoOps"; +import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager"; const networkInterface = { sendGetRequestAsync(): T { @@ -94,53 +95,67 @@ const testCacheRecord: CacheRecord = { describe("NativeInteractionClient Tests", () => { globalThis.MessageChannel = require("worker_threads").MessageChannel; // jsdom does not include an implementation for MessageChannel - let pca = new PublicClientApplication({ - auth: { - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - }, - }); + let pca: PublicClientApplication; + let nativeInteractionClient: NativeInteractionClient; + + let browserCacheManager: BrowserCacheManager; + let internalStorage: BrowserCacheManager; - //Implementation of PCA was moved to controller. - pca = (pca as any).controller; - - const wamProvider = new NativeMessageHandler( - pca.getLogger(), - 2000, - getDefaultPerformanceClient(), - new CryptoOps(new Logger({})) - ); - // @ts-ignore - const nativeInteractionClient = new NativeInteractionClient( - // @ts-ignore - pca.config, - // @ts-ignore - pca.browserStorage, - // @ts-ignore - pca.browserCrypto, - pca.getLogger(), - // @ts-ignore - pca.eventHandler, - // @ts-ignore - pca.navigationClient, - ApiId.acquireTokenRedirect, - // @ts-ignore - pca.performanceClient, - wamProvider, - "nativeAccountId", - // @ts-ignore - pca.nativeInternalStorage, - RANDOM_TEST_GUID - ); + let wamProvider: NativeMessageHandler; let postMessageSpy: sinon.SinonSpy; let mcPort: MessagePort; beforeEach(() => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + }); + + //Implementation of PCA was moved to controller. + pca = (pca as any).controller; + + //@ts-ignore + browserCacheManager = pca.browserStorage; + //@ts-ignore + internalStorage = pca.nativeInternalStorage; + + wamProvider = new NativeMessageHandler( + pca.getLogger(), + 2000, + getDefaultPerformanceClient(), + new CryptoOps(new Logger({})) + ); + + nativeInteractionClient = new NativeInteractionClient( + // @ts-ignore + pca.config, + // @ts-ignore + pca.browserStorage, + // @ts-ignore + pca.browserCrypto, + pca.getLogger(), + // @ts-ignore + pca.eventHandler, + // @ts-ignore + pca.navigationClient, + ApiId.acquireTokenRedirect, + // @ts-ignore + pca.performanceClient, + wamProvider, + "nativeAccountId", + // @ts-ignore + pca.nativeInternalStorage, + RANDOM_TEST_GUID + ); + postMessageSpy = sinon.spy(window, "postMessage"); sinon.stub(MessageEvent.prototype, "source").get(() => window); // source property not set by jsdom window messaging APIs }); afterEach(() => { mcPort && mcPort.close(); + jest.restoreAllMocks(); sinon.restore(); sessionStorage.clear(); localStorage.clear(); @@ -577,6 +592,102 @@ describe("NativeInteractionClient Tests", () => { expect(response.account).toEqual(testAccount); expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); + + describe("storeInCache tests", () => { + const mockWamResponse = { + access_token: TEST_TOKENS.ACCESS_TOKEN, + id_token: TEST_TOKENS.IDTOKEN_V2, + scope: "User.Read", + expires_in: 3600, + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + account: { + id: "nativeAccountId", + }, + properties: {}, + }; + + beforeEach(() => { + jest.spyOn( + NativeMessageHandler.prototype, + "sendMessage" + ).mockResolvedValue(mockWamResponse); + }); + + it("does not store idToken if storeInCache.idToken = false", async () => { + const response = await nativeInteractionClient.acquireToken({ + scopes: ["User.Read"], + storeInCache: { + idToken: false, + }, + }); + expect(response.accessToken).toEqual( + mockWamResponse.access_token + ); + expect(response.idToken).toEqual(mockWamResponse.id_token); + + // Browser Storage should not contain tokens + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(0); + expect(tokenKeys.accessToken).toHaveLength(0); + expect(tokenKeys.refreshToken).toHaveLength(0); + + // Cache should not contain tokens which were turned off + const internalTokenKeys = internalStorage.getTokenKeys(); + expect(internalTokenKeys.idToken).toHaveLength(0); + expect(internalTokenKeys.accessToken).toHaveLength(1); + expect(internalTokenKeys.refreshToken).toHaveLength(0); // RT will never be returned by WAM + }); + + it("does not store accessToken if storeInCache.accessToken = false", async () => { + const response = await nativeInteractionClient.acquireToken({ + scopes: ["User.Read"], + storeInCache: { + accessToken: false, + }, + }); + expect(response.accessToken).toEqual( + mockWamResponse.access_token + ); + expect(response.idToken).toEqual(mockWamResponse.id_token); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(0); + expect(tokenKeys.accessToken).toHaveLength(0); + expect(tokenKeys.refreshToken).toHaveLength(0); + + // Cache should not contain tokens which were turned off + const internalTokenKeys = internalStorage.getTokenKeys(); + expect(internalTokenKeys.idToken).toHaveLength(1); + expect(internalTokenKeys.accessToken).toHaveLength(0); + expect(internalTokenKeys.refreshToken).toHaveLength(0); // RT will never be returned by WAM + }); + + it("does not store refreshToken if storeInCache.refreshToken = false", async () => { + const response = await nativeInteractionClient.acquireToken({ + scopes: ["User.Read"], + storeInCache: { + refreshToken: false, + }, + }); + expect(response.accessToken).toEqual( + mockWamResponse.access_token + ); + expect(response.idToken).toEqual(mockWamResponse.id_token); + + // Browser Storage should not contain tokens + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(0); + expect(tokenKeys.accessToken).toHaveLength(0); + expect(tokenKeys.refreshToken).toHaveLength(0); + + // Cache should not contain tokens which were turned off + const internalTokenKeys = internalStorage.getTokenKeys(); + expect(internalTokenKeys.idToken).toHaveLength(1); + expect(internalTokenKeys.accessToken).toHaveLength(1); + expect(internalTokenKeys.refreshToken).toHaveLength(0); // RT will never be returned by WAM + }); + }); }); describe("acquireTokenRedirect tests", () => { diff --git a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts index e21fdf848d..d4862a3b63 100644 --- a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts @@ -18,6 +18,8 @@ import { TEST_SSH_VALUES, DEFAULT_OPENID_CONFIG_RESPONSE, DEFAULT_TENANT_DISCOVERY_RESPONSE, + TEST_TOKEN_RESPONSE, + ID_TOKEN_CLAIMS, } from "../utils/StringConstants"; import { Constants, @@ -36,8 +38,9 @@ import { CommonAuthorizationCodeRequest, AuthError, Logger, + NetworkManager, + ProtocolUtils, ProtocolMode, - ServerResponseType, } from "@azure/msal-common"; import { TemporaryCacheKeys, @@ -58,6 +61,7 @@ import { FetchClient } from "../../src/network/FetchClient"; import { InteractionHandler } from "../../src/interaction_handler/InteractionHandler"; import { getDefaultPerformanceClient } from "../utils/TelemetryUtils"; import { AuthenticationResult } from "../../src/response/AuthenticationResult"; +import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager"; const testPopupWondowDefaults = { height: BrowserConstants.POPUP_HEIGHT, @@ -70,6 +74,7 @@ describe("PopupClient", () => { globalThis.MessageChannel = require("worker_threads").MessageChannel; // jsdom does not include an implementation for MessageChannel let popupClient: PopupClient; let pca: PublicClientApplication; + let browserCacheManager: BrowserCacheManager; beforeEach(async () => { pca = new PublicClientApplication({ auth: { @@ -81,6 +86,9 @@ describe("PopupClient", () => { pca = (pca as any).controller; await pca.initialize(); + //@ts-ignore + browserCacheManager = pca.browserStorage; + //@ts-ignore popupClient = new PopupClient( //@ts-ignore @@ -105,6 +113,7 @@ describe("PopupClient", () => { }); afterEach(() => { + jest.restoreAllMocks(); sinon.restore(); window.location.hash = ""; window.sessionStorage.clear(); @@ -596,6 +605,100 @@ describe("PopupClient", () => { expect(tokenResp).toEqual(testTokenResponse); }); + describe("storeInCache tests", () => { + beforeEach(() => { + jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue( + TEST_STATE_VALUES.TEST_STATE_POPUP + ); + jest.spyOn(PopupClient.prototype, "openPopup").mockReturnValue( + window + ); + jest.spyOn( + PopupClient.prototype, + "monitorPopupForHash" + ).mockResolvedValue(TEST_HASHES.TEST_SUCCESS_CODE_HASH_POPUP); + jest.spyOn( + NetworkManager.prototype, + "sendPostRequest" + ).mockResolvedValue(TEST_TOKEN_RESPONSE); + }); + + it("does not store idToken if storeInCache.idToken = false", async () => { + const tokenResp = await popupClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + idToken: false, + }, + nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(0); + expect(tokenKeys.accessToken).toHaveLength(1); + expect(tokenKeys.refreshToken).toHaveLength(1); + }); + + it("does not store accessToken if storeInCache.accessToken = false", async () => { + const tokenResp = await popupClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + accessToken: false, + }, + nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(1); + expect(tokenKeys.accessToken).toHaveLength(0); + expect(tokenKeys.refreshToken).toHaveLength(1); + }); + + it("does not store refreshToken if storeInCache.refreshToken = false", async () => { + const tokenResp = await popupClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + refreshToken: false, + }, + nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(1); + expect(tokenKeys.accessToken).toHaveLength(1); + expect(tokenKeys.refreshToken).toHaveLength(0); + }); + }); + it("catches error and cleans cache before rethrowing", async () => { const testError: AuthError = new AuthError( "create_login_url_error", diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index 6e53dc600a..b275be8268 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -19,6 +19,8 @@ import { DEFAULT_TENANT_DISCOVERY_RESPONSE, testLogoutUrl, TEST_SSH_VALUES, + ID_TOKEN_CLAIMS, + TEST_TOKEN_RESPONSE, } from "../utils/StringConstants"; import { ServerError, @@ -45,6 +47,7 @@ import { AccountEntity, ClientConfigurationError, AuthError, + NetworkManager, } from "@azure/msal-common"; import { BrowserUtils } from "../../src/utils/BrowserUtils"; import { @@ -146,6 +149,7 @@ describe("RedirectClient", () => { }); afterEach(() => { + jest.restoreAllMocks(); sinon.restore(); window.location.hash = ""; window.sessionStorage.clear(); @@ -2958,6 +2962,126 @@ describe("RedirectClient", () => { acquireTokenUrlSpy.calledWith(validatedRequest) ).toBeTruthy(); }); + + describe("storeInCache tests", () => { + beforeEach(() => { + jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue( + TEST_STATE_VALUES.TEST_STATE_REDIRECT + ); + jest.spyOn( + NetworkManager.prototype, + "sendPostRequest" + ).mockResolvedValue(TEST_TOKEN_RESPONSE); + }); + + it("does not store idToken if storeInCache.idToken = false", async () => { + browserStorage.setInteractionInProgress(true); + await redirectClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + idToken: false, + }, + nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken + onRedirectNavigate: () => { + return false; // Supress navigation + }, + }); + + const tokenResp = await redirectClient.handleRedirectPromise( + TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT + ); + if (!tokenResp) { + throw "Response should not be null!"; + } + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserStorage.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(0); + expect(tokenKeys.accessToken).toHaveLength(1); + expect(tokenKeys.refreshToken).toHaveLength(1); + }); + + it("does not store accessToken if storeInCache.accessToken = false", async () => { + browserStorage.setInteractionInProgress(true); + await redirectClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + accessToken: false, + }, + nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken + onRedirectNavigate: () => { + return false; // Supress navigation + }, + }); + + const tokenResp = await redirectClient.handleRedirectPromise( + TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT + ); + if (!tokenResp) { + throw "Response should not be null!"; + } + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserStorage.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(1); + expect(tokenKeys.accessToken).toHaveLength(0); + expect(tokenKeys.refreshToken).toHaveLength(1); + }); + + it("does not store refreshToken if storeInCache.refreshToken = false", async () => { + browserStorage.setInteractionInProgress(true); + await redirectClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + refreshToken: false, + }, + nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken + onRedirectNavigate: () => { + return false; // Supress navigation + }, + }); + + const tokenResp = await redirectClient.handleRedirectPromise( + TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT + ); + if (!tokenResp) { + throw "Response should not be null!"; + } + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserStorage.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(1); + expect(tokenKeys.accessToken).toHaveLength(1); + expect(tokenKeys.refreshToken).toHaveLength(0); + }); + }); }); describe("logout", () => { diff --git a/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts index 4b2f9f7434..2f5342076e 100644 --- a/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts @@ -13,17 +13,20 @@ import { TEST_TOKEN_LIFETIMES, RANDOM_TEST_GUID, testNavUrl, + TEST_TOKEN_RESPONSE, } from "../utils/StringConstants"; import { AccountInfo, TokenClaims, AuthorizationCodeClient, AuthenticationScheme, + NetworkManager, } from "@azure/msal-common"; import { BrowserAuthError } from "../../src/error/BrowserAuthError"; import { SilentHandler } from "../../src/interaction_handler/SilentHandler"; import { CryptoOps } from "../../src/crypto/CryptoOps"; import { SilentAuthCodeClient } from "../../src/interaction_client/SilentAuthCodeClient"; +import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager"; import { ApiId, AuthorizationCodeRequest, @@ -32,6 +35,7 @@ import { describe("SilentAuthCodeClient", () => { let silentAuthCodeClient: SilentAuthCodeClient; + let browserCacheManager: BrowserCacheManager; beforeEach(() => { let pca = new PublicClientApplication({ @@ -43,6 +47,9 @@ describe("SilentAuthCodeClient", () => { //Implementation of PCA was moved to controller. pca = (pca as any).controller; + //@ts-ignore + browserCacheManager = pca.browserStorage; + // @ts-ignore silentAuthCodeClient = new SilentAuthCodeClient( //@ts-ignore @@ -153,6 +160,90 @@ describe("SilentAuthCodeClient", () => { ).toBe(true); expect(tokenResp).toEqual(testTokenResponse); }); + + describe("storeInCache tests", () => { + beforeEach(() => { + jest.spyOn( + NetworkManager.prototype, + "sendPostRequest" + ).mockResolvedValue(TEST_TOKEN_RESPONSE); + }); + + it("does not store idToken if storeInCache.idToken = false", async () => { + const tokenResp = await silentAuthCodeClient.acquireToken({ + code: "test-code", + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + idToken: false, + }, + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(0); + expect(tokenKeys.accessToken).toHaveLength(1); + expect(tokenKeys.refreshToken).toHaveLength(1); + }); + + it("does not store accessToken if storeInCache.accessToken = false", async () => { + const tokenResp = await silentAuthCodeClient.acquireToken({ + code: "test-code", + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + accessToken: false, + }, + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(1); + expect(tokenKeys.accessToken).toHaveLength(0); + expect(tokenKeys.refreshToken).toHaveLength(1); + }); + + it("does not store refreshToken if storeInCache.refreshToken = false", async () => { + const tokenResp = await silentAuthCodeClient.acquireToken({ + code: "test-code", + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + refreshToken: false, + }, + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(1); + expect(tokenKeys.accessToken).toHaveLength(1); + expect(tokenKeys.refreshToken).toHaveLength(0); + }); + }); }); describe("logout", () => { diff --git a/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts index 7052f64d1a..6d9bac017c 100644 --- a/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts @@ -14,6 +14,9 @@ import { TEST_TOKEN_LIFETIMES, RANDOM_TEST_GUID, testNavUrl, + TEST_STATE_VALUES, + TEST_TOKEN_RESPONSE, + ID_TOKEN_CLAIMS, } from "../utils/StringConstants"; import { AccountInfo, @@ -24,6 +27,8 @@ import { ResponseMode, AuthenticationScheme, ServerTelemetryManager, + ProtocolUtils, + NetworkManager, } from "@azure/msal-common"; import { BrowserAuthError, @@ -42,6 +47,7 @@ describe("SilentIframeClient", () => { globalThis.MessageChannel = require("worker_threads").MessageChannel; // jsdom does not include an implementation for MessageChannel let silentIframeClient: SilentIframeClient; let pca: PublicClientApplication; + let browserCacheManager: BrowserCacheManager; beforeEach(() => { pca = new PublicClientApplication({ @@ -53,6 +59,9 @@ describe("SilentIframeClient", () => { //Implementation of PCA was moved to controller. pca = (pca as any).controller; + //@ts-ignore + browserCacheManager = pca.browserStorage; + // @ts-ignore silentIframeClient = new SilentIframeClient( //@ts-ignore @@ -541,6 +550,97 @@ describe("SilentIframeClient", () => { done(); }); }); + + describe("storeInCache tests", () => { + beforeEach(() => { + jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue( + TEST_STATE_VALUES.TEST_STATE_SILENT + ); + jest.spyOn( + SilentHandler.prototype, + "monitorIframeForHash" + ).mockResolvedValue(TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT); + jest.spyOn( + NetworkManager.prototype, + "sendPostRequest" + ).mockResolvedValue(TEST_TOKEN_RESPONSE); + }); + + it("does not store idToken if storeInCache.idToken = false", async () => { + const tokenResp = await silentIframeClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + idToken: false, + }, + nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(0); + expect(tokenKeys.accessToken).toHaveLength(1); + expect(tokenKeys.refreshToken).toHaveLength(1); + }); + + it("does not store accessToken if storeInCache.accessToken = false", async () => { + const tokenResp = await silentIframeClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + accessToken: false, + }, + nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(1); + expect(tokenKeys.accessToken).toHaveLength(0); + expect(tokenKeys.refreshToken).toHaveLength(1); + }); + + it("does not store refreshToken if storeInCache.refreshToken = false", async () => { + const tokenResp = await silentIframeClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + refreshToken: false, + }, + nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(1); + expect(tokenKeys.accessToken).toHaveLength(1); + expect(tokenKeys.refreshToken).toHaveLength(0); + }); + }); }); describe("logout", () => { diff --git a/lib/msal-browser/test/interaction_client/SilentRefreshClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentRefreshClient.spec.ts index c01aa62a0b..3bc0989af6 100644 --- a/lib/msal-browser/test/interaction_client/SilentRefreshClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentRefreshClient.spec.ts @@ -11,6 +11,7 @@ import { TEST_DATA_CLIENT_INFO, TEST_TOKEN_LIFETIMES, RANDOM_TEST_GUID, + TEST_TOKEN_RESPONSE, } from "../utils/StringConstants"; import { Constants, @@ -20,13 +21,37 @@ import { AuthenticationScheme, RefreshTokenClient, CommonSilentFlowRequest, + NetworkManager, + RefreshTokenEntity, + AccountEntity, } from "@azure/msal-common"; import { CryptoOps } from "../../src/crypto/CryptoOps"; import { BrowserAuthError } from "../../src/error/BrowserAuthError"; import { SilentRefreshClient } from "../../src/interaction_client/SilentRefreshClient"; +import { BrowserCacheManager } from "../../src/internals"; +import { CLIENT_INFO } from "@azure/msal-common/dist/utils/Constants"; + +const testIdTokenClaims: TokenClaims = { + ver: "2.0", + iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", +}; +const testAccount: AccountInfo = { + homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, + localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, + environment: "login.windows.net", + tenantId: testIdTokenClaims.tid || "", + username: testIdTokenClaims.preferred_username || "", +}; describe("SilentRefreshClient", () => { let silentRefreshClient: SilentRefreshClient; + let browserCacheManager: BrowserCacheManager; beforeEach(() => { let pca = new PublicClientApplication({ @@ -38,6 +63,9 @@ describe("SilentRefreshClient", () => { //Implementation of PCA was moved to controller. pca = (pca as any).controller; + //@ts-ignore + browserCacheManager = pca.browserStorage; + sinon .stub(CryptoOps.prototype, "createNewGuid") .returns(RANDOM_TEST_GUID); @@ -61,6 +89,7 @@ describe("SilentRefreshClient", () => { }); afterEach(() => { + jest.restoreAllMocks(); sinon.restore(); window.location.hash = ""; window.sessionStorage.clear(); @@ -78,23 +107,6 @@ describe("SilentRefreshClient", () => { refresh_token: TEST_TOKENS.REFRESH_TOKEN, id_token: TEST_TOKENS.IDTOKEN_V2, }; - const testIdTokenClaims: TokenClaims = { - ver: "2.0", - iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; - const testAccount: AccountInfo = { - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, - environment: "login.windows.net", - tenantId: testIdTokenClaims.tid || "", - username: testIdTokenClaims.preferred_username || "", - }; const testTokenResponse: AuthenticationResult = { authority: TEST_CONFIG.validAuthority, uniqueId: testIdTokenClaims.oid || "", @@ -138,6 +150,107 @@ describe("SilentRefreshClient", () => { expect(silentATStub.calledWith(expectedTokenRequest)).toBeTruthy(); expect(tokenResp).toEqual(testTokenResponse); }); + + describe("storeInCache tests", () => { + beforeEach(() => { + const rtEntity = new RefreshTokenEntity(); + rtEntity.secret = TEST_TOKEN_RESPONSE.body.refresh_token!; + const accountEntity = new AccountEntity(); + jest.spyOn( + BrowserCacheManager.prototype, + "getAccount" + ).mockReturnValue(accountEntity); + jest.spyOn( + BrowserCacheManager.prototype, + "getRefreshToken" + ).mockReturnValue(rtEntity); + jest.spyOn( + NetworkManager.prototype, + "sendPostRequest" + ).mockResolvedValue(TEST_TOKEN_RESPONSE); + }); + + it("does not store idToken if storeInCache.idToken = false", async () => { + const tokenResp = await silentRefreshClient.acquireToken({ + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + account: testAccount, + forceRefresh: true, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + idToken: false, + }, + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(0); + expect(tokenKeys.accessToken).toHaveLength(1); + expect(tokenKeys.refreshToken).toHaveLength(1); + }); + + it("does not store accessToken if storeInCache.accessToken = false", async () => { + const tokenResp = await silentRefreshClient.acquireToken({ + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + account: testAccount, + forceRefresh: true, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + accessToken: false, + }, + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(1); + expect(tokenKeys.accessToken).toHaveLength(0); + expect(tokenKeys.refreshToken).toHaveLength(1); + }); + + it("does not store refreshToken if storeInCache.refreshToken = false", async () => { + const tokenResp = await silentRefreshClient.acquireToken({ + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + account: testAccount, + forceRefresh: true, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + storeInCache: { + refreshToken: false, + }, + }); + + // Response should still contain acquired tokens + expect(tokenResp.idToken).toEqual( + TEST_TOKEN_RESPONSE.body.id_token + ); + expect(tokenResp.accessToken).toEqual( + TEST_TOKEN_RESPONSE.body.access_token + ); + + // Cache should not contain tokens which were turned off + const tokenKeys = browserCacheManager.getTokenKeys(); + expect(tokenKeys.idToken).toHaveLength(1); + expect(tokenKeys.accessToken).toHaveLength(1); + expect(tokenKeys.refreshToken).toHaveLength(0); + }); + }); }); describe("logout", () => { diff --git a/lib/msal-browser/test/utils/StringConstants.ts b/lib/msal-browser/test/utils/StringConstants.ts index 8f2529f484..4a049a94fc 100644 --- a/lib/msal-browser/test/utils/StringConstants.ts +++ b/lib/msal-browser/test/utils/StringConstants.ts @@ -7,6 +7,8 @@ import { AuthenticationScheme, Constants, NetworkResponse, + OIDC_DEFAULT_SCOPES, + ServerAuthorizationTokenResponse, } from "@azure/msal-common"; import { version } from "../../src/packageMetadata"; @@ -354,6 +356,21 @@ export const ALTERNATE_OPENID_CONFIG_RESPONSE = { }, }; +export const TEST_TOKEN_RESPONSE: NetworkResponse = + { + headers: {}, + body: { + token_type: AuthenticationScheme.BEARER, + scope: OIDC_DEFAULT_SCOPES.join(", "), + expires_in: TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN, + access_token: TEST_TOKENS.ACCESS_TOKEN, + refresh_token: TEST_TOKENS.REFRESH_TOKEN, + id_token: TEST_TOKENS.IDTOKEN_V2_NEWCLAIM, + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + }, + status: 200, + }; + export const testNavUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${encodeURIComponent( `${TEST_CONFIG.MSAL_CLIENT_ID}` )}&scope=user.read%20openid%20profile%20offline_access&redirect_uri=https%3A%2F%2Flocalhost%3A8081%2Findex.html&client-request-id=${encodeURIComponent( diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 78f6430651..17a4925053 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -39,6 +39,7 @@ import { AuthorityMetadataEntity } from "./entities/AuthorityMetadataEntity"; import { BaseAuthRequest } from "../request/BaseAuthRequest"; import { Logger } from "../logger/Logger"; import { name, version } from "../packageMetadata"; +import { StoreInCache } from "../request/StoreInCache"; /** * Interface class which implement cache storage functions used by MSAL to perform validity checks, and store tokens. @@ -281,7 +282,10 @@ export abstract class CacheManager implements ICacheManager { * saves a cache record * @param cacheRecord */ - async saveCacheRecord(cacheRecord: CacheRecord): Promise { + async saveCacheRecord( + cacheRecord: CacheRecord, + storeInCache?: StoreInCache + ): Promise { if (!cacheRecord) { throw ClientAuthError.createNullOrUndefinedCacheRecord(); } @@ -290,15 +294,18 @@ export abstract class CacheManager implements ICacheManager { this.setAccount(cacheRecord.account); } - if (!!cacheRecord.idToken) { + if (!!cacheRecord.idToken && storeInCache?.idToken !== false) { this.setIdTokenCredential(cacheRecord.idToken); } - if (!!cacheRecord.accessToken) { + if (!!cacheRecord.accessToken && storeInCache?.accessToken !== false) { await this.saveAccessToken(cacheRecord.accessToken); } - if (!!cacheRecord.refreshToken) { + if ( + !!cacheRecord.refreshToken && + storeInCache?.refreshToken !== false + ) { this.setRefreshTokenCredential(cacheRecord.refreshToken); } diff --git a/lib/msal-common/src/cache/interface/ICacheManager.ts b/lib/msal-common/src/cache/interface/ICacheManager.ts index 134f9b7d4f..7993d85822 100644 --- a/lib/msal-common/src/cache/interface/ICacheManager.ts +++ b/lib/msal-common/src/cache/interface/ICacheManager.ts @@ -14,6 +14,7 @@ import { IdTokenEntity } from "../entities/IdTokenEntity"; import { AccessTokenEntity } from "../entities/AccessTokenEntity"; import { RefreshTokenEntity } from "../entities/RefreshTokenEntity"; import { AuthorityMetadataEntity } from "../entities/AuthorityMetadataEntity"; +import { StoreInCache } from "../../request/StoreInCache"; export interface ICacheManager { /** @@ -164,7 +165,10 @@ export interface ICacheManager { * saves a cache record * @param cacheRecord */ - saveCacheRecord(cacheRecord: CacheRecord): Promise; + saveCacheRecord( + cacheRecord: CacheRecord, + storeInCache?: StoreInCache + ): Promise; /** * retrieve accounts matching all provided filters; if no filter is set, get all accounts diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index 31d45be71f..fe0334a4dd 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -116,6 +116,7 @@ export { CommonUsernamePasswordRequest } from "./request/CommonUsernamePasswordR export { NativeRequest } from "./request/NativeRequest"; export { NativeSignOutRequest } from "./request/NativeSignOutRequest"; export { RequestParameterBuilder } from "./request/RequestParameterBuilder"; +export { StoreInCache } from "./request/StoreInCache"; // Response export { AzureRegion } from "./authority/AzureRegion"; export { AzureRegionConfiguration } from "./authority/AzureRegionConfiguration"; diff --git a/lib/msal-common/src/request/BaseAuthRequest.ts b/lib/msal-common/src/request/BaseAuthRequest.ts index fe9dd7f85d..69c3dd0ec8 100644 --- a/lib/msal-common/src/request/BaseAuthRequest.ts +++ b/lib/msal-common/src/request/BaseAuthRequest.ts @@ -6,6 +6,7 @@ import { AuthenticationScheme } from "../utils/Constants"; import { AzureCloudOptions } from "../config/ClientConfiguration"; import { StringDict } from "../utils/MsalTypes"; +import { StoreInCache } from "./StoreInCache"; /** * BaseAuthRequest @@ -23,6 +24,7 @@ import { StringDict } from "../utils/MsalTypes"; * - azureCloudOptions - Convenience string enums for users to provide public/sovereign cloud ids * - requestedClaimsHash - SHA 256 hash string of the requested claims string, used as part of an access token cache key so tokens can be filtered by requested claims * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - storeInCache - Object containing boolean values indicating whether to store tokens in the cache or not (default is true) */ export type BaseAuthRequest = { authority: string; @@ -40,4 +42,5 @@ export type BaseAuthRequest = { requestedClaimsHash?: string; maxAge?: number; tokenQueryParameters?: StringDict; + storeInCache?: StoreInCache; }; diff --git a/lib/msal-common/src/request/StoreInCache.ts b/lib/msal-common/src/request/StoreInCache.ts new file mode 100644 index 0000000000..d4449df835 --- /dev/null +++ b/lib/msal-common/src/request/StoreInCache.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Controls whether tokens should be stored in the cache or not. If set to false, tokens may still be acquired and returned but will not be cached for later retrieval. + */ +export type StoreInCache = { + /* Indicates whether or not the acquired accessToken will be stored in the cache */ + accessToken?: boolean; + /* Indicates whether or not the acquired idToken will be stored in the cache */ + idToken?: boolean; + /* Indicates whether or not the acquired refreshToken will be stored in the cache */ + refreshToken?: boolean; +}; diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index 0477dcd2f5..741baffd73 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -312,7 +312,10 @@ export class ResponseHandler { ); } } - await this.cacheStorage.saveCacheRecord(cacheRecord); + await this.cacheStorage.saveCacheRecord( + cacheRecord, + request.storeInCache + ); } finally { if ( this.persistencePlugin && diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index 7317b1e2a7..1f86e9f4b0 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -20,6 +20,8 @@ import { TEST_POP_VALUES, TEST_SSH_VALUES, TEST_CRYPTO_VALUES, + TEST_ACCOUNT_INFO, + TEST_TOKEN_LIFETIMES, } from "../test_kit/StringConstants"; import { ClientAuthError, @@ -62,98 +64,167 @@ describe("CacheManager.ts test cases", () => { sinon.restore(); }); - it("save account", async () => { - const ac = new AccountEntity(); - ac.homeAccountId = "someUid.someUtid"; - ac.environment = "login.microsoftonline.com"; - ac.realm = "microsoft"; - ac.localAccountId = "object1234"; - ac.username = "Jane Goodman"; - ac.authorityType = "MSSTS"; + describe("saveCacheRecord tests", () => { + it("save account", async () => { + const ac = new AccountEntity(); + ac.homeAccountId = "someUid.someUtid"; + ac.environment = "login.microsoftonline.com"; + ac.realm = "microsoft"; + ac.localAccountId = "object1234"; + ac.username = "Jane Goodman"; + ac.authorityType = "MSSTS"; + + const accountKey = ac.generateAccountKey(); + const cacheRecord = new CacheRecord(); + cacheRecord.account = ac; + await mockCache.cacheManager.saveCacheRecord(cacheRecord); + const mockCacheAccount = mockCache.cacheManager.getAccount( + accountKey + ) as AccountEntity; + if (!mockCacheAccount) { + throw TestError.createTestSetupError( + "mockCacheAccount does not have a value" + ); + } + expect(mockCacheAccount.homeAccountId).toEqual("someUid.someUtid"); + }); - const accountKey = ac.generateAccountKey(); - const cacheRecord = new CacheRecord(); - cacheRecord.account = ac; - await mockCache.cacheManager.saveCacheRecord(cacheRecord); - const mockCacheAccount = mockCache.cacheManager.getAccount( - accountKey - ) as AccountEntity; - if (!mockCacheAccount) { - throw TestError.createTestSetupError( - "mockCacheAccount does not have a value" + it("save accessToken", async () => { + const at = new AccessTokenEntity(); + Object.assign(at, { + homeAccountId: "someUid.someUtid", + environment: "login.microsoftonline.com", + credentialType: "AccessToken", + clientId: "mock_client_id", + secret: "an access token sample", + realm: "microsoft", + target: "scope6 scope7", + cachedAt: "1000", + expiresOn: "4600", + extendedExpiresOn: "4600", + tokenType: "Bearer", + }); + + const atKey = at.generateCredentialKey(); + const cacheRecord = new CacheRecord(); + cacheRecord.accessToken = at; + await mockCache.cacheManager.saveCacheRecord(cacheRecord); + const mockCacheAT = mockCache.cacheManager.getAccessTokenCredential( + atKey + ) as AccessTokenEntity; + if (!mockCacheAT) { + throw TestError.createTestSetupError( + "mockCacheAT does not have a value" + ); + } + expect(mockCacheAT.homeAccountId).toEqual("someUid.someUtid"); + expect(mockCacheAT.credentialType).toEqual( + CredentialType.ACCESS_TOKEN ); - } - expect(mockCacheAccount.homeAccountId).toEqual("someUid.someUtid"); - }); + expect(mockCacheAT.tokenType).toEqual(AuthenticationScheme.BEARER); + }); - it("save accessToken", async () => { - const at = new AccessTokenEntity(); - Object.assign(at, { - homeAccountId: "someUid.someUtid", - environment: "login.microsoftonline.com", - credentialType: "AccessToken", - clientId: "mock_client_id", - secret: "an access token sample", - realm: "microsoft", - target: "scope6 scope7", - cachedAt: "1000", - expiresOn: "4600", - extendedExpiresOn: "4600", - tokenType: "Bearer", + it("does not save accessToken if storeInCache.accessToken = false", async () => { + const at = AccessTokenEntity.createAccessTokenEntity( + TEST_ACCOUNT_INFO.homeAccountId, + TEST_ACCOUNT_INFO.environment, + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + TEST_CONFIG.MSAL_TENANT_ID, + "User.Read", + TEST_TOKEN_LIFETIMES.TEST_ACCESS_TOKEN_EXP, + TEST_TOKEN_LIFETIMES.TEST_ACCESS_TOKEN_EXP, + mockCrypto + ); + + const atKey = at.generateCredentialKey(); + const cacheRecord = new CacheRecord(); + cacheRecord.accessToken = at; + await mockCache.cacheManager.saveCacheRecord(cacheRecord, { + accessToken: false, + }); + const mockCacheAT = + mockCache.cacheManager.getAccessTokenCredential(atKey); + expect(mockCacheAT).toBe(null); }); - const atKey = at.generateCredentialKey(); - const cacheRecord = new CacheRecord(); - cacheRecord.accessToken = at; - await mockCache.cacheManager.saveCacheRecord(cacheRecord); - const mockCacheAT = mockCache.cacheManager.getAccessTokenCredential( - atKey - ) as AccessTokenEntity; - if (!mockCacheAT) { - throw TestError.createTestSetupError( - "mockCacheAT does not have a value" + it("save accessToken with Auth Scheme (pop)", async () => { + const at = new AccessTokenEntity(); + Object.assign(at, { + homeAccountId: "someUid.someUtid", + environment: "login.microsoftonline.com", + credentialType: "AccessToken_With_AuthScheme", + clientId: "mock_client_id", + secret: "an access token sample", + realm: "microsoft", + target: "scope6 scope7", + cachedAt: "1000", + expiresOn: "4600", + extendedExpiresOn: "4600", + keyId: "some_key", + tokenType: "pop", + }); + + const atKey = at.generateCredentialKey(); + const cacheRecord = new CacheRecord(); + cacheRecord.accessToken = at; + await mockCache.cacheManager.saveCacheRecord(cacheRecord); + const mockCacheAT = mockCache.cacheManager.getAccessTokenCredential( + atKey + ) as AccessTokenEntity; + if (!mockCacheAT) { + throw TestError.createTestSetupError( + "mockCacheAT does not have a value" + ); + } + expect(mockCacheAT.homeAccountId).toEqual("someUid.someUtid"); + expect(mockCacheAT.credentialType).toEqual( + CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME ); - } - expect(mockCacheAT.homeAccountId).toEqual("someUid.someUtid"); - expect(mockCacheAT.credentialType).toEqual(CredentialType.ACCESS_TOKEN); - expect(mockCacheAT.tokenType).toEqual(AuthenticationScheme.BEARER); - }); + expect(mockCacheAT.tokenType).toEqual(AuthenticationScheme.POP); + expect(mockCacheAT.keyId).toBeDefined(); + }); - it("save accessToken with Auth Scheme (pop)", async () => { - const at = new AccessTokenEntity(); - Object.assign(at, { - homeAccountId: "someUid.someUtid", - environment: "login.microsoftonline.com", - credentialType: "AccessToken_With_AuthScheme", - clientId: "mock_client_id", - secret: "an access token sample", - realm: "microsoft", - target: "scope6 scope7", - cachedAt: "1000", - expiresOn: "4600", - extendedExpiresOn: "4600", - keyId: "some_key", - tokenType: "pop", + it("does not save idToken if storeInCache.idToken = false", async () => { + const idToken = IdTokenEntity.createIdTokenEntity( + TEST_ACCOUNT_INFO.homeAccountId, + TEST_ACCOUNT_INFO.environment, + TEST_TOKENS.IDTOKEN_V2_NEWCLAIM, + TEST_CONFIG.MSAL_CLIENT_ID, + TEST_CONFIG.MSAL_TENANT_ID + ); + + const idTokenKey = idToken.generateCredentialKey(); + const cacheRecord = new CacheRecord(); + cacheRecord.idToken = idToken; + await mockCache.cacheManager.saveCacheRecord(cacheRecord, { + idToken: false, + }); + const mockCacheId = + mockCache.cacheManager.getIdTokenCredential(idTokenKey); + expect(mockCacheId).toBe(null); }); - const atKey = at.generateCredentialKey(); - const cacheRecord = new CacheRecord(); - cacheRecord.accessToken = at; - await mockCache.cacheManager.saveCacheRecord(cacheRecord); - const mockCacheAT = mockCache.cacheManager.getAccessTokenCredential( - atKey - ) as AccessTokenEntity; - if (!mockCacheAT) { - throw TestError.createTestSetupError( - "mockCacheAT does not have a value" + it("does not save refreshToken if storeInCache.refreshToken = false", async () => { + const refreshToken = RefreshTokenEntity.createRefreshTokenEntity( + TEST_ACCOUNT_INFO.homeAccountId, + TEST_ACCOUNT_INFO.environment, + TEST_TOKENS.REFRESH_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID ); - } - expect(mockCacheAT.homeAccountId).toEqual("someUid.someUtid"); - expect(mockCacheAT.credentialType).toEqual( - CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME - ); - expect(mockCacheAT.tokenType).toEqual(AuthenticationScheme.POP); - expect(mockCacheAT.keyId).toBeDefined(); + + const refreshTokenKey = refreshToken.generateCredentialKey(); + const cacheRecord = new CacheRecord(); + cacheRecord.refreshToken = refreshToken; + await mockCache.cacheManager.saveCacheRecord(cacheRecord, { + refreshToken: false, + }); + const mockCacheRT = + mockCache.cacheManager.getRefreshTokenCredential( + refreshTokenKey + ); + expect(mockCacheRT).toBe(null); + }); }); it("getAccounts (gets all AccountInfo objects)", async () => { diff --git a/lib/msal-node/src/request/AuthorizationCodeRequest.ts b/lib/msal-node/src/request/AuthorizationCodeRequest.ts index fafd300ef8..44f89f63a9 100644 --- a/lib/msal-node/src/request/AuthorizationCodeRequest.ts +++ b/lib/msal-node/src/request/AuthorizationCodeRequest.ts @@ -29,6 +29,7 @@ export type AuthorizationCodeRequest = Partial< | "resourceRequestMethod" | "resourceRequestUri" | "requestedClaimsHash" + | "storeInCache" > > & { scopes: Array; diff --git a/lib/msal-node/src/request/AuthorizationUrlRequest.ts b/lib/msal-node/src/request/AuthorizationUrlRequest.ts index 5d5f333fe5..e81ff22244 100644 --- a/lib/msal-node/src/request/AuthorizationUrlRequest.ts +++ b/lib/msal-node/src/request/AuthorizationUrlRequest.ts @@ -42,6 +42,7 @@ export type AuthorizationUrlRequest = Partial< | "resourceRequestUri" | "authenticationScheme" | "requestedClaimsHash" + | "storeInCache" > > & { scopes: Array; diff --git a/lib/msal-node/src/request/ClientCredentialRequest.ts b/lib/msal-node/src/request/ClientCredentialRequest.ts index dd19f696db..eabd045c8c 100644 --- a/lib/msal-node/src/request/ClientCredentialRequest.ts +++ b/lib/msal-node/src/request/ClientCredentialRequest.ts @@ -22,6 +22,7 @@ export type ClientCredentialRequest = Partial< | "resourceRequestUri" | "requestedClaimsHash" | "clientAssertion" + | "storeInCache" > > & { clientAssertion?: string; diff --git a/lib/msal-node/src/request/DeviceCodeRequest.ts b/lib/msal-node/src/request/DeviceCodeRequest.ts index a9442ff7fe..3b7fbde82d 100644 --- a/lib/msal-node/src/request/DeviceCodeRequest.ts +++ b/lib/msal-node/src/request/DeviceCodeRequest.ts @@ -26,6 +26,7 @@ export type DeviceCodeRequest = Partial< | "resourceRequestMethod" | "resourceRequestUri" | "requestedClaimsHash" + | "storeInCache" > > & { scopes: Array; diff --git a/lib/msal-node/src/request/OnBehalfOfRequest.ts b/lib/msal-node/src/request/OnBehalfOfRequest.ts index d65153a5c5..7baa15103e 100644 --- a/lib/msal-node/src/request/OnBehalfOfRequest.ts +++ b/lib/msal-node/src/request/OnBehalfOfRequest.ts @@ -22,6 +22,7 @@ export type OnBehalfOfRequest = Partial< | "resourceRequestMethod" | "resourceRequestUri" | "requestedClaimsHash" + | "storeInCache" > > & { oboAssertion: string; diff --git a/lib/msal-node/src/request/RefreshTokenRequest.ts b/lib/msal-node/src/request/RefreshTokenRequest.ts index 3c3b86aba4..cb43ee3f9e 100644 --- a/lib/msal-node/src/request/RefreshTokenRequest.ts +++ b/lib/msal-node/src/request/RefreshTokenRequest.ts @@ -25,6 +25,7 @@ export type RefreshTokenRequest = Partial< | "resourceRequestMethod" | "resourceRequestUri" | "requestedClaimsHash" + | "storeInCache" > > & { scopes: Array; diff --git a/lib/msal-node/src/request/SilentFlowRequest.ts b/lib/msal-node/src/request/SilentFlowRequest.ts index 6ac4423730..dc6b006209 100644 --- a/lib/msal-node/src/request/SilentFlowRequest.ts +++ b/lib/msal-node/src/request/SilentFlowRequest.ts @@ -24,6 +24,7 @@ export type SilentFlowRequest = Partial< | "resourceRequestMethod" | "resourceRequestUri" | "requestedClaimsHash" + | "storeInCache" > > & { account: AccountInfo; diff --git a/lib/msal-node/src/request/UsernamePasswordRequest.ts b/lib/msal-node/src/request/UsernamePasswordRequest.ts index 0ae0105051..cee0d5ed24 100644 --- a/lib/msal-node/src/request/UsernamePasswordRequest.ts +++ b/lib/msal-node/src/request/UsernamePasswordRequest.ts @@ -27,6 +27,7 @@ export type UsernamePasswordRequest = Partial< | "username" | "password" | "requestedClaimsHash" + | "storeInCache" > > & { scopes: Array;