@azure/msal-browser
Version:
Microsoft Authentication Library for js
349 lines (346 loc) • 14.4 kB
JavaScript
/*! @azure/msal-browser v5.6.3 2026-04-01 */
;
import { UrlUtils, ProtocolUtils, UrlString } from '@azure/msal-common/browser';
export { invoke, invokeAsync } from '@azure/msal-common/browser';
import { WaitForBridgeLateResponse } from '../telemetry/BrowserPerformanceEvents.mjs';
import { createBrowserAuthError } from '../error/BrowserAuthError.mjs';
import { BrowserCacheLocation, InteractionType } from './BrowserConstants.mjs';
import { createNewGuid } from '../crypto/BrowserCrypto.mjs';
import { createBrowserConfigurationAuthError } from '../error/BrowserConfigurationAuthError.mjs';
import { nonBrowserEnvironment, redirectInIframe, blockIframeReload, blockNestedPopups, timedOut, redirectBridgeEmptyResponse, emptyResponse, noStateInHash, unableToParseState, uninitializedPublicClientApplication, interactionInProgressCancelled } from '../error/BrowserAuthErrorCodes.mjs';
import { base64Decode } from '../encode/Base64Decode.mjs';
import { inMemRedirectUnavailable } from '../error/BrowserConfigurationAuthErrorCodes.mjs';
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Extracts and parses the authentication response from URL (hash and/or query string).
* This is a shared utility used across multiple components in msal-browser.
*
* @returns {Object} An object containing the parsed state information and URL parameters.
* @returns {URLSearchParams} params - The parsed URL parameters from the payload.
* @returns {string} payload - The combined query string and hash content.
* @returns {string} urlHash - The original URL hash.
* @returns {string} urlQuery - The original URL query string.
* @returns {LibraryStateObject} libraryState - The decoded library state from the state parameter.
*
* @throws {AuthError} If no authentication payload is found in the URL.
* @throws {AuthError} If the state parameter is missing.
* @throws {AuthError} If the state is missing required 'id' or 'meta' attributes.
*/
function parseAuthResponseFromUrl() {
// Extract both hash and query string to support hybrid response format
const urlHash = window.location.hash;
const urlQuery = window.location.search;
// Determine which part contains the auth response by checking for 'state' parameter
let hasResponseInHash = false;
let hasResponseInQuery = false;
let payload = "";
let params = undefined;
if (urlHash && urlHash.length > 1) {
const hashContent = urlHash.charAt(0) === "#" ? urlHash.substring(1) : urlHash;
const hashParams = new URLSearchParams(hashContent);
if (hashParams.has("state")) {
hasResponseInHash = true;
payload = hashContent;
params = hashParams;
}
}
if (urlQuery && urlQuery.length > 1) {
const queryContent = urlQuery.charAt(0) === "?" ? urlQuery.substring(1) : urlQuery;
const queryParams = new URLSearchParams(queryContent);
if (queryParams.has("state")) {
hasResponseInQuery = true;
payload = queryContent;
params = queryParams;
}
}
// If response is in both, combine them (hybrid format)
if (hasResponseInHash && hasResponseInQuery) {
const queryContent = urlQuery.charAt(0) === "?" ? urlQuery.substring(1) : urlQuery;
const hashContent = urlHash.charAt(0) === "#" ? urlHash.substring(1) : urlHash;
payload = `${queryContent}${hashContent}`;
params = new URLSearchParams(payload);
}
if (!payload || !params) {
throw createBrowserAuthError(emptyResponse);
}
const state = params.get("state");
if (!state) {
throw createBrowserAuthError(noStateInHash);
}
const { libraryState } = ProtocolUtils.parseRequestState(base64Decode, state);
const { id, meta } = libraryState;
if (!id || !meta) {
throw createBrowserAuthError(unableToParseState, "missing_library_state");
}
return {
params,
payload,
urlHash,
urlQuery,
hasResponseInHash,
hasResponseInQuery,
libraryState: {
id,
meta,
},
};
}
/**
* Clears hash from window url.
*/
function clearHash(contentWindow) {
// Office.js sets history.replaceState to null
contentWindow.location.hash = "";
if (typeof contentWindow.history.replaceState === "function") {
// Full removes "#" from url
contentWindow.history.replaceState(null, "", `${contentWindow.location.origin}${contentWindow.location.pathname}${contentWindow.location.search}`);
}
}
/**
* Replaces current hash with hash from provided url
*/
function replaceHash(url) {
const urlParts = url.split("#");
urlParts.shift(); // Remove part before the hash
window.location.hash = urlParts.length > 0 ? urlParts.join("#") : "";
}
/**
* Returns boolean of whether the current window is in an iframe or not.
*/
function isInIframe() {
return window.parent !== window;
}
/**
* Returns boolean of whether or not the current window is a popup opened by msal
*/
function isInPopup() {
if (isInIframe()) {
return false;
}
try {
const { libraryState } = parseAuthResponseFromUrl();
const { meta } = libraryState;
return meta["interactionType"] === InteractionType.Popup;
}
catch (e) {
// If parsing fails (no state, invalid URL, etc.), we're not in a popup
return false;
}
}
/**
* Await a response from a redirect bridge using BroadcastChannel.
* This unified function works for both popup and iframe scenarios by listening on a
* BroadcastChannel for the server payload.
*
* @param timeoutMs - Timeout in milliseconds.
* @param logger - Logger instance for logging monitoring events.
* @param browserCrypto - Browser crypto instance for decoding state.
* @param request - The authorization or end session request.
* @returns Promise<string> - Resolves with the response string (query or hash) from the window.
*/
// Track the active bridge monitor to allow cancellation when overriding interactions
let activeBridgeMonitor = null;
/**
* Cancels the pending bridge response monitor if one exists.
* This is called when overrideInteractionInProgress is used to cancel
* any pending popup interaction before starting a new one.
*/
function cancelPendingBridgeResponse(logger, correlationId) {
if (activeBridgeMonitor) {
logger.verbose("18y01k", correlationId);
clearTimeout(activeBridgeMonitor.timeoutId);
activeBridgeMonitor.channel.close();
activeBridgeMonitor.reject(createBrowserAuthError(interactionInProgressCancelled));
activeBridgeMonitor = null;
}
}
async function waitForBridgeResponse(timeoutMs, logger, browserCrypto, request, performanceClient, experimentalConfig) {
return new Promise((resolve, reject) => {
logger.verbose("1rf6em", request.correlationId);
const correlationId = request.correlationId;
performanceClient.addFields({
redirectBridgeTimeoutMs: timeoutMs,
lateResponseExperimentEnabled: experimentalConfig?.iframeTimeoutTelemetry || false,
}, correlationId);
const { libraryState } = ProtocolUtils.parseRequestState(browserCrypto.base64Decode, request.state || "");
const channel = new BroadcastChannel(libraryState.id);
let responseString = undefined;
let timedOut$1 = false;
let lateTimeoutId;
let lateMeasurement;
const timeoutId = window.setTimeout(() => {
// Clear the active monitor
activeBridgeMonitor = null;
if (experimentalConfig?.iframeTimeoutTelemetry) {
lateMeasurement = performanceClient.startMeasurement(WaitForBridgeLateResponse, correlationId);
timedOut$1 = true;
lateTimeoutId = window.setTimeout(() => {
lateMeasurement?.end({ success: false });
clearTimeout(lateTimeoutId);
channel.close();
}, 60000); // 60s late response timeout to allow for telemetry of late responses
}
else {
channel.close();
}
reject(createBrowserAuthError(timedOut, "redirect_bridge_timeout"));
}, timeoutMs);
// Track this monitor so it can be cancelled if needed
activeBridgeMonitor = {
timeoutId,
channel,
reject,
};
channel.onmessage = (event) => {
responseString = event.data.payload;
const messageVersion = event?.data && typeof event.data.v === "number"
? event.data.v
: undefined;
if (timedOut$1) {
lateMeasurement?.end({
success: responseString ? true : false,
});
clearTimeout(lateTimeoutId);
channel.close();
return;
}
performanceClient.addFields({
redirectBridgeMessageVersion: messageVersion,
}, correlationId);
// Clear the active monitor
activeBridgeMonitor = null;
clearTimeout(timeoutId);
channel.close();
if (responseString) {
resolve(responseString);
}
else {
reject(createBrowserAuthError(redirectBridgeEmptyResponse));
}
};
});
}
// #endregion
/**
* Returns current window URL as redirect uri
*/
function getCurrentUri() {
return typeof window !== "undefined" && window.location
? window.location.href.split("?")[0].split("#")[0]
: "";
}
/**
* Gets the homepage url for the current window location.
*/
function getHomepage() {
const currentUrl = new UrlString(window.location.href);
const urlComponents = currentUrl.getUrlComponents();
return `${urlComponents.Protocol}//${urlComponents.HostNameAndPort}/`;
}
/**
* Throws error if we have completed an auth and are
* attempting another auth request inside an iframe.
*/
function blockReloadInHiddenIframes() {
const isResponseHash = UrlUtils.getDeserializedResponse(window.location.hash);
// return an error if called from the hidden iframe created by the msal js silent calls
if (isResponseHash && isInIframe()) {
throw createBrowserAuthError(blockIframeReload);
}
}
/**
* Block redirect operations in iframes unless explicitly allowed
* @param interactionType Interaction type for the request
* @param allowRedirectInIframe Config value to allow redirects when app is inside an iframe
*/
function blockRedirectInIframe(allowRedirectInIframe) {
if (isInIframe() && !allowRedirectInIframe) {
// If we are not in top frame, we shouldn't redirect. This is also handled by the service.
throw createBrowserAuthError(redirectInIframe);
}
}
/**
* Block redirectUri loaded in popup from calling AcquireToken APIs
*/
function blockAcquireTokenInPopups() {
// Popups opened by msal popup APIs are given a name that starts with "msal."
if (isInPopup()) {
throw createBrowserAuthError(blockNestedPopups);
}
}
/**
* Throws error if token requests are made in non-browser environment
* @param isBrowserEnvironment Flag indicating if environment is a browser.
*/
function blockNonBrowserEnvironment() {
if (typeof window === "undefined") {
throw createBrowserAuthError(nonBrowserEnvironment);
}
}
/**
* Throws error if initialize hasn't been called
* @param initialized
*/
function blockAPICallsBeforeInitialize(initialized) {
if (!initialized) {
throw createBrowserAuthError(uninitializedPublicClientApplication);
}
}
/**
* Helper to validate app environment before making an auth request
* @param initialized
*/
function preflightCheck(initialized) {
// Block request if not in browser environment
blockNonBrowserEnvironment();
// Block auth requests inside a hidden iframe
blockReloadInHiddenIframes();
// Block redirectUri opened in a popup from calling MSAL APIs
blockAcquireTokenInPopups();
// Block token acquisition before initialize has been called
blockAPICallsBeforeInitialize(initialized);
}
/**
* Helper to validate app enviornment before making redirect request
* @param initialized
* @param config
*/
function redirectPreflightCheck(initialized, config) {
preflightCheck(initialized);
blockRedirectInIframe(config.system.allowRedirectInIframe);
// Block redirects if memory storage is enabled
if (config.cache.cacheLocation === BrowserCacheLocation.MemoryStorage) {
throw createBrowserConfigurationAuthError(inMemRedirectUnavailable);
}
}
/**
* Adds a preconnect link element to the header which begins DNS resolution and SSL connection in anticipation of the /token request
* @param loginDomain Authority domain, including https protocol e.g. https://login.microsoftonline.com
* @returns
*/
function preconnect(authority) {
const link = document.createElement("link");
link.rel = "preconnect";
link.href = new URL(authority).origin;
link.crossOrigin = "anonymous";
document.head.appendChild(link);
// The browser will close connection if not used within a few seconds, remove element from the header after 10s
window.setTimeout(() => {
try {
document.head.removeChild(link);
}
catch { }
}, 10000); // 10s Timeout
}
/**
* Wrapper function that creates a UUID v7 from the current timestamp.
* @returns {string}
*/
function createGuid() {
return createNewGuid();
}
export { blockAPICallsBeforeInitialize, blockAcquireTokenInPopups, blockNonBrowserEnvironment, blockRedirectInIframe, blockReloadInHiddenIframes, cancelPendingBridgeResponse, clearHash, createGuid, getCurrentUri, getHomepage, isInIframe, isInPopup, parseAuthResponseFromUrl, preconnect, preflightCheck, redirectPreflightCheck, replaceHash, waitForBridgeResponse };
//# sourceMappingURL=BrowserUtils.mjs.map