Skip to content

Commit fbb3fbf

Browse files
authored
Add support for authorize call using method POST (#7920)
This PR: - Adds the `httpMethod` and `authorizePostBodyParameters` options to `BaseAuthRequest` - Enables calls to the `/authorize` endpoint using HTTP method "POST" using the `Redirect`, `Popup`, and `SilentIFrame` flows - Ensures `extraQueryParameters` are still encoded into the request URL in `POST` flow - Ensures `httpMethod` cannot be set to 'GET' when using the EAR protocol mode (throws when the request is validated) - Ensures request validation to make sure the combinations of `httpMethod` and `authorizePostBodyParameters` as well as `httpMethod` and protocol mode happens before synchronous popup is opened.
1 parent df420da commit fbb3fbf

24 files changed

+1173
-155
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Add support for authorize call using method POST #7920",
4+
"packageName": "@azure/msal-browser",
5+
"email": "hemoral@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Add support for authorize call using method POST #7920",
4+
"packageName": "@azure/msal-common",
5+
"email": "hemoral@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

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

Lines changed: 161 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
invoke,
2020
PkceCodes,
2121
CommonAuthorizationUrlRequest,
22+
HttpMethod,
2223
} from "@azure/msal-common/browser";
2324
import { StandardInteractionClient } from "./StandardInteractionClient.js";
2425
import { EventType } from "../event/EventType.js";
@@ -48,6 +49,7 @@ import { generatePkceCodes } from "../crypto/PkceGenerator.js";
4849
import { isPlatformAuthAllowed } from "../broker/nativeBroker/PlatformAuthProvider.js";
4950
import { generateEarKey } from "../crypto/BrowserCrypto.js";
5051
import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js";
52+
import { validateRequestMethod } from "../request/RequestHelpers.js";
5153

5254
export type PopupParams = {
5355
popup?: Window | null;
@@ -98,12 +100,13 @@ export class PopupClient extends StandardInteractionClient {
98100
request: PopupRequest,
99101
pkceCodes?: PkceCodes
100102
): Promise<AuthenticationResult> {
103+
let popupParams: PopupParams | undefined = undefined;
101104
try {
102105
const popupName = this.generatePopupName(
103106
request.scopes || OIDC_DEFAULT_SCOPES,
104107
request.authority || this.config.auth.authority
105108
);
106-
const popupParams: PopupParams = {
109+
popupParams = {
107110
popupName,
108111
popupWindowAttributes: request.popupWindowAttributes || {},
109112
popupWindowParent: request.popupWindowParent ?? window,
@@ -124,6 +127,14 @@ export class PopupClient extends StandardInteractionClient {
124127
pkceCodes
125128
);
126129
} else {
130+
// Pre-validate request method to avoid opening popup if the request is invalid
131+
const validatedRequest: PopupRequest = {
132+
...request,
133+
httpMethod: validateRequestMethod(
134+
request,
135+
this.config.auth.protocolMode
136+
),
137+
};
127138
// asyncPopups flag is set to false. Opens popup before acquiring token.
128139
this.logger.verbose(
129140
"asyncPopup set to false, opening popup before acquiring token"
@@ -133,7 +144,7 @@ export class PopupClient extends StandardInteractionClient {
133144
popupParams
134145
);
135146
return this.acquireTokenPopupAsync(
136-
request,
147+
validatedRequest,
137148
popupParams,
138149
pkceCodes
139150
);
@@ -286,71 +297,80 @@ export class PopupClient extends StandardInteractionClient {
286297
account: popupRequest.account,
287298
});
288299

289-
// Create acquire token url.
290-
const navigateUrl = await invokeAsync(
291-
Authorize.getAuthCodeRequestUrl,
292-
PerformanceEvents.GetAuthCodeUrl,
293-
this.logger,
294-
this.performanceClient,
295-
correlationId
296-
)(
297-
this.config,
298-
authClient.authority,
299-
popupRequest,
300-
this.logger,
301-
this.performanceClient
302-
);
300+
if (popupRequest.httpMethod === HttpMethod.POST) {
301+
return await this.executeCodeFlowWithPost(
302+
popupRequest,
303+
popupParams,
304+
authClient,
305+
pkce.verifier
306+
);
307+
} else {
308+
// Create acquire token url.
309+
const navigateUrl = await invokeAsync(
310+
Authorize.getAuthCodeRequestUrl,
311+
PerformanceEvents.GetAuthCodeUrl,
312+
this.logger,
313+
this.performanceClient,
314+
correlationId
315+
)(
316+
this.config,
317+
authClient.authority,
318+
popupRequest,
319+
this.logger,
320+
this.performanceClient
321+
);
303322

304-
// Show the UI once the url has been created. Get the window handle for the popup.
305-
const popupWindow: Window = this.initiateAuthRequest(
306-
navigateUrl,
307-
popupParams
308-
);
309-
this.eventHandler.emitEvent(
310-
EventType.POPUP_OPENED,
311-
InteractionType.Popup,
312-
{ popupWindow },
313-
null
314-
);
323+
// Show the UI once the url has been created. Get the window handle for the popup.
324+
const popupWindow: Window = this.initiateAuthRequest(
325+
navigateUrl,
326+
popupParams
327+
);
328+
this.eventHandler.emitEvent(
329+
EventType.POPUP_OPENED,
330+
InteractionType.Popup,
331+
{ popupWindow },
332+
null
333+
);
315334

316-
// Monitor the window for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds.
317-
const responseString = await this.monitorPopupForHash(
318-
popupWindow,
319-
popupParams.popupWindowParent
320-
);
335+
// Monitor the window for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds.
336+
const responseString = await this.monitorPopupForHash(
337+
popupWindow,
338+
popupParams.popupWindowParent
339+
);
321340

322-
const serverParams = invoke(
323-
ResponseHandler.deserializeResponse,
324-
PerformanceEvents.DeserializeResponse,
325-
this.logger,
326-
this.performanceClient,
327-
this.correlationId
328-
)(
329-
responseString,
330-
this.config.auth.OIDCOptions.serverResponseType,
331-
this.logger
332-
);
341+
const serverParams = invoke(
342+
ResponseHandler.deserializeResponse,
343+
PerformanceEvents.DeserializeResponse,
344+
this.logger,
345+
this.performanceClient,
346+
this.correlationId
347+
)(
348+
responseString,
349+
this.config.auth.OIDCOptions.serverResponseType,
350+
this.logger
351+
);
333352

334-
return await invokeAsync(
335-
Authorize.handleResponseCode,
336-
PerformanceEvents.HandleResponseCode,
337-
this.logger,
338-
this.performanceClient,
339-
correlationId
340-
)(
341-
request,
342-
serverParams,
343-
pkce.verifier,
344-
ApiId.acquireTokenPopup,
345-
this.config,
346-
authClient,
347-
this.browserStorage,
348-
this.nativeStorage,
349-
this.eventHandler,
350-
this.logger,
351-
this.performanceClient,
352-
this.platformAuthProvider
353-
);
353+
return await invokeAsync(
354+
Authorize.handleResponseCode,
355+
PerformanceEvents.HandleResponseCode,
356+
this.logger,
357+
this.performanceClient,
358+
correlationId
359+
)(
360+
request,
361+
serverParams,
362+
pkce.verifier,
363+
ApiId.acquireTokenPopup,
364+
this.config,
365+
authClient,
366+
this.browserStorage,
367+
this.nativeStorage,
368+
this.eventHandler,
369+
this.logger,
370+
this.performanceClient,
371+
this.platformAuthProvider
372+
);
373+
}
354374
} catch (e) {
355375
// Close the synchronous popup if an error is thrown before the window unload event is registered
356376
popupParams.popup?.close();
@@ -452,6 +472,84 @@ export class PopupClient extends StandardInteractionClient {
452472
);
453473
}
454474

475+
async executeCodeFlowWithPost(
476+
request: CommonAuthorizationUrlRequest,
477+
popupParams: PopupParams,
478+
authClient: AuthorizationCodeClient,
479+
pkceVerifier: string
480+
): Promise<AuthenticationResult> {
481+
const correlationId = request.correlationId;
482+
// Get the frame handle for the silent request
483+
const discoveredAuthority = await invokeAsync(
484+
this.getDiscoveredAuthority.bind(this),
485+
PerformanceEvents.StandardInteractionClientGetDiscoveredAuthority,
486+
this.logger,
487+
this.performanceClient,
488+
correlationId
489+
)({
490+
requestAuthority: request.authority,
491+
requestAzureCloudOptions: request.azureCloudOptions,
492+
requestExtraQueryParameters: request.extraQueryParameters,
493+
account: request.account,
494+
});
495+
496+
const popupWindow =
497+
popupParams.popup || this.openPopup("about:blank", popupParams);
498+
499+
const form = await Authorize.getCodeForm(
500+
popupWindow.document,
501+
this.config,
502+
discoveredAuthority,
503+
request,
504+
this.logger,
505+
this.performanceClient
506+
);
507+
508+
form.submit();
509+
510+
// Monitor the popup for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds.
511+
const responseString = await invokeAsync(
512+
this.monitorPopupForHash.bind(this),
513+
PerformanceEvents.SilentHandlerMonitorIframeForHash,
514+
this.logger,
515+
this.performanceClient,
516+
correlationId
517+
)(popupWindow, popupParams.popupWindowParent);
518+
519+
const serverParams = invoke(
520+
ResponseHandler.deserializeResponse,
521+
PerformanceEvents.DeserializeResponse,
522+
this.logger,
523+
this.performanceClient,
524+
this.correlationId
525+
)(
526+
responseString,
527+
this.config.auth.OIDCOptions.serverResponseType,
528+
this.logger
529+
);
530+
531+
return invokeAsync(
532+
Authorize.handleResponseCode,
533+
PerformanceEvents.HandleResponseCode,
534+
this.logger,
535+
this.performanceClient,
536+
correlationId
537+
)(
538+
request,
539+
serverParams,
540+
pkceVerifier,
541+
ApiId.acquireTokenPopup,
542+
this.config,
543+
authClient,
544+
this.browserStorage,
545+
this.nativeStorage,
546+
this.eventHandler,
547+
this.logger,
548+
this.performanceClient,
549+
this.platformAuthProvider
550+
);
551+
}
552+
455553
/**
456554
*
457555
* @param validRequest

0 commit comments

Comments
 (0)