Estructura inicial del proyecto

This commit is contained in:
2026-06-02 16:57:08 +00:00
commit 8b306b9afc
9864 changed files with 1435687 additions and 0 deletions

View File

@ -0,0 +1,258 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Constants } from "@azure/msal-common/browser";
import { PopupRequest } from "../request/PopupRequest.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
/**
* Constants
*/
export const BrowserConstants = {
/**
* Interaction in progress cache value
*/
INTERACTION_IN_PROGRESS_VALUE: "interaction_in_progress",
/**
* Invalid grant error code
*/
INVALID_GRANT_ERROR: "invalid_grant",
/**
* Default popup window width
*/
POPUP_WIDTH: 483,
/**
* Default popup window height
*/
POPUP_HEIGHT: 600,
/**
* Name of the popup window starts with
*/
POPUP_NAME_PREFIX: "msal",
/**
* Msal-browser SKU
*/
MSAL_SKU: "msal.js.browser",
};
export const PlatformAuthConstants = {
CHANNEL_ID: "53ee284d-920a-4b59-9d30-a60315b26836",
PREFERRED_EXTENSION_ID: "ppnbnpeolgkicgegkbkbjmhlideopiji",
MATS_TELEMETRY: "MATS",
MICROSOFT_ENTRA_BROKERID: "MicrosoftEntra",
DOM_API_NAME: "DOM API",
PLATFORM_DOM_APIS: "get-token-and-sign-out",
PLATFORM_DOM_PROVIDER: "PlatformAuthDOMHandler",
PLATFORM_EXTENSION_PROVIDER: "PlatformAuthExtensionHandler",
};
export const NativeExtensionMethod = {
HandshakeRequest: "Handshake",
HandshakeResponse: "HandshakeResponse",
GetToken: "GetToken",
Response: "Response",
} as const;
export type NativeExtensionMethod =
(typeof NativeExtensionMethod)[keyof typeof NativeExtensionMethod];
export const BrowserCacheLocation = {
LocalStorage: "localStorage",
SessionStorage: "sessionStorage",
MemoryStorage: "memoryStorage",
} as const;
export type BrowserCacheLocation =
(typeof BrowserCacheLocation)[keyof typeof BrowserCacheLocation];
/**
* HTTP Request types supported by MSAL.
*/
export const HTTP_REQUEST_TYPE = {
GET: "GET",
POST: "POST",
} as const;
export type HTTP_REQUEST_TYPE =
(typeof HTTP_REQUEST_TYPE)[keyof typeof HTTP_REQUEST_TYPE];
export const INTERACTION_TYPE = {
SIGNIN: "signin",
SIGNOUT: "signout",
} as const;
export type INTERACTION_TYPE =
(typeof INTERACTION_TYPE)[keyof typeof INTERACTION_TYPE];
/**
* Temporary cache keys for MSAL, deleted after any request.
*/
export 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",
} as const;
export type TemporaryCacheKeys =
(typeof TemporaryCacheKeys)[keyof typeof TemporaryCacheKeys];
/**
* Cache keys stored in-memory
*/
export const InMemoryCacheKeys = {
WRAPPER_SKU: "wrapper.sku",
WRAPPER_VER: "wrapper.version",
} as const;
export type InMemoryCacheKeys =
(typeof InMemoryCacheKeys)[keyof typeof InMemoryCacheKeys];
/**
* 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
*/
export const ApiId = {
acquireTokenRedirect: 861,
acquireTokenPopup: 862,
ssoSilent: 863,
acquireTokenSilent_authCode: 864,
handleRedirectPromise: 865,
acquireTokenByCode: 866,
acquireTokenSilent_silentFlow: 61,
logout: 961,
logoutPopup: 962,
hydrateCache: 963,
loadExternalTokens: 964,
} as const;
export type ApiId = (typeof ApiId)[keyof typeof ApiId];
/**
* API Names for Telemetry purposes.
*/
export const ApiName = {
861: "acquireTokenRedirect",
862: "acquireTokenPopup",
863: "ssoSilent",
864: "acquireTokenSilent_authCode",
865: "handleRedirectPromise",
866: "acquireTokenByCode",
61: "acquireTokenSilent_silentFlow",
961: "logout",
962: "logoutPopup",
963: "hydrateCache",
964: "loadExternalTokens",
} as const satisfies Record<ApiId, string>;
export const apiIdToName = (id: number | undefined): string => {
if (typeof id === "number" && id in ApiName) {
return ApiName[id as ApiId];
}
return "unknown";
};
/*
* Interaction type of the API - used for state and telemetry
*/
export enum InteractionType {
Redirect = "redirect",
Popup = "popup",
Silent = "silent",
None = "none",
}
/**
* Types of interaction currently in progress.
* Used in events in wrapper libraries to invoke functions when certain interaction is in progress or all interactions are complete.
*/
export const InteractionStatus = {
/**
* Initial status before interaction occurs
*/
Startup: "startup",
/**
* Status set when logout call occuring
*/
Logout: "logout",
/**
* Status set for acquireToken calls
*/
AcquireToken: "acquireToken",
/**
* Status set when handleRedirect in progress
*/
HandleRedirect: "handleRedirect",
/**
* Status set when interaction is complete
*/
None: "none",
} as const;
export type InteractionStatus =
(typeof InteractionStatus)[keyof typeof InteractionStatus];
export const DEFAULT_REQUEST: RedirectRequest | PopupRequest = {
scopes: Constants.OIDC_DEFAULT_SCOPES,
};
/**
* JWK Key Format string (Type MUST be defined for window crypto APIs)
*/
export const KEY_FORMAT_JWK = "jwk";
// Supported wrapper SKUs
export const WrapperSKU = {
React: "@azure/msal-react",
Angular: "@azure/msal-angular",
} as const;
export type WrapperSKU = (typeof WrapperSKU)[keyof typeof WrapperSKU];
// DatabaseStorage Constants
export const DB_NAME = "msal.db";
export const DB_VERSION = 1;
export const DB_TABLE_NAME = `${DB_NAME}.keys`;
export const CacheLookupPolicy = {
/*
* acquireTokenSilent will attempt to retrieve an access token from the cache. If the access token is expired
* or cannot be found the refresh token will be used to acquire a new one. Finally, if the refresh token
* is expired acquireTokenSilent will attempt to acquire new access and refresh tokens.
*/
Default: 0, // 0 is falsy, is equivalent to not passing in a CacheLookupPolicy
/*
* acquireTokenSilent will only look for access tokens in the cache. It will not attempt to renew access or
* refresh tokens.
*/
AccessToken: 1,
/*
* acquireTokenSilent will attempt to retrieve an access token from the cache. If the access token is expired or
* cannot be found, the refresh token will be used to acquire a new one. If the refresh token is expired, it
* will not be renewed and acquireTokenSilent will fail.
*/
AccessTokenAndRefreshToken: 2,
/*
* acquireTokenSilent will not attempt to retrieve access tokens from the cache and will instead attempt to
* exchange the cached refresh token for a new access token. If the refresh token is expired, it will not be
* renewed and acquireTokenSilent will fail.
*/
RefreshToken: 3,
/*
* acquireTokenSilent will not look in the cache for the access token. It will go directly to network with the
* cached refresh token. If the refresh token is expired an attempt will be made to renew it. This is equivalent to
* setting "forceRefresh: true".
*/
RefreshTokenAndNetwork: 4,
/*
* acquireTokenSilent will attempt to renew both access and refresh tokens. It will not look in the cache. This will
* always fail if 3rd party cookies are blocked by the browser.
*/
Skip: 5,
} as const;
export type CacheLookupPolicy =
(typeof CacheLookupPolicy)[keyof typeof CacheLookupPolicy];
export const iFrameRenewalPolicies: CacheLookupPolicy[] = [
CacheLookupPolicy.Default,
CacheLookupPolicy.Skip,
CacheLookupPolicy.RefreshTokenAndNetwork,
];

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { InteractionType } from "./BrowserConstants.js";
import {
ICrypto,
RequestStateObject,
ProtocolUtils,
createClientAuthError,
ClientAuthErrorCodes,
} from "@azure/msal-common/browser";
export type BrowserStateObject = {
interactionType: InteractionType;
};
/**
* Extracts the BrowserStateObject from the state string.
* @param browserCrypto
* @param state
*/
export function extractBrowserRequestState(
browserCrypto: ICrypto,
state: string
): BrowserStateObject | null {
if (!state) {
return null;
}
try {
const requestStateObj: RequestStateObject =
ProtocolUtils.parseRequestState(browserCrypto.base64Decode, state);
return requestStateObj.libraryState.meta as BrowserStateObject;
} catch (e) {
throw createClientAuthError(ClientAuthErrorCodes.invalidState);
}
}

View File

@ -0,0 +1,492 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
UrlString,
invoke,
invokeAsync,
UrlUtils,
RequestParameterBuilder,
ICrypto,
IPerformanceClient,
InProgressPerformanceEvent,
Logger,
CommonAuthorizationUrlRequest,
CommonEndSessionRequest,
ProtocolUtils,
} from "@azure/msal-common/browser";
import * as BrowserPerformanceEvents from "../telemetry/BrowserPerformanceEvents.js";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { BrowserCacheLocation, InteractionType } from "./BrowserConstants.js";
import * as BrowserCrypto from "../crypto/BrowserCrypto.js";
import {
BrowserConfigurationAuthErrorCodes,
createBrowserConfigurationAuthError,
} from "../error/BrowserConfigurationAuthError.js";
import {
BrowserConfiguration,
BrowserExperimentalOptions,
} from "../config/Configuration.js";
import { redirectBridgeEmptyResponse } from "../error/BrowserAuthErrorCodes.js";
import { base64Decode } from "../encode/Base64Decode.js";
/**
* 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.
*/
export function parseAuthResponseFromUrl(): {
params: URLSearchParams;
payload: string;
urlHash: string;
urlQuery: string;
hasResponseInHash: boolean;
hasResponseInQuery: boolean;
libraryState: {
id: string;
meta: Record<string, string>;
};
} {
// 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: URLSearchParams | undefined = 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(BrowserAuthErrorCodes.emptyResponse);
}
const state = params.get("state");
if (!state) {
throw createBrowserAuthError(BrowserAuthErrorCodes.noStateInHash);
}
const { libraryState } = ProtocolUtils.parseRequestState(
base64Decode,
state
);
const { id, meta } = libraryState;
if (!id || !meta) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToParseState,
"missing_library_state"
);
}
return {
params,
payload,
urlHash,
urlQuery,
hasResponseInHash,
hasResponseInQuery,
libraryState: {
id,
meta,
},
};
}
/**
* Clears hash from window url.
*/
export function clearHash(contentWindow: Window): void {
// 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
*/
export function replaceHash(url: string): void {
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.
*/
export function isInIframe(): boolean {
return window.parent !== window;
}
/**
* Returns boolean of whether or not the current window is a popup opened by msal
*/
export function isInPopup(): boolean {
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: {
timeoutId: number;
channel: BroadcastChannel;
reject: (reason?: unknown) => void;
} | null = 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.
*/
export function cancelPendingBridgeResponse(
logger: Logger,
correlationId: string
): void {
if (activeBridgeMonitor) {
logger.verbose(
"BrowserUtils.cancelPendingBridgeResponse - Cancelling pending bridge monitor",
correlationId
);
clearTimeout(activeBridgeMonitor.timeoutId);
activeBridgeMonitor.channel.close();
activeBridgeMonitor.reject(
createBrowserAuthError(
BrowserAuthErrorCodes.interactionInProgressCancelled
)
);
activeBridgeMonitor = null;
}
}
export async function waitForBridgeResponse(
timeoutMs: number,
logger: Logger,
browserCrypto: ICrypto,
request: CommonAuthorizationUrlRequest | CommonEndSessionRequest,
performanceClient: IPerformanceClient,
experimentalConfig?: BrowserExperimentalOptions
): Promise<string> {
return new Promise<string>((resolve, reject) => {
logger.verbose(
"BrowserUtils.waitForBridgeResponse - started",
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: string | undefined = undefined;
let timedOut = false;
let lateTimeoutId: number | undefined;
let lateMeasurement: InProgressPerformanceEvent | undefined;
const timeoutId = window.setTimeout(() => {
// Clear the active monitor
activeBridgeMonitor = null;
if (experimentalConfig?.iframeTimeoutTelemetry) {
lateMeasurement = performanceClient.startMeasurement(
BrowserPerformanceEvents.WaitForBridgeLateResponse,
correlationId
);
timedOut = 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(
BrowserAuthErrorCodes.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) {
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
*/
export function getCurrentUri(): string {
return typeof window !== "undefined" && window.location
? window.location.href.split("?")[0].split("#")[0]
: "";
}
/**
* Gets the homepage url for the current window location.
*/
export function getHomepage(): string {
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.
*/
export function blockReloadInHiddenIframes(): void {
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(BrowserAuthErrorCodes.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
*/
export function blockRedirectInIframe(allowRedirectInIframe: boolean): void {
if (isInIframe() && !allowRedirectInIframe) {
// If we are not in top frame, we shouldn't redirect. This is also handled by the service.
throw createBrowserAuthError(BrowserAuthErrorCodes.redirectInIframe);
}
}
/**
* Block redirectUri loaded in popup from calling AcquireToken APIs
*/
export function blockAcquireTokenInPopups(): void {
// Popups opened by msal popup APIs are given a name that starts with "msal."
if (isInPopup()) {
throw createBrowserAuthError(BrowserAuthErrorCodes.blockNestedPopups);
}
}
/**
* Throws error if token requests are made in non-browser environment
* @param isBrowserEnvironment Flag indicating if environment is a browser.
*/
export function blockNonBrowserEnvironment(): void {
if (typeof window === "undefined") {
throw createBrowserAuthError(
BrowserAuthErrorCodes.nonBrowserEnvironment
);
}
}
/**
* Throws error if initialize hasn't been called
* @param initialized
*/
export function blockAPICallsBeforeInitialize(initialized: boolean): void {
if (!initialized) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.uninitializedPublicClientApplication
);
}
}
/**
* Helper to validate app environment before making an auth request
* @param initialized
*/
export function preflightCheck(initialized: boolean): void {
// 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
*/
export function redirectPreflightCheck(
initialized: boolean,
config: BrowserConfiguration
): void {
preflightCheck(initialized);
blockRedirectInIframe(config.system.allowRedirectInIframe);
// Block redirects if memory storage is enabled
if (config.cache.cacheLocation === BrowserCacheLocation.MemoryStorage) {
throw createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.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
*/
export function preconnect(authority: string): void {
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}
*/
export function createGuid(): string {
return BrowserCrypto.createNewGuid();
}
export { invoke };
export { invokeAsync };
export const addClientCapabilitiesToClaims =
RequestParameterBuilder.addClientCapabilitiesToClaims;

View File

@ -0,0 +1,19 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Utility function to remove an element from an array in place.
* @param array - The array from which to remove the element.
* @param element - The element to remove from the array.
*/
export function removeElementFromArray(
array: Array<string>,
element: string
): void {
const index = array.indexOf(element);
if (index > -1) {
array.splice(index, 1);
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { InProgressPerformanceEvent, Logger } from "@azure/msal-common/browser";
interface NetworkConnection {
effectiveType?: string;
rtt?: number;
}
/**
* Get network information for telemetry purposes. This is only supported in Chromium-based browsers.
* @returns Network connection information, or an empty object if not available.
*/
export function getNetworkInfo(): NetworkConnection {
if (typeof window === "undefined" || !window.navigator) {
return {};
}
const connection: NetworkConnection | undefined =
"connection" in window.navigator
? (
window.navigator as Navigator & {
connection: NetworkConnection;
}
).connection
: undefined;
return {
effectiveType: connection?.effectiveType,
rtt: connection?.rtt,
};
}
export function collectInstanceStats(
currentClientId: string,
performanceEvent: InProgressPerformanceEvent,
logger: Logger,
correlationId: string
): void {
const frameInstances: string[] =
// @ts-ignore
window.msal?.clientIds || [];
const msalInstanceCount = frameInstances.length;
const sameClientIdInstanceCount = frameInstances.filter(
(i) => i === currentClientId
).length;
if (sameClientIdInstanceCount > 1) {
logger.warning(
"There is already an instance of MSAL.js in the window with the same client id.",
correlationId
);
}
performanceEvent.add({
msalInstanceCount: msalInstanceCount,
sameClientIdInstanceCount: sameClientIdInstanceCount,
});
}