Skip to content
Draft
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": "EAR flow falls back to auth code when /authorize returns code #8111",
"packageName": "@azure/msal-browser",
"email": "[email protected]",
"dependentChangeType": "patch"
}
94 changes: 73 additions & 21 deletions lib/msal-browser/src/interaction_client/PopupClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export class PopupClient extends StandardInteractionClient {
validRequest.platformBroker = isPlatformBroker;

if (this.config.auth.protocolMode === ProtocolMode.EAR) {
return this.executeEarFlow(validRequest, popupParams);
return this.executeEarFlow(validRequest, popupParams, pkceCodes);
} else {
return this.executeCodeFlow(validRequest, popupParams, pkceCodes);
}
Expand Down Expand Up @@ -389,7 +389,8 @@ export class PopupClient extends StandardInteractionClient {
*/
async executeEarFlow(
request: CommonAuthorizationUrlRequest,
popupParams: PopupParams
popupParams: PopupParams,
pkceCodes?: PkceCodes
): Promise<AuthenticationResult> {
const correlationId = request.correlationId;
// Get the frame handle for the silent request
Expand All @@ -413,9 +414,20 @@ export class PopupClient extends StandardInteractionClient {
this.performanceClient,
correlationId
)();
const pkce =
pkceCodes ||
(await invokeAsync(
generatePkceCodes,
PerformanceEvents.GeneratePkceCodes,
this.logger,
this.performanceClient,
correlationId
)(this.performanceClient, this.logger, correlationId));

const popupRequest = {
...request,
earJwk: earJwk,
codeChallenge: pkce.challenge,
};
const popupWindow =
popupParams.popup || this.openPopup("about:blank", popupParams);
Expand Down Expand Up @@ -451,25 +463,65 @@ export class PopupClient extends StandardInteractionClient {
this.logger
);

return invokeAsync(
Authorize.handleResponseEAR,
PerformanceEvents.HandleResponseEar,
this.logger,
this.performanceClient,
correlationId
)(
popupRequest,
serverParams,
ApiId.acquireTokenPopup,
this.config,
discoveredAuthority,
this.browserStorage,
this.nativeStorage,
this.eventHandler,
this.logger,
this.performanceClient,
this.platformAuthProvider
);
if (!serverParams.ear_jwe && serverParams.code) {
const authClient = await invokeAsync(
this.createAuthCodeClient.bind(this),
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
this.logger,
this.performanceClient,
correlationId
)({
serverTelemetryManager: this.initializeServerTelemetryManager(
ApiId.acquireTokenPopup
),
requestAuthority: request.authority,
requestAzureCloudOptions: request.azureCloudOptions,
requestExtraQueryParameters: request.extraQueryParameters,
account: request.account,
authority: discoveredAuthority,
});

return invokeAsync(
Authorize.handleResponseCode,
PerformanceEvents.HandleResponseCode,
this.logger,
this.performanceClient,
correlationId
)(
popupRequest,
serverParams,
pkce.verifier,
ApiId.acquireTokenPopup,
this.config,
authClient,
this.browserStorage,
this.nativeStorage,
this.eventHandler,
this.logger,
this.performanceClient,
this.platformAuthProvider
);
} else {
return invokeAsync(
Authorize.handleResponseEAR,
PerformanceEvents.HandleResponseEar,
this.logger,
this.performanceClient,
correlationId
)(
popupRequest,
serverParams,
ApiId.acquireTokenPopup,
this.config,
discoveredAuthority,
this.browserStorage,
this.nativeStorage,
this.eventHandler,
this.logger,
this.performanceClient,
this.platformAuthProvider
);
}
}

async executeCodeFlowWithPost(
Expand Down
15 changes: 14 additions & 1 deletion lib/msal-browser/src/interaction_client/RedirectClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,24 @@ export class RedirectClient extends StandardInteractionClient {
this.performanceClient,
correlationId
)();
const pkceCodes = await invokeAsync(
generatePkceCodes,
PerformanceEvents.GeneratePkceCodes,
this.logger,
this.performanceClient,
correlationId
)(this.performanceClient, this.logger, correlationId);

const redirectRequest = {
...request,
earJwk: earJwk,
codeChallenge: pkceCodes.challenge,
};
this.browserStorage.cacheAuthorizeRequest(redirectRequest);

this.browserStorage.cacheAuthorizeRequest(
redirectRequest,
pkceCodes.verifier
);

const form = await Authorize.getEARForm(
document,
Expand Down
87 changes: 68 additions & 19 deletions lib/msal-browser/src/interaction_client/SilentIframeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,17 @@ export class SilentIframeClient extends StandardInteractionClient {
this.performanceClient,
correlationId
)();
const pkceCodes = await invokeAsync(
generatePkceCodes,
PerformanceEvents.GeneratePkceCodes,
this.logger,
this.performanceClient,
correlationId
)(this.performanceClient, this.logger, correlationId);
const silentRequest = {
...request,
earJwk: earJwk,
codeChallenge: pkceCodes.challenge,
};
const msalFrame = await invokeAsync(
initiateEarRequest,
Expand Down Expand Up @@ -279,25 +287,66 @@ export class SilentIframeClient extends StandardInteractionClient {
correlationId
)(responseString, responseType, this.logger);

return invokeAsync(
Authorize.handleResponseEAR,
PerformanceEvents.HandleResponseEar,
this.logger,
this.performanceClient,
correlationId
)(
silentRequest,
serverParams,
this.apiId,
this.config,
discoveredAuthority,
this.browserStorage,
this.nativeStorage,
this.eventHandler,
this.logger,
this.performanceClient,
this.platformAuthProvider
);
if (!serverParams.ear_jwe && serverParams.code) {
// If server doesn't support EAR, they may fallback to auth code flow instead
const authClient = await invokeAsync(
this.createAuthCodeClient.bind(this),
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
this.logger,
this.performanceClient,
correlationId
)({
serverTelemetryManager: this.initializeServerTelemetryManager(
this.apiId
),
requestAuthority: request.authority,
requestAzureCloudOptions: request.azureCloudOptions,
requestExtraQueryParameters: request.extraQueryParameters,
account: request.account,
authority: discoveredAuthority,
});

return invokeAsync(
Authorize.handleResponseCode,
PerformanceEvents.HandleResponseCode,
this.logger,
this.performanceClient,
correlationId
)(
silentRequest,
serverParams,
pkceCodes.verifier,
this.apiId,
this.config,
authClient,
this.browserStorage,
this.nativeStorage,
this.eventHandler,
this.logger,
this.performanceClient,
this.platformAuthProvider
);
} else {
return invokeAsync(
Authorize.handleResponseEAR,
PerformanceEvents.HandleResponseEar,
this.logger,
this.performanceClient,
correlationId
)(
silentRequest,
serverParams,
this.apiId,
this.config,
discoveredAuthority,
this.browserStorage,
this.nativeStorage,
this.eventHandler,
this.logger,
this.performanceClient,
this.platformAuthProvider
);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
BaseAuthRequest,
StringDict,
CommonAuthorizationUrlRequest,
Authority,
} from "@azure/msal-common/browser";
import { BaseInteractionClient } from "./BaseInteractionClient.js";
import {
Expand Down Expand Up @@ -186,6 +187,7 @@ export abstract class StandardInteractionClient extends BaseInteractionClient {
requestAzureCloudOptions?: AzureCloudOptions;
requestExtraQueryParameters?: StringDict;
account?: AccountInfo;
authority?: Authority;
}): Promise<AuthorizationCodeClient> {
this.performanceClient.addQueueMeasurement(
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
Expand Down Expand Up @@ -222,6 +224,7 @@ export abstract class StandardInteractionClient extends BaseInteractionClient {
requestAzureCloudOptions?: AzureCloudOptions;
requestExtraQueryParameters?: StringDict;
account?: AccountInfo;
authority?: Authority;
}): Promise<ClientConfiguration> {
const {
serverTelemetryManager,
Expand All @@ -235,18 +238,20 @@ export abstract class StandardInteractionClient extends BaseInteractionClient {
PerformanceEvents.StandardInteractionClientGetClientConfiguration,
this.correlationId
);
const discoveredAuthority = await invokeAsync(
this.getDiscoveredAuthority.bind(this),
PerformanceEvents.StandardInteractionClientGetDiscoveredAuthority,
this.logger,
this.performanceClient,
this.correlationId
)({
requestAuthority,
requestAzureCloudOptions,
requestExtraQueryParameters,
account,
});
const discoveredAuthority =
params.authority ||
(await invokeAsync(
this.getDiscoveredAuthority.bind(this),
PerformanceEvents.StandardInteractionClientGetDiscoveredAuthority,
this.logger,
this.performanceClient,
this.correlationId
)({
requestAuthority,
requestAzureCloudOptions,
requestExtraQueryParameters,
account,
}));
const logger = this.config.system.loggerOptions;

return {
Expand Down
7 changes: 7 additions & 0 deletions lib/msal-browser/src/protocol/Authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,13 @@ export async function getEARForm(
);
RequestParameterBuilder.addEARParameters(parameters, request.earJwk);

// Also add codeChallenge as backup in case EAR is not supported
RequestParameterBuilder.addCodeChallengeParams(
parameters,
request.codeChallenge,
Constants.S256_CODE_CHALLENGE_METHOD
);

const queryParams = new Map<string, string>();
RequestParameterBuilder.addExtraQueryParameters(
queryParams,
Expand Down
37 changes: 37 additions & 0 deletions lib/msal-browser/test/interaction_client/PopupClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,43 @@ describe("PopupClient", () => {
expect(earFormSpy).toHaveBeenCalled();
});

it("EAR flow falls back to Auth Code if service returns code instead of ear_jwe", async () => {
const validRequest: PopupRequest = {
authority: TEST_CONFIG.validAuthority,
scopes: ["openid", "profile", "offline_access"],
correlationId: TEST_CONFIG.CORRELATION_ID,
redirectUri: window.location.href,
state: TEST_STATE_VALUES.USER_STATE,
nonce: ID_TOKEN_CLAIMS.nonce,
};
jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue(
TEST_STATE_VALUES.TEST_STATE_POPUP
);
jest.spyOn(
PopupClient.prototype,
"openSizedPopup"
).mockReturnValue(popupWindow);
const earFormSpy = jest
.spyOn(HTMLFormElement.prototype, "submit")
.mockImplementation(() => {
// Suppress navigation
});
jest.spyOn(
PopupClient.prototype,
"monitorPopupForHash"
).mockResolvedValue(
`#code=validCode&state=${TEST_STATE_VALUES.TEST_STATE_POPUP}`
);
jest.spyOn(
AuthorizeProtocol,
"handleResponseCode"
).mockResolvedValue(getTestAuthenticationResult());

const result = await pca.acquireTokenPopup(validRequest);
expect(result).toEqual(getTestAuthenticationResult());
expect(earFormSpy).toHaveBeenCalled();
});

it("throws error when ProtocolMode is set to EAR and httpMethod is set to GET", async () => {
const validRequest: PopupRequest = {
authority: TEST_CONFIG.validAuthority,
Expand Down
Loading