/*! @azure/msal-browser v5.7.0 2026-04-16 */
|
'use strict';
|
import { Constants, invokeAsync, ProtocolMode, PerformanceEvents, invoke, AuthError, UrlString, ProtocolUtils } from '@azure/msal-common/browser';
|
import { StandardInteractionClient, initializeAuthorizationRequest } from './StandardInteractionClient.mjs';
|
import { StandardInteractionClientInitializeAuthorizationRequest, GeneratePkceCodes, StandardInteractionClientCreateAuthCodeClient, DeserializeResponse, HandleResponseCode, StandardInteractionClientGetDiscoveredAuthority, GenerateEarKey, SilentHandlerMonitorIframeForHash, HandleResponseEar } from '../telemetry/BrowserPerformanceEvents.mjs';
|
import { EventType } from '../event/EventType.mjs';
|
import { InteractionType, ApiId, BrowserConstants } from '../utils/BrowserConstants.mjs';
|
import { preconnect, waitForBridgeResponse, getCurrentUri } from '../utils/BrowserUtils.mjs';
|
import { createBrowserAuthError } from '../error/BrowserAuthError.mjs';
|
import { deserializeResponse } from '../response/ResponseHandler.mjs';
|
import { getAuthCodeRequestUrl, handleResponseCode, getEARForm, handleResponseEAR, getCodeForm } from '../protocol/Authorize.mjs';
|
import { generatePkceCodes } from '../crypto/PkceGenerator.mjs';
|
import { isPlatformAuthAllowed } from '../broker/nativeBroker/PlatformAuthProvider.mjs';
|
import { generateEarKey } from '../crypto/BrowserCrypto.mjs';
|
import { initializeServerTelemetryManager, getDiscoveredAuthority, clearCacheOnLogout } from './BaseInteractionClient.mjs';
|
import { validateRequestMethod } from '../request/RequestHelpers.mjs';
|
import { emptyNavigateUri, emptyWindowError, popupWindowError } from '../error/BrowserAuthErrorCodes.mjs';
|
|
/*
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Licensed under the MIT License.
|
*/
|
class PopupClient extends StandardInteractionClient {
|
constructor(config, storageImpl, browserCrypto, logger, eventHandler, navigationClient, performanceClient, nativeStorageImpl, correlationId, platformAuthHandler) {
|
super(config, storageImpl, browserCrypto, logger, eventHandler, navigationClient, performanceClient, correlationId, platformAuthHandler);
|
this.nativeStorage = nativeStorageImpl;
|
this.eventHandler = eventHandler;
|
}
|
/**
|
* Acquires tokens by opening a popup window to the /authorize endpoint of the authority
|
* @param request
|
* @param pkceCodes
|
*/
|
acquireToken(request, pkceCodes) {
|
let popupParams = undefined;
|
try {
|
const popupName = this.generatePopupName(request.scopes || Constants.OIDC_DEFAULT_SCOPES, request.authority || this.config.auth.authority);
|
popupParams = {
|
popupName,
|
popupWindowAttributes: request.popupWindowAttributes || {},
|
popupWindowParent: request.popupWindowParent ?? window,
|
};
|
this.performanceClient.addFields({ isAsyncPopup: !this.config.system.navigatePopups }, this.correlationId);
|
// navigatePopups flag is false. Acquires token without first opening popup. Popup will be opened later asynchronously.
|
if (!this.config.system.navigatePopups) {
|
this.logger.verbose("162h4u", this.correlationId);
|
// Passes on popup position and dimensions if in request
|
return this.acquireTokenPopupAsync(request, popupParams, pkceCodes);
|
}
|
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 = {
|
...request,
|
httpMethod: validateRequestMethod(request, this.config.system.protocolMode),
|
};
|
this.logger.verbose("1f9ok3", this.correlationId);
|
popupParams.popup = this.openSizedPopup("about:blank", popupParams);
|
return this.acquireTokenPopupAsync(validatedRequest, popupParams, pkceCodes);
|
}
|
}
|
catch (e) {
|
return Promise.reject(e);
|
}
|
}
|
/**
|
* Clears local cache for the current user then opens a popup window prompting the user to sign-out of the server
|
* @param logoutRequest
|
*/
|
logout(logoutRequest) {
|
try {
|
this.logger.verbose("068rup", this.correlationId);
|
const validLogoutRequest = this.initializeLogoutRequest(logoutRequest);
|
const popupParams = {
|
popupName: this.generateLogoutPopupName(validLogoutRequest),
|
popupWindowAttributes: logoutRequest?.popupWindowAttributes || {},
|
popupWindowParent: logoutRequest?.popupWindowParent ?? window,
|
};
|
const authority = logoutRequest && logoutRequest.authority;
|
const mainWindowRedirectUri = logoutRequest && logoutRequest.mainWindowRedirectUri;
|
// navigatePopups flag set to false. Acquires token without first opening popup. Popup will be opened later asynchronously.
|
if (!this.config.system.navigatePopups) {
|
this.logger.verbose("1phd8u", this.correlationId);
|
// Passes on popup position and dimensions if in request
|
return this.logoutPopupAsync(validLogoutRequest, popupParams, authority, mainWindowRedirectUri);
|
}
|
else {
|
// navigatePopups flag is set to true. Opens popup before logging out.
|
this.logger.verbose("1a28da", this.correlationId);
|
popupParams.popup = this.openSizedPopup("about:blank", popupParams);
|
return this.logoutPopupAsync(validLogoutRequest, popupParams, authority, mainWindowRedirectUri);
|
}
|
}
|
catch (e) {
|
// Since this function is synchronous we need to reject
|
return Promise.reject(e);
|
}
|
}
|
/**
|
* Helper which obtains an access_token for your API via opening a popup window in the user's browser
|
* @param request
|
* @param popupParams
|
* @param pkceCodes
|
*
|
* @returns A promise that is fulfilled when this function has completed, or rejected if an error was raised.
|
*/
|
async acquireTokenPopupAsync(request, popupParams, pkceCodes) {
|
this.logger.verbose("1g77pg", this.correlationId);
|
const validRequest = await invokeAsync(initializeAuthorizationRequest, StandardInteractionClientInitializeAuthorizationRequest, this.logger, this.performanceClient, this.correlationId)(request, InteractionType.Popup, this.config, this.browserCrypto, this.browserStorage, this.logger, this.performanceClient, this.correlationId);
|
/*
|
* Skip pre-connect for async popups to reduce time between user interaction and popup window creation to avoid
|
* popup from being blocked by browsers with shorter popup timers
|
*/
|
if (popupParams.popup) {
|
preconnect(validRequest.authority);
|
}
|
const isPlatformBroker = isPlatformAuthAllowed(this.config, this.logger, this.correlationId, this.platformAuthProvider, request.authenticationScheme);
|
validRequest.platformBroker = isPlatformBroker;
|
if (this.config.system.protocolMode === ProtocolMode.EAR) {
|
return this.executeEarFlow(validRequest, popupParams, pkceCodes);
|
}
|
else {
|
return this.executeCodeFlow(validRequest, popupParams, pkceCodes);
|
}
|
}
|
/**
|
* Executes auth code + PKCE flow
|
* @param request
|
* @param popupParams
|
* @param pkceCodes
|
* @returns
|
*/
|
async executeCodeFlow(request, popupParams, pkceCodes) {
|
const correlationId = request.correlationId;
|
const serverTelemetryManager = initializeServerTelemetryManager(ApiId.acquireTokenPopup, this.config.auth.clientId, this.correlationId, this.browserStorage, this.logger);
|
const pkce = pkceCodes ||
|
(await invokeAsync(generatePkceCodes, GeneratePkceCodes, this.logger, this.performanceClient, correlationId)(this.performanceClient, this.logger, correlationId));
|
const popupRequest = {
|
...request,
|
codeChallenge: pkce.challenge,
|
};
|
try {
|
// Initialize the client
|
const authClient = await invokeAsync(this.createAuthCodeClient.bind(this), StandardInteractionClientCreateAuthCodeClient, this.logger, this.performanceClient, correlationId)({
|
serverTelemetryManager,
|
requestAuthority: popupRequest.authority,
|
requestAzureCloudOptions: popupRequest.azureCloudOptions,
|
requestExtraQueryParameters: popupRequest.extraQueryParameters,
|
account: popupRequest.account,
|
});
|
if (popupRequest.httpMethod === Constants.HttpMethod.POST) {
|
return await this.executeCodeFlowWithPost(popupRequest, popupParams, authClient, pkce.verifier);
|
}
|
else {
|
// Create acquire token url.
|
const navigateUrl = await invokeAsync(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 = this.initiateAuthRequest(navigateUrl, popupParams);
|
this.eventHandler.emitEvent(EventType.POPUP_OPENED, correlationId, InteractionType.Popup, { popupWindow }, null);
|
// Wait for the redirect bridge response
|
const responseString = await waitForBridgeResponse(this.config.system.popupBridgeTimeout, this.logger, this.browserCrypto, request, this.performanceClient);
|
const serverParams = invoke(deserializeResponse, DeserializeResponse, this.logger, this.performanceClient, this.correlationId)(responseString, this.config.auth.OIDCOptions.responseMode, this.logger, this.correlationId);
|
return await invokeAsync(handleResponseCode, 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();
|
if (e instanceof AuthError) {
|
e.setCorrelationId(this.correlationId);
|
serverTelemetryManager.cacheFailedRequest(e);
|
}
|
throw e;
|
}
|
}
|
/**
|
* Executes EAR flow
|
* @param request
|
*/
|
async executeEarFlow(request, popupParams, pkceCodes) {
|
const { correlationId, authority, azureCloudOptions, extraQueryParameters, account, } = request;
|
// Get the frame handle for the silent request
|
const discoveredAuthority = await invokeAsync(getDiscoveredAuthority, StandardInteractionClientGetDiscoveredAuthority, this.logger, this.performanceClient, correlationId)(this.config, this.correlationId, this.performanceClient, this.browserStorage, this.logger, authority, azureCloudOptions, extraQueryParameters, account);
|
const earJwk = await invokeAsync(generateEarKey, GenerateEarKey, this.logger, this.performanceClient, correlationId)();
|
const pkce = pkceCodes ||
|
(await invokeAsync(generatePkceCodes, GeneratePkceCodes, this.logger, this.performanceClient, correlationId)(this.performanceClient, this.logger, correlationId));
|
const popupRequest = {
|
...request,
|
earJwk: earJwk,
|
codeChallenge: pkce.challenge,
|
};
|
const popupWindow = popupParams.popup || this.openPopup("about:blank", popupParams);
|
const form = await getEARForm(popupWindow.document, this.config, discoveredAuthority, popupRequest, 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(waitForBridgeResponse, SilentHandlerMonitorIframeForHash, this.logger, this.performanceClient, correlationId)(this.config.system.popupBridgeTimeout, this.logger, this.browserCrypto, popupRequest, this.performanceClient);
|
const serverParams = invoke(deserializeResponse, DeserializeResponse, this.logger, this.performanceClient, this.correlationId)(responseString, this.config.auth.OIDCOptions.responseMode, this.logger, this.correlationId);
|
if (!serverParams.ear_jwe && serverParams.code) {
|
const authClient = await invokeAsync(this.createAuthCodeClient.bind(this), StandardInteractionClientCreateAuthCodeClient, this.logger, this.performanceClient, correlationId)({
|
serverTelemetryManager: initializeServerTelemetryManager(ApiId.acquireTokenPopup, this.config.auth.clientId, correlationId, this.browserStorage, this.logger),
|
requestAuthority: request.authority,
|
requestAzureCloudOptions: request.azureCloudOptions,
|
requestExtraQueryParameters: request.extraQueryParameters,
|
account: request.account,
|
authority: discoveredAuthority,
|
});
|
return invokeAsync(handleResponseCode, HandleResponseCode, this.logger, this.performanceClient, correlationId)(popupRequest, serverParams, pkce.verifier, ApiId.acquireTokenPopup, this.config, authClient, this.browserStorage, this.nativeStorage, this.eventHandler, this.logger, this.performanceClient, this.platformAuthProvider);
|
}
|
else {
|
return invokeAsync(handleResponseEAR, HandleResponseEar, this.logger, this.performanceClient, correlationId)(popupRequest, serverParams, ApiId.acquireTokenPopup, this.config, discoveredAuthority, this.browserStorage, this.nativeStorage, this.eventHandler, this.logger, this.performanceClient, this.platformAuthProvider);
|
}
|
}
|
async executeCodeFlowWithPost(request, popupParams, authClient, pkceVerifier) {
|
const correlationId = request.correlationId;
|
// Get the frame handle for the silent request
|
const discoveredAuthority = await invokeAsync(getDiscoveredAuthority, 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 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(waitForBridgeResponse, SilentHandlerMonitorIframeForHash, this.logger, this.performanceClient, correlationId)(this.config.system.popupBridgeTimeout, this.logger, this.browserCrypto, request, this.performanceClient);
|
const serverParams = invoke(deserializeResponse, DeserializeResponse, this.logger, this.performanceClient, this.correlationId)(responseString, this.config.auth.OIDCOptions.responseMode, this.logger, this.correlationId);
|
return invokeAsync(handleResponseCode, 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
|
* @param popupName
|
* @param requestAuthority
|
* @param popup
|
* @param mainWindowRedirectUri
|
* @param popupWindowAttributes
|
*/
|
async logoutPopupAsync(validRequest, popupParams, requestAuthority, mainWindowRedirectUri) {
|
this.logger.verbose("0b7yrk", this.correlationId);
|
this.eventHandler.emitEvent(EventType.LOGOUT_START, this.correlationId, InteractionType.Popup, validRequest);
|
const serverTelemetryManager = initializeServerTelemetryManager(ApiId.logoutPopup, this.config.auth.clientId, this.correlationId, this.browserStorage, this.logger);
|
try {
|
// Clear cache on logout
|
await clearCacheOnLogout(this.browserStorage, this.browserCrypto, this.logger, this.correlationId, validRequest.account);
|
// Initialize the client
|
const authClient = await invokeAsync(this.createAuthCodeClient.bind(this), StandardInteractionClientCreateAuthCodeClient, this.logger, this.performanceClient, this.correlationId)({
|
serverTelemetryManager,
|
requestAuthority: requestAuthority,
|
account: validRequest.account || undefined,
|
});
|
try {
|
authClient.authority.endSessionEndpoint;
|
}
|
catch {
|
if (validRequest.account?.homeAccountId &&
|
validRequest.postLogoutRedirectUri &&
|
authClient.authority.protocolMode === ProtocolMode.OIDC) {
|
this.eventHandler.emitEvent(EventType.LOGOUT_SUCCESS, validRequest.correlationId, InteractionType.Popup, validRequest);
|
if (mainWindowRedirectUri) {
|
const navigationOptions = {
|
apiId: ApiId.logoutPopup,
|
timeout: this.config.system.redirectNavigationTimeout,
|
noHistory: false,
|
};
|
const absoluteUrl = UrlString.getAbsoluteUrl(mainWindowRedirectUri, getCurrentUri());
|
await this.navigationClient.navigateInternal(absoluteUrl, navigationOptions);
|
}
|
popupParams.popup?.close();
|
return;
|
}
|
}
|
// Redirect bridge requires "state" param to work properly
|
validRequest.state = ProtocolUtils.setRequestState(this.browserCrypto, validRequest.state || "", {
|
interactionType: InteractionType.Popup,
|
});
|
// Create logout string and navigate user window to logout.
|
const logoutUri = authClient.getLogoutUri(validRequest);
|
this.eventHandler.emitEvent(EventType.LOGOUT_SUCCESS, validRequest.correlationId, InteractionType.Popup, validRequest);
|
// Open the popup window to requestUrl.
|
const popupWindow = this.openPopup(logoutUri, popupParams);
|
this.eventHandler.emitEvent(EventType.POPUP_OPENED, validRequest.correlationId, InteractionType.Popup, { popupWindow }, null);
|
await waitForBridgeResponse(this.config.system.popupBridgeTimeout, this.logger, this.browserCrypto, validRequest, this.performanceClient).catch(() => {
|
// Swallow any errors related to monitoring the window. Server logout is best effort
|
});
|
if (mainWindowRedirectUri) {
|
const navigationOptions = {
|
apiId: ApiId.logoutPopup,
|
timeout: this.config.system.redirectNavigationTimeout,
|
noHistory: false,
|
};
|
const absoluteUrl = UrlString.getAbsoluteUrl(mainWindowRedirectUri, getCurrentUri());
|
this.logger.verbose("0qcur2", this.correlationId);
|
this.logger.verbosePii("0oj7lk", this.correlationId);
|
await this.navigationClient.navigateInternal(absoluteUrl, navigationOptions);
|
}
|
else {
|
this.logger.verbose("03zgcf", this.correlationId);
|
}
|
}
|
catch (e) {
|
// Close the synchronous popup if an error is thrown before the window unload event is registered
|
popupParams.popup?.close();
|
if (e instanceof AuthError) {
|
e.setCorrelationId(this.correlationId);
|
serverTelemetryManager.cacheFailedRequest(e);
|
}
|
this.eventHandler.emitEvent(EventType.LOGOUT_FAILURE, this.correlationId, InteractionType.Popup, null, e);
|
this.eventHandler.emitEvent(EventType.LOGOUT_END, this.correlationId, InteractionType.Popup);
|
throw e;
|
}
|
this.eventHandler.emitEvent(EventType.LOGOUT_END, this.correlationId, InteractionType.Popup);
|
}
|
/**
|
* Opens a popup window with given request Url.
|
* @param requestUrl
|
*/
|
initiateAuthRequest(requestUrl, params) {
|
// Check that request url is not empty.
|
if (requestUrl) {
|
this.logger.infoPii("1kcr9k", this.correlationId);
|
// Open the popup window to requestUrl.
|
return this.openPopup(requestUrl, params);
|
}
|
else {
|
// Throw error if request URL is empty.
|
this.logger.error("1l7hyp", this.correlationId);
|
throw createBrowserAuthError(emptyNavigateUri);
|
}
|
}
|
/**
|
* @hidden
|
*
|
* Configures popup window for login.
|
*
|
* @param urlNavigate
|
* @param title
|
* @param popUpWidth
|
* @param popUpHeight
|
* @param popupWindowAttributes
|
* @ignore
|
* @hidden
|
*/
|
openPopup(urlNavigate, popupParams) {
|
try {
|
let popupWindow;
|
// Popup window passed in, setting url to navigate to
|
if (popupParams.popup) {
|
popupWindow = popupParams.popup;
|
this.logger.verbosePii("0cgeo7", this.correlationId);
|
popupWindow.location.assign(urlNavigate);
|
}
|
else if (typeof popupParams.popup === "undefined") {
|
// Popup will be undefined if it was not passed in
|
this.logger.verbosePii("0c2awd", this.correlationId);
|
popupWindow = this.openSizedPopup(urlNavigate, popupParams);
|
}
|
// Popup will be null if popups are blocked
|
if (!popupWindow) {
|
throw createBrowserAuthError(emptyWindowError);
|
}
|
if (popupWindow.focus) {
|
popupWindow.focus();
|
}
|
this.currentWindow = popupWindow;
|
return popupWindow;
|
}
|
catch (e) {
|
this.logger.error("0dxfb9", this.correlationId);
|
throw createBrowserAuthError(popupWindowError);
|
}
|
}
|
/**
|
* Helper function to set popup window dimensions and position
|
* @param urlNavigate
|
* @param popupName
|
* @param popupWindowAttributes
|
* @returns
|
*/
|
openSizedPopup(urlNavigate, { popupName, popupWindowAttributes, popupWindowParent }) {
|
/**
|
* adding winLeft and winTop to account for dual monitor
|
* using screenLeft and screenTop for IE8 and earlier
|
*/
|
const winLeft = popupWindowParent.screenLeft
|
? popupWindowParent.screenLeft
|
: popupWindowParent.screenX;
|
const winTop = popupWindowParent.screenTop
|
? popupWindowParent.screenTop
|
: popupWindowParent.screenY;
|
/**
|
* window.innerWidth displays browser window"s height and width excluding toolbars
|
* using document.documentElement.clientWidth for IE8 and earlier
|
*/
|
const winWidth = popupWindowParent.innerWidth ||
|
document.documentElement.clientWidth ||
|
document.body.clientWidth;
|
const winHeight = popupWindowParent.innerHeight ||
|
document.documentElement.clientHeight ||
|
document.body.clientHeight;
|
let width = popupWindowAttributes.popupSize?.width;
|
let height = popupWindowAttributes.popupSize?.height;
|
let top = popupWindowAttributes.popupPosition?.top;
|
let left = popupWindowAttributes.popupPosition?.left;
|
if (!width || width < 0 || width > winWidth) {
|
this.logger.verbose("08vfmo", this.correlationId);
|
width = BrowserConstants.POPUP_WIDTH;
|
}
|
if (!height || height < 0 || height > winHeight) {
|
this.logger.verbose("09cxa0", this.correlationId);
|
height = BrowserConstants.POPUP_HEIGHT;
|
}
|
if (!top || top < 0 || top > winHeight) {
|
this.logger.verbose("1qh4wo", this.correlationId);
|
top = Math.max(0, winHeight / 2 - BrowserConstants.POPUP_HEIGHT / 2 + winTop);
|
}
|
if (!left || left < 0 || left > winWidth) {
|
this.logger.verbose("1sz3en", this.correlationId);
|
left = Math.max(0, winWidth / 2 - BrowserConstants.POPUP_WIDTH / 2 + winLeft);
|
}
|
return popupWindowParent.open(urlNavigate, popupName, `width=${width}, height=${height}, top=${top}, left=${left}, scrollbars=yes`);
|
}
|
/**
|
* Generates the name for the popup based on the client id and request
|
* @param clientId
|
* @param request
|
*/
|
generatePopupName(scopes, authority) {
|
return `${BrowserConstants.POPUP_NAME_PREFIX}.${this.config.auth.clientId}.${scopes.join("-")}.${authority}.${this.correlationId}`;
|
}
|
/**
|
* Generates the name for the popup based on the client id and request for logouts
|
* @param clientId
|
* @param request
|
*/
|
generateLogoutPopupName(request) {
|
const homeAccountId = request.account && request.account.homeAccountId;
|
return `${BrowserConstants.POPUP_NAME_PREFIX}.${this.config.auth.clientId}.${homeAccountId}.${this.correlationId}`;
|
}
|
}
|
|
export { PopupClient };
|
//# sourceMappingURL=PopupClient.mjs.map
|