@azure/msal-browser
Version:
Microsoft Authentication Library for js
470 lines (467 loc) • 25 kB
JavaScript
/*! @azure/msal-browser v4.12.0 2025-05-06 */
'use strict';
import { OIDC_DEFAULT_SCOPES, invokeAsync, PerformanceEvents, ProtocolMode, invoke, AuthError, UrlString, ServerResponseType } from '@azure/msal-common/browser';
import { StandardInteractionClient } from './StandardInteractionClient.mjs';
import { EventType } from '../event/EventType.mjs';
import { InteractionType, ApiId, BrowserConstants } from '../utils/BrowserConstants.mjs';
import { preconnect, getCurrentUri } from '../utils/BrowserUtils.mjs';
import { NativeMessageHandler } from '../broker/nativeBroker/NativeMessageHandler.mjs';
import { createBrowserAuthError } from '../error/BrowserAuthError.mjs';
import { deserializeResponse } from '../response/ResponseHandler.mjs';
import { getAuthCodeRequestUrl, handleResponseCode, getEARForm, handleResponseEAR } from '../protocol/Authorize.mjs';
import { generatePkceCodes } from '../crypto/PkceGenerator.mjs';
import { generateEarKey } from '../crypto/BrowserCrypto.mjs';
import { emptyNavigateUri, userCancelled, 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, nativeMessageHandler, correlationId) {
super(config, storageImpl, browserCrypto, logger, eventHandler, navigationClient, performanceClient, nativeMessageHandler, correlationId);
// Properly sets this reference for the unload event.
this.unloadWindow = this.unloadWindow.bind(this);
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) {
try {
const popupName = this.generatePopupName(request.scopes || OIDC_DEFAULT_SCOPES, request.authority || this.config.auth.authority);
const popupParams = {
popupName,
popupWindowAttributes: request.popupWindowAttributes || {},
popupWindowParent: request.popupWindowParent ?? window,
};
this.performanceClient.addFields({ isAsyncPopup: this.config.system.asyncPopups }, this.correlationId);
// asyncPopups flag is true. Acquires token without first opening popup. Popup will be opened later asynchronously.
if (this.config.system.asyncPopups) {
this.logger.verbose("asyncPopups set to true, acquiring token");
// Passes on popup position and dimensions if in request
return this.acquireTokenPopupAsync(request, popupParams, pkceCodes);
}
else {
// asyncPopups flag is set to false. Opens popup before acquiring token.
this.logger.verbose("asyncPopup set to false, opening popup before acquiring token");
popupParams.popup = this.openSizedPopup("about:blank", popupParams);
return this.acquireTokenPopupAsync(request, 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("logoutPopup called");
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;
// asyncPopups flag is true. Acquires token without first opening popup. Popup will be opened later asynchronously.
if (this.config.system.asyncPopups) {
this.logger.verbose("asyncPopups set to true");
// Passes on popup position and dimensions if in request
return this.logoutPopupAsync(validLogoutRequest, popupParams, authority, mainWindowRedirectUri);
}
else {
// asyncPopups flag is set to false. Opens popup before logging out.
this.logger.verbose("asyncPopup set to false, opening popup");
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("acquireTokenPopupAsync called");
const validRequest = await invokeAsync(this.initializeAuthorizationRequest.bind(this), PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest, this.logger, this.performanceClient, this.correlationId)(request, InteractionType.Popup);
/*
* 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 = NativeMessageHandler.isPlatformBrokerAvailable(this.config, this.logger, this.nativeMessageHandler, request.authenticationScheme);
validRequest.platformBroker = isPlatformBroker;
if (this.config.auth.protocolMode === ProtocolMode.EAR) {
return this.executeEarFlow(validRequest, popupParams);
}
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 = this.initializeServerTelemetryManager(ApiId.acquireTokenPopup);
const pkce = pkceCodes ||
(await invokeAsync(generatePkceCodes, PerformanceEvents.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), PerformanceEvents.StandardInteractionClientCreateAuthCodeClient, this.logger, this.performanceClient, correlationId)({
serverTelemetryManager,
requestAuthority: popupRequest.authority,
requestAzureCloudOptions: popupRequest.azureCloudOptions,
requestExtraQueryParameters: popupRequest.extraQueryParameters,
account: popupRequest.account,
});
// 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, 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 this.monitorPopupForHash(popupWindow, popupParams.popupWindowParent);
const serverParams = invoke(deserializeResponse, PerformanceEvents.DeserializeResponse, this.logger, this.performanceClient, this.correlationId)(responseString, this.config.auth.OIDCOptions.serverResponseType, this.logger);
return await invokeAsync(handleResponseCode, PerformanceEvents.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.nativeMessageHandler);
}
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) {
const correlationId = request.correlationId;
// Get the frame handle for the silent request
const discoveredAuthority = await invokeAsync(this.getDiscoveredAuthority.bind(this), PerformanceEvents.StandardInteractionClientGetDiscoveredAuthority, this.logger, this.performanceClient, correlationId)({
requestAuthority: request.authority,
requestAzureCloudOptions: request.azureCloudOptions,
requestExtraQueryParameters: request.extraQueryParameters,
account: request.account,
});
const earJwk = await invokeAsync(generateEarKey, PerformanceEvents.GenerateEarKey, this.logger, this.performanceClient, correlationId)();
const popupRequest = {
...request,
earJwk: earJwk,
};
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(this.monitorPopupForHash.bind(this), PerformanceEvents.SilentHandlerMonitorIframeForHash, this.logger, this.performanceClient, correlationId)(popupWindow, popupParams.popupWindowParent);
const serverParams = invoke(deserializeResponse, PerformanceEvents.DeserializeResponse, this.logger, this.performanceClient, this.correlationId)(responseString, this.config.auth.OIDCOptions.serverResponseType, this.logger);
return invokeAsync(handleResponseEAR, PerformanceEvents.HandleResponseEar, this.logger, this.performanceClient, correlationId)(popupRequest, serverParams, ApiId.acquireTokenPopup, this.config, discoveredAuthority, this.browserStorage, this.nativeStorage, this.eventHandler, this.logger, this.performanceClient, this.nativeMessageHandler);
}
/**
*
* @param validRequest
* @param popupName
* @param requestAuthority
* @param popup
* @param mainWindowRedirectUri
* @param popupWindowAttributes
*/
async logoutPopupAsync(validRequest, popupParams, requestAuthority, mainWindowRedirectUri) {
this.logger.verbose("logoutPopupAsync called");
this.eventHandler.emitEvent(EventType.LOGOUT_START, InteractionType.Popup, validRequest);
const serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.logoutPopup);
try {
// Clear cache on logout
await this.clearCacheOnLogout(validRequest.account);
// Initialize the client
const authClient = await invokeAsync(this.createAuthCodeClient.bind(this), PerformanceEvents.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) {
void this.browserStorage.removeAccount(validRequest.account?.homeAccountId);
this.eventHandler.emitEvent(EventType.LOGOUT_SUCCESS, 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;
}
}
// Create logout string and navigate user window to logout.
const logoutUri = authClient.getLogoutUri(validRequest);
this.eventHandler.emitEvent(EventType.LOGOUT_SUCCESS, InteractionType.Popup, validRequest);
// Open the popup window to requestUrl.
const popupWindow = this.openPopup(logoutUri, popupParams);
this.eventHandler.emitEvent(EventType.POPUP_OPENED, InteractionType.Popup, { popupWindow }, null);
await this.monitorPopupForHash(popupWindow, popupParams.popupWindowParent).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("Redirecting main window to url specified in the request");
this.logger.verbosePii(`Redirecting main window to: ${absoluteUrl}`);
await this.navigationClient.navigateInternal(absoluteUrl, navigationOptions);
}
else {
this.logger.verbose("No main window navigation requested");
}
}
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, InteractionType.Popup, null, e);
this.eventHandler.emitEvent(EventType.LOGOUT_END, InteractionType.Popup);
throw e;
}
this.eventHandler.emitEvent(EventType.LOGOUT_END, 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(`Navigate to: ${requestUrl}`);
// Open the popup window to requestUrl.
return this.openPopup(requestUrl, params);
}
else {
// Throw error if request URL is empty.
this.logger.error("Navigate url is empty");
throw createBrowserAuthError(emptyNavigateUri);
}
}
/**
* Monitors a window until it loads a url with the same origin.
* @param popupWindow - window that is being monitored
* @param timeout - timeout for processing hash once popup is redirected back to application
*/
monitorPopupForHash(popupWindow, popupWindowParent) {
return new Promise((resolve, reject) => {
this.logger.verbose("PopupHandler.monitorPopupForHash - polling started");
const intervalId = setInterval(() => {
// Window is closed
if (popupWindow.closed) {
this.logger.error("PopupHandler.monitorPopupForHash - window closed");
clearInterval(intervalId);
reject(createBrowserAuthError(userCancelled));
return;
}
let href = "";
try {
/*
* Will throw if cross origin,
* which should be caught and ignored
* since we need the interval to keep running while on STS UI.
*/
href = popupWindow.location.href;
}
catch (e) { }
// Don't process blank pages or cross domain
if (!href || href === "about:blank") {
return;
}
clearInterval(intervalId);
let responseString = "";
const responseType = this.config.auth.OIDCOptions.serverResponseType;
if (popupWindow) {
if (responseType === ServerResponseType.QUERY) {
responseString = popupWindow.location.search;
}
else {
responseString = popupWindow.location.hash;
}
}
this.logger.verbose("PopupHandler.monitorPopupForHash - popup window is on same origin as caller");
resolve(responseString);
}, this.config.system.pollIntervalMilliseconds);
}).finally(() => {
this.cleanPopup(popupWindow, popupWindowParent);
});
}
/**
* @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(`Navigating popup window to: ${urlNavigate}`);
popupWindow.location.assign(urlNavigate);
}
else if (typeof popupParams.popup === "undefined") {
// Popup will be undefined if it was not passed in
this.logger.verbosePii(`Opening popup window to: ${urlNavigate}`);
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;
popupParams.popupWindowParent.addEventListener("beforeunload", this.unloadWindow);
return popupWindow;
}
catch (e) {
this.logger.error("error opening popup " + e.message);
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("Default popup window width used. Window width not configured or invalid.");
width = BrowserConstants.POPUP_WIDTH;
}
if (!height || height < 0 || height > winHeight) {
this.logger.verbose("Default popup window height used. Window height not configured or invalid.");
height = BrowserConstants.POPUP_HEIGHT;
}
if (!top || top < 0 || top > winHeight) {
this.logger.verbose("Default popup window top position used. Window top not configured or invalid.");
top = Math.max(0, winHeight / 2 - BrowserConstants.POPUP_HEIGHT / 2 + winTop);
}
if (!left || left < 0 || left > winWidth) {
this.logger.verbose("Default popup window left position used. Window left not configured or invalid.");
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`);
}
/**
* Event callback to unload main window.
*/
unloadWindow(e) {
if (this.currentWindow) {
this.currentWindow.close();
}
// Guarantees browser unload will happen, so no other errors will be thrown.
e.preventDefault();
}
/**
* Closes popup, removes any state vars created during popup calls.
* @param popupWindow
*/
cleanPopup(popupWindow, popupWindowParent) {
// Close window.
popupWindow.close();
// Remove window unload function
popupWindowParent.removeEventListener("beforeunload", this.unloadWindow);
}
/**
* 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