Skip to content

Commit a6690f1

Browse files
tnorlingCopilot
andauthored
[v5] EAR Fallback (#8156)
This pull request enhances the EAR (Encrypted Authorization Request) authentication flow in `@azure/msal-browser` by enabling a fallback to the standard authorization code flow when the authorization server does not support EAR and returns an authorization code instead. It also improves PKCE (Proof Key for Code Exchange) handling and adds comprehensive tests to ensure correct fallback behavior. The changes ensure a more robust and interoperable authentication experience. **EAR Flow Improvements and Fallback Logic:** * Updated the EAR flow in `PopupClient`, `RedirectClient`, and `SilentIframeClient` to detect when the server returns an authorization code instead of an EAR token, and to automatically fall back to the standard authorization code flow in such cases. This includes passing PKCE parameters and handling the response appropriately. [[1]](diffhunk://#diff-3f43afd5556603a80064728bd701519ec2e22979f09ae6095b7fdea0507ad593R528-R570) [[2]](diffhunk://#diff-379febb046eaaa641bafb36c0a72f4c585eda5881b889dd8942919112539e5faR320-R363) [[3]](diffhunk://#diff-06ec3818a1cb128320c6ece84eed04190a54c03a09a455ca2d5c6947e29d5de1R306-R322) * Modified the EAR request generation to always include PKCE code challenge parameters as a backup, ensuring a seamless transition to auth code flow if EAR is not supported by the server. **PKCE and Authority Handling Enhancements:** * Refactored PKCE code generation to ensure PKCE codes are always available for both EAR and fallback flows. Also, updated the authority discovery logic to allow passing a pre-discovered authority, improving efficiency and flexibility. [[1]](diffhunk://#diff-3f43afd5556603a80064728bd701519ec2e22979f09ae6095b7fdea0507ad593R471-R483) [[2]](diffhunk://#diff-08cf22a8c9098053582d32ec25dad54e2ab9e5ea54242db2a55b261ee0e67349R203) [[3]](diffhunk://#diff-08cf22a8c9098053582d32ec25dad54e2ab9e5ea54242db2a55b261ee0e67349R236) [[4]](diffhunk://#diff-08cf22a8c9098053582d32ec25dad54e2ab9e5ea54242db2a55b261ee0e67349L243-R248) [[5]](diffhunk://#diff-08cf22a8c9098053582d32ec25dad54e2ab9e5ea54242db2a55b261ee0e67349L259-R264) **Testing and Validation:** * Added and updated unit tests for `PopupClient`, `RedirectClient`, and `SilentIframeClient` to verify that the EAR flow correctly falls back to the authorization code flow when the server returns a code. Also, updated protocol tests to check for the inclusion of PKCE parameters in EAR requests. [[1]](diffhunk://#diff-0e4f86d8a16dd8be09b5f3b33d82dafe4c4325e3fd92b994120b0290beb0c2d6R964-R997) [[2]](diffhunk://#diff-6892d11e0bf0f9de499d836141fe1b0e7fe40f2b7d1d28e57830a083ba0080a4R3257-R3287) [[3]](diffhunk://#diff-ee3bc256edbe0cc9b07264ac953b539bf0be0c3737eb24a91cf11b35e20086a3R1421-R1445) [[4]](diffhunk://#diff-383f979b9a05a2d7fe02052138e8087f6ca2167e78d44dc4d41c391463d4da04R71) [[5]](diffhunk://#diff-383f979b9a05a2d7fe02052138e8087f6ca2167e78d44dc4d41c391463d4da04R183-R190) **Release and Metadata:** * Updated the package change log to document the new fallback behavior for the EAR flow, indicating a patch release. --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: tnorling <5307810+tnorling@users.noreply.github.com>
1 parent 29b2daf commit a6690f1

File tree

10 files changed

+295
-58
lines changed

10 files changed

+295
-58
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": "EAR flow falls back to auth code when /authorize returns code [#8156](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8156)",
4+
"packageName": "@azure/msal-browser",
5+
"email": "thomas.norling@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

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

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export class PopupClient extends StandardInteractionClient {
276276
validRequest.platformBroker = isPlatformBroker;
277277

278278
if (this.config.system.protocolMode === ProtocolMode.EAR) {
279-
return this.executeEarFlow(validRequest, popupParams);
279+
return this.executeEarFlow(validRequest, popupParams, pkceCodes);
280280
} else {
281281
return this.executeCodeFlow(validRequest, popupParams, pkceCodes);
282282
}
@@ -432,7 +432,8 @@ export class PopupClient extends StandardInteractionClient {
432432
*/
433433
async executeEarFlow(
434434
request: CommonAuthorizationUrlRequest,
435-
popupParams: PopupParams
435+
popupParams: PopupParams,
436+
pkceCodes?: PkceCodes
436437
): Promise<AuthenticationResult> {
437438
const {
438439
correlationId,
@@ -467,9 +468,19 @@ export class PopupClient extends StandardInteractionClient {
467468
this.performanceClient,
468469
correlationId
469470
)();
471+
const pkce =
472+
pkceCodes ||
473+
(await invokeAsync(
474+
generatePkceCodes,
475+
BrowserPerformanceEvents.GeneratePkceCodes,
476+
this.logger,
477+
this.performanceClient,
478+
correlationId
479+
)(this.performanceClient, this.logger, correlationId));
470480
const popupRequest = {
471481
...request,
472482
earJwk: earJwk,
483+
codeChallenge: pkce.challenge,
473484
};
474485
const popupWindow =
475486
popupParams.popup || this.openPopup("about:blank", popupParams);
@@ -514,25 +525,69 @@ export class PopupClient extends StandardInteractionClient {
514525
this.correlationId
515526
);
516527

517-
return invokeAsync(
518-
Authorize.handleResponseEAR,
519-
BrowserPerformanceEvents.HandleResponseEar,
520-
this.logger,
521-
this.performanceClient,
522-
correlationId
523-
)(
524-
popupRequest,
525-
serverParams,
526-
ApiId.acquireTokenPopup,
527-
this.config,
528-
discoveredAuthority,
529-
this.browserStorage,
530-
this.nativeStorage,
531-
this.eventHandler,
532-
this.logger,
533-
this.performanceClient,
534-
this.platformAuthProvider
535-
);
528+
if (!serverParams.ear_jwe && serverParams.code) {
529+
const authClient = await invokeAsync(
530+
this.createAuthCodeClient.bind(this),
531+
BrowserPerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
532+
this.logger,
533+
this.performanceClient,
534+
correlationId
535+
)({
536+
serverTelemetryManager: initializeServerTelemetryManager(
537+
ApiId.acquireTokenPopup,
538+
this.config.auth.clientId,
539+
correlationId,
540+
this.browserStorage,
541+
this.logger
542+
),
543+
requestAuthority: request.authority,
544+
requestAzureCloudOptions: request.azureCloudOptions,
545+
requestExtraQueryParameters: request.extraQueryParameters,
546+
account: request.account,
547+
authority: discoveredAuthority,
548+
});
549+
550+
return invokeAsync(
551+
Authorize.handleResponseCode,
552+
BrowserPerformanceEvents.HandleResponseCode,
553+
this.logger,
554+
this.performanceClient,
555+
correlationId
556+
)(
557+
popupRequest,
558+
serverParams,
559+
pkce.verifier,
560+
ApiId.acquireTokenPopup,
561+
this.config,
562+
authClient,
563+
this.browserStorage,
564+
this.nativeStorage,
565+
this.eventHandler,
566+
this.logger,
567+
this.performanceClient,
568+
this.platformAuthProvider
569+
);
570+
} else {
571+
return invokeAsync(
572+
Authorize.handleResponseEAR,
573+
BrowserPerformanceEvents.HandleResponseEar,
574+
this.logger,
575+
this.performanceClient,
576+
correlationId
577+
)(
578+
popupRequest,
579+
serverParams,
580+
ApiId.acquireTokenPopup,
581+
this.config,
582+
discoveredAuthority,
583+
this.browserStorage,
584+
this.nativeStorage,
585+
this.eventHandler,
586+
this.logger,
587+
this.performanceClient,
588+
this.platformAuthProvider
589+
);
590+
}
536591
}
537592

538593
async executeCodeFlowWithPost(

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,13 +303,24 @@ export class RedirectClient extends StandardInteractionClient {
303303
this.performanceClient,
304304
correlationId
305305
)();
306+
const pkceCodes = await invokeAsync(
307+
generatePkceCodes,
308+
BrowserPerformanceEvents.GeneratePkceCodes,
309+
this.logger,
310+
this.performanceClient,
311+
correlationId
312+
)(this.performanceClient, this.logger, correlationId);
313+
306314
const redirectRequest = {
307315
...request,
308316
earJwk: earJwk,
317+
codeChallenge: pkceCodes.challenge,
309318
};
319+
310320
this.browserStorage.cacheAuthorizeRequest(
311321
redirectRequest,
312-
this.correlationId
322+
this.correlationId,
323+
pkceCodes.verifier
313324
);
314325

315326
const form = await Authorize.getEARForm(

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

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,17 @@ export class SilentIframeClient extends StandardInteractionClient {
265265
this.performanceClient,
266266
correlationId
267267
)();
268+
const pkceCodes = await invokeAsync(
269+
generatePkceCodes,
270+
BrowserPerformanceEvents.GeneratePkceCodes,
271+
this.logger,
272+
this.performanceClient,
273+
correlationId
274+
)(this.performanceClient, this.logger, correlationId);
268275
const silentRequest = {
269276
...request,
270277
earJwk: earJwk,
278+
codeChallenge: pkceCodes.challenge,
271279
};
272280
const msalFrame = await invokeAsync(
273281
initiateEarRequest,
@@ -309,25 +317,70 @@ export class SilentIframeClient extends StandardInteractionClient {
309317
correlationId
310318
)(responseString, responseType, this.logger, this.correlationId);
311319

312-
return invokeAsync(
313-
Authorize.handleResponseEAR,
314-
BrowserPerformanceEvents.HandleResponseEar,
315-
this.logger,
316-
this.performanceClient,
317-
correlationId
318-
)(
319-
silentRequest,
320-
serverParams,
321-
this.apiId,
322-
this.config,
323-
discoveredAuthority,
324-
this.browserStorage,
325-
this.nativeStorage,
326-
this.eventHandler,
327-
this.logger,
328-
this.performanceClient,
329-
this.platformAuthProvider
330-
);
320+
if (!serverParams.ear_jwe && serverParams.code) {
321+
// If server doesn't support EAR, they may fallback to auth code flow instead
322+
const authClient = await invokeAsync(
323+
this.createAuthCodeClient.bind(this),
324+
BrowserPerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
325+
this.logger,
326+
this.performanceClient,
327+
correlationId
328+
)({
329+
serverTelemetryManager: initializeServerTelemetryManager(
330+
this.apiId,
331+
this.config.auth.clientId,
332+
correlationId,
333+
this.browserStorage,
334+
this.logger
335+
),
336+
requestAuthority: request.authority,
337+
requestAzureCloudOptions: request.azureCloudOptions,
338+
requestExtraQueryParameters: request.extraQueryParameters,
339+
account: request.account,
340+
authority: discoveredAuthority,
341+
});
342+
343+
return invokeAsync(
344+
Authorize.handleResponseCode,
345+
BrowserPerformanceEvents.HandleResponseCode,
346+
this.logger,
347+
this.performanceClient,
348+
correlationId
349+
)(
350+
silentRequest,
351+
serverParams,
352+
pkceCodes.verifier,
353+
this.apiId,
354+
this.config,
355+
authClient,
356+
this.browserStorage,
357+
this.nativeStorage,
358+
this.eventHandler,
359+
this.logger,
360+
this.performanceClient,
361+
this.platformAuthProvider
362+
);
363+
} else {
364+
return invokeAsync(
365+
Authorize.handleResponseEAR,
366+
BrowserPerformanceEvents.HandleResponseEar,
367+
this.logger,
368+
this.performanceClient,
369+
correlationId
370+
)(
371+
silentRequest,
372+
serverParams,
373+
this.apiId,
374+
this.config,
375+
discoveredAuthority,
376+
this.browserStorage,
377+
this.nativeStorage,
378+
this.eventHandler,
379+
this.logger,
380+
this.performanceClient,
381+
this.platformAuthProvider
382+
);
383+
}
331384
}
332385

333386
/**

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

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
ICrypto,
2121
Logger,
2222
IPerformanceClient,
23+
Authority,
2324
} from "@azure/msal-common/browser";
2425
import {
2526
BaseInteractionClient,
@@ -199,6 +200,7 @@ export abstract class StandardInteractionClient extends BaseInteractionClient {
199200
requestAzureCloudOptions?: AzureCloudOptions;
200201
requestExtraQueryParameters?: StringDict;
201202
account?: AccountInfo;
203+
authority?: Authority;
202204
}): Promise<AuthorizationCodeClient> {
203205
// Create auth module.
204206
const clientConfig = await invokeAsync(
@@ -231,6 +233,7 @@ export abstract class StandardInteractionClient extends BaseInteractionClient {
231233
requestAzureCloudOptions?: AzureCloudOptions;
232234
requestExtraQueryParameters?: StringDict;
233235
account?: AccountInfo;
236+
authority?: Authority;
234237
}): Promise<ClientConfiguration> {
235238
const {
236239
serverTelemetryManager,
@@ -240,23 +243,25 @@ export abstract class StandardInteractionClient extends BaseInteractionClient {
240243
account,
241244
} = params;
242245

243-
const discoveredAuthority = await invokeAsync(
244-
getDiscoveredAuthority,
245-
BrowserPerformanceEvents.StandardInteractionClientGetDiscoveredAuthority,
246-
this.logger,
247-
this.performanceClient,
248-
this.correlationId
249-
)(
250-
this.config,
251-
this.correlationId,
252-
this.performanceClient,
253-
this.browserStorage,
254-
this.logger,
255-
requestAuthority,
256-
requestAzureCloudOptions,
257-
requestExtraQueryParameters,
258-
account
259-
);
246+
const discoveredAuthority =
247+
params.authority ||
248+
(await invokeAsync(
249+
getDiscoveredAuthority,
250+
BrowserPerformanceEvents.StandardInteractionClientGetDiscoveredAuthority,
251+
this.logger,
252+
this.performanceClient,
253+
this.correlationId
254+
)(
255+
this.config,
256+
this.correlationId,
257+
this.performanceClient,
258+
this.browserStorage,
259+
this.logger,
260+
requestAuthority,
261+
requestAzureCloudOptions,
262+
requestExtraQueryParameters,
263+
account
264+
));
260265
const logger = this.config.system.loggerOptions;
261266

262267
return {

lib/msal-browser/src/protocol/Authorize.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,13 @@ export async function getEARForm(
198198
);
199199
RequestParameterBuilder.addEARParameters(parameters, request.earJwk);
200200

201+
// Also add codeChallenge as backup in case EAR is not supported
202+
RequestParameterBuilder.addCodeChallengeParams(
203+
parameters,
204+
request.codeChallenge,
205+
Constants.S256_CODE_CHALLENGE_METHOD
206+
);
207+
201208
RequestParameterBuilder.addExtraParameters(parameters, {
202209
...request.extraParameters,
203210
});

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,40 @@ describe("PopupClient", () => {
961961
expect(earFormSpy).toHaveBeenCalled();
962962
});
963963

964+
it("EAR flow falls back to Auth Code if service returns code instead of ear_jwe", async () => {
965+
const validRequest: PopupRequest = {
966+
authority: TEST_CONFIG.validAuthority,
967+
scopes: ["openid", "profile", "offline_access"],
968+
correlationId: TEST_CONFIG.CORRELATION_ID,
969+
redirectUri: window.location.href,
970+
state: TEST_STATE_VALUES.USER_STATE,
971+
nonce: ID_TOKEN_CLAIMS.nonce,
972+
};
973+
jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue(
974+
TEST_STATE_VALUES.TEST_STATE_POPUP
975+
);
976+
jest.spyOn(
977+
PopupClient.prototype,
978+
"openSizedPopup"
979+
).mockReturnValue(popupWindow);
980+
const earFormSpy = jest
981+
.spyOn(HTMLFormElement.prototype, "submit")
982+
.mockImplementation(() => {
983+
// Suppress navigation
984+
});
985+
jest.spyOn(PopupUtils, "monitorPopupForHash").mockResolvedValue(
986+
`#code=validCode&state=${TEST_STATE_VALUES.TEST_STATE_POPUP}`
987+
);
988+
jest.spyOn(
989+
AuthorizeProtocol,
990+
"handleResponseCode"
991+
).mockResolvedValue(getTestAuthenticationResult());
992+
993+
const result = await pca.acquireTokenPopup(validRequest);
994+
expect(result).toEqual(getTestAuthenticationResult());
995+
expect(earFormSpy).toHaveBeenCalled();
996+
});
997+
964998
it("throws error when ProtocolMode is set to EAR and httpMethod is set to GET", async () => {
965999
const validRequest: PopupRequest = {
9661000
authority: TEST_CONFIG.validAuthority,

0 commit comments

Comments
 (0)