Skip to content

Commit e300043

Browse files
tnorlingsameerag
andauthored
Fix ATS request deduplication for pairwise broker requests (#7612)
acquireTokenSilent deduplicates identical requests fired off in parallel. This logic has 2 bugs that may result in broker_timeout errors in PWB flows. 1. Request thumbprint used for deduplication does not take embeddedClientId into account, resulting in 2 requests originating from different child apps being incorrectly deduped. 2. CorrelationId is shared for deduped requests, preventing embedded apps from resolving the promise upon receiving the response from the broker. This PR addresses both of these issues and enables telemetry collection for deduped requests (will need to filter these out in our dashboards/queries) --------- Co-authored-by: Sameera Gajjarapu <[email protected]>
1 parent 20674a1 commit e300043

20 files changed

+290
-146
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Fix acquireTokenSilent deduplication for pairwise broker requests",
4+
"packageName": "@azure/msal-browser",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Add embeddedClientId to RequestThumbprint",
4+
"packageName": "@azure/msal-common",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-browser/apiReview/msal-browser.api.md

+1-6
Original file line numberDiff line numberDiff line change
@@ -1341,7 +1341,6 @@ export type PopupPosition = {
13411341
export type PopupRequest = Partial<Omit<CommonAuthorizationUrlRequest, "responseMode" | "scopes" | "codeChallenge" | "codeChallengeMethod" | "requestedClaimsHash" | "platformBroker">> & {
13421342
scopes: Array<string>;
13431343
popupWindowAttributes?: PopupWindowAttributes;
1344-
tokenBodyParameters?: StringDict;
13451344
popupWindowParent?: Window;
13461345
};
13471346

@@ -1600,7 +1599,6 @@ export type RedirectRequest = Partial<Omit<CommonAuthorizationUrlRequest, "respo
16001599
scopes: Array<string>;
16011600
redirectStartPage?: string;
16021601
onRedirectNavigate?: (url: string) => boolean | void;
1603-
tokenBodyParameters?: StringDict;
16041602
};
16051603

16061604
// Warning: (ae-missing-release-tag) "replaceHash" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -1679,7 +1677,6 @@ export type SilentRequest = Omit<CommonSilentFlowRequest, "authority" | "correla
16791677
cacheLookupPolicy?: CacheLookupPolicy;
16801678
prompt?: string;
16811679
state?: string;
1682-
tokenBodyParameters?: StringDict;
16831680
};
16841681

16851682
// Warning: (ae-missing-release-tag) "spaCodeAndNativeAccountIdPresent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -1690,9 +1687,7 @@ const spaCodeAndNativeAccountIdPresent = "spa_code_and_nativeAccountId_present";
16901687
// Warning: (ae-missing-release-tag) "SsoSilentRequest" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
16911688
//
16921689
// @public
1693-
export type SsoSilentRequest = Partial<Omit<CommonAuthorizationUrlRequest, "responseMode" | "codeChallenge" | "codeChallengeMethod" | "requestedClaimsHash" | "platformBroker">> & {
1694-
tokenBodyParameters?: StringDict;
1695-
};
1690+
export type SsoSilentRequest = Partial<Omit<CommonAuthorizationUrlRequest, "responseMode" | "codeChallenge" | "codeChallengeMethod" | "requestedClaimsHash" | "platformBroker">>;
16961691

16971692
// Warning: (ae-missing-release-tag) "stateInteractionTypeMismatch" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
16981693
//

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

+66-51
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
BaseAuthRequest,
2121
PromptValue,
2222
InProgressPerformanceEvent,
23-
RequestThumbprint,
23+
getRequestThumbprint,
2424
AccountEntity,
2525
invokeAsync,
2626
createClientAuthError,
@@ -1960,30 +1960,71 @@ export class StandardController implements IController {
19601960
}
19611961
atsMeasurement.add({ accountType: getAccountType(account) });
19621962

1963-
const thumbprint: RequestThumbprint = {
1964-
clientId: this.config.auth.clientId,
1965-
authority: request.authority || Constants.EMPTY_STRING,
1966-
scopes: request.scopes,
1967-
homeAccountIdentifier: account.homeAccountId,
1968-
claims: request.claims,
1969-
authenticationScheme: request.authenticationScheme,
1970-
resourceRequestMethod: request.resourceRequestMethod,
1971-
resourceRequestUri: request.resourceRequestUri,
1972-
shrClaims: request.shrClaims,
1973-
sshKid: request.sshKid,
1974-
shrOptions: request.shrOptions,
1975-
};
1963+
return this.acquireTokenSilentDeduped(request, account, correlationId)
1964+
.then((result) => {
1965+
atsMeasurement.end({
1966+
success: true,
1967+
fromCache: result.fromCache,
1968+
isNativeBroker: result.fromNativeBroker,
1969+
accessTokenSize: result.accessToken.length,
1970+
idTokenSize: result.idToken.length,
1971+
});
1972+
return {
1973+
...result,
1974+
state: request.state,
1975+
correlationId: correlationId, // Ensures PWB scenarios can correctly match request to response
1976+
};
1977+
})
1978+
.catch((error: Error) => {
1979+
if (error instanceof AuthError) {
1980+
// Ensures PWB scenarios can correctly match request to response
1981+
error.setCorrelationId(correlationId);
1982+
}
1983+
1984+
atsMeasurement.end(
1985+
{
1986+
success: false,
1987+
},
1988+
error
1989+
);
1990+
throw error;
1991+
});
1992+
}
1993+
1994+
/**
1995+
* Checks if identical request is already in flight and returns reference to the existing promise or fires off a new one if this is the first
1996+
* @param request
1997+
* @param account
1998+
* @param correlationId
1999+
* @returns
2000+
*/
2001+
private async acquireTokenSilentDeduped(
2002+
request: SilentRequest,
2003+
account: AccountInfo,
2004+
correlationId: string
2005+
): Promise<AuthenticationResult> {
2006+
const thumbprint = getRequestThumbprint(
2007+
this.config.auth.clientId,
2008+
{
2009+
...request,
2010+
authority: request.authority || this.config.auth.authority,
2011+
correlationId: correlationId,
2012+
},
2013+
account.homeAccountId
2014+
);
19762015
const silentRequestKey = JSON.stringify(thumbprint);
19772016

1978-
const cachedResponse =
2017+
const inProgressRequest =
19792018
this.activeSilentTokenRequests.get(silentRequestKey);
1980-
if (typeof cachedResponse === "undefined") {
2019+
2020+
if (typeof inProgressRequest === "undefined") {
19812021
this.logger.verbose(
19822022
"acquireTokenSilent called for the first time, storing active request",
19832023
correlationId
19842024
);
2025+
this.performanceClient.addFields({ deduped: false }, correlationId);
19852026

1986-
const response = invokeAsync(
2027+
const activeRequest = invokeAsync(
19872028
this.acquireTokenSilentAsync.bind(this),
19882029
PerformanceEvents.AcquireTokenSilentAsync,
19892030
this.logger,
@@ -1995,45 +2036,19 @@ export class StandardController implements IController {
19952036
correlationId,
19962037
},
19972038
account
1998-
)
1999-
.then((result) => {
2000-
this.activeSilentTokenRequests.delete(silentRequestKey);
2001-
atsMeasurement.end({
2002-
success: true,
2003-
fromCache: result.fromCache,
2004-
isNativeBroker: result.fromNativeBroker,
2005-
cacheLookupPolicy: request.cacheLookupPolicy,
2006-
accessTokenSize: result.accessToken.length,
2007-
idTokenSize: result.idToken.length,
2008-
});
2009-
return result;
2010-
})
2011-
.catch((error: Error) => {
2012-
this.activeSilentTokenRequests.delete(silentRequestKey);
2013-
atsMeasurement.end(
2014-
{
2015-
success: false,
2016-
},
2017-
error
2018-
);
2019-
throw error;
2020-
});
2021-
this.activeSilentTokenRequests.set(silentRequestKey, response);
2022-
return {
2023-
...(await response),
2024-
state: request.state,
2025-
};
2039+
);
2040+
this.activeSilentTokenRequests.set(silentRequestKey, activeRequest);
2041+
2042+
return activeRequest.finally(() => {
2043+
this.activeSilentTokenRequests.delete(silentRequestKey);
2044+
});
20262045
} else {
20272046
this.logger.verbose(
20282047
"acquireTokenSilent has been called previously, returning the result from the first call",
20292048
correlationId
20302049
);
2031-
// Discard measurements for memoized calls, as they are usually only a couple of ms and will artificially deflate metrics
2032-
atsMeasurement.discard();
2033-
return {
2034-
...(await cachedResponse),
2035-
state: request.state,
2036-
};
2050+
this.performanceClient.addFields({ deduped: true }, correlationId);
2051+
return inProgressRequest;
20372052
}
20382053
}
20392054

lib/msal-browser/src/request/PopupRequest.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import {
7-
CommonAuthorizationUrlRequest,
8-
StringDict,
9-
} from "@azure/msal-common/browser";
6+
import { CommonAuthorizationUrlRequest } from "@azure/msal-common/browser";
107
import { PopupWindowAttributes } from "./PopupWindowAttributes.js";
118

129
/**
@@ -51,6 +48,5 @@ export type PopupRequest = Partial<
5148
> & {
5249
scopes: Array<string>;
5350
popupWindowAttributes?: PopupWindowAttributes;
54-
tokenBodyParameters?: StringDict;
5551
popupWindowParent?: Window;
5652
};

lib/msal-browser/src/request/RedirectRequest.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import {
7-
CommonAuthorizationUrlRequest,
8-
StringDict,
9-
} from "@azure/msal-common/browser";
6+
import { CommonAuthorizationUrlRequest } from "@azure/msal-common/browser";
107

118
/**
129
* RedirectRequest: Request object passed by user to retrieve a Code from the
@@ -55,5 +52,4 @@ export type RedirectRequest = Partial<
5552
* Set onRedirectNavigate in Configuration instead.
5653
*/
5754
onRedirectNavigate?: (url: string) => boolean | void;
58-
tokenBodyParameters?: StringDict;
5955
};

lib/msal-browser/src/request/SilentRequest.ts

-1
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,4 @@ export type SilentRequest = Omit<
4646
cacheLookupPolicy?: CacheLookupPolicy;
4747
prompt?: string;
4848
state?: string;
49-
tokenBodyParameters?: StringDict;
5049
};

lib/msal-browser/src/request/SsoSilentRequest.ts

+2-7
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import {
7-
CommonAuthorizationUrlRequest,
8-
StringDict,
9-
} from "@azure/msal-common/browser";
6+
import { CommonAuthorizationUrlRequest } from "@azure/msal-common/browser";
107

118
/**
129
* Request object passed by user to ssoSilent to retrieve a Code from the server (first leg of authorization code grant flow)
@@ -42,6 +39,4 @@ export type SsoSilentRequest = Partial<
4239
| "requestedClaimsHash"
4340
| "platformBroker"
4441
>
45-
> & {
46-
tokenBodyParameters?: StringDict;
47-
};
42+
>;

0 commit comments

Comments
 (0)