Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix native brokering use case for cache lookups #7601

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
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": "Fix double brokering bug: Native flow should not be triggered for cache only look up",
"packageName": "@azure/msal-browser",
"email": "[email protected]",
"dependentChangeType": "patch"
}
11 changes: 11 additions & 0 deletions lib/msal-browser/src/controllers/StandardController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2223,7 +2223,9 @@ export class StandardController implements IController {
silentRequest: CommonSilentFlowRequest,
cacheLookupPolicy: CacheLookupPolicy
): Promise<AuthenticationResult> {
// if the cache policy is set to access_token only, we should not be hitting the native layer yet
if (
cacheLookupPolicy !== CacheLookupPolicy.AccessToken &&
NativeMessageHandler.isPlatformBrokerAvailable(
this.config,
this.logger,
Expand Down Expand Up @@ -2257,6 +2259,12 @@ export class StandardController implements IController {
this.logger.verbose(
"acquireTokenSilent - attempting to acquire token from web flow"
);
// add logs to identify embedded cache retrieval
if (cacheLookupPolicy === CacheLookupPolicy.AccessToken) {
this.logger.verbose(
"acquireTokenSilent - cache lookup policy set to AccessToken, attempting to acquire token from local cache"
);
}
return invokeAsync(
this.acquireTokenFromCache.bind(this),
PerformanceEvents.AcquireTokenFromCache,
Expand All @@ -2266,6 +2274,9 @@ export class StandardController implements IController {
)(silentRequest, cacheLookupPolicy).catch(
(cacheError: AuthError) => {
if (cacheLookupPolicy === CacheLookupPolicy.AccessToken) {
this.logger.verbose(
"Failed to retrieve token from cache"
);
throw cacheError;
}

Expand Down
85 changes: 81 additions & 4 deletions lib/msal-browser/test/app/PublicClientApplication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ import {
Configuration,
} from "../../src/config/Configuration.js";
import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils";
import { nativeConnectionNotEstablished } from "../../src/error/BrowserAuthErrorCodes.js";

const cacheConfig = {
temporaryCacheLocation: BrowserCacheLocation.SessionStorage,
Expand Down Expand Up @@ -5773,10 +5774,46 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
expect(silentIframeSpy).toHaveBeenCalledTimes(0);
});

it("Calls SilentCacheClient.acquireToken and SilentRefreshClient.acquireToken, and does not call SilentIframeClient.acquireToken if cache lookup throws and refresh token is expired when CacheLookupPolicy is set to AccessTokenAndRefreshToken", async () => {
it("Calls SilentCacheClient.acquireToken, and does not call NativeInteractionClient.acquireToken when CacheLookupPolicy is set to AccessToken", async () => {
const silentCacheSpy: jest.SpyInstance = jest
.spyOn(SilentCacheClient.prototype, "acquireToken")
.mockRejectedValue(refreshRequiredCacheError);
const silentRefreshSpy = jest
.spyOn(SilentRefreshClient.prototype, "acquireToken")
.mockImplementation();
const silentIframeSpy = jest
.spyOn(SilentIframeClient.prototype, "acquireToken")
.mockImplementation();

const nativeRequestSpy = jest
.spyOn(NativeInteractionClient.prototype, "acquireToken")
.mockImplementation();
const isPlatformBrokerAvailableSpy = jest
.spyOn(NativeMessageHandler, "isPlatformBrokerAvailable")
.mockReturnValue(true);
const nativeAcquireTokenSpy: jest.SpyInstance = jest
.spyOn(NativeInteractionClient.prototype, "acquireToken")
.mockImplementation();
const account = testAccount;
account.nativeAccountId = "nativeAccountId";

await expect(
pca.acquireTokenSilent({
scopes: ["openid"],
account,
cacheLookupPolicy: CacheLookupPolicy.AccessToken,
})
).rejects.toThrow(refreshRequiredCacheError);
expect(silentCacheSpy).toHaveBeenCalledTimes(1);
expect(silentRefreshSpy).toHaveBeenCalledTimes(0);
expect(silentIframeSpy).toHaveBeenCalledTimes(0);
expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(0);
});

it("Calls SilentRefreshClient.acquireToken, and does not call SilentCacheClient.acquireToken or SilentIframeClient.acquireToken if refresh token is expired when CacheLookupPolicy is set to RefreshToken", async () => {
const silentCacheSpy = jest
.spyOn(SilentCacheClient.prototype, "acquireToken")
.mockImplementation();
const silentRefreshSpy: jest.SpyInstance = jest
.spyOn(SilentRefreshClient.prototype, "acquireToken")
.mockRejectedValue(refreshRequiredServerError);
Expand All @@ -5788,15 +5825,55 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
pca.acquireTokenSilent({
scopes: ["openid"],
account: testAccount,
cacheLookupPolicy:
CacheLookupPolicy.AccessTokenAndRefreshToken,
cacheLookupPolicy: CacheLookupPolicy.RefreshToken,
})
).rejects.toThrow(refreshRequiredServerError);
expect(silentCacheSpy).toHaveBeenCalledTimes(1);
expect(silentCacheSpy).toHaveBeenCalledTimes(0);
expect(silentRefreshSpy).toHaveBeenCalledTimes(1);
expect(silentIframeSpy).toHaveBeenCalledTimes(0);
});

it("Calls NativeInteractionClient.acquireToken when CacheLookupPolicy is set to AccessTokenAndRefreshToken", async () => {
const silentCacheSpy: jest.SpyInstance = jest
.spyOn(SilentCacheClient.prototype, "acquireToken")
.mockRejectedValue(refreshRequiredCacheError);
const silentRefreshSpy: jest.SpyInstance = jest
.spyOn(SilentRefreshClient.prototype, "acquireToken")
.mockRejectedValue(refreshRequiredServerError);
const silentIframeSpy = jest
.spyOn(SilentIframeClient.prototype, "acquireToken")
.mockImplementation();

const nativeAcquireTokenSpy: jest.SpyInstance = jest
.spyOn(NativeInteractionClient.prototype, "acquireToken")
.mockImplementation();
const account = testAccount;
account.nativeAccountId = "nativeAccountId";
const isPlatformBrokerAvailableSpy = jest
.spyOn(NativeMessageHandler, "isPlatformBrokerAvailable")
.mockReturnValue(true);
testAccount.nativeAccountId = "nativeAccountId";

await expect(
pca.acquireTokenSilent({
scopes: ["openid"],
account,
cacheLookupPolicy:
CacheLookupPolicy.AccessTokenAndRefreshToken,
})
)
.rejects.toThrow(BrowserAuthError)
.catch((error) => {
expect(error.errorCode).toBe(
BrowserAuthErrorCodes.nativeConnectionNotEstablished
);
});
expect(silentCacheSpy).toHaveBeenCalledTimes(0);
expect(silentRefreshSpy).toHaveBeenCalledTimes(0);
expect(silentIframeSpy).toHaveBeenCalledTimes(0);
expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(0);
});

it("Calls SilentRefreshClient.acquireToken, and does not call SilentCacheClient.acquireToken or SilentIframeClient.acquireToken if refresh token is expired when CacheLookupPolicy is set to RefreshToken", async () => {
const silentCacheSpy = jest
.spyOn(SilentCacheClient.prototype, "acquireToken")
Expand Down