UNPKG

@azure/msal-browser

Version:
741 lines (717 loc) 28.9 kB
/*! @azure/msal-browser v5.7.0 2026-04-16 */ 'use strict'; (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.msalRedirectBridge = {})); })(this, (function (exports) { 'use strict'; /*! @azure/msal-common v16.5.0 2026-04-16 */ // Resource delimiter - used for certain cache entries const RESOURCE_DELIM = "|"; const FORWARD_SLASH = "/"; /** * String constants related to AAD Authority */ const AADAuthority = { COMMON: "common", ORGANIZATIONS: "organizations"}; /*! @azure/msal-common v16.5.0 2026-04-16 */ /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ function getDefaultErrorMessage$1(code) { return `See https://aka.ms/msal.js.errors#${code} for details`; } /** * General error class thrown by the MSAL.js library. */ class AuthError extends Error { constructor(errorCode, errorMessage, suberror) { const message = errorMessage || (errorCode ? getDefaultErrorMessage$1(errorCode) : ""); const errorString = message ? `${errorCode}: ${message}` : errorCode; super(errorString); Object.setPrototypeOf(this, AuthError.prototype); this.errorCode = errorCode || ""; this.errorMessage = message || ""; this.subError = suberror || ""; this.name = "AuthError"; } setCorrelationId(correlationId) { this.correlationId = correlationId; } } /*! @azure/msal-common v16.5.0 2026-04-16 */ /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Error thrown when there is an error in configuration of the MSAL.js library. */ class ClientConfigurationError extends AuthError { constructor(errorCode) { super(errorCode); this.name = "ClientConfigurationError"; Object.setPrototypeOf(this, ClientConfigurationError.prototype); } } function createClientConfigurationError(errorCode) { return new ClientConfigurationError(errorCode); } /*! @azure/msal-common v16.5.0 2026-04-16 */ /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * @hidden */ class StringUtils { /** * Check if stringified object is empty * @param strObj */ static isEmptyObj(strObj) { if (strObj) { try { const obj = JSON.parse(strObj); return Object.keys(obj).length === 0; } catch (e) { } } return true; } static startsWith(str, search) { return str.indexOf(search) === 0; } static endsWith(str, search) { return (str.length >= search.length && str.lastIndexOf(search) === str.length - search.length); } /** * Parses string into an object. * * @param query */ static queryStringToObject(query) { const obj = {}; const params = query.split("&"); const decode = (s) => decodeURIComponent(s.replace(/\+/g, " ")); params.forEach((pair) => { if (pair.trim()) { const [key, value] = pair.split(/=(.+)/g, 2); // Split on the first occurence of the '=' character if (key && value) { obj[decode(key)] = decode(value); } } }); return obj; } /** * Trims entries in an array. * * @param arr */ static trimArrayEntries(arr) { return arr.map((entry) => entry.trim()); } /** * Removes empty strings from array * @param arr */ static removeEmptyStringsFromArray(arr) { return arr.filter((entry) => { return !!entry; }); } /** * Attempts to parse a string into JSON * @param str */ static jsonParseHelper(str) { try { return JSON.parse(str); } catch (e) { return null; } } } /*! @azure/msal-common v16.5.0 2026-04-16 */ /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * ClientAuthErrorMessage class containing string constants used by error codes and messages. */ /** * Error thrown when there is an error in the client code running on the browser. */ class ClientAuthError extends AuthError { constructor(errorCode, additionalMessage) { super(errorCode, additionalMessage); this.name = "ClientAuthError"; Object.setPrototypeOf(this, ClientAuthError.prototype); } } function createClientAuthError(errorCode, additionalMessage) { return new ClientAuthError(errorCode, additionalMessage); } /*! @azure/msal-common v16.5.0 2026-04-16 */ const authorityUriInsecure = "authority_uri_insecure"; const urlParseError = "url_parse_error"; const urlEmptyError = "empty_url_error"; /*! @azure/msal-common v16.5.0 2026-04-16 */ const invalidState = "invalid_state"; const noCryptoObject = "no_crypto_object"; /*! @azure/msal-common v16.5.0 2026-04-16 */ /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Url object class which can perform various transformations on url strings. */ class UrlString { get urlString() { return this._urlString; } constructor(url) { this._urlString = url; if (!this._urlString) { // Throws error if url is empty throw createClientConfigurationError(urlEmptyError); } if (!url.includes("#")) { this._urlString = UrlString.canonicalizeUri(url); } } /** * Ensure urls are lower case and end with a / character. * @param url */ static canonicalizeUri(url) { if (url) { let lowerCaseUrl = url.toLowerCase(); if (StringUtils.endsWith(lowerCaseUrl, "?")) { lowerCaseUrl = lowerCaseUrl.slice(0, -1); } else if (StringUtils.endsWith(lowerCaseUrl, "?/")) { lowerCaseUrl = lowerCaseUrl.slice(0, -2); } if (!StringUtils.endsWith(lowerCaseUrl, "/")) { lowerCaseUrl += "/"; } return lowerCaseUrl; } return url; } /** * Throws if urlString passed is not a valid authority URI string. */ validateAsUri() { // Attempts to parse url for uri components let components; try { components = this.getUrlComponents(); } catch (e) { throw createClientConfigurationError(urlParseError); } // Throw error if URI or path segments are not parseable. if (!components.HostNameAndPort || !components.PathSegments) { throw createClientConfigurationError(urlParseError); } // Throw error if uri is insecure. if (!components.Protocol || components.Protocol.toLowerCase() !== "https:") { throw createClientConfigurationError(authorityUriInsecure); } } /** * Given a url and a query string return the url with provided query string appended * @param url * @param queryString */ static appendQueryString(url, queryString) { if (!queryString) { return url; } return url.indexOf("?") < 0 ? `${url}?${queryString}` : `${url}&${queryString}`; } /** * Returns a url with the hash removed * @param url */ static removeHashFromUrl(url) { return UrlString.canonicalizeUri(url.split("#")[0]); } /** * Given a url like https://a:b/common/d?e=f#g, and a tenantId, returns https://a:b/tenantId/d * @param href The url * @param tenantId The tenant id to replace */ replaceTenantPath(tenantId) { const urlObject = this.getUrlComponents(); const pathArray = urlObject.PathSegments; if (tenantId && pathArray.length !== 0 && (pathArray[0] === AADAuthority.COMMON || pathArray[0] === AADAuthority.ORGANIZATIONS)) { pathArray[0] = tenantId; } return UrlString.constructAuthorityUriFromObject(urlObject); } /** * Parses out the components from a url string. * @returns An object with the various components. Please cache this value insted of calling this multiple times on the same url. */ getUrlComponents() { // https://gist.github.com/curtisz/11139b2cfcaef4a261e0 const regEx = RegExp("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?"); // If url string does not match regEx, we throw an error const match = this.urlString.match(regEx); if (!match) { throw createClientConfigurationError(urlParseError); } // Url component object const urlComponents = { Protocol: match[1], HostNameAndPort: match[4], AbsolutePath: match[5], QueryString: match[7], }; let pathSegments = urlComponents.AbsolutePath.split("/"); pathSegments = pathSegments.filter((val) => val && val.length > 0); // remove empty elements urlComponents.PathSegments = pathSegments; if (urlComponents.QueryString && urlComponents.QueryString.endsWith("/")) { urlComponents.QueryString = urlComponents.QueryString.substring(0, urlComponents.QueryString.length - 1); } return urlComponents; } static getDomainFromUrl(url) { const regEx = RegExp("^([^:/?#]+://)?([^/?#]*)"); const match = url.match(regEx); if (!match) { throw createClientConfigurationError(urlParseError); } return match[2]; } static getAbsoluteUrl(relativeUrl, baseUrl) { if (relativeUrl[0] === FORWARD_SLASH) { const url = new UrlString(baseUrl); const baseComponents = url.getUrlComponents(); return (baseComponents.Protocol + "//" + baseComponents.HostNameAndPort + relativeUrl); } return relativeUrl; } static constructAuthorityUriFromObject(urlObject) { return new UrlString(urlObject.Protocol + "//" + urlObject.HostNameAndPort + "/" + urlObject.PathSegments.join("/")); } } /*! @azure/msal-common v16.5.0 2026-04-16 */ /** * Parses the state into the RequestStateObject, which contains the LibraryState info and the state passed by the user. * @param base64Decode * @param state */ function parseRequestState(base64Decode, state) { if (!base64Decode) { throw createClientAuthError(noCryptoObject); } if (!state) { throw createClientAuthError(invalidState); } try { // Split the state between library state and user passed state and decode them separately const splitState = state.split(RESOURCE_DELIM); const libraryState = splitState[0]; const userState = splitState.length > 1 ? splitState.slice(1).join(RESOURCE_DELIM) : ""; const libraryStateString = base64Decode(libraryState); const libraryStateObj = JSON.parse(libraryStateString); return { userRequestState: userState || "", libraryState: libraryStateObj, }; } catch (e) { throw createClientAuthError(invalidState); } } /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ const noStateInHash = "no_state_in_hash"; const unableToParseState = "unable_to_parse_state"; const invalidBase64String = "invalid_base64_string"; const timedOut = "timed_out"; const emptyResponse = "empty_response"; /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ function getDefaultErrorMessage(code) { return `See https://aka.ms/msal.js.errors#${code} for details`; } /** * Browser library error class thrown by the MSAL.js library for SPAs */ class BrowserAuthError extends AuthError { constructor(errorCode, subError) { super(errorCode, getDefaultErrorMessage(errorCode), subError); Object.setPrototypeOf(this, BrowserAuthError.prototype); this.name = "BrowserAuthError"; } } function createBrowserAuthError(errorCode, subError) { return new BrowserAuthError(errorCode, subError); } /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ const INTERACTION_TYPE = { SIGNOUT: "signout", }; /** * Temporary cache keys for MSAL, deleted after any request. */ const TemporaryCacheKeys = { ORIGIN_URI: "request.origin", URL_HASH: "urlHash", REQUEST_PARAMS: "request.params", VERIFIER: "code.verifier", INTERACTION_STATUS_KEY: "interaction.status", NATIVE_REQUEST: "request.native", }; /** * API Codes for Telemetry purposes. * Before adding a new code you must claim it in the MSAL Telemetry tracker as these number spaces are shared across all MSALs * 0-99 Silent Flow * 800-899 Auth Code Flow * 900-999 Misc */ const ApiId = { handleRedirectPromise: 865, logout: 961}; /* * Interaction type of the API - used for state and telemetry */ var InteractionType; (function (InteractionType) { InteractionType["Redirect"] = "redirect"; InteractionType["Popup"] = "popup"; InteractionType["Silent"] = "silent"; InteractionType["None"] = "none"; })(InteractionType || (InteractionType = {})); /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Class which exposes APIs to decode base64 strings to plaintext. See here for implementation details: * https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */ /** * Returns a URL-safe plaintext decoded string from b64 encoded input. * @param input */ function base64Decode(input) { return new TextDecoder().decode(base64DecToArr(input)); } /** * Decodes base64 into Uint8Array * @param base64String */ function base64DecToArr(base64String) { let encodedString = base64String.replace(/-/g, "+").replace(/_/g, "/"); switch (encodedString.length % 4) { case 0: break; case 2: encodedString += "=="; break; case 3: encodedString += "="; break; default: throw createBrowserAuthError(invalidBase64String); } const binString = atob(encodedString); return Uint8Array.from(binString, (m) => m.codePointAt(0) || 0); } /* * 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 } = 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, }, }; } /** * 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}/`; } /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ class NavigationClient { /** * Navigates to other pages within the same web application * @param url * @param options */ navigateInternal(url, options) { return NavigationClient.defaultNavigateWindow(url, options); } /** * Navigates to other pages outside the web application i.e. the Identity Provider * @param url * @param options */ navigateExternal(url, options) { return NavigationClient.defaultNavigateWindow(url, options); } /** * Default navigation implementation invoked by the internal and external functions * @param url * @param options */ static defaultNavigateWindow(url, options) { if (options.noHistory) { window.location.replace(url); // CodeQL [SM03712] Application owner controls the URL. User can't change it. } else { window.location.assign(url); // CodeQL [SM03712] Application owner controls the URL. User can't change it. } return new Promise((resolve, reject) => { setTimeout(() => { reject(createBrowserAuthError(timedOut, "failed_to_redirect")); }, options.timeout); }); } } /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ const DEFAULT_REDIRECT_TIMEOUT_MS = 30000; /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ const PREFIX = "msal"; /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Processes the authentication response from the redirect URL * For SSO and popup scenarios broadcasts it to the main frame * For redirect scenario navigates to the home page * * @param {NavigationClient} navigationClient - Optional navigation client for redirect scenario. * * @returns {Promise<void>} A promise that resolves when the response has been broadcast and cleanup is complete. * * @throws {AuthError} If no authentication payload is found in the URL (hash or query string). * @throws {AuthError} If the state parameter is missing from the redirect URL. * @throws {AuthError} If the state is missing required 'id' or 'meta' attributes. */ async function broadcastResponseToMainFrame(navigationClient) { let parsedResponse; try { parsedResponse = parseAuthResponseFromUrl(); } catch (error) { // Clear hash and query string before re-throwing parse errors if (typeof window.history.replaceState === "function") { window.history.replaceState(null, "", `${window.location.origin}${window.location.pathname}`); } throw error; } const { payload, urlHash, urlQuery, hasResponseInHash, hasResponseInQuery, libraryState, } = parsedResponse; const { id, meta } = libraryState; if (meta["interactionType"] === InteractionType.Redirect) { const navClient = navigationClient || new NavigationClient(); let navigateToUrl = ""; let clientId = ""; let interactionType = ""; const interactionKey = `${PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`; /* * Retrieve the clientId and original navigation URL from * sessionStorage. If sessionStorage access or JSON.parse fails, * we fall back to URL-based navigation below. */ try { const rawInteractionStatus = window.sessionStorage.getItem(interactionKey); const interactionStatus = JSON.parse(rawInteractionStatus || ""); clientId = interactionStatus.clientId || ""; interactionType = interactionStatus.type; if (clientId) { const originKey = `${PREFIX}.${clientId}.${TemporaryCacheKeys.ORIGIN_URI}`; navigateToUrl = window.sessionStorage.getItem(originKey) || ""; } } catch { // sessionStorage access or JSON.parse failed } const navigationOptions = { apiId: interactionType === INTERACTION_TYPE.SIGNOUT ? ApiId.logout : ApiId.handleRedirectPromise, noHistory: true, timeout: DEFAULT_REDIRECT_TIMEOUT_MS, }; /* * Cache the auth response payload in sessionStorage under the URL_HASH * key, then navigate directly to the origin URL. This replicates what * RedirectClient.handleRedirectPromise does when the current page is * not the loginRequestUrl: it caches the response and navigates. * * On the target page, handleRedirectPromise will find no response in * the URL but will pick up the cached payload from sessionStorage. * This avoids appending the auth response to the URL, which would * create malformed URLs for hash-routed SPAs (e.g. /#/route#code=...). * * If caching fails (clientId unavailable, quota exceeded, storage * disabled), we still navigate to the origin/homepage. The target * page's handleRedirectPromise will return null and the app can * handle re-authentication. Appending auth params to the URL would * not help because handleRedirectPromise also relies on * sessionStorage to persist tokens. */ if (clientId) { try { window.sessionStorage.setItem(`${PREFIX}.${clientId}.${TemporaryCacheKeys.URL_HASH}`, payload); } catch { // sessionStorage write failed — navigate anyway; handleRedirectPromise will return null } } const url = navigateToUrl || getHomepage(); // Strip bare trailing "?" (empty query string) to match canonical URL form const navigationUrl = url.endsWith("?") ? url.slice(0, -1) : url; await navClient.navigateInternal(navigationUrl, navigationOptions); // Do NOT clear URL for redirect flow - we're navigating away anyway return; } // Clear only the part(s) containing the auth response from redirect bridge URL if (typeof window.history.replaceState === "function") { let newUrl = `${window.location.origin}${window.location.pathname}`; // Preserve hash if it didn't contain the response if (!hasResponseInHash && urlHash) { newUrl += urlHash; } // Preserve query if it didn't contain the response if (!hasResponseInQuery && urlQuery) { newUrl += urlQuery; } window.history.replaceState(null, "", newUrl); } // Send the raw URL payload to the main frame const channel = new BroadcastChannel(id); channel.postMessage({ v: 1, payload, }); channel.close(); try { window.close(); } catch { } } exports.broadcastResponseToMainFrame = broadcastResponseToMainFrame; })); //# sourceMappingURL=msal-redirect-bridge.js.map