Skip to content

Fix native brokering use case for cache lookups #7601

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

Merged
merged 9 commits into from
Mar 4, 2025
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": "Fix double brokering bug: Native flow should not be triggered for cache only look up",
"packageName": "@azure/msal-browser",
"email": "[email protected]",
"dependentChangeType": "patch"
}
16 changes: 13 additions & 3 deletions lib/msal-browser/src/controllers/StandardController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1519,7 +1519,8 @@ export class StandardController implements IController {
public async acquireTokenNative(
request: PopupRequest | SilentRequest | SsoSilentRequest,
apiId: ApiId,
accountId?: string
accountId?: string,
cacheLookupPolicy?: CacheLookupPolicy
): Promise<AuthenticationResult> {
this.logger.trace("acquireTokenNative called");
if (!this.nativeExtensionProvider) {
Expand All @@ -1543,7 +1544,7 @@ export class StandardController implements IController {
request.correlationId
);

return nativeClient.acquireToken(request);
return nativeClient.acquireToken(request, cacheLookupPolicy);
}

/**
Expand Down Expand Up @@ -2223,6 +2224,7 @@ 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 (
NativeMessageHandler.isPlatformBrokerAvailable(
this.config,
Expand All @@ -2237,7 +2239,9 @@ export class StandardController implements IController {
);
return this.acquireTokenNative(
silentRequest,
ApiId.acquireTokenSilent_silentFlow
ApiId.acquireTokenSilent_silentFlow,
silentRequest.account.nativeAccountId,
cacheLookupPolicy
).catch(async (e: AuthError) => {
// If native token acquisition fails for availability reasons fallback to web flow
if (e instanceof NativeAuthError && isFatalNativeAuthError(e)) {
Expand All @@ -2257,6 +2261,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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
TemporaryCacheKeys,
NativeConstants,
BrowserConstants,
CacheLookupPolicy,
} from "../utils/BrowserConstants.js";
import {
NativeExtensionRequestBody,
Expand Down Expand Up @@ -157,7 +158,8 @@ export class NativeInteractionClient extends BaseInteractionClient {
* @param request
*/
async acquireToken(
request: PopupRequest | SilentRequest | SsoSilentRequest
request: PopupRequest | SilentRequest | SsoSilentRequest,
cacheLookupPolicy?: CacheLookupPolicy
): Promise<AuthenticationResult> {
this.performanceClient.addQueueMeasurement(
PerformanceEvents.NativeInteractionClientAcquireToken,
Expand Down Expand Up @@ -192,6 +194,12 @@ export class NativeInteractionClient extends BaseInteractionClient {
});
return result;
} catch (e) {
if (cacheLookupPolicy === CacheLookupPolicy.AccessToken) {
this.logger.info(
"MSAL internal Cache does not contain tokens, return error as per cache policy"
);
throw e;
}
// continue with a native call for any and all errors
this.logger.info(
"MSAL internal Cache does not contain tokens, proceed to make a native call"
Expand Down
93 changes: 89 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,52 @@ 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 calls 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 isPlatformBrokerAvailableSpy = jest
.spyOn(NativeMessageHandler, "isPlatformBrokerAvailable")
.mockReturnValue(true);
const nativeAcquireTokenSpy: jest.SpyInstance = jest
.spyOn(NativeInteractionClient.prototype, "acquireToken")
.mockImplementation();
const cacheAccount = testAccount;
cacheAccount.nativeAccountId = "nativeAccountId";

await expect(
pca.acquireTokenSilent({
scopes: ["openid"],
account: cacheAccount,
cacheLookupPolicy: CacheLookupPolicy.AccessToken,
})
)
.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);

nativeAcquireTokenSpy.mockRestore();
isPlatformBrokerAvailableSpy.mockRestore();
});

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 +5831,57 @@ 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 cacheAccount = testAccount;
cacheAccount.nativeAccountId = "nativeAccountId";
const isPlatformBrokerAvailableSpy = jest
.spyOn(NativeMessageHandler, "isPlatformBrokerAvailable")
.mockReturnValue(true);
testAccount.nativeAccountId = "nativeAccountId";

await expect(
pca.acquireTokenSilent({
scopes: ["openid"],
account: cacheAccount,
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);
nativeAcquireTokenSpy.mockRestore();
isPlatformBrokerAvailableSpy.mockRestore();
});

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