diff --git a/change/@azure-msal-browser-4609ddb5-5e7c-4f8d-ac16-4ae33329fcaf.json b/change/@azure-msal-browser-4609ddb5-5e7c-4f8d-ac16-4ae33329fcaf.json new file mode 100644 index 0000000000..e7c07df2e8 --- /dev/null +++ b/change/@azure-msal-browser-4609ddb5-5e7c-4f8d-ac16-4ae33329fcaf.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add support for authorize call using method POST #7997", + "packageName": "@azure/msal-browser", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-common-fceb1ded-fc1f-498a-ba0a-c2b18d4b3e12.json b/change/@azure-msal-common-fceb1ded-fc1f-498a-ba0a-c2b18d4b3e12.json new file mode 100644 index 0000000000..ac77a38768 --- /dev/null +++ b/change/@azure-msal-common-fceb1ded-fc1f-498a-ba0a-c2b18d4b3e12.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add support for authorize call using method POST#7997", + "packageName": "@azure/msal-common", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-node-af4e8122-18b1-4997-9d45-4c2aba0eb9f5.json b/change/@azure-msal-node-af4e8122-18b1-4997-9d45-4c2aba0eb9f5.json new file mode 100644 index 0000000000..3c786a76e3 --- /dev/null +++ b/change/@azure-msal-node-af4e8122-18b1-4997-9d45-4c2aba0eb9f5.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "Update tests to account for changes in extra param configuration", + "packageName": "@azure/msal-node", + "email": "hemoral@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/docs/errors.md b/docs/errors.md index a03d28f171..0923b0e1b0 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -262,6 +262,9 @@ This error occurs when MSAL.js surpasses the allotted storage limit when attempt - Authority mismatch error. Authority provided in login request or PublicClientApplication config does not match the environment of the provided account. Please use a matching account or make an interactive request to login to this authority. +### `invalid_request_method_for_EAR` +- The EAR protocol cannot be used with HTTP method `GET`. The `httpMethod` parameter in all requests using `protocolMode: ProtocolMode.EAR` must be either unset or `"POST"`/`HttpMethod.POST`. + ## Interaction required errors ### `no_tokens_found` diff --git a/lib/msal-browser/apiReview/msal-browser.api.md b/lib/msal-browser/apiReview/msal-browser.api.md index de67b9c7c7..c400759d76 100644 --- a/lib/msal-browser/apiReview/msal-browser.api.md +++ b/lib/msal-browser/apiReview/msal-browser.api.md @@ -551,7 +551,7 @@ const emptyWindowError = "empty_window_error"; // Warning: (ae-missing-release-tag) "EndSessionPopupRequest" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type EndSessionPopupRequest = Partial> & { +export type EndSessionPopupRequest = Partial & { authority?: string; mainWindowRedirectUri?: string; popupWindowAttributes?: PopupWindowAttributes; @@ -561,7 +561,7 @@ export type EndSessionPopupRequest = Partial> & { +export type EndSessionRequest = Partial & { authority?: string; }; diff --git a/lib/msal-browser/docs/v4-migration.md b/lib/msal-browser/docs/v4-migration.md index 72ce21cf01..65771a5434 100644 --- a/lib/msal-browser/docs/v4-migration.md +++ b/lib/msal-browser/docs/v4-migration.md @@ -24,29 +24,31 @@ if (result) { // AFTER const shr = new SignedHttpRequest(shrParameters, shrOptions); -await shr.removeKeys(thumbprint).then(() => { - // do something on success -}).catch(e => { - // do something on failure - console.log(e); -}); +await shr + .removeKeys(thumbprint) + .then(() => { + // do something on success + }) + .catch((e) => { + // do something on failure + console.log(e); + }); ``` ### TokenCache and loadExternalTokens MSAL JS API for [loadExternalTokens](../testing.md#the-loadexternaltokens-api) is modified. The changes include: -* `TokenCache` object and `getTokenCache()` have been removed -* The `loadExternalTokens()` API is now a separate export and requires `Configuration` as a parameter + +- `TokenCache` object and `getTokenCache()` have been removed +- The `loadExternalTokens()` API is now a separate export and requires `Configuration` as a parameter ```js // BEFORE const pca = new PublicClientApplication(config); -await pca.getTokenCache().loadExternalTokens( - silentRequest, - serverResponse, - loadTokenOptions -); +await pca + .getTokenCache() + .loadExternalTokens(silentRequest, serverResponse, loadTokenOptions); //AFTER @@ -65,19 +67,19 @@ Previously, `PublicClientApplication.handleRedirectPromise` took in an optional ```javascript // BEFORE const hash = window.location.hash; // Arbitrary example value -pca.handleRedirectPromise(hash) - +pca.handleRedirectPromise(hash); // AFTER pca.handleRedirectPromise({ hash: window.location.hash, // Option nested inside a `HandleRedirectPromiseOptions` object - navigateToLoginRequestUrl: true // Additional option -}) + navigateToLoginRequestUrl: true, // Additional option +}); ``` -### Removal of some functions in `PublicClientApplication` +### Removal of some functions in `PublicClientApplication` The following functions in `PublicClientApplication` have been removed: + 1. `enableAccountStorageEvents()` and `disableAccountStorageEvents()`: account storage events are now always enabled. These function calls are no longer necessary. 1. `getAccountByHomeId()`, `getAccountByLocalId()`, and `getAccountByUsername()`: use `getAccount()` instead. @@ -88,10 +90,15 @@ The following functions in `PublicClientApplication` have been removed: const account3 = accountManager.getAccountByUsername(yourUsername); // AFTER - const account1 = accountManager.getAccount({ homeAccountId: yourHomeAccountId }); - const account2 = accountManager.getAccount({ localAccountId: yourLocalAccountId }); + const account1 = accountManager.getAccount({ + homeAccountId: yourHomeAccountId, + }); + const account2 = accountManager.getAccount({ + localAccountId: yourLocalAccountId, + }); const account3 = accountManager.getAccount({ username: yourUsername }); ``` + 1. `logout()`: use `logoutRedirect()` or `logoutPopup()` instead. ### Removal of `startPerformanceMeasurement()` @@ -108,10 +115,12 @@ The following functions in `PublicClientApplication` have been removed: 1. The `navigateTologinRequestUrl` parameter has been removed from BrowserAuthOptions in Configuration and can instead now be provided inside an options object as a parameter on the call to `handleRedirectPromise`: ```typescript - pca.handleRedirectPromise({ navigateToLoginRequestUrl: false }) + pca.handleRedirectPromise({ navigateToLoginRequestUrl: false }); ``` + 1. The `encodeExtraQueryParams` parameter has been removed. All extra query params will be encoded. 1. The `supportsNestedAppAuth` parameter has been removed. Use `createNestablePublicClientApplication()` instead. + ```typescript // BEFORE const pca = new PublicClientApplication({ @@ -130,6 +139,7 @@ The following functions in `PublicClientApplication` have been removed: } }); ``` + 1. The `OIDCOptions` parameter now takes in a `ResponseMode` instead of a `ServerResponseType`. Please use `ResponseMode.QUERY` in place of `ServerResponseType.QUERY` and `ResponseMode.FRAGMENT` instead of `ServerResponseType.FRAGMENT`. ### CacheOptions changes @@ -161,6 +171,88 @@ See the [Configuration doc](./configuration.md#system-config-options) for more d The `onRedirectNavigate` parameter will *only be supported* from `Configuration` object going forward and is removed from `RedirectRequest` and `EndSessionRequest` objects. Please ensure to set it in msal config if you need to use it. +### Consolidation of extra request parameters + +The following request parameters have been removed: + +- `authorizePostBodyParams` +- `tokenBodyParameters` +- `tokenQueryParameters` + +In order to simplify extra request parameters, generic extra parameters should go in the new `extraParameters` request option. When `extraParameters` are set in a request, they will be sent on all token service calls in either the URL query string or the request body, depending on the `httpMethod` configured (default is `GET`) in the request. **To submit extra parameters that MUST go in the URL query string, `extraQueryParameters` is still available.** + +> Note: If you're unsure whether the extra parameter should go in the `extraQueryStringParameters` or the `extraParameters`, it should most likely go in `extraParameters`. + + +#### v4 (previous) request example: + +```javascript +// Example of a GET request with extra parameters +const authRequest = { + scopes: ["SAMPLE_SCOPE"], + extraQueryParamters: { + "dc": "DC_VALUE" // This was sent on the query string on GET /authorize + }, + tokenBodyParameters: { + "extra_parameters_assertion": "ASSERTION_VALUE" // This was sent on the POST body to /token + }, + tokenQueryParamters: { + "slice": "SLICE_VALUE" // This was sent on the query string on POST /token + } +} + +// Example of a POST request with extra parameters +const authRequest = { + scopes: ["SAMPLE_SCOPE"], + httpMethod: "POST", // default is "GET" -> Determines method for "/authorize" call. Calls to "/token" are always POST + extraQueryParamters: { + "dc": "DC_VALUE" // This was sent on the query string on POST /authorize + }, + authorizePostBodyParameters: { + "extra_parameters_assertion": "ASSERTION_VALUE", // This was sent on the body on POST /authorize + } + tokenBodyParameters: { + "extra_parameters_assertion": "ASSERTION_VALUE" // This was sent on the POST body to /token + }, + tokenQueryParamters: { + "slice": "SLICE_VALUE" // This was sent on the query string on POST /token + } +} +``` + +#### v5 Request Example + +```javascript +// Example of a GET request with extra parameters +const authRequest = { + scopes: ["SAMPLE_SCOPE"], + extraQueryParamters: { + // Will be sent in query string to /authorize and /token + "dc": "DC_VALUE", + "slice": "SLICE_VALUE" + }, + extraParameters: { + "extra_parameters_assertion": "ASSERTION_VALUE", // Will be sent in query string to /authorize and in body to /token + }, +}; + +// Example of a POST request with extra parameters +const authRequest = { + scopes: ["SAMPLE_SCOPE"], + httpMethod: "POST", // default is "GET" -> Determines method for "/authorize" call. Calls to "/token" are always POST + extraQueryParamters: { + // Will be sent in query string to /authorize and /token + "dc": "DC_VALUE", + "slice": "SLICE_VALUE" + }, + extraParameters: { + extra_parameter_assertion: "assertion_value", // Will be sent in post body to /authorize and /token + }, +}; +``` + +> Note: In cases where MSAL determines `extraParameters` must be encoded into the URL string, `extraParameters` will be merged with `extraQueryParams` in a way that will cause same-named parameters to be overwritten. In these cases, the value for the parameter in `extraParameters` will take precedence over the value in the `extraQueryParams`. + ## Behavioral Breaking Changes ### Event types and InteractionStatus changes diff --git a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts index 0ffa3cec8b..95df0e5315 100644 --- a/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts @@ -963,8 +963,7 @@ export class PlatformAuthInteractionClient extends BaseInteractionClient { tokenType: request.authenticationScheme, windowTitleSubstring: document.title, extraParameters: { - ...request.extraQueryParameters, - ...request.tokenQueryParameters, + ...request.extraParameters, }, extendedExpiryToken: false, // Make this configurable? keyId: request.popKid, diff --git a/lib/msal-browser/src/interaction_client/PopupClient.ts b/lib/msal-browser/src/interaction_client/PopupClient.ts index 033b7986d2..7c0c1f2536 100644 --- a/lib/msal-browser/src/interaction_client/PopupClient.ts +++ b/lib/msal-browser/src/interaction_client/PopupClient.ts @@ -57,6 +57,7 @@ import { initializeServerTelemetryManager, } from "./BaseInteractionClient.js"; import { monitorPopupForHash } from "../utils/PopupUtils.js"; +import { validateRequestMethod } from "../request/RequestHelpers.js"; export type PopupParams = { popup?: Window | null; @@ -107,12 +108,13 @@ export class PopupClient extends StandardInteractionClient { request: PopupRequest, pkceCodes?: PkceCodes ): Promise { + let popupParams: PopupParams | undefined = undefined; try { const popupName = this.generatePopupName( request.scopes || Constants.OIDC_DEFAULT_SCOPES, request.authority || this.config.auth.authority ); - const popupParams: PopupParams = { + popupParams = { popupName, popupWindowAttributes: request.popupWindowAttributes || {}, popupWindowParent: request.popupWindowParent ?? window, @@ -137,6 +139,16 @@ export class PopupClient extends StandardInteractionClient { ); } else { // navigatePopups flag is set to true. Opens popup before acquiring token. + + // Pre-validate request method to avoid opening popup if the request is invalid + const validatedRequest: PopupRequest = { + ...request, + httpMethod: validateRequestMethod( + request, + this.config.system.protocolMode + ), + }; + this.logger.verbose( "navigatePopups set to true, opening popup before acquiring token", this.correlationId @@ -146,7 +158,7 @@ export class PopupClient extends StandardInteractionClient { popupParams ); return this.acquireTokenPopupAsync( - request, + validatedRequest, popupParams, pkceCodes ); @@ -322,77 +334,86 @@ export class PopupClient extends StandardInteractionClient { account: popupRequest.account, }); - // Create acquire token url. - const navigateUrl = await invokeAsync( - Authorize.getAuthCodeRequestUrl, - PerformanceEvents.GetAuthCodeUrl, - this.logger, - this.performanceClient, - correlationId - )( - this.config, - authClient.authority, - popupRequest, - this.logger, - this.performanceClient - ); + if (popupRequest.httpMethod === Constants.HttpMethod.POST) { + return await this.executeCodeFlowWithPost( + popupRequest, + popupParams, + authClient, + pkce.verifier + ); + } else { + // Create acquire token url. + const navigateUrl = await invokeAsync( + Authorize.getAuthCodeRequestUrl, + PerformanceEvents.GetAuthCodeUrl, + this.logger, + this.performanceClient, + correlationId + )( + this.config, + authClient.authority, + popupRequest, + this.logger, + this.performanceClient + ); - // Show the UI once the url has been created. Get the window handle for the popup. - const popupWindow: Window = this.initiateAuthRequest( - navigateUrl, - popupParams - ); - this.eventHandler.emitEvent( - EventType.POPUP_OPENED, - InteractionType.Popup, - { popupWindow }, - null - ); + // Show the UI once the url has been created. Get the window handle for the popup. + const popupWindow: Window = this.initiateAuthRequest( + navigateUrl, + popupParams + ); + this.eventHandler.emitEvent( + EventType.POPUP_OPENED, + InteractionType.Popup, + { popupWindow }, + null + ); - // Monitor the window for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds. - const responseString = await monitorPopupForHash( - popupWindow, - popupParams.popupWindowParent, - this.config.auth.OIDCOptions.responseMode, - this.config.system.pollIntervalMilliseconds, - this.logger, - this.unloadWindow, - this.correlationId - ); + // Monitor the window for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds. + const responseString = await monitorPopupForHash( + popupWindow, + popupParams.popupWindowParent, + this.config.auth.OIDCOptions.responseMode, + this.config.system.pollIntervalMilliseconds, + this.logger, + this.unloadWindow, + this.correlationId + ); - const serverParams = invoke( - ResponseHandler.deserializeResponse, - BrowserPerformanceEvents.DeserializeResponse, - this.logger, - this.performanceClient, - this.correlationId - )( - responseString, - this.config.auth.OIDCOptions.responseMode, - this.logger, - this.correlationId - ); + const serverParams = invoke( + ResponseHandler.deserializeResponse, + BrowserPerformanceEvents.DeserializeResponse, + this.logger, + this.performanceClient, + this.correlationId + )( + responseString, + this.config.auth.OIDCOptions.responseMode, + this.logger, + this.correlationId + ); - return await invokeAsync( - Authorize.handleResponseCode, - BrowserPerformanceEvents.HandleResponseCode, - this.logger, - this.performanceClient, - correlationId - )( - request, - serverParams, - pkce.verifier, - ApiId.acquireTokenPopup, - this.config, - authClient, - this.browserStorage, - this.nativeStorage, - this.eventHandler, - this.logger, - this.performanceClient, - this.platformAuthProvider - ); + return await invokeAsync( + Authorize.handleResponseCode, + BrowserPerformanceEvents.HandleResponseCode, + this.logger, + this.performanceClient, + correlationId + )( + request, + serverParams, + pkce.verifier, + ApiId.acquireTokenPopup, + this.config, + authClient, + this.browserStorage, + this.nativeStorage, + this.eventHandler, + this.logger, + this.performanceClient, + this.platformAuthProvider + ); + } } catch (e) { // Close the synchronous popup if an error is thrown before the window unload event is registered popupParams.popup?.close(); @@ -514,6 +535,94 @@ export class PopupClient extends StandardInteractionClient { ); } + async executeCodeFlowWithPost( + request: CommonAuthorizationUrlRequest, + popupParams: PopupParams, + authClient: AuthorizationCodeClient, + pkceVerifier: string + ): Promise { + const correlationId = request.correlationId; + // Get the frame handle for the silent request + const discoveredAuthority = await invokeAsync( + getDiscoveredAuthority, + BrowserPerformanceEvents.StandardInteractionClientGetDiscoveredAuthority, + this.logger, + this.performanceClient, + correlationId + )( + this.config, + this.correlationId, + this.performanceClient, + this.browserStorage, + this.logger + ); + + const popupWindow = + popupParams.popup || this.openPopup("about:blank", popupParams); + + const form = await Authorize.getCodeForm( + popupWindow.document, + this.config, + discoveredAuthority, + request, + this.logger, + this.performanceClient + ); + + form.submit(); + + // Monitor the popup for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds. + const responseString = await invokeAsync( + monitorPopupForHash, + BrowserPerformanceEvents.SilentHandlerMonitorIframeForHash, + this.logger, + this.performanceClient, + correlationId + )( + popupWindow, + popupParams.popupWindowParent, + this.config.auth.OIDCOptions.responseMode, + this.config.system.pollIntervalMilliseconds, + this.logger, + this.unloadWindow, + this.correlationId + ); + + const serverParams = invoke( + ResponseHandler.deserializeResponse, + BrowserPerformanceEvents.DeserializeResponse, + this.logger, + this.performanceClient, + this.correlationId + )( + responseString, + this.config.auth.OIDCOptions.responseMode, + this.logger, + this.correlationId + ); + + return invokeAsync( + Authorize.handleResponseCode, + BrowserPerformanceEvents.HandleResponseCode, + this.logger, + this.performanceClient, + correlationId + )( + request, + serverParams, + pkceVerifier, + ApiId.acquireTokenPopup, + this.config, + authClient, + this.browserStorage, + this.nativeStorage, + this.eventHandler, + this.logger, + this.performanceClient, + this.platformAuthProvider + ); + } + /** * * @param validRequest diff --git a/lib/msal-browser/src/interaction_client/RedirectClient.ts b/lib/msal-browser/src/interaction_client/RedirectClient.ts index 6862312a28..92eee9b965 100644 --- a/lib/msal-browser/src/interaction_client/RedirectClient.ts +++ b/lib/msal-browser/src/interaction_client/RedirectClient.ts @@ -218,38 +218,42 @@ export class RedirectClient extends StandardInteractionClient { ); try { - // Initialize the client - const authClient: AuthorizationCodeClient = await invokeAsync( - this.createAuthCodeClient.bind(this), - BrowserPerformanceEvents.StandardInteractionClientCreateAuthCodeClient, - this.logger, - this.performanceClient, - this.correlationId - )({ - serverTelemetryManager, - requestAuthority: redirectRequest.authority, - requestAzureCloudOptions: redirectRequest.azureCloudOptions, - requestExtraQueryParameters: - redirectRequest.extraQueryParameters, - account: redirectRequest.account, - }); - - // Create acquire token url. - const navigateUrl = await invokeAsync( - Authorize.getAuthCodeRequestUrl, - PerformanceEvents.GetAuthCodeUrl, - this.logger, - this.performanceClient, - request.correlationId - )( - this.config, - authClient.authority, - redirectRequest, - this.logger, - this.performanceClient - ); - // Show the UI once the url has been created. Response will come back in the hash, which will be handled in the handleRedirectCallback function. - return await this.initiateAuthRequest(navigateUrl); + if (redirectRequest.httpMethod === Constants.HttpMethod.POST) { + return await this.executeCodeFlowWithPost(redirectRequest); + } else { + // Initialize the client + const authClient: AuthorizationCodeClient = await invokeAsync( + this.createAuthCodeClient.bind(this), + BrowserPerformanceEvents.StandardInteractionClientCreateAuthCodeClient, + this.logger, + this.performanceClient, + this.correlationId + )({ + serverTelemetryManager, + requestAuthority: redirectRequest.authority, + requestAzureCloudOptions: redirectRequest.azureCloudOptions, + requestExtraQueryParameters: + redirectRequest.extraQueryParameters, + account: redirectRequest.account, + }); + + // Create acquire token url. + const navigateUrl = await invokeAsync( + Authorize.getAuthCodeRequestUrl, + PerformanceEvents.GetAuthCodeUrl, + this.logger, + this.performanceClient, + request.correlationId + )( + this.config, + authClient.authority, + redirectRequest, + this.logger, + this.performanceClient + ); + // Show the UI once the url has been created. Response will come back in the hash, which will be handled in the handleRedirectCallback function. + return await this.initiateAuthRequest(navigateUrl); + } } catch (e) { if (e instanceof AuthError) { e.setCorrelationId(this.correlationId); @@ -329,6 +333,53 @@ export class RedirectClient extends StandardInteractionClient { }); } + /** + * Executes classic Authorization Code flow with a POST request. + * @param request + */ + async executeCodeFlowWithPost( + request: CommonAuthorizationUrlRequest + ): Promise { + const correlationId = request.correlationId; + // Get the frame handle for the silent request + const discoveredAuthority = await invokeAsync( + getDiscoveredAuthority, + BrowserPerformanceEvents.StandardInteractionClientGetDiscoveredAuthority, + this.logger, + this.performanceClient, + correlationId + )( + this.config, + this.correlationId, + this.performanceClient, + this.browserStorage, + this.logger + ); + + this.browserStorage.cacheAuthorizeRequest(request, this.correlationId); + + const form = await Authorize.getCodeForm( + document, + this.config, + discoveredAuthority, + request, + this.logger, + this.performanceClient + ); + + form.submit(); + return new Promise((resolve, reject) => { + setTimeout(() => { + reject( + createBrowserAuthError( + BrowserAuthErrorCodes.timedOut, + "failed_to_redirect" + ) + ); + }, this.config.system.redirectNavigationTimeout); + }); + } + /** * Checks if navigateToLoginRequestUrl is set, and: * - if true, performs logic to cache and navigate diff --git a/lib/msal-browser/src/interaction_client/SilentIframeClient.ts b/lib/msal-browser/src/interaction_client/SilentIframeClient.ts index a290b49abb..093096f05b 100644 --- a/lib/msal-browser/src/interaction_client/SilentIframeClient.ts +++ b/lib/msal-browser/src/interaction_client/SilentIframeClient.ts @@ -36,6 +36,7 @@ import { } from "../utils/BrowserConstants.js"; import { initiateCodeRequest, + initiateCodeFlowWithPost, initiateEarRequest, monitorIframeForHash, } from "../interaction_handler/SilentHandler.js"; @@ -364,29 +365,48 @@ export class SilentIframeClient extends StandardInteractionClient { ...request, codeChallenge: pkceCodes.challenge, }; - // Create authorize request url - const navigateUrl = await invokeAsync( - Authorize.getAuthCodeRequestUrl, - PerformanceEvents.GetAuthCodeUrl, - this.logger, - this.performanceClient, - correlationId - )( - this.config, - authClient.authority, - silentRequest, - this.logger, - this.performanceClient - ); - // Get the frame handle for the silent request - const msalFrame = await invokeAsync( - initiateCodeRequest, - BrowserPerformanceEvents.SilentHandlerInitiateAuthRequest, - this.logger, - this.performanceClient, - correlationId - )(navigateUrl, this.performanceClient, this.logger, correlationId); + let msalFrame: HTMLIFrameElement; + + if (request.httpMethod === Constants.HttpMethod.POST) { + msalFrame = await invokeAsync( + initiateCodeFlowWithPost, + BrowserPerformanceEvents.SilentHandlerInitiateAuthRequest, + this.logger, + this.performanceClient, + correlationId + )( + this.config, + authClient.authority, + silentRequest, + this.logger, + this.performanceClient + ); + } else { + // Create authorize request url + const navigateUrl = await invokeAsync( + Authorize.getAuthCodeRequestUrl, + PerformanceEvents.GetAuthCodeUrl, + this.logger, + this.performanceClient, + correlationId + )( + this.config, + authClient.authority, + silentRequest, + this.logger, + this.performanceClient + ); + + // Get the frame handle for the silent request + msalFrame = await invokeAsync( + initiateCodeRequest, + BrowserPerformanceEvents.SilentHandlerInitiateAuthRequest, + this.logger, + this.performanceClient, + correlationId + )(navigateUrl, this.performanceClient, this.logger, correlationId); + } const responseType = this.config.auth.OIDCOptions.responseMode; // Monitor the window for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds. diff --git a/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts b/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts index 99068f3ce5..3ba8b0dfdb 100644 --- a/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts @@ -39,9 +39,12 @@ import { RedirectRequest } from "../request/RedirectRequest.js"; import { PopupRequest } from "../request/PopupRequest.js"; import { SsoSilentRequest } from "../request/SsoSilentRequest.js"; import { createNewGuid } from "../crypto/BrowserCrypto.js"; -import { initializeBaseRequest } from "../request/RequestHelpers.js"; import { BrowserConfiguration } from "../config/Configuration.js"; import { BrowserCacheManager } from "../cache/BrowserCacheManager.js"; +import { + initializeBaseRequest, + validateRequestMethod, +} from "../request/RequestHelpers.js"; /** * Defines the class structure and helper functions used by the "standard", non-brokered auth flows (popup, redirect, silent (RT), silent (iframe)) @@ -341,7 +344,7 @@ export async function initializeAuthorizationRequest( correlationId ); - const validatedRequest: CommonAuthorizationUrlRequest = { + const interactionRequest: CommonAuthorizationUrlRequest = { ...baseRequest, redirectUri: redirectUri, state: state, @@ -349,6 +352,14 @@ export async function initializeAuthorizationRequest( responseMode: config.auth.OIDCOptions.responseMode, }; + const validatedRequest = { + ...interactionRequest, + httpMethod: validateRequestMethod( + interactionRequest, + config.system.protocolMode + ), + }; + // Skip active account lookup if either login hint or session id is set if (request.loginHint || request.sid) { return validatedRequest; diff --git a/lib/msal-browser/src/interaction_handler/SilentHandler.ts b/lib/msal-browser/src/interaction_handler/SilentHandler.ts index 05cf1cf9b1..f1e83e97f1 100644 --- a/lib/msal-browser/src/interaction_handler/SilentHandler.ts +++ b/lib/msal-browser/src/interaction_handler/SilentHandler.ts @@ -20,7 +20,7 @@ import { BrowserConfiguration, DEFAULT_IFRAME_TIMEOUT_MS, } from "../config/Configuration.js"; -import { getEARForm } from "../protocol/Authorize.js"; +import { getCodeForm, getEARForm } from "../protocol/Authorize.js"; /** * Creates a hidden iframe to given URL using user-requested scopes as an id. @@ -48,6 +48,29 @@ export async function initiateCodeRequest( )(requestUrl); } +export async function initiateCodeFlowWithPost( + config: BrowserConfiguration, + authority: Authority, + request: CommonAuthorizationUrlRequest, + logger: Logger, + performanceClient: IPerformanceClient +): Promise { + const frame = createHiddenIframe(); + if (!frame.contentDocument) { + throw "No document associated with iframe!"; + } + const form = await getCodeForm( + frame.contentDocument, + config, + authority, + request, + logger, + performanceClient + ); + form.submit(); + return frame; +} + export async function initiateEarRequest( config: BrowserConfiguration, authority: Authority, diff --git a/lib/msal-browser/src/protocol/Authorize.ts b/lib/msal-browser/src/protocol/Authorize.ts index 6330c48bed..f38d911950 100644 --- a/lib/msal-browser/src/protocol/Authorize.ts +++ b/lib/msal-browser/src/protocol/Authorize.ts @@ -160,10 +160,11 @@ export async function getAuthCodeRequestUrl( Constants.S256_CODE_CHALLENGE_METHOD ); - RequestParameterBuilder.addExtraQueryParameters( - parameters, - request.extraQueryParameters || {} - ); + // Merge extraQueryParameters and extraParameters to be appended to request URL + RequestParameterBuilder.addExtraParameters(parameters, { + ...request.extraQueryParameters, + ...request.extraParameters, + }); return AuthorizeProtocol.getAuthorizeUrl(authority, parameters); } @@ -197,8 +198,12 @@ export async function getEARForm( ); RequestParameterBuilder.addEARParameters(parameters, request.earJwk); + RequestParameterBuilder.addExtraParameters(parameters, { + ...request.extraParameters, + }); + const queryParams = new Map(); - RequestParameterBuilder.addExtraQueryParameters( + RequestParameterBuilder.addExtraParameters( queryParams, request.extraQueryParameters || {} ); @@ -207,6 +212,53 @@ export async function getEARForm( return createForm(frame, url, parameters); } +/** + * Gets the form that will be posted to /authorize with request parameters when using POST method + */ +export async function getCodeForm( + frame: Document, + config: BrowserConfiguration, + authority: Authority, + request: CommonAuthorizationUrlRequest, + logger: Logger, + performanceClient: IPerformanceClient +): Promise { + const parameters = await getStandardParameters( + config, + authority, + request, + logger, + performanceClient + ); + + RequestParameterBuilder.addResponseType( + parameters, + Constants.OAuthResponseType.CODE + ); + + RequestParameterBuilder.addCodeChallengeParams( + parameters, + request.codeChallenge, + request.codeChallengeMethod || Constants.S256_CODE_CHALLENGE_METHOD + ); + + // Add extraParameters to the request body + RequestParameterBuilder.addExtraParameters(parameters, { + ...request.extraParameters, + }); + + // Add extraQueryParameters to be appended to request URL + const queryParams = new Map(); + RequestParameterBuilder.addExtraParameters( + queryParams, + request.extraQueryParameters || {} + ); + + const url = AuthorizeProtocol.getAuthorizeUrl(authority, queryParams); + + return createForm(frame, url, parameters); +} + /** * Creates form element in the provided document with auth parameters in the post body * @param frame diff --git a/lib/msal-browser/src/request/EndSessionPopupRequest.ts b/lib/msal-browser/src/request/EndSessionPopupRequest.ts index 181be631e4..18c08919ca 100644 --- a/lib/msal-browser/src/request/EndSessionPopupRequest.ts +++ b/lib/msal-browser/src/request/EndSessionPopupRequest.ts @@ -18,9 +18,7 @@ import { PopupWindowAttributes } from "./PopupWindowAttributes.js"; * - logoutHint - A string that specifies the account that is being logged out in order to skip the server account picker on logout * - popupWindowParent - Optional window object to use as the parent when opening popup windows. Uses global `window` if not given. */ -export type EndSessionPopupRequest = Partial< - Omit -> & { +export type EndSessionPopupRequest = Partial & { authority?: string; mainWindowRedirectUri?: string; popupWindowAttributes?: PopupWindowAttributes; diff --git a/lib/msal-browser/src/request/EndSessionRequest.ts b/lib/msal-browser/src/request/EndSessionRequest.ts index 02eeb6a431..115ba59d05 100644 --- a/lib/msal-browser/src/request/EndSessionRequest.ts +++ b/lib/msal-browser/src/request/EndSessionRequest.ts @@ -14,8 +14,6 @@ import { CommonEndSessionRequest } from "@azure/msal-common/browser"; * - idTokenHint - ID Token used by B2C to validate logout if required by the policy * - logoutHint - A string that specifies the account that is being logged out in order to skip the server account picker on logout */ -export type EndSessionRequest = Partial< - Omit -> & { +export type EndSessionRequest = Partial & { authority?: string; }; diff --git a/lib/msal-browser/src/request/PopupRequest.ts b/lib/msal-browser/src/request/PopupRequest.ts index d88b8eb59b..6936f04385 100644 --- a/lib/msal-browser/src/request/PopupRequest.ts +++ b/lib/msal-browser/src/request/PopupRequest.ts @@ -26,9 +26,8 @@ import { PopupWindowAttributes } from "./PopupWindowAttributes.js"; * - loginHint - Can be used to pre-fill the username/email address field of the sign-in page for the user, if you know the username/email address ahead of time. Often apps use this parameter during re-authentication, having already extracted the username from a previous sign-in using the login_hint or preferred_username claim. * - sid - Session ID, unique identifier for the session. Available as an optional claim on ID tokens. * - domainHint - Provides a hint about the tenant or domain that the user should use to sign in. The value of the domain hint is a registered domain for the tenant. - * - extraQueryParameters - String to string map of custom query parameters added to the /authorize call - * - tokenBodyParameters - String to string map of custom token request body parameters added to the /token call. Only used when renewing access tokens. - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests * - claims - In cases where Azure AD tenant admin has enabled conditional access policies, and the policy has not been met, exceptions will contain claims that need to be consented to. * - nonce - A value included in the request that is returned in the id token. A randomly generated unique value is typically used to mitigate replay attacks. * - popupWindowAttributes - Optional popup window attributes. popupSize with height and width, and popupPosition with top and left can be set. diff --git a/lib/msal-browser/src/request/RedirectRequest.ts b/lib/msal-browser/src/request/RedirectRequest.ts index c49b66d198..924c38f392 100644 --- a/lib/msal-browser/src/request/RedirectRequest.ts +++ b/lib/msal-browser/src/request/RedirectRequest.ts @@ -25,9 +25,8 @@ import { CommonAuthorizationUrlRequest } from "@azure/msal-common/browser"; * - loginHint - Can be used to pre-fill the username/email address field of the sign-in page for the user, if you know the username/email address ahead of time. Often apps use this parameter during re-authentication, having already extracted the username from a previous sign-in using the login_hint or preferred_username claim. * - sid - Session ID, unique identifier for the session. Available as an optional claim on ID tokens. * - domainHint - Provides a hint about the tenant or domain that the user should use to sign in. The value of the domain hint is a registered domain for the tenant. - * - extraQueryParameters - String to string map of custom query parameters added to the /authorize call - * - tokenBodyParameters - String to string map of custom token request body parameters added to the /token call. Only used when renewing access tokens. - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests * - claims - In cases where Azure AD tenant admin has enabled conditional access policies, and the policy has not been met, exceptions will contain claims that need to be consented to. * - nonce - A value included in the request that is returned in the id token. A randomly generated unique value is typically used to mitigate replay attacks. * - redirectStartPage - The page that should be returned to after loginRedirect or acquireTokenRedirect. This should only be used if this is different from the redirectUri and will default to the page that initiates the request. When the navigateToLoginRequestUrl config option is set to false this parameter will be ignored. diff --git a/lib/msal-browser/src/request/RequestHelpers.ts b/lib/msal-browser/src/request/RequestHelpers.ts index 75f0fc3ede..aabb6dbe74 100644 --- a/lib/msal-browser/src/request/RequestHelpers.ts +++ b/lib/msal-browser/src/request/RequestHelpers.ts @@ -11,12 +11,15 @@ import { CommonSilentFlowRequest, IPerformanceClient, Logger, + ProtocolMode, createClientConfigurationError, invokeAsync, } from "@azure/msal-common/browser"; import * as BrowserPerformanceEvents from "../telemetry/BrowserPerformanceEvents.js"; import { BrowserConfiguration } from "../config/Configuration.js"; import { SilentRequest } from "./SilentRequest.js"; +import { PopupRequest } from "./PopupRequest.js"; +import { RedirectRequest } from "./RedirectRequest.js"; /** * Initializer function for all request APIs @@ -98,3 +101,34 @@ export async function initializeSilentRequest( forceRefresh: request.forceRefresh || false, }; } + +/** + * Validates that the combination of request method, protocol mode and authorize body parameters is correct. + * Returns the validated or defaulted HTTP method or throws if the configured combination is invalid. + * @param interactionRequest + * @param protocolMode + * @returns + */ +export function validateRequestMethod( + interactionRequest: BaseAuthRequest | PopupRequest | RedirectRequest, + protocolMode: ProtocolMode +): Constants.HttpMethod { + let httpMethod: Constants.HttpMethod | undefined; + const requestMethod = interactionRequest.httpMethod; + + if (protocolMode === ProtocolMode.EAR) { + // Validate that method can only be POST when protocol mode is EAR + if (requestMethod && requestMethod !== Constants.HttpMethod.POST) { + throw createClientConfigurationError( + ClientConfigurationErrorCodes.invalidRequestMethodForEAR + ); + } else { + httpMethod = Constants.HttpMethod.POST; + } + } else { + // For non-EAR protocol modes, default to GET if httpMethod is not set + httpMethod = requestMethod || Constants.HttpMethod.GET; + } + + return httpMethod; +} diff --git a/lib/msal-browser/src/request/SilentRequest.ts b/lib/msal-browser/src/request/SilentRequest.ts index 4f6227517d..021c4ebb67 100644 --- a/lib/msal-browser/src/request/SilentRequest.ts +++ b/lib/msal-browser/src/request/SilentRequest.ts @@ -20,9 +20,8 @@ import { CacheLookupPolicy } from "../utils/BrowserConstants.js"; * - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes. * - account - Account entity to lookup the credentials. * - forceRefresh - Forces silent requests to make network calls if true. - * - extraQueryParameters - String to string map of custom query parameters added to the /authorize call. Only used when renewing the refresh token. - * - tokenBodyParameters - String to string map of custom token request body parameters added to the /token call. Only used when renewing access tokens. - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call. Only used when renewing access tokens. + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests. Only used when renewing access tokens. + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests. only used when renewing access tokens. * - redirectUri - The redirect URI where authentication responses can be received by your application. It must exactly match one of the redirect URIs registered in the Azure portal. Only used for cases where refresh token is expired. * - cacheLookupPolicy - Enum of different ways the silent token can be retrieved. * - prompt - Indicates the type of user interaction that is required. diff --git a/lib/msal-browser/src/request/SsoSilentRequest.ts b/lib/msal-browser/src/request/SsoSilentRequest.ts index e53d2a4da1..326e455cc7 100644 --- a/lib/msal-browser/src/request/SsoSilentRequest.ts +++ b/lib/msal-browser/src/request/SsoSilentRequest.ts @@ -25,9 +25,8 @@ import { CommonAuthorizationUrlRequest } from "@azure/msal-common/browser"; * - loginHint - Can be used to pre-fill the username/email address field of the sign-in page for the user, if you know the username/email address ahead of time. Often apps use this parameter during re-authentication, having already extracted the username from a previous sign-in using the login_hint or preferred_username claim. * - sid - Session ID, unique identifier for the session. Available as an optional claim on ID tokens. * - domainHint - Provides a hint about the tenant or domain that the user should use to sign in. The value of the domain hint is a registered domain for the tenant. - * - extraQueryParameters - String to string map of custom query parameters added to the /authorize call - * - tokenBodyParameters - String to string map of custom token request body parameters added to the /token call. Only used when renewing access tokens. - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests. Only used when renewing access tokens. + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests. Only used when renewing access tokens. * - nonce - A value included in the request that is returned in the id token. A randomly generated unique value is typically used to mitigate replay attacks. */ export type SsoSilentRequest = Partial< diff --git a/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts index a296408819..6f95f3e80d 100644 --- a/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts @@ -276,9 +276,9 @@ describe("PlatformAuthInteractionClient Tests", () => { }); const response = await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], - extraQueryParameters: { - testQP1: "testQP1", - testQP2: "testQP2", + extraParameters: { + testEP1: "testEP1", + testEP2: "testEP2", }, }); @@ -286,8 +286,8 @@ describe("PlatformAuthInteractionClient Tests", () => { "mock.calls[0][0].extraParameters", { telemetry: "MATS", - testQP1: "testQP1", - testQP2: "testQP2", + testEP1: "testEP1", + testEP2: "testEP2", "x-client-xtra-sku": `${BrowserConstants.MSAL_SKU}|${version},|,|,|`, } ); @@ -343,9 +343,9 @@ describe("PlatformAuthInteractionClient Tests", () => { }); const response = await testInterctionClient.acquireToken({ scopes: ["User.Read"], - extraQueryParameters: { - testQP1: "testQP1", - testQP2: "testQP2", + extraParameters: { + testEP1: "testEP1", + testEP2: "testEP2", }, }); @@ -353,8 +353,8 @@ describe("PlatformAuthInteractionClient Tests", () => { "mock.calls[0][0].extraParameters", { telemetry: "MATS", - testQP1: "testQP1", - testQP2: "testQP2", + testEP1: "testEP1", + testEP2: "testEP2", "x-client-xtra-sku": `${BrowserConstants.MSAL_SKU}|${version},|,|,|`, } ); @@ -382,9 +382,9 @@ describe("PlatformAuthInteractionClient Tests", () => { }); const response = await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], - extraQueryParameters: { - testQP1: "testQP1", - testQP2: "testQP2", + extraParameters: { + testEP1: "testEP1", + testEP2: "testEP2", }, }); @@ -392,8 +392,8 @@ describe("PlatformAuthInteractionClient Tests", () => { "mock.calls[0][0].extraParameters", { telemetry: "MATS", - testQP1: "testQP1", - testQP2: "testQP2", + testEP1: "testEP1", + testEP2: "testEP2", "x-client-xtra-sku": `${BrowserConstants.MSAL_SKU}|${version},|,|,|`, } ); @@ -449,9 +449,9 @@ describe("PlatformAuthInteractionClient Tests", () => { }); const response = await testInterctionClient.acquireToken({ scopes: ["User.Read"], - extraQueryParameters: { - testQP1: "testQP1", - testQP2: "testQP2", + extraParameters: { + testEP1: "testEP1", + testEP2: "testEP2", }, }); @@ -459,8 +459,8 @@ describe("PlatformAuthInteractionClient Tests", () => { "mock.calls[0][0].extraParameters", { telemetry: "MATS", - testQP1: "testQP1", - testQP2: "testQP2", + testEP1: "testEP1", + testEP2: "testEP2", "x-client-xtra-sku": `${BrowserConstants.MSAL_SKU}|${version},|,|,|`, } ); @@ -488,9 +488,9 @@ describe("PlatformAuthInteractionClient Tests", () => { }); const response = await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], - extraQueryParameters: { - testQP1: "testQP1", - testQP2: "testQP2", + extraParameters: { + testEP1: "testEP1", + testEP2: "testEP2", }, }); @@ -498,8 +498,8 @@ describe("PlatformAuthInteractionClient Tests", () => { "mock.calls[0][0].extraParameters", { telemetry: "MATS", - testQP1: "testQP1", - testQP2: "testQP2", + testEP1: "testEP1", + testEP2: "testEP2", "x-client-xtra-sku": `${BrowserConstants.MSAL_SKU}|${version},|,|,|`, } ); @@ -555,9 +555,9 @@ describe("PlatformAuthInteractionClient Tests", () => { }); const response = await testInterctionClient.acquireToken({ scopes: ["User.Read"], - extraQueryParameters: { - testQP1: "testQP1", - testQP2: "testQP2", + extraParameters: { + testEP1: "testEP1", + testEP2: "testEP2", }, }); @@ -565,8 +565,8 @@ describe("PlatformAuthInteractionClient Tests", () => { "mock.calls[0][0].extraParameters", { telemetry: "MATS", - testQP1: "testQP1", - testQP2: "testQP2", + testEP1: "testEP1", + testEP2: "testEP2", "x-client-xtra-sku": `${BrowserConstants.MSAL_SKU}|${version},|,|,|`, } ); @@ -863,7 +863,7 @@ describe("PlatformAuthInteractionClient Tests", () => { .acquireToken({ scopes: ["User.Read"], redirectUri: "localhost", - extraQueryParameters: { + extraParameters: { brk_client_id: "broker_client_id", brk_redirect_uri: "https://broker_redirect_uri.com", client_id: "parent_client_id", @@ -1671,7 +1671,7 @@ describe("PlatformAuthInteractionClient Tests", () => { scopes: ["User.Read"], prompt: Constants.PromptValue.LOGIN, redirectUri: "localhost", - extraQueryParameters: { + extraParameters: { brk_client_id: "broker_client_id", brk_redirect_uri: "https://broker_redirect_uri.com", client_id: "parent_client_id", @@ -1690,14 +1690,14 @@ describe("PlatformAuthInteractionClient Tests", () => { ); }); - it("pick up user input extra query parameters", async () => { + it("pick up user input extra parameters", async () => { const nativeRequest = // @ts-ignore await platformAuthInteractionClient.initializeNativeRequest({ scopes: ["User.Read"], prompt: Constants.PromptValue.LOGIN, redirectUri: "localhost", - extraQueryParameters: { + extraParameters: { userEQP1: "customUserParam1", userEQP2: "customUserParam2", }, diff --git a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts index 79ee9ced4d..97da653eac 100644 --- a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts @@ -67,6 +67,7 @@ import { TestTimeUtils } from "msal-test-utils"; import { PopupRequest } from "../../src/request/PopupRequest.js"; import { version } from "../../src/packageMetadata.js"; import * as CacheKeys from "../../src/cache/CacheKeys.js"; +import * as Authorize from "../../src/protocol/Authorize.js"; const testPopupWondowDefaults = { height: BrowserConstants.POPUP_HEIGHT, @@ -156,7 +157,7 @@ describe("PopupClient", () => { authenticationScheme: Constants.AuthenticationScheme.SSH, }; - expect(popupClient.acquireToken(request)).rejects.toThrow( + await expect(popupClient.acquireToken(request)).rejects.toThrow( createClientConfigurationError( ClientConfigurationErrorCodes.missingSshJwk ) @@ -177,7 +178,7 @@ describe("PopupClient", () => { sshJwk: TEST_SSH_VALUES.SSH_JWK, }; - expect(popupClient.acquireToken(request)).rejects.toThrow( + await expect(popupClient.acquireToken(request)).rejects.toThrow( createClientConfigurationError( ClientConfigurationErrorCodes.missingSshKid ) @@ -678,6 +679,75 @@ describe("PopupClient", () => { }); }); + it("uses POST code flow when httpMethod is set to POST", async () => { + const testServerTokenResponse = { + token_type: TEST_CONFIG.TOKEN_TYPE_BEARER, + scope: TEST_CONFIG.DEFAULT_SCOPES.join(" "), + expires_in: TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN, + ext_expires_in: TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN, + access_token: TEST_TOKENS.ACCESS_TOKEN, + refresh_token: TEST_TOKENS.REFRESH_TOKEN, + id_token: TEST_TOKENS.IDTOKEN_V2, + }; + const testIdTokenClaims: TokenClaims = { + ver: "2.0", + iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + }; + const testAccount: AccountInfo = { + homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, + localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, + environment: "login.windows.net", + tenantId: testIdTokenClaims.tid || "", + username: testIdTokenClaims.preferred_username || "", + }; + const testTokenResponse: AuthenticationResult = { + authority: TEST_CONFIG.validAuthority, + uniqueId: testIdTokenClaims.oid || "", + tenantId: testIdTokenClaims.tid || "", + scopes: TEST_CONFIG.DEFAULT_SCOPES, + idToken: testServerTokenResponse.id_token, + idTokenClaims: testIdTokenClaims, + accessToken: testServerTokenResponse.access_token, + correlationId: RANDOM_TEST_GUID, + fromCache: false, + expiresOn: TestTimeUtils.nowDateWithOffset( + testServerTokenResponse.expires_in + ), + account: testAccount, + tokenType: Constants.AuthenticationScheme.BEARER, + }; + jest.spyOn(PopupUtils, "monitorPopupForHash").mockResolvedValue( + TEST_HASHES.TEST_SUCCESS_CODE_HASH_POPUP + ); + jest.spyOn( + InteractionHandler.prototype, + "handleCodeResponse" + ).mockResolvedValue(testTokenResponse); + jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ + challenge: TEST_CONFIG.TEST_CHALLENGE, + verifier: TEST_CONFIG.TEST_VERIFIER, + }); + jest.spyOn(BrowserCrypto, "createNewGuid").mockReturnValue( + RANDOM_TEST_GUID + ); + + const postCodeFlowSpy = jest + .spyOn(PopupClient.prototype, "executeCodeFlowWithPost") + .mockResolvedValue(testTokenResponse); + const tokenResp = await popupClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + httpMethod: Constants.HttpMethod.POST, + }); + expect(tokenResp).toEqual(testTokenResponse); + expect(postCodeFlowSpy).toHaveBeenCalled(); + }); describe("storeInCache tests", () => { beforeEach(() => { jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue( @@ -890,6 +960,26 @@ describe("PopupClient", () => { 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, + 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, + httpMethod: Constants.HttpMethod.GET, + }; + + await expect( + pca.acquireTokenPopup(validRequest) + ).rejects.toThrow( + createClientConfigurationError( + ClientConfigurationErrorCodes.invalidRequestMethodForEAR + ) + ); + }); }); }); diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index 20491e8013..2572d05d89 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -23,6 +23,7 @@ import { validEarJWK, getTestAuthenticationResult, validEarJWE, + testNavUrl, } from "../utils/StringConstants.js"; import { ServerError, @@ -63,6 +64,7 @@ import { BrowserAuthError, getDefaultErrorMessage, } from "../../src/error/BrowserAuthError.js"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; import { CryptoOps } from "../../src/crypto/CryptoOps.js"; import * as BrowserCrypto from "../../src/crypto/BrowserCrypto.js"; import * as PkceGenerator from "../../src/crypto/PkceGenerator.js"; @@ -1797,7 +1799,9 @@ describe("RedirectClient", () => { authenticationScheme: Constants.AuthenticationScheme.SSH, }; - expect(redirectClient.acquireToken(loginRequest)).rejects.toThrow( + await expect( + redirectClient.acquireToken(loginRequest) + ).rejects.toThrow( createClientConfigurationError( ClientConfigurationErrorCodes.missingSshJwk ) @@ -1818,7 +1822,7 @@ describe("RedirectClient", () => { sshJwk: TEST_SSH_VALUES.SSH_JWK, }; - expect(redirectClient.acquireToken(request)).rejects.toThrow( + await expect(redirectClient.acquireToken(request)).rejects.toThrow( createClientConfigurationError( ClientConfigurationErrorCodes.missingSshKid ) @@ -2069,6 +2073,89 @@ describe("RedirectClient", () => { }); }); + it("executes authorize request as GET when httpMethod is set to GET", async () => { + const loginRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: ["user.read"], + state: TEST_STATE_VALUES.USER_STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + responseMode: + TEST_CONFIG.RESPONSE_MODE as Constants.ResponseMode, + nonce: "", + httpMethod: Constants.HttpMethod.GET, + }; + + jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ + challenge: TEST_CONFIG.TEST_CHALLENGE, + verifier: TEST_CONFIG.TEST_VERIFIER, + }); + + const getFlowSpy = jest + .spyOn(AuthorizeProtocol, "getAuthCodeRequestUrl") + .mockImplementation(() => { + return Promise.resolve(testNavUrl); + }); + + await redirectClient.acquireToken(loginRequest); + expect(getFlowSpy).toHaveBeenCalled(); + }); + + it("executes authorize request as GET when httpMethod is not explicitly set", async () => { + const loginRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: ["user.read"], + state: TEST_STATE_VALUES.USER_STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + responseMode: + TEST_CONFIG.RESPONSE_MODE as Constants.ResponseMode, + nonce: "", + }; + + jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ + challenge: TEST_CONFIG.TEST_CHALLENGE, + verifier: TEST_CONFIG.TEST_VERIFIER, + }); + + const getFlowSpy = jest + .spyOn(AuthorizeProtocol, "getAuthCodeRequestUrl") + .mockImplementation(() => { + return Promise.resolve(testNavUrl); + }); + + await redirectClient.acquireToken(loginRequest); + expect(getFlowSpy).toHaveBeenCalled(); + }); + + it("executes authorize request as POST when httpMethod is set to POST", async () => { + const loginRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: ["user.read"], + state: TEST_STATE_VALUES.USER_STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + responseMode: + TEST_CONFIG.RESPONSE_MODE as Constants.ResponseMode, + nonce: "", + httpMethod: Constants.HttpMethod.POST, + }; + + jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ + challenge: TEST_CONFIG.TEST_CHALLENGE, + verifier: TEST_CONFIG.TEST_VERIFIER, + }); + + const postFlowSpy = jest + .spyOn(RedirectClient.prototype, "executeCodeFlowWithPost") + .mockImplementation(() => { + return Promise.resolve(); + }); + + await redirectClient.acquireToken(loginRequest); + expect(postFlowSpy).toHaveBeenCalled(); + }); + describe("storeInCache tests", () => { beforeEach(() => { jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue( diff --git a/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts index 06e710a46d..ecf8db5467 100644 --- a/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts @@ -29,6 +29,8 @@ import { TenantProfile, Authority, ProtocolMode, + createClientConfigurationError, + ClientConfigurationErrorCodes, } from "@azure/msal-common/browser"; import { createBrowserAuthError, @@ -1151,6 +1153,118 @@ describe("SilentIframeClient", () => { ); }); + describe("httpMethod tests", () => { + const testServerTokenResponse = { + token_type: TEST_CONFIG.TOKEN_TYPE_BEARER, + scope: TEST_CONFIG.DEFAULT_SCOPES.join(" "), + expires_in: TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN, + ext_expires_in: TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN, + access_token: TEST_TOKENS.ACCESS_TOKEN, + refresh_token: TEST_TOKENS.REFRESH_TOKEN, + id_token: TEST_TOKENS.IDTOKEN_V2, + }; + const testIdTokenClaims: TokenClaims = { + ver: "2.0", + iss: "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0", + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + }; + const testAccount: AccountInfo = { + homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, + localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, + environment: "login.windows.net", + tenantId: testIdTokenClaims.tid || "", + username: testIdTokenClaims.preferred_username || "", + }; + const testTokenResponse: AuthenticationResult = { + authority: TEST_CONFIG.validAuthority, + uniqueId: testIdTokenClaims.oid || "", + tenantId: testIdTokenClaims.tid || "", + scopes: TEST_CONFIG.DEFAULT_SCOPES, + idToken: testServerTokenResponse.id_token, + idTokenClaims: testIdTokenClaims, + accessToken: testServerTokenResponse.access_token, + fromCache: false, + correlationId: RANDOM_TEST_GUID, + expiresOn: TestTimeUtils.nowDateWithOffset( + testServerTokenResponse.expires_in + ), + account: testAccount, + tokenType: Constants.AuthenticationScheme.BEARER, + }; + + const generateAuthoritySpy = jest.spyOn( + Authority, + "generateAuthority" + ); + + beforeEach(() => { + jest.spyOn( + SilentHandler, + "monitorIframeForHash" + ).mockResolvedValue(TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT); + jest.spyOn( + InteractionHandler.prototype, + "handleCodeResponse" + ).mockResolvedValue(testTokenResponse); + jest.spyOn( + PkceGenerator, + "generatePkceCodes" + ).mockResolvedValue({ + challenge: TEST_CONFIG.TEST_CHALLENGE, + verifier: TEST_CONFIG.TEST_VERIFIER, + }); + jest.spyOn(BrowserCrypto, "createNewGuid").mockReturnValue( + RANDOM_TEST_GUID + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("uses POST code flow if httpMethod is set to POST", async () => { + const postCodeFlowSpy = jest + .spyOn(SilentHandler, "initiateCodeFlowWithPost") + .mockResolvedValue(document.createElement("iframe")); + const tokenResp = await silentIframeClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + loginHint: "testLoginHint", + prompt: Constants.PromptValue.SELECT_ACCOUNT, + account: testAccount, + httpMethod: Constants.HttpMethod.POST, + }); + + expect(postCodeFlowSpy).toHaveBeenCalled(); + expect(tokenResp).toEqual(testTokenResponse); + }); + + it("uses GET code flow if httpMethod is set to GET", async () => { + const getAuthCodeRequestUrlSpy = jest + .spyOn(AuthorizeProtocol, "getAuthCodeRequestUrl") + .mockResolvedValue(testNavUrl); + const initiateCodeRequestSpy = jest + .spyOn(SilentHandler, "initiateCodeRequest") + .mockResolvedValue(document.createElement("iframe")); + + const tokenResp = await silentIframeClient.acquireToken({ + redirectUri: TEST_URIS.TEST_REDIR_URI, + loginHint: "testLoginHint", + prompt: Constants.PromptValue.SELECT_ACCOUNT, + account: testAccount, + httpMethod: Constants.HttpMethod.GET, + }); + + expect(getAuthCodeRequestUrlSpy).toHaveBeenCalled(); + expect(initiateCodeRequestSpy).toHaveBeenCalled(); + expect(tokenResp).toEqual(testTokenResponse); + }); + }); + describe("storeInCache tests", () => { beforeEach(() => { jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue( @@ -1250,6 +1364,15 @@ describe("SilentIframeClient", () => { }); describe("EAR Flow Tests", () => { + const validRequest: SsoSilentRequest = { + 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, + }; + beforeAll(() => { jest.useFakeTimers(); }); @@ -1272,34 +1395,41 @@ describe("SilentIframeClient", () => { jest.spyOn(BrowserCrypto, "generateEarKey").mockResolvedValue( validEarJWK ); - }); - it("Invokes EAR flow when protocolMode is set to EAR", async () => { - const validRequest: SsoSilentRequest = { - 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_SILENT ); - const earFormSpy = jest - .spyOn(SilentHandler, "initiateEarRequest") - .mockResolvedValue(document.createElement("iframe")); + jest.spyOn( SilentHandler, "monitorIframeForHash" ).mockResolvedValue( `#ear_jwe=${validEarJWE}&state=${TEST_STATE_VALUES.TEST_STATE_SILENT}` ); + }); + + it("Invokes EAR flow when protocolMode is set to EAR", async () => { + const earFormSpy = jest + .spyOn(SilentHandler, "initiateEarRequest") + .mockResolvedValue(document.createElement("iframe")); const result = await pca.ssoSilent(validRequest); expect(result).toEqual(getTestAuthenticationResult()); expect(earFormSpy).toHaveBeenCalled(); }); + + it("throws if protocolMode is set to EAR and httpMethod is set to GET", async () => { + await expect( + pca.ssoSilent({ + ...validRequest, + httpMethod: Constants.HttpMethod.GET, + }) + ).rejects.toThrow( + createClientConfigurationError( + ClientConfigurationErrorCodes.invalidRequestMethodForEAR + ) + ); + }); }); }); diff --git a/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts index c6571cd0f7..1510108309 100644 --- a/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts @@ -13,6 +13,8 @@ import { AccountInfo, CommonAuthorizationUrlRequest, AccountEntityUtils, + ClientConfigurationError, + ClientConfigurationErrorCodes, } from "@azure/msal-common"; import { PublicClientApplication } from "../../src/app/PublicClientApplication.js"; import { @@ -26,9 +28,7 @@ import { TEST_URIS, DEFAULT_TENANT_DISCOVERY_RESPONSE, DEFAULT_OPENID_CONFIG_RESPONSE, - TEST_REQ_CNF_DATA, ID_TOKEN_CLAIMS, - TEST_TOKENS, } from "../utils/StringConstants.js"; import { RedirectRequest } from "../../src/request/RedirectRequest.js"; import * as PkceGenerator from "../../src/crypto/PkceGenerator.js"; @@ -325,6 +325,24 @@ describe("StandardInteractionClient", () => { expect(authCodeRequest.account).toEqual(request.account); expect(authCodeRequest.sid).toEqual(request.sid); }); + + it("initializeAuthorizationRequest sets httpMethod to GET by default", async () => { + const request: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: ["scope"], + state: TEST_STATE_VALUES.USER_STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + responseMode: Constants.ResponseMode.QUERY, + nonce: "", + }; + + const authCodeRequest = await testClient.initializeAuthorizationRequest( + request, + InteractionType.Redirect + ); + expect(authCodeRequest.httpMethod).toEqual(Constants.HttpMethod.GET); + }); }); describe("StandardInteractionClient OIDCOptions Tests", () => { @@ -335,7 +353,9 @@ describe("StandardInteractionClient OIDCOptions Tests", () => { pca = new PublicClientApplication({ auth: { clientId: TEST_CONFIG.MSAL_CLIENT_ID, - OIDCOptions: { responseMode: Constants.ResponseMode.QUERY }, + OIDCOptions: { + responseMode: Constants.ResponseMode.QUERY, + }, }, system: { protocolMode: ProtocolMode.OIDC, @@ -406,3 +426,88 @@ describe("StandardInteractionClient OIDCOptions Tests", () => { expect(authCodeRequest.responseMode).toBe(Constants.ResponseMode.QUERY); }); }); + +describe("StandardInteractionClient EAR Tests", () => { + let pca: PublicClientApplication; + let testClient: testStandardInteractionClient; + + beforeEach(() => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + system: { + protocolMode: ProtocolMode.EAR, + }, + }); + + //Implementation of PCA was moved to controller. + pca = (pca as any).controller; + + // @ts-ignore + testClient = new testStandardInteractionClient( + //@ts-ignore + pca.config, + //@ts-ignore + pca.browserStorage, + //@ts-ignore + pca.browserCrypto, + //@ts-ignore + pca.logger, + //@ts-ignore + pca.eventHandler, + //@ts-ignore + null, + //@ts-ignore + pca.performanceClient + ); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("initializeAuthorizationRequest throws error when protocolMode is EAR and httpMethod is GET", async () => { + const request: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: ["scope"], + state: TEST_STATE_VALUES.USER_STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + responseMode: Constants.ResponseMode.QUERY, + nonce: "", + httpMethod: Constants.HttpMethod.GET, + }; + + try { + await testClient.initializeAuthorizationRequest( + request, + InteractionType.Redirect + ); + throw "Unexpected! Should throw"; + } catch (e) { + expect(e).toBeInstanceOf(ClientConfigurationError); + expect((e as ClientConfigurationError).errorCode).toEqual( + ClientConfigurationErrorCodes.invalidRequestMethodForEAR + ); + } + }); + + it("initializeAuthorizationRequest sets httpMethod to POST when protocolMode is EAR and httpMethod is not set", async () => { + const request: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: ["scope"], + state: TEST_STATE_VALUES.USER_STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + responseMode: Constants.ResponseMode.QUERY, + nonce: "", + }; + + const authCodeRequest = await testClient.initializeAuthorizationRequest( + request, + InteractionType.Redirect + ); + expect(authCodeRequest.httpMethod).toEqual(Constants.HttpMethod.POST); + }); +}); diff --git a/lib/msal-common/apiReview/msal-common.api.md b/lib/msal-common/apiReview/msal-common.api.md index f8277db35a..96329f3f71 100644 --- a/lib/msal-common/apiReview/msal-common.api.md +++ b/lib/msal-common/apiReview/msal-common.api.md @@ -322,11 +322,10 @@ function addDomainHint(parameters: Map, domainHint: string): voi // @public function addEARParameters(parameters: Map, jwk: string): void; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "addExtraQueryParameters" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "addExtraParameters" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function addExtraQueryParameters(parameters: Map, eQParams: StringDict): void; +function addExtraParameters(parameters: Map, extraParams: StringDict): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "addGrantType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -955,12 +954,13 @@ export type BaseAuthRequest = { sshKid?: string; azureCloudOptions?: AzureCloudOptions; maxAge?: number; - tokenBodyParameters?: StringDict; - tokenQueryParameters?: StringDict; storeInCache?: StoreInCache; scenarioId?: string; popKid?: string; embeddedClientId?: string; + httpMethod?: HttpMethod; + extraQueryParameters?: StringDict; + extraParameters?: StringDict; }; // Warning: (ae-internal-missing-underscore) The name "BaseClient" should be prefixed with an underscore because the declaration is marked as @internal @@ -1575,7 +1575,8 @@ declare namespace ClientConfigurationErrorCodes { invalidAuthenticationHeader, cannotSetOIDCOptions, cannotAllowPlatformBroker, - authorityMismatch + authorityMismatch, + invalidRequestMethodForEAR } } export { ClientConfigurationErrorCodes } @@ -1655,7 +1656,6 @@ export type CommonAuthorizationUrlRequest = BaseAuthRequest & { codeChallenge?: string; codeChallengeMethod?: string; domainHint?: string; - extraQueryParameters?: StringDict; extraScopesToConsent?: Array; loginHint?: string; nonce: string; @@ -1754,6 +1754,7 @@ declare namespace Constants { HTTP_GATEWAY_TIMEOUT, HTTP_SERVER_ERROR_RANGE_END, HTTP_MULTI_SIDED_ERROR, + HttpMethod, OIDC_DEFAULT_SCOPES, OIDC_SCOPES, HeaderNames, @@ -2436,6 +2437,18 @@ const HTTP_TOO_MANY_REQUESTS: number; // @public (undocumented) const HTTP_UNAUTHORIZED: number; +// Warning: (ae-missing-release-tag) "HttpMethod" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "HttpMethod" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const HttpMethod: { + readonly GET: "GET"; + readonly POST: "POST"; +}; + +// @public (undocumented) +type HttpMethod = (typeof HttpMethod)[keyof typeof HttpMethod]; + // Warning: (ae-missing-release-tag) "IAppTokenProvider" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -2674,6 +2687,11 @@ const invalidCloudDiscoveryMetadata = "invalid_cloud_discovery_metadata"; // @public (undocumented) const invalidCodeChallengeMethod = "invalid_code_challenge_method"; +// Warning: (ae-missing-release-tag) "invalidRequestMethodForEAR" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const invalidRequestMethodForEAR = "invalid_request_method_for_EAR"; + // Warning: (ae-missing-release-tag) "invalidState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -3769,7 +3787,7 @@ declare namespace RequestParameterBuilder { addGrantType, addClientInfo, addInstanceAware, - addExtraQueryParameters, + addExtraParameters, addClientCapabilitiesToClaims, addUsername, addPassword, @@ -4658,7 +4676,7 @@ const X_MS_LIB_CAPABILITY_VALUE: string; // src/client/AuthorizationCodeClient.ts:151:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/AuthorizationCodeClient.ts:152:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/AuthorizationCodeClient.ts:210:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/AuthorizationCodeClient.ts:446:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/AuthorizationCodeClient.ts:444:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/RefreshTokenClient.ts:183:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/RefreshTokenClient.ts:270:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/RefreshTokenClient.ts:271:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen diff --git a/lib/msal-common/src/client/AuthorizationCodeClient.ts b/lib/msal-common/src/client/AuthorizationCodeClient.ts index 0b83c4097f..32f3928daf 100644 --- a/lib/msal-common/src/client/AuthorizationCodeClient.ts +++ b/lib/msal-common/src/client/AuthorizationCodeClient.ts @@ -217,7 +217,7 @@ export class AuthorizationCodeClient extends BaseClient { RequestParameterBuilder.addClientId( parameters, request.embeddedClientId || - request.tokenBodyParameters?.[AADServerParamKeys.CLIENT_ID] || + request.extraParameters?.[AADServerParamKeys.CLIENT_ID] || this.config.authOptions.clientId ); @@ -413,22 +413,20 @@ export class AuthorizationCodeClient extends BaseClient { ); } - if (request.tokenBodyParameters) { - RequestParameterBuilder.addExtraQueryParameters( + if (request.extraParameters) { + RequestParameterBuilder.addExtraParameters( parameters, - request.tokenBodyParameters + request.extraParameters ); } // Add hybrid spa parameters if not already provided if ( request.enableSpaAuthorizationCode && - (!request.tokenBodyParameters || - !request.tokenBodyParameters[ - AADServerParamKeys.RETURN_SPA_CODE - ]) + (!request.extraParameters || + !request.extraParameters[AADServerParamKeys.RETURN_SPA_CODE]) ) { - RequestParameterBuilder.addExtraQueryParameters(parameters, { + RequestParameterBuilder.addExtraParameters(parameters, { [AADServerParamKeys.RETURN_SPA_CODE]: "1", }); } @@ -483,7 +481,7 @@ export class AuthorizationCodeClient extends BaseClient { } if (request.extraQueryParameters) { - RequestParameterBuilder.addExtraQueryParameters( + RequestParameterBuilder.addExtraParameters( parameters, request.extraQueryParameters ); diff --git a/lib/msal-common/src/client/BaseClient.ts b/lib/msal-common/src/client/BaseClient.ts index 25c544c178..c16f630c8f 100644 --- a/lib/msal-common/src/client/BaseClient.ts +++ b/lib/msal-common/src/client/BaseClient.ts @@ -286,10 +286,10 @@ export abstract class BaseClient { ); } - if (request.tokenQueryParameters) { - RequestParameterBuilder.addExtraQueryParameters( + if (request.extraQueryParameters) { + RequestParameterBuilder.addExtraParameters( parameters, - request.tokenQueryParameters + request.extraQueryParameters ); } diff --git a/lib/msal-common/src/client/RefreshTokenClient.ts b/lib/msal-common/src/client/RefreshTokenClient.ts index 9a086eac39..14462e3143 100644 --- a/lib/msal-common/src/client/RefreshTokenClient.ts +++ b/lib/msal-common/src/client/RefreshTokenClient.ts @@ -317,7 +317,7 @@ export class RefreshTokenClient extends BaseClient { RequestParameterBuilder.addClientId( parameters, request.embeddedClientId || - request.tokenBodyParameters?.[AADServerParamKeys.CLIENT_ID] || + request.extraParameters?.[AADServerParamKeys.CLIENT_ID] || this.config.authOptions.clientId ); @@ -476,11 +476,10 @@ export class RefreshTokenClient extends BaseClient { ); } - if (request.tokenBodyParameters) { - RequestParameterBuilder.addExtraQueryParameters( - parameters, - request.tokenBodyParameters - ); + if (request.extraParameters) { + RequestParameterBuilder.addExtraParameters(parameters, { + ...request.extraParameters, + }); } RequestParameterBuilder.instrumentBrokerParams( diff --git a/lib/msal-common/src/error/ClientConfigurationErrorCodes.ts b/lib/msal-common/src/error/ClientConfigurationErrorCodes.ts index 47bbec4386..97a5925d25 100644 --- a/lib/msal-common/src/error/ClientConfigurationErrorCodes.ts +++ b/lib/msal-common/src/error/ClientConfigurationErrorCodes.ts @@ -25,3 +25,4 @@ export const invalidAuthenticationHeader = "invalid_authentication_header"; export const cannotSetOIDCOptions = "cannot_set_OIDCOptions"; export const cannotAllowPlatformBroker = "cannot_allow_platform_broker"; export const authorityMismatch = "authority_mismatch"; +export const invalidRequestMethodForEAR = "invalid_request_method_for_EAR"; diff --git a/lib/msal-common/src/network/RequestThumbprint.ts b/lib/msal-common/src/network/RequestThumbprint.ts index 5f2b223d95..b33600982b 100644 --- a/lib/msal-common/src/network/RequestThumbprint.ts +++ b/lib/msal-common/src/network/RequestThumbprint.ts @@ -42,6 +42,6 @@ export function getRequestThumbprint( shrClaims: request.shrClaims, sshKid: request.sshKid, embeddedClientId: - request.embeddedClientId || request.tokenBodyParameters?.clientId, + request.embeddedClientId || request.extraParameters?.clientId, }; } diff --git a/lib/msal-common/src/request/BaseAuthRequest.ts b/lib/msal-common/src/request/BaseAuthRequest.ts index 746d819ac9..a87fb06812 100644 --- a/lib/msal-common/src/request/BaseAuthRequest.ts +++ b/lib/msal-common/src/request/BaseAuthRequest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { AuthenticationScheme } from "../utils/Constants.js"; +import { AuthenticationScheme, HttpMethod } from "../utils/Constants.js"; import type { AzureCloudOptions } from "../config/ClientConfiguration.js"; import { StringDict } from "../utils/MsalTypes.js"; import { StoreInCache } from "./StoreInCache.js"; @@ -24,12 +24,13 @@ import { ShrOptions } from "../crypto/SignedHttpRequest.js"; * - sshJwk - A stringified JSON Web Key representing a public key that can be signed by an SSH certificate. * - sshKid - Key ID that uniquely identifies the SSH public key mentioned above. * - azureCloudOptions - Convenience string enums for users to provide public/sovereign cloud ids - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call - * - tokenBodyParameters - String to string map of custom parameters added to the body of the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests * - storeInCache - Object containing boolean values indicating whether to store tokens in the cache or not (default is true) * - scenarioId - Scenario id to track custom user prompts * - popKid - Key ID to identify the public key for PoP token request * - embeddedClientId - Embedded client id. When specified, broker client id (brk_client_id) and redirect uri (brk_redirect_uri) params are set with values from the config, overriding the corresponding extra parameters, if present. + * - httpMethod - HTTP method to use for the /authorize request. Defaults to GET, but can be set to POST if the request requires body parameters + * - extraParameters - String to string map of custom parameters added to outgoing token service requests */ export type BaseAuthRequest = { authority: string; @@ -46,10 +47,11 @@ export type BaseAuthRequest = { sshKid?: string; azureCloudOptions?: AzureCloudOptions; maxAge?: number; - tokenBodyParameters?: StringDict; - tokenQueryParameters?: StringDict; storeInCache?: StoreInCache; scenarioId?: string; popKid?: string; embeddedClientId?: string; + httpMethod?: HttpMethod; + extraQueryParameters?: StringDict; + extraParameters?: StringDict; }; diff --git a/lib/msal-common/src/request/CommonAuthorizationCodeRequest.ts b/lib/msal-common/src/request/CommonAuthorizationCodeRequest.ts index d6d2d9a3f3..931d21cc23 100644 --- a/lib/msal-common/src/request/CommonAuthorizationCodeRequest.ts +++ b/lib/msal-common/src/request/CommonAuthorizationCodeRequest.ts @@ -19,7 +19,8 @@ import { CcsCredential } from "../account/CcsCredential.js"; * - resourceRequestMethod - HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows. * - resourceRequestUri - URI that token will be used for. Used for proof-of-possession flows. * - enableSpaAuthCode - Enables the acqusition of a spa authorization code (confidential clients only) - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests */ export type CommonAuthorizationCodeRequest = BaseAuthRequest & { code: string; diff --git a/lib/msal-common/src/request/CommonAuthorizationUrlRequest.ts b/lib/msal-common/src/request/CommonAuthorizationUrlRequest.ts index 27cc217316..796f713d86 100644 --- a/lib/msal-common/src/request/CommonAuthorizationUrlRequest.ts +++ b/lib/msal-common/src/request/CommonAuthorizationUrlRequest.ts @@ -4,7 +4,6 @@ */ import { ResponseMode } from "../utils/Constants.js"; -import { StringDict } from "../utils/MsalTypes.js"; import { BaseAuthRequest } from "./BaseAuthRequest.js"; import { AccountInfo } from "../account/AccountInfo.js"; @@ -32,8 +31,8 @@ import { AccountInfo } from "../account/AccountInfo.js"; * - loginHint - Can be used to pre-fill the username/email address field of the sign-in page for the user, if you know the username/email address ahead of time. Often apps use this parameter during re-authentication, having already extracted the username from a previous sign-in using the preferred_username claim. * - sid - Session ID, unique identifier for the session. Available as an optional claim on ID tokens. * - domainHint - Provides a hint about the tenant or domain that the user should use to sign in. The value of the domain hint is a registered domain for the tenant. - * - extraQueryParameters - String to string map of custom query parameters added to the /authorize call - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests * - nonce - A value included in the request that is returned in the id token. A randomly generated unique value is typically used to mitigate replay attacks. * - resourceRequestMethod - HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows. * - resourceRequestUri - URI that token will be used for. Used for proof-of-possession flows. @@ -46,7 +45,6 @@ export type CommonAuthorizationUrlRequest = BaseAuthRequest & { codeChallenge?: string; codeChallengeMethod?: string; domainHint?: string; - extraQueryParameters?: StringDict; extraScopesToConsent?: Array; loginHint?: string; nonce: string; diff --git a/lib/msal-common/src/request/CommonRefreshTokenRequest.ts b/lib/msal-common/src/request/CommonRefreshTokenRequest.ts index 00ea3c3e4b..76640992b9 100644 --- a/lib/msal-common/src/request/CommonRefreshTokenRequest.ts +++ b/lib/msal-common/src/request/CommonRefreshTokenRequest.ts @@ -16,7 +16,8 @@ import { CcsCredential } from "../account/CcsCredential.js"; * - resourceRequestMethod - HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows. * - resourceRequestUri - URI that token will be used for. Used for proof-of-possession flows. * - forceCache - Force MSAL to cache a refresh token flow response when there is no account in the cache. Used for migration scenarios. - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests */ export type CommonRefreshTokenRequest = BaseAuthRequest & { refreshToken: string; diff --git a/lib/msal-common/src/request/CommonSilentFlowRequest.ts b/lib/msal-common/src/request/CommonSilentFlowRequest.ts index 68d7fa26b0..e8e4ef0747 100644 --- a/lib/msal-common/src/request/CommonSilentFlowRequest.ts +++ b/lib/msal-common/src/request/CommonSilentFlowRequest.ts @@ -16,7 +16,8 @@ import { BaseAuthRequest } from "./BaseAuthRequest.js"; * - forceRefresh - Forces silent requests to make network calls if true. * - resourceRequestMethod - HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows. * - resourceRequestUri - URI that token will be used for. Used for proof-of-possession flows. - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests */ export type CommonSilentFlowRequest = BaseAuthRequest & { /** Account object to lookup the credentials */ diff --git a/lib/msal-common/src/request/RequestParameterBuilder.ts b/lib/msal-common/src/request/RequestParameterBuilder.ts index f379178e97..d093797371 100644 --- a/lib/msal-common/src/request/RequestParameterBuilder.ts +++ b/lib/msal-common/src/request/RequestParameterBuilder.ts @@ -455,14 +455,14 @@ export function addInstanceAware(parameters: Map): void { } /** - * add extraQueryParams - * @param eQParams + * Add extraParameters + * @param extraParams - String dictionary containing extra parameters to be added. */ -export function addExtraQueryParameters( +export function addExtraParameters( parameters: Map, - eQParams: StringDict + extraParams: StringDict ): void { - Object.entries(eQParams).forEach(([key, value]) => { + Object.entries(extraParams).forEach(([key, value]) => { if (!parameters.has(key) && value) { parameters.set(key, value); } diff --git a/lib/msal-common/src/utils/Constants.ts b/lib/msal-common/src/utils/Constants.ts index f7623cf852..50f60acdf1 100644 --- a/lib/msal-common/src/utils/Constants.ts +++ b/lib/msal-common/src/utils/Constants.ts @@ -68,6 +68,12 @@ export const HTTP_GATEWAY_TIMEOUT: number = 504; export const HTTP_SERVER_ERROR_RANGE_END: number = 599; export const HTTP_MULTI_SIDED_ERROR: number = 600; +export const HttpMethod = { + GET: "GET", + POST: "POST", +} as const; +export type HttpMethod = (typeof HttpMethod)[keyof typeof HttpMethod]; + export const OIDC_DEFAULT_SCOPES = [ OPENID_SCOPE, PROFILE_SCOPE, diff --git a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts index bc8c747fa9..3958117559 100644 --- a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts +++ b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts @@ -1111,7 +1111,7 @@ describe("AuthorizationCodeClient unit tests", () => { }); }); - it("Adds tokenQueryParameters to the /token request", (done) => { + it("Adds extraQueryParameters to the /token request", (done) => { jest.spyOn( Authority.prototype, "getEndpointMetadataFromNetwork" @@ -1150,7 +1150,7 @@ describe("AuthorizationCodeClient unit tests", () => { claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, authenticationScheme: Constants.AuthenticationScheme.BEARER, - tokenQueryParameters: { + extraQueryParameters: { testParam1: "testValue1", testParam2: "", testParam3: "testValue3", @@ -1162,7 +1162,7 @@ describe("AuthorizationCodeClient unit tests", () => { }); }); - it("Adds tokenBodyParameters to the /token request", (done) => { + it("Adds extraParameters to the /token request", (done) => { jest.spyOn( Authority.prototype, "getEndpointMetadataFromNetwork" @@ -1198,7 +1198,7 @@ describe("AuthorizationCodeClient unit tests", () => { claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, authenticationScheme: Constants.AuthenticationScheme.BEARER, - tokenBodyParameters: { + extraParameters: { extra_body_parameter: "true", }, }; @@ -1208,6 +1208,132 @@ describe("AuthorizationCodeClient unit tests", () => { }); }); + it("Adds both extraQueryParameters and extraParameters to the /token request", (done) => { + jest.spyOn( + Authority.prototype, + "getEndpointMetadataFromNetwork" + ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); + jest.spyOn( + AuthorizationCodeClient.prototype, + "executePostToTokenEndpoint" + // @ts-expect-error + ).mockImplementation((url: string, body: string) => { + try { + // Verify extraQueryParameters are in the URL + expect( + url.includes( + "/token?queryParam1=queryValue1&queryParam2=queryValue2" + ) + ).toBeTruthy(); + // Verify extraParameters are in the body + expect(body).toContain("bodyParam1=bodyValue1"); + expect(body).toContain("bodyParam2=bodyValue2"); + done(); + } catch (error) { + done(error); + } + }); + + if (!config.cryptoInterface || !config.systemOptions) { + throw TestError.createTestSetupError( + "configuration cryptoInterface or systemOptions not initialized correctly." + ); + } + const client = new AuthorizationCodeClient( + config, + stubPerformanceClient + ); + + const authCodeRequest: CommonAuthorizationCodeRequest = { + authority: Constants.DEFAULT_AUTHORITY, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + code: TEST_TOKENS.AUTHORIZATION_CODE, + codeVerifier: TEST_CONFIG.TEST_VERIFIER, + claims: TEST_CONFIG.CLAIMS, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: Constants.AuthenticationScheme.BEARER, + extraQueryParameters: { + queryParam1: "queryValue1", + queryParam2: "queryValue2", + }, + extraParameters: { + bodyParam1: "bodyValue1", + bodyParam2: "bodyValue2", + }, + }; + + client.acquireToken(authCodeRequest).catch((error) => { + // Catch errors thrown after the function call this test is testing + }); + }); + + it("Does not overwrite extraQueryParameters with extraParameters in /token request when they have the same parameter name", (done) => { + jest.spyOn( + Authority.prototype, + "getEndpointMetadataFromNetwork" + ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); + jest.spyOn( + AuthorizationCodeClient.prototype, + "executePostToTokenEndpoint" + // @ts-expect-error + ).mockImplementation((url: string, body: string) => { + try { + // Verify extraQueryParameters value is in the URL (not overwritten) + expect(url.includes("sharedParam=queryValue")).toBeTruthy(); + expect(!url.includes("sharedParam=bodyValue")).toBeTruthy(); + // Verify extraParameters value is in the body + expect(body).toContain("sharedParam=bodyValue"); + // Verify the body doesn't contain the query value + expect( + !body.includes("sharedParam=queryValue") + ).toBeTruthy(); + done(); + } catch (error) { + done(error); + } + }); + + if (!config.cryptoInterface || !config.systemOptions) { + throw TestError.createTestSetupError( + "configuration cryptoInterface or systemOptions not initialized correctly." + ); + } + const client = new AuthorizationCodeClient( + config, + stubPerformanceClient + ); + + const authCodeRequest: CommonAuthorizationCodeRequest = { + authority: Constants.DEFAULT_AUTHORITY, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + code: TEST_TOKENS.AUTHORIZATION_CODE, + codeVerifier: TEST_CONFIG.TEST_VERIFIER, + claims: TEST_CONFIG.CLAIMS, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: Constants.AuthenticationScheme.BEARER, + extraQueryParameters: { + sharedParam: "queryValue", + uniqueQueryParam: "uniqueQueryValue", + }, + extraParameters: { + sharedParam: "bodyValue", + uniqueBodyParam: "uniqueBodyValue", + }, + }; + + client.acquireToken(authCodeRequest).catch((error) => { + // Catch errors thrown after the function call this test is testing + }); + }); + it("Adds return_spa_code=1 to body when enableSpaAuthCode is set", (done) => { jest.spyOn( Authority.prototype, @@ -1291,7 +1417,7 @@ describe("AuthorizationCodeClient unit tests", () => { claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, authenticationScheme: Constants.AuthenticationScheme.BEARER, - tokenBodyParameters: { + extraParameters: { extra_body_parameter: "true", }, }; @@ -2650,7 +2776,7 @@ describe("AuthorizationCodeClient unit tests", () => { await client.createTokenRequestBody({ scopes: ["User.Read"], redirectUri: "localhost", - tokenBodyParameters: { + extraParameters: { client_id: "child_client_id", }, }); @@ -2697,7 +2823,7 @@ describe("AuthorizationCodeClient unit tests", () => { scopes: ["User.Read"], redirectUri: "localhost", embeddedClientId: "child_client_id_1", - tokenBodyParameters: { + extraParameters: { client_id: "child_client_id_2", brk_client_id: "broker_client_id_2", brk_redirect_uri: "broker_redirect_uri_2", diff --git a/lib/msal-common/test/client/RefreshTokenClient.spec.ts b/lib/msal-common/test/client/RefreshTokenClient.spec.ts index f280e0de96..8d773fa828 100644 --- a/lib/msal-common/test/client/RefreshTokenClient.spec.ts +++ b/lib/msal-common/test/client/RefreshTokenClient.spec.ts @@ -176,7 +176,7 @@ describe("RefreshTokenClient unit tests", () => { correlationId: TEST_CONFIG.CORRELATION_ID, authenticationScheme: TEST_CONFIG.TOKEN_TYPE_BEARER as Constants.AuthenticationScheme, - tokenQueryParameters: { + extraQueryParameters: { testParam: "testValue", }, }; @@ -186,7 +186,7 @@ describe("RefreshTokenClient unit tests", () => { }); }); - it("Adds tokenQueryParameters to the /token request", (done) => { + it("Adds extraQueryParameters to the /token request", (done) => { jest.spyOn( RefreshTokenClient.prototype, "executePostToTokenEndpoint" @@ -208,7 +208,7 @@ describe("RefreshTokenClient unit tests", () => { correlationId: TEST_CONFIG.CORRELATION_ID, authenticationScheme: TEST_CONFIG.TOKEN_TYPE_BEARER as Constants.AuthenticationScheme, - tokenQueryParameters: { + extraQueryParameters: { testParam: "testValue", }, }; @@ -218,7 +218,7 @@ describe("RefreshTokenClient unit tests", () => { }); }); - it("Adds tokenBodyParameters to the /token request", (done) => { + it("Adds extraParameters to the /token request", (done) => { jest.spyOn( RefreshTokenClient.prototype, "executePostToTokenEndpoint" @@ -241,7 +241,7 @@ describe("RefreshTokenClient unit tests", () => { correlationId: TEST_CONFIG.CORRELATION_ID, authenticationScheme: TEST_CONFIG.TOKEN_TYPE_BEARER as Constants.AuthenticationScheme, - tokenBodyParameters: { + extraParameters: { testParam: "testValue", }, }; @@ -251,6 +251,104 @@ describe("RefreshTokenClient unit tests", () => { }); }); + it("Adds both extraQueryParameters and extraParameters to the /token request", (done) => { + jest.spyOn( + RefreshTokenClient.prototype, + "executePostToTokenEndpoint" + // @ts-expect-error + ).mockImplementation((url: string, body: string) => { + try { + // Verify extraQueryParameters are in the URL + expect( + url.includes( + "/token?queryParam1=queryValue1&queryParam2=queryValue2" + ) + ).toBe(true); + // Verify extraParameters are in the body + expect(body).toContain("bodyParam1=bodyValue1"); + expect(body).toContain("bodyParam2=bodyValue2"); + done(); + } catch (error) { + done(error); + } + }); + + const client = new RefreshTokenClient( + config, + stubPerformanceClient + ); + + const refreshTokenRequest: CommonRefreshTokenRequest = { + scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + refreshToken: TEST_TOKENS.REFRESH_TOKEN, + claims: TEST_CONFIG.CLAIMS, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + authenticationScheme: + TEST_CONFIG.TOKEN_TYPE_BEARER as Constants.AuthenticationScheme, + extraQueryParameters: { + queryParam1: "queryValue1", + queryParam2: "queryValue2", + }, + extraParameters: { + bodyParam1: "bodyValue1", + bodyParam2: "bodyValue2", + }, + }; + + client.acquireToken(refreshTokenRequest).catch((error) => { + // Catch errors thrown after the function call this test is testing + }); + }); + + it("Does not overwrite extraQueryParameters with extraParameters when they have the same parameter name", (done) => { + jest.spyOn( + RefreshTokenClient.prototype, + "executePostToTokenEndpoint" + // @ts-expect-error + ).mockImplementation((url: string, body: string) => { + try { + // Verify extraQueryParameters value is in the URL (not overwritten) + expect(url.includes("sharedParam=queryValue")).toBe(true); + expect(url.includes("sharedParam=bodyValue")).toBe(false); + // Verify extraParameters value is in the body + expect(body).toContain("sharedParam=bodyValue"); + // Verify the body doesn't contain the query value + expect(body.includes("sharedParam=queryValue")).toBe(false); + done(); + } catch (error) { + done(error); + } + }); + + const client = new RefreshTokenClient( + config, + stubPerformanceClient + ); + + const refreshTokenRequest: CommonRefreshTokenRequest = { + scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + refreshToken: TEST_TOKENS.REFRESH_TOKEN, + claims: TEST_CONFIG.CLAIMS, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + authenticationScheme: + TEST_CONFIG.TOKEN_TYPE_BEARER as Constants.AuthenticationScheme, + extraQueryParameters: { + sharedParam: "queryValue", + uniqueQueryParam: "uniqueQueryValue", + }, + extraParameters: { + sharedParam: "bodyValue", + uniqueBodyParam: "uniqueBodyValue", + }, + }; + + client.acquireToken(refreshTokenRequest).catch((error) => { + // Catch errors thrown after the function call this test is testing + }); + }); + it("Checks whether performance telemetry startMeasurement method is called", async () => { const spy = jest.spyOn(stubPerformanceClient, "startMeasurement"); @@ -541,7 +639,7 @@ describe("RefreshTokenClient unit tests", () => { ).toBe(true); }); - it("Adds tokenQueryParameters to the /token request", (done) => { + it("Adds extraQueryParameters to the /token request", (done) => { jest.spyOn( RefreshTokenClient.prototype, "executePostToTokenEndpoint" @@ -572,7 +670,7 @@ describe("RefreshTokenClient unit tests", () => { correlationId: TEST_CONFIG.CORRELATION_ID, authenticationScheme: TEST_CONFIG.TOKEN_TYPE_BEARER as Constants.AuthenticationScheme, - tokenQueryParameters: { + extraQueryParameters: { testParam1: "testValue1", testParam2: "", testParam3: "testValue3", @@ -584,6 +682,137 @@ describe("RefreshTokenClient unit tests", () => { }); }); + it("Adds extraParameters to the /token request", (done) => { + jest.spyOn( + RefreshTokenClient.prototype, + "executePostToTokenEndpoint" + // @ts-expect-error + ).mockImplementation((url: string, body: string) => { + expect(body).toContain("testParam=testValue"); + done(); + }); + + const client = new RefreshTokenClient( + config, + stubPerformanceClient + ); + + const refreshTokenRequest: CommonRefreshTokenRequest = { + scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + refreshToken: TEST_TOKENS.REFRESH_TOKEN, + claims: TEST_CONFIG.CLAIMS, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + authenticationScheme: + TEST_CONFIG.TOKEN_TYPE_BEARER as Constants.AuthenticationScheme, + extraParameters: { + testParam: "testValue", + }, + }; + + client.acquireToken(refreshTokenRequest).catch((error) => { + // Catch errors thrown after the function call this test is testing + }); + }); + + it("Adds both extraQueryParameters and extraParameters to the /token request", (done) => { + jest.spyOn( + RefreshTokenClient.prototype, + "executePostToTokenEndpoint" + // @ts-expect-error + ).mockImplementation((url: string, body: string) => { + try { + // Verify extraQueryParameters are in the URL + expect( + url.includes( + "/token?queryParam1=queryValue1&queryParam2=queryValue2" + ) + ).toBe(true); + // Verify extraParameters are in the body + expect(body).toContain("bodyParam1=bodyValue1"); + expect(body).toContain("bodyParam2=bodyValue2"); + done(); + } catch (error) { + done(error); + } + }); + + const client = new RefreshTokenClient( + config, + stubPerformanceClient + ); + + const refreshTokenRequest: CommonRefreshTokenRequest = { + scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + refreshToken: TEST_TOKENS.REFRESH_TOKEN, + claims: TEST_CONFIG.CLAIMS, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + authenticationScheme: + TEST_CONFIG.TOKEN_TYPE_BEARER as Constants.AuthenticationScheme, + extraQueryParameters: { + queryParam1: "queryValue1", + queryParam2: "queryValue2", + }, + extraParameters: { + bodyParam1: "bodyValue1", + bodyParam2: "bodyValue2", + }, + }; + + client.acquireToken(refreshTokenRequest).catch((error) => { + // Catch errors thrown after the function call this test is testing + }); + }); + + it("Does not overwrite extraQueryParameters with extraParameters when they have the same parameter name", (done) => { + jest.spyOn( + RefreshTokenClient.prototype, + "executePostToTokenEndpoint" + // @ts-expect-error + ).mockImplementation((url: string, body: string) => { + try { + // Verify extraQueryParameters value is in the URL (not overwritten) + expect(url.includes("sharedParam=queryValue")).toBe(true); + expect(url.includes("sharedParam=bodyValue")).toBe(false); + // Verify extraParameters value is in the body + expect(body).toContain("sharedParam=bodyValue"); + // Verify the body doesn't contain the query value + expect(body.includes("sharedParam=queryValue")).toBe(false); + done(); + } catch (error) { + done(error); + } + }); + + const client = new RefreshTokenClient( + config, + stubPerformanceClient + ); + + const refreshTokenRequest: CommonRefreshTokenRequest = { + scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + refreshToken: TEST_TOKENS.REFRESH_TOKEN, + claims: TEST_CONFIG.CLAIMS, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + authenticationScheme: + TEST_CONFIG.TOKEN_TYPE_BEARER as Constants.AuthenticationScheme, + extraQueryParameters: { + sharedParam: "queryValue", + uniqueQueryParam: "uniqueQueryValue", + }, + extraParameters: { + sharedParam: "bodyValue", + uniqueBodyParam: "uniqueBodyValue", + }, + }; + + client.acquireToken(refreshTokenRequest).catch((error) => { + // Catch errors thrown after the function call this test is testing + }); + }); + it("acquireTokenByRefreshToken refreshes a token", async () => { jest.spyOn( RefreshTokenClient.prototype, @@ -1580,7 +1809,7 @@ describe("RefreshTokenClient unit tests", () => { ); }); - it("broker params take precedence over token body params", async () => { + it("broker params take precedence over extra params", async () => { const config: ClientConfiguration = await ClientTestUtils.createTestClientConfiguration(); const client = new RefreshTokenClient( @@ -1594,7 +1823,7 @@ describe("RefreshTokenClient unit tests", () => { scopes: ["User.Read"], redirectUri: "localhost", embeddedClientId: "child_client_id_1", - tokenBodyParameters: { + extraParameters: { client_id: "child_client_id_2", brk_client_id: "broker_client_id_2", brk_redirect_uri: "broker_redirect_uri_2", diff --git a/lib/msal-common/test/protocol/Authorize.spec.ts b/lib/msal-common/test/protocol/Authorize.spec.ts index 73b0b8691d..f676f718eb 100644 --- a/lib/msal-common/test/protocol/Authorize.spec.ts +++ b/lib/msal-common/test/protocol/Authorize.spec.ts @@ -1287,7 +1287,7 @@ describe("Authorize Protocol Tests", () => { request, new Logger({}) ); - RequestParameterBuilder.addExtraQueryParameters( + RequestParameterBuilder.addExtraParameters( params, request.extraQueryParameters! ); @@ -1319,7 +1319,7 @@ describe("Authorize Protocol Tests", () => { request, new Logger({}) ); - RequestParameterBuilder.addExtraQueryParameters( + RequestParameterBuilder.addExtraParameters( params, request.extraQueryParameters! ); @@ -1379,7 +1379,7 @@ describe("Authorize Protocol Tests", () => { request, new Logger({}) ); - RequestParameterBuilder.addExtraQueryParameters( + RequestParameterBuilder.addExtraParameters( params, request.extraQueryParameters! ); diff --git a/lib/msal-common/test/request/RequestParameterBuilder.spec.ts b/lib/msal-common/test/request/RequestParameterBuilder.spec.ts index d6606b7b55..1a5e19f6e7 100644 --- a/lib/msal-common/test/request/RequestParameterBuilder.spec.ts +++ b/lib/msal-common/test/request/RequestParameterBuilder.spec.ts @@ -223,7 +223,7 @@ describe("RequestParameterBuilder unit tests", () => { it("Encodes extra params", () => { const parameters = new Map(); - RequestParameterBuilder.addExtraQueryParameters(parameters, { + RequestParameterBuilder.addExtraParameters(parameters, { extra_params: "param1,param2", }); @@ -648,7 +648,7 @@ describe("RequestParameterBuilder unit tests", () => { }); }); - describe("addExtraQueryParameters tests", () => { + describe("addExtraParameters tests", () => { it("adds extra query parameters to the request", () => { const parameters = new Map(); RequestParameterBuilder.addClientId( @@ -660,7 +660,7 @@ describe("RequestParameterBuilder unit tests", () => { testKey2: "testVal2", }; - RequestParameterBuilder.addExtraQueryParameters(parameters, eqp); + RequestParameterBuilder.addExtraParameters(parameters, eqp); const expectedString = `client_id=${TEST_CONFIG.MSAL_CLIENT_ID}&testKey1=testVal1&testKey2=testVal2`; expect(UrlUtils.mapToQueryString(parameters)).toBe(expectedString); @@ -678,7 +678,7 @@ describe("RequestParameterBuilder unit tests", () => { testKey3: "", }; - RequestParameterBuilder.addExtraQueryParameters(parameters, eqp); + RequestParameterBuilder.addExtraParameters(parameters, eqp); const expectedString = `client_id=${TEST_CONFIG.MSAL_CLIENT_ID}&testKey1=testVal1&testKey2=testVal2`; expect(UrlUtils.mapToQueryString(parameters)).toBe(expectedString); @@ -696,7 +696,7 @@ describe("RequestParameterBuilder unit tests", () => { client_id: "some-other-client-id", }; - RequestParameterBuilder.addExtraQueryParameters(parameters, eqp); + RequestParameterBuilder.addExtraParameters(parameters, eqp); const expectedString = `client_id=${TEST_CONFIG.MSAL_CLIENT_ID}&testKey1=testVal1&testKey2=testVal2`; expect(UrlUtils.mapToQueryString(parameters)).toBe(expectedString); @@ -714,7 +714,7 @@ describe("RequestParameterBuilder unit tests", () => { client_id: "some-other-client-id", }; - RequestParameterBuilder.addExtraQueryParameters(parameters, eqp); + RequestParameterBuilder.addExtraParameters(parameters, eqp); expect(Object.keys(eqp)).toEqual([ "testKey1", @@ -790,7 +790,7 @@ describe("RequestParameterBuilder unit tests", () => { TEST_CONFIG.CORRELATION_ID ); - RequestParameterBuilder.addExtraQueryParameters(parameters, { + RequestParameterBuilder.addExtraParameters(parameters, { client_id: "embedded-client-id", }); RequestParameterBuilder.instrumentBrokerParams( diff --git a/lib/msal-node/src/client/DeviceCodeClient.ts b/lib/msal-node/src/client/DeviceCodeClient.ts index 8ad4634f69..b36e16ca42 100644 --- a/lib/msal-node/src/client/DeviceCodeClient.ts +++ b/lib/msal-node/src/client/DeviceCodeClient.ts @@ -116,7 +116,7 @@ export class DeviceCodeClient extends BaseClient { const parameters = new Map(); if (request.extraQueryParameters) { - RequestParameterBuilder.addExtraQueryParameters( + RequestParameterBuilder.addExtraParameters( parameters, request.extraQueryParameters ); @@ -183,7 +183,7 @@ export class DeviceCodeClient extends BaseClient { ); if (request.extraQueryParameters) { - RequestParameterBuilder.addExtraQueryParameters( + RequestParameterBuilder.addExtraParameters( parameters, request.extraQueryParameters ); diff --git a/lib/msal-node/src/client/PublicClientApplication.ts b/lib/msal-node/src/client/PublicClientApplication.ts index 05c39b629c..b46e564eae 100644 --- a/lib/msal-node/src/client/PublicClientApplication.ts +++ b/lib/msal-node/src/client/PublicClientApplication.ts @@ -166,7 +166,7 @@ export class PublicClientApplication correlationId: correlationId, extraParameters: { ...remainingProperties.extraQueryParameters, - ...remainingProperties.tokenQueryParameters, + ...remainingProperties.extraParameters, [AADServerParamKeys.X_CLIENT_EXTRA_SKU]: this.skus, }, accountId: remainingProperties.account?.nativeAccountId, @@ -261,7 +261,8 @@ export class PublicClientApplication authority: request.authority || this.config.auth.authority, correlationId: correlationId, extraParameters: { - ...request.tokenQueryParameters, + ...request.extraQueryParameters, + ...request.extraParameters, [AADServerParamKeys.X_CLIENT_EXTRA_SKU]: this.skus, }, accountId: request.account.nativeAccountId, diff --git a/lib/msal-node/src/config/ManagedIdentityRequestParameters.ts b/lib/msal-node/src/config/ManagedIdentityRequestParameters.ts index 93bdeb3e45..8b439a24e6 100644 --- a/lib/msal-node/src/config/ManagedIdentityRequestParameters.ts +++ b/lib/msal-node/src/config/ManagedIdentityRequestParameters.ts @@ -38,7 +38,7 @@ export class ManagedIdentityRequestParameters { const parameters = new Map(); if (this.queryParameters) { - RequestParameterBuilder.addExtraQueryParameters( + RequestParameterBuilder.addExtraParameters( parameters, this.queryParameters ); @@ -56,7 +56,7 @@ export class ManagedIdentityRequestParameters { const parameters = new Map(); if (this.bodyParameters) { - RequestParameterBuilder.addExtraQueryParameters( + RequestParameterBuilder.addExtraParameters( parameters, this.bodyParameters ); diff --git a/lib/msal-node/src/protocol/Authorize.ts b/lib/msal-node/src/protocol/Authorize.ts index c028082c19..9c093165de 100644 --- a/lib/msal-node/src/protocol/Authorize.ts +++ b/lib/msal-node/src/protocol/Authorize.ts @@ -63,7 +63,7 @@ export function getAuthCodeRequestUrl( ); } - RequestParameterBuilder.addExtraQueryParameters( + RequestParameterBuilder.addExtraParameters( parameters, request.extraQueryParameters || {} ); diff --git a/lib/msal-node/src/request/AuthorizationCodeRequest.ts b/lib/msal-node/src/request/AuthorizationCodeRequest.ts index bc3b9c36d2..b3c8467a95 100644 --- a/lib/msal-node/src/request/AuthorizationCodeRequest.ts +++ b/lib/msal-node/src/request/AuthorizationCodeRequest.ts @@ -13,7 +13,8 @@ import { CommonAuthorizationCodeRequest } from "@azure/msal-common/node"; * - authority: - URL of the authority, the security token service (STS) from which MSAL will acquire tokens. If authority is set on client application object, this will override that value. Overriding the value will cause for authority validation to happen each time. If the same authority will be used for all request, set on the application object instead of the requests. * - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes. * - redirectUri - The redirect URI of your app, where the authority will redirect to after the user inputs credentials and consents. It must exactly match one of the redirect URIs you registered in the portal. - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests * - code - The authorization_code that the user acquired in the first leg of the flow. * - codeVerifier - The same code_verifier that was used to obtain the authorization_code. Required if PKCE was used in the authorization code grant request.For more information, see the PKCE RFC: https://tools.ietf.org/html/rfc7636 * - state - Unique GUID generated by the user that is cached by the user and sent to the server during the first leg of the flow. This string is sent back by the server with the authorization code. The user cached state is then compared with the state received from the server to mitigate the risk of CSRF attacks. See https://datatracker.ietf.org/doc/html/rfc6819#section-3.6. diff --git a/lib/msal-node/src/request/AuthorizationUrlRequest.ts b/lib/msal-node/src/request/AuthorizationUrlRequest.ts index 3e48e7215b..55a1fa8915 100644 --- a/lib/msal-node/src/request/AuthorizationUrlRequest.ts +++ b/lib/msal-node/src/request/AuthorizationUrlRequest.ts @@ -28,8 +28,8 @@ import { CommonAuthorizationUrlRequest } from "@azure/msal-common/node"; * - loginHint - Can be used to pre-fill the username/email address field of the sign-in page for the user, if you know the username/email address ahead of time. Often apps use this parameter during re-authentication, having already extracted the username from a previous sign-in using the preferred_username claim. * - sid - Session ID, unique identifier for the session. Available as an optional claim on ID tokens. * - domainHint - Provides a hint about the tenant or domain that the user should use to sign in. The value of the domain hint is a registered domain for the tenant. - * - extraQueryParameters - String to string map of custom query parameters added to the /authorize call - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests * - nonce - A value included in the request that is returned in the id token. A randomly generated unique value is typically used to mitigate replay attacks. * @public */ diff --git a/lib/msal-node/src/request/ClientCredentialRequest.ts b/lib/msal-node/src/request/ClientCredentialRequest.ts index 8db27d3843..9f2b41a521 100644 --- a/lib/msal-node/src/request/ClientCredentialRequest.ts +++ b/lib/msal-node/src/request/ClientCredentialRequest.ts @@ -13,7 +13,8 @@ import { CommonClientCredentialRequest } from "./CommonClientCredentialRequest.j * - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes. * - skipCache - Skip token cache lookup and force request to authority to get a a new token. Defaults to false. * - clientAssertion - An assertion string or a callback function that returns an assertion string (both are Base64Url-encoded signed JWTs) used in the Client Credential flow - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests * @public */ export type ClientCredentialRequest = Partial< diff --git a/lib/msal-node/src/request/CommonClientCredentialRequest.ts b/lib/msal-node/src/request/CommonClientCredentialRequest.ts index 579041935f..6a43e5b3d7 100644 --- a/lib/msal-node/src/request/CommonClientCredentialRequest.ts +++ b/lib/msal-node/src/request/CommonClientCredentialRequest.ts @@ -16,7 +16,10 @@ import { * - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes. * - skipCache - Skip token cache lookup and force request to authority to get a a new token. Defaults to false. * - preferredAzureRegionOptions - Options of the user's preferred azure region - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - clientAssertion - An assertion string or a callback function that returns an assertion string (both are Base64Url-encoded signed JWTs) used in the Client Credential flow + * - azureRegion - Azure region to be used for regional authentication + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests */ export type CommonClientCredentialRequest = BaseAuthRequest & { skipCache?: boolean; diff --git a/lib/msal-node/src/request/CommonDeviceCodeRequest.ts b/lib/msal-node/src/request/CommonDeviceCodeRequest.ts index 1509936c3a..1a5e2a1518 100644 --- a/lib/msal-node/src/request/CommonDeviceCodeRequest.ts +++ b/lib/msal-node/src/request/CommonDeviceCodeRequest.ts @@ -19,13 +19,10 @@ import { * - resourceRequestMethod - HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows. * - resourceRequestUri - URI that token will be used for. Used for proof-of-possession flows. * - timeout - Timeout period in seconds which the user explicitly configures for the polling of the device code endpoint. At the end of this period; assuming the device code has not expired yet; the device code polling is stopped and the request cancelled. The device code expiration window will always take precedence over this set period. - * - extraQueryParameters - String to string map of custom query parameters added to the query string + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests */ -// export type CommonDeviceCodeRequest = BaseAuthRequest & { -export type CommonDeviceCodeRequest = Omit< - BaseAuthRequest, - "tokenQueryParameters" | "tokenBodyParameters" -> & { +export type CommonDeviceCodeRequest = BaseAuthRequest & { deviceCodeCallback: (response: DeviceCodeResponse) => void; cancel?: boolean; timeout?: number; diff --git a/lib/msal-node/src/request/CommonOnBehalfOfRequest.ts b/lib/msal-node/src/request/CommonOnBehalfOfRequest.ts index 9a5826fb1f..d610e88246 100644 --- a/lib/msal-node/src/request/CommonOnBehalfOfRequest.ts +++ b/lib/msal-node/src/request/CommonOnBehalfOfRequest.ts @@ -11,7 +11,8 @@ import { BaseAuthRequest } from "@azure/msal-common/node"; * - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes. * - oboAssertion - The access token that was sent to the middle-tier API. This token must have an audience of the app making this OBO request. * - skipCache - Skip token cache lookup and force request to authority to get a a new token. Defaults to false. - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests */ export type CommonOnBehalfOfRequest = BaseAuthRequest & { oboAssertion: string; diff --git a/lib/msal-node/src/request/CommonUsernamePasswordRequest.ts b/lib/msal-node/src/request/CommonUsernamePasswordRequest.ts index 4857b4ac96..f092fa6efe 100644 --- a/lib/msal-node/src/request/CommonUsernamePasswordRequest.ts +++ b/lib/msal-node/src/request/CommonUsernamePasswordRequest.ts @@ -15,7 +15,8 @@ import { BaseAuthRequest } from "@azure/msal-common/node"; * - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes. * - username - username of the client * - password - credentials - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests */ export type CommonUsernamePasswordRequest = BaseAuthRequest & { username: string; diff --git a/lib/msal-node/src/request/OnBehalfOfRequest.ts b/lib/msal-node/src/request/OnBehalfOfRequest.ts index 4f3c7e4b17..5d916c8c24 100644 --- a/lib/msal-node/src/request/OnBehalfOfRequest.ts +++ b/lib/msal-node/src/request/OnBehalfOfRequest.ts @@ -11,7 +11,8 @@ import { CommonOnBehalfOfRequest } from "./CommonOnBehalfOfRequest.js"; * - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes. * - oboAssertion - The access token that was sent to the middle-tier API. This token must have an audience of the app making this OBO request. * - skipCache - Skip token cache lookup and force request to authority to get a a new token. Defaults to false. - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests * @public */ export type OnBehalfOfRequest = Partial< diff --git a/lib/msal-node/src/request/RefreshTokenRequest.ts b/lib/msal-node/src/request/RefreshTokenRequest.ts index 965c004d6a..6c11a6deec 100644 --- a/lib/msal-node/src/request/RefreshTokenRequest.ts +++ b/lib/msal-node/src/request/RefreshTokenRequest.ts @@ -12,7 +12,8 @@ import { CommonRefreshTokenRequest } from "@azure/msal-common/node"; * - authority - URL of the authority, the security token service (STS) from which MSAL will acquire tokens. * - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes. * - refreshToken - A refresh token returned from a previous request to the Identity provider. - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests * - forceCache - Force MSAL to cache a refresh token flow response when there is no account in the cache. Used for migration scenarios. * @public */ diff --git a/lib/msal-node/src/request/SilentFlowRequest.ts b/lib/msal-node/src/request/SilentFlowRequest.ts index 50e040400f..0b3b993ecb 100644 --- a/lib/msal-node/src/request/SilentFlowRequest.ts +++ b/lib/msal-node/src/request/SilentFlowRequest.ts @@ -11,7 +11,8 @@ import { AccountInfo, CommonSilentFlowRequest } from "@azure/msal-common/node"; * - claims - A stringified claims request which will be added to all /authorize and /token calls. When included on a silent request, cache lookup will be skipped and token will be refreshed. * - authority - Url of the authority which the application acquires tokens from. * - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes. - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests * - account - Account entity to lookup the credentials. * - forceRefresh - Forces silent requests to make network calls if true. * @public diff --git a/lib/msal-node/src/request/UsernamePasswordRequest.ts b/lib/msal-node/src/request/UsernamePasswordRequest.ts index 565fb9c94e..37d562ea6a 100644 --- a/lib/msal-node/src/request/UsernamePasswordRequest.ts +++ b/lib/msal-node/src/request/UsernamePasswordRequest.ts @@ -15,7 +15,8 @@ import { CommonUsernamePasswordRequest } from "./CommonUsernamePasswordRequest.j * - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes. * - username - username of the client * - password - credentials - * - tokenQueryParameters - String to string map of custom query parameters added to the /token call + * - extraQueryParameters - String to string map of custom query parameters added to outgoing token service requests + * - extraParameters - String to string map of custom query parameters added to outgoing token service requests * @public */ export type UsernamePasswordRequest = Partial< diff --git a/lib/msal-node/test/client/ClientCredentialClient.spec.ts b/lib/msal-node/test/client/ClientCredentialClient.spec.ts index 7a96b5b65e..ba417fca1f 100644 --- a/lib/msal-node/test/client/ClientCredentialClient.spec.ts +++ b/lib/msal-node/test/client/ClientCredentialClient.spec.ts @@ -121,7 +121,7 @@ describe("ClientCredentialClient unit tests", () => { checkMockedNetworkRequest(returnVal, checks); }); - it("Adds tokenQueryParameters to the /token request", async () => { + it("Adds extraQueryParameters to the /token request", async () => { const badExecutePostToTokenEndpointMock = jest.spyOn( ClientCredentialClient.prototype, "executePostToTokenEndpoint" @@ -138,7 +138,7 @@ describe("ClientCredentialClient unit tests", () => { authority: TEST_CONFIG.validAuthority, correlationId: TEST_CONFIG.CORRELATION_ID, scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - tokenQueryParameters: { + extraQueryParameters: { testParam1: "testValue1", testParam2: "", testParam3: "testValue3", diff --git a/lib/msal-node/test/client/OnBehalfOfClient.spec.ts b/lib/msal-node/test/client/OnBehalfOfClient.spec.ts index 699813866e..987803421a 100644 --- a/lib/msal-node/test/client/OnBehalfOfClient.spec.ts +++ b/lib/msal-node/test/client/OnBehalfOfClient.spec.ts @@ -231,7 +231,7 @@ describe("OnBehalfOf unit tests", () => { ); }); - it("Adds tokenQueryParameters to the /token request", async () => { + it("Adds extraQueryParameters to the /token request", async () => { const badExecutePostToTokenEndpointMock = jest.spyOn( OnBehalfOfClient.prototype, "executePostToTokenEndpoint" @@ -249,7 +249,7 @@ describe("OnBehalfOf unit tests", () => { oboAssertion: "user_assertion_hash", skipCache: true, claims: TEST_CONFIG.CLAIMS, - tokenQueryParameters: { + extraQueryParameters: { testParam1: "testValue1", testParam2: "", testParam3: "testValue3", diff --git a/lib/msal-node/test/client/PublicClientApplication.spec.ts b/lib/msal-node/test/client/PublicClientApplication.spec.ts index 55a3eb2e17..51a4a0a812 100644 --- a/lib/msal-node/test/client/PublicClientApplication.spec.ts +++ b/lib/msal-node/test/client/PublicClientApplication.spec.ts @@ -560,7 +560,7 @@ describe("PublicClientApplication", () => { ); }); - it("Adds tokenQueryParameters to the /token request", (done) => { + it("Adds extraQueryParameters to the /token request", (done) => { AUTHENTICATION_RESULT.body.client_info = TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO; jest.spyOn( @@ -618,7 +618,7 @@ describe("PublicClientApplication", () => { authority: TEST_CONFIG.validAuthority, correlationId: TEST_CONFIG.CORRELATION_ID, forceRefresh: false, - tokenQueryParameters: { + extraQueryParameters: { testParam1: "testValue1", testParam2: "", testParam3: "testValue3", diff --git a/lib/msal-node/test/client/UsernamePasswordClient.spec.ts b/lib/msal-node/test/client/UsernamePasswordClient.spec.ts index ac342d4128..220cb761ba 100644 --- a/lib/msal-node/test/client/UsernamePasswordClient.spec.ts +++ b/lib/msal-node/test/client/UsernamePasswordClient.spec.ts @@ -116,7 +116,7 @@ describe("Username Password unit tests", () => { checkMockedNetworkRequest(returnVal, checks); }); - it("Adds tokenQueryParameters to the /token request", async () => { + it("Adds extraQueryParameters to the /token request", async () => { const badExecutePostToTokenEndpointMock = jest.spyOn( UsernamePasswordClient.prototype, "executePostToTokenEndpoint" @@ -136,7 +136,7 @@ describe("Username Password unit tests", () => { password: MOCK_PASSWORD, claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, - tokenQueryParameters: { + extraQueryParameters: { testParam1: "testValue1", testParam2: "", testParam3: "testValue3", diff --git a/samples/msal-browser-samples/ExpressSample/server.js b/samples/msal-browser-samples/ExpressSample/server.js index 3fd0e7cd51..4443e5df5b 100644 --- a/samples/msal-browser-samples/ExpressSample/server.js +++ b/samples/msal-browser-samples/ExpressSample/server.js @@ -49,6 +49,11 @@ let availableVersions = { path: '/lib/msal-browser/msal-browser.min.js', description: 'Locally built version from repository' }, + 'local-debug': { + name: 'Local Build (Debug)', + path: '/lib/msal-browser/msal-browser.js', + description: 'Locally built debug version from repository' + }, 'latest': { name: 'Latest', path: 'https://cdn.jsdelivr.net/npm/@azure/msal-browser@latest/lib/msal-browser.min.js', diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js index 672de7e794..7c50feefe7 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/auth.js @@ -85,8 +85,11 @@ function signOut(signOutType) { } } -async function getTokenPopup() { - const request = requestConfig; +async function getTokenPopup(method = "GET") { + const request = { ...requestConfig }; + if (method === "POST") { + request.httpMethod = "POST"; + } const currentAcc = myMSALObj.getActiveAccount(); if (currentAcc) { request.account = currentAcc; @@ -96,8 +99,11 @@ async function getTokenPopup() { } } -async function getTokenRedirect() { - const request = requestConfig; +async function getTokenRedirect(method = "GET") { + const request = { ...requestConfig }; + if (method === "POST") { + request.httpMethod = "POST"; + } const currentAcc = myMSALObj.getActiveAccount(); if (currentAcc) { request.account = currentAcc; diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/index.html b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/index.html index 8b100eb5ac..148c2d4c00 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/index.html +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/index.html @@ -40,10 +40,17 @@
Please sign-in to see your profile an

+ onclick="getTokenRedirect()">acquireTokenRedirect (GET)

- + +
+
+ +
+
+