UNPKG

@azure/msal-browser

Version:
349 lines (346 loc) 14.4 kB
/*! @azure/msal-browser v5.6.3 2026-04-01 */ 'use strict'; 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