UNPKG

@azure/msal-node-extensions

Version:

![npm (scoped)](https://img.shields.io/npm/v/@azure/msal-node-extensions) ![npm](https://img.shields.io/npm/dw/@azure/msal-node-extensions)

1,380 lines (1,350 loc) 83.7 kB
/*! @azure/msal-node-extensions v5.2.2 2026-05-19 */ 'use strict'; 'use strict'; var fs = require('fs'); var process$1 = require('process'); var path = require('path'); var module$1 = require('module'); var keytar = require('keytar'); var msalNodeRuntime = require('@azure/msal-node-runtime'); var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ const Constants = { /** * An existing file was the target of an operation that required that the target not exist */ EEXIST_ERROR: "EEXIST", /** * No such file or directory: Commonly raised by fs operations to indicate that a component * of the specified pathname does not exist. No entity (file or directory) could be found * by the given path */ ENOENT_ERROR: "ENOENT", /** * Operation not permitted. An attempt was made to perform an operation that requires * elevated privileges. */ EPERM_ERROR: "EPERM", /** * Default service name for using MSAL Keytar */ DEFAULT_SERVICE_NAME: "msal-node-extensions", /** * Test data used to verify underlying persistence mechanism */ PERSISTENCE_TEST_DATA: "Dummy data to verify underlying persistence mechanism", /** * This is the value of a the guid if the process is being ran by the root user */ LINUX_ROOT_USER_GUID: 0, /** * List of environment variables */ ENVIRONMENT: { HOME: "HOME", LOGNAME: "LOGNAME", USER: "USER", LNAME: "LNAME", USERNAME: "USERNAME", PLATFORM: "platform", LOCAL_APPLICATION_DATA: "LOCALAPPDATA", }, // Name of the default cache file DEFAULT_CACHE_FILE_NAME: "cache.json", }; const Platform = { WINDOWS: "win32", LINUX: "linux", MACOS: "darwin", }; const ErrorCodes = { INTERATION_REQUIRED_ERROR_CODE: "interaction_required", SERVER_UNAVAILABLE: "server_unavailable", UNKNOWN: "unknown_error", }; /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Error thrown when trying to write MSAL cache to persistence. */ class PersistenceError extends Error { constructor(errorCode, errorMessage) { const errorString = errorMessage ? `${errorCode}: ${errorMessage}` : errorCode; super(errorString); Object.setPrototypeOf(this, PersistenceError.prototype); this.errorCode = errorCode; this.errorMessage = errorMessage; this.name = "PersistenceError"; } /** * Error thrown when trying to access the file system. */ static createFileSystemError(errorCode, errorMessage) { return new PersistenceError(errorCode, errorMessage); } /** * Error thrown when trying to write, load, or delete data from secret service on linux. * Libsecret is used to access secret service. */ static createLibSecretError(errorMessage) { return new PersistenceError("GnomeKeyringError", errorMessage); } /** * Error thrown when trying to write, load, or delete data from keychain on macOs. */ static createKeychainPersistenceError(errorMessage) { return new PersistenceError("KeychainError", errorMessage); } /** * Error thrown when trying to encrypt or decrypt data using DPAPI on Windows. */ static createFilePersistenceWithDPAPIError(errorMessage) { return new PersistenceError("DPAPIEncryptedFileError", errorMessage); } /** * Error thrown when using the cross platform lock. */ static createCrossPlatformLockError(errorMessage) { return new PersistenceError("CrossPlatformLockError", errorMessage); } /** * Throw cache persistence error * * @param errorMessage string * @returns PersistenceError */ static createCachePersistenceError(errorMessage) { return new PersistenceError("CachePersistenceError", errorMessage); } /** * Throw unsupported error * * @param errorMessage string * @returns PersistenceError */ static createNotSupportedError(errorMessage) { return new PersistenceError("NotSupportedError", errorMessage); } /** * Throw persistence not verified error * * @param errorMessage string * @returns PersistenceError */ static createPersistenceNotVerifiedError(errorMessage) { return new PersistenceError("PersistenceNotVerifiedError", errorMessage); } /** * Throw persistence creation validation error * * @param errorMessage string * @returns PersistenceError */ static createPersistenceNotValidatedError(errorMessage) { return new PersistenceError("PersistenceNotValidatedError", errorMessage); } } /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Returns whether or not the given object is a Node.js error */ const isNodeError = (error) => { return !!error && typeof error === "object" && error.hasOwnProperty("code"); }; /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Cross-process lock that works on all platforms. */ class CrossPlatformLock { constructor(lockFilePath, logger, lockOptions) { this.lockFilePath = lockFilePath; this.retryNumber = lockOptions ? lockOptions.retryNumber : 500; this.retryDelay = lockOptions ? lockOptions.retryDelay : 100; this.logger = logger; } /** * Locks cache from read or writes by creating file with same path and name as * cache file but with .lockfile extension. If another process has already created * the lockfile, will back off and retry based on configuration settings set by CrossPlatformLockOptions */ async lock() { for (let tryCount = 0; tryCount < this.retryNumber; tryCount++) { try { this.logger.info(`Pid ${process$1.pid} trying to acquire lock`, ""); this.lockFileHandle = await fs.promises.open(this.lockFilePath, "wx+"); this.logger.info(`Pid ${process$1.pid} acquired lock`, ""); await this.lockFileHandle.write(process$1.pid.toString()); return; } catch (err) { if (isNodeError(err)) { if (err.code === Constants.EEXIST_ERROR || err.code === Constants.EPERM_ERROR) { this.logger.info(err.message, ""); await this.sleep(this.retryDelay); } else { this.logger.error(`${process$1.pid} was not able to acquire lock. Ran into error: ${err.message}`, ""); throw PersistenceError.createCrossPlatformLockError(err.message); } } else { throw err; } } } this.logger.error(`${process$1.pid} was not able to acquire lock. Exceeded amount of retries set in the options`, ""); throw PersistenceError.createCrossPlatformLockError("Not able to acquire lock. Exceeded amount of retries set in options"); } /** * unlocks cache file by deleting .lockfile. */ async unlock() { try { if (this.lockFileHandle) { // if we have a file handle to the .lockfile, delete lock file await fs.promises.unlink(this.lockFilePath); await this.lockFileHandle.close(); this.logger.info("lockfile deleted", ""); } else { this.logger.warning("lockfile handle does not exist, so lockfile could not be deleted", ""); } } catch (err) { if (isNodeError(err)) { if (err.code === Constants.ENOENT_ERROR) { this.logger.info("Tried to unlock but lockfile does not exist", ""); } else { this.logger.error(`${process$1.pid} was not able to release lock. Ran into error: ${err.message}`, ""); throw PersistenceError.createCrossPlatformLockError(err.message); } } else { throw err; } } } sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } } /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * MSAL cache plugin which enables callers to write the MSAL cache to disk on Windows, * macOs, and Linux. * * - Persistence can be one of: * - FilePersistence: Writes and reads from an unencrypted file. Can be used on Windows, * macOs, or Linux. * - FilePersistenceWithDataProtection: Used on Windows, writes and reads from file encrypted * with windows dpapi-addon. * - KeychainPersistence: Used on macOs, writes and reads from keychain. * - LibSecretPersistence: Used on linux, writes and reads from secret service API. Requires * libsecret be installed. */ class PersistenceCachePlugin { constructor(persistence, lockOptions) { this.persistence = persistence; // initialize logger this.logger = persistence.getLogger(); // create file lock this.lockFilePath = `${this.persistence.getFilePath()}.lockfile`; this.crossPlatformLock = new CrossPlatformLock(this.lockFilePath, this.logger, lockOptions); // initialize default values this.lastSync = 0; this.currentCache = null; } /** * Reads from storage and saves an in-memory copy. If persistence has not been updated * since last time data was read, in memory copy is used. * * If cacheContext.cacheHasChanged === true, then file lock is created and not deleted until * afterCacheAccess() is called, to prevent the cache file from changing in between * beforeCacheAccess() and afterCacheAccess(). */ async beforeCacheAccess(cacheContext) { this.logger.info("Executing before cache access", ""); const reloadNecessary = await this.persistence.reloadNecessary(this.lastSync); if (!reloadNecessary && this.currentCache !== null) { if (cacheContext.cacheHasChanged) { this.logger.verbose("Cache context has changed", ""); await this.crossPlatformLock.lock(); } return; } try { this.logger.info(`Reload necessary. Last sync time: ${this.lastSync}`, ""); await this.crossPlatformLock.lock(); this.currentCache = await this.persistence.load(); this.lastSync = new Date().getTime(); if (this.currentCache) { cacheContext.tokenCache.deserialize(this.currentCache); } else { this.logger.info("Cache empty.", ""); } this.logger.info(`Last sync time updated to: ${this.lastSync}`, ""); } finally { if (!cacheContext.cacheHasChanged) { await this.crossPlatformLock.unlock(); this.logger.info(`Pid ${process$1.pid} released lock`, ""); } else { this.logger.info(`Pid ${process$1.pid} beforeCacheAccess did not release lock`, ""); } } } /** * Writes to storage if MSAL in memory copy of cache has been changed. */ async afterCacheAccess(cacheContext) { this.logger.info("Executing after cache access", ""); try { if (cacheContext.cacheHasChanged) { this.logger.info("Msal in-memory cache has changed. Writing changes to persistence", ""); this.currentCache = cacheContext.tokenCache.serialize(); await this.persistence.save(this.currentCache); } else { this.logger.info("Msal in-memory cache has not changed. Did not write to persistence", ""); } } finally { await this.crossPlatformLock.unlock(); this.logger.info(`Pid ${process$1.pid} afterCacheAccess released lock`, ""); } } } /*! @azure/msal-common v16.6.2 2026-05-19 */ /** * we considered making this "enum" in the request instead of string, however it looks like the allowed list of * prompt values kept changing over past couple of years. There are some undocumented prompt values for some * internal partners too, hence the choice of generic "string" type instead of the "enum" */ const PromptValue = { LOGIN: "login", SELECT_ACCOUNT: "select_account", NONE: "none", CREATE: "create"}; /** * Separators used in cache */ const CACHE_KEY_SEPARATOR = "-"; const SERVER_TELEM_SCHEMA_VERSION = 5; const SERVER_TELEM_MAX_LAST_HEADER_BYTES = 330; // ESTS limit is 350B, set to 330 to provide a 20B buffer, const SERVER_TELEM_MAX_CACHED_ERRORS = 50; // Limit the number of errors that can be stored to prevent uncontrolled size gains const SERVER_TELEM_CACHE_KEY = "server-telemetry"; const SERVER_TELEM_CATEGORY_SEPARATOR = "|"; const SERVER_TELEM_VALUE_SEPARATOR = ","; const SERVER_TELEM_OVERFLOW_TRUE = "1"; const SERVER_TELEM_OVERFLOW_FALSE = "0"; const SERVER_TELEM_UNKNOWN_ERROR = "unknown_error"; /** * Type of the authentication request */ const AuthenticationScheme = { BEARER: "Bearer", POP: "pop"}; /** * Specifies the reason for fetching the access token from the identity provider */ const CacheOutcome = { // When a token is found in the cache or the cache is not supposed to be hit when making the request NOT_APPLICABLE: "0"}; /*! @azure/msal-common v16.6.2 2026-05-19 */ const X_CLIENT_EXTRA_SKU = "x-client-xtra-sku"; const RESOURCE = "resource"; /*! @azure/msal-common v16.6.2 2026-05-19 */ /* * 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`; } /** * General error class thrown by the MSAL.js library. */ class AuthError extends Error { constructor(errorCode, errorMessage, suberror) { const message = errorMessage || (errorCode ? getDefaultErrorMessage(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.6.2 2026-05-19 */ /* * 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.6.2 2026-05-19 */ /* * 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.6.2 2026-05-19 */ const untrustedAuthority = "untrusted_authority"; /*! @azure/msal-common v16.6.2 2026-05-19 */ const noAccountFound = "no_account_found"; const noNetworkConnectivity = "no_network_connectivity"; const userCanceled = "user_canceled"; const platformBrokerError = "platform_broker_error"; /*! @azure/msal-common v16.6.2 2026-05-19 */ /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Log message level. */ var LogLevel; (function (LogLevel) { LogLevel[LogLevel["Error"] = 0] = "Error"; LogLevel[LogLevel["Warning"] = 1] = "Warning"; LogLevel[LogLevel["Info"] = 2] = "Info"; LogLevel[LogLevel["Verbose"] = 3] = "Verbose"; LogLevel[LogLevel["Trace"] = 4] = "Trace"; })(LogLevel || (LogLevel = {})); // Shared cache state for better minification - using Map's insertion order for LRU const CACHE_CAPACITY = 50; const MAX_LOGS_PER_CORRELATION = 500; const correlationCache = new Map(); /** * Mark correlation ID as recently used by moving it to end of Map * @param correlationId * @param {CorrelationLogData} data */ function markAsRecentlyUsed(correlationId, data) { correlationCache.delete(correlationId); correlationCache.set(correlationId, data); } /** * Add log message to cache for specific correlation ID * @param correlationId * @param {LoggedMessage} loggedMessage */ function addLogToCache(correlationId, loggedMessage) { const currentTime = Date.now(); let data = correlationCache.get(correlationId); if (data) { // Mark as recently used markAsRecentlyUsed(correlationId, data); } else { // Create new entry data = { logs: [], firstEventTime: currentTime }; correlationCache.set(correlationId, data); // Remove LRU (first entry) if capacity exceeded if (correlationCache.size > CACHE_CAPACITY) { const firstKey = correlationCache.keys().next().value; if (firstKey) { correlationCache.delete(firstKey); } } } // Add log to the data, maintaining max logs per correlation data.logs.push({ ...loggedMessage, milliseconds: currentTime - data.firstEventTime, }); if (data.logs.length > MAX_LOGS_PER_CORRELATION) { data.logs.shift(); // Remove oldest log } } /** * Checks if a string is already a hashed logging string (6 alphanumeric characters) */ function isHashedString(str) { if (str.length !== 6) { return false; } for (let i = 0; i < str.length; i++) { const char = str[i]; const isAlphaNumeric = (char >= "a" && char <= "z") || (char >= "A" && char <= "Z") || (char >= "0" && char <= "9"); if (!isAlphaNumeric) { return false; } } return true; } /** * Class which facilitates logging of messages to a specific place. */ class Logger { constructor(loggerOptions, packageName, packageVersion) { // Current log level, defaults to info. this.level = LogLevel.Info; const defaultLoggerCallback = () => { return; }; const setLoggerOptions = loggerOptions || Logger.createDefaultLoggerOptions(); this.localCallback = setLoggerOptions.loggerCallback || defaultLoggerCallback; this.piiLoggingEnabled = setLoggerOptions.piiLoggingEnabled || false; this.level = typeof setLoggerOptions.logLevel === "number" ? setLoggerOptions.logLevel : LogLevel.Info; this.packageName = packageName || ""; this.packageVersion = packageVersion || ""; } static createDefaultLoggerOptions() { return { loggerCallback: () => { // allow users to not set loggerCallback }, piiLoggingEnabled: false, logLevel: LogLevel.Info, }; } /** * Create new Logger with existing configurations. */ clone(packageName, packageVersion) { return new Logger({ loggerCallback: this.localCallback, piiLoggingEnabled: this.piiLoggingEnabled, logLevel: this.level, }, packageName, packageVersion); } /** * Log message with required options. */ logMessage(logMessage, options) { const correlationId = options.correlationId; const isHashedInput = isHashedString(logMessage); if (isHashedInput) { const loggedMessage = { hash: logMessage, level: options.logLevel, containsPii: options.containsPii || false, milliseconds: 0, // Will be calculated in addLogToCache }; addLogToCache(correlationId, loggedMessage); } if (options.logLevel > this.level || (!this.piiLoggingEnabled && options.containsPii)) { return; } const timestamp = new Date().toUTCString(); // Add correlationId to logs if set, correlationId provided on log messages take precedence const logHeader = `[${timestamp}] : [${correlationId}]`; const log = `${logHeader} : ${this.packageName}@${this.packageVersion} : ${LogLevel[options.logLevel]} - ${logMessage}`; this.executeCallback(options.logLevel, log, options.containsPii || false); } /** * Execute callback with message. */ executeCallback(level, message, containsPii) { if (this.localCallback) { this.localCallback(level, message, containsPii); } } /** * Logs error messages. */ error(message, correlationId) { this.logMessage(message, { logLevel: LogLevel.Error, containsPii: false, correlationId: correlationId, }); } /** * Logs error messages with PII. */ errorPii(message, correlationId) { this.logMessage(message, { logLevel: LogLevel.Error, containsPii: true, correlationId: correlationId, }); } /** * Logs warning messages. */ warning(message, correlationId) { this.logMessage(message, { logLevel: LogLevel.Warning, containsPii: false, correlationId: correlationId, }); } /** * Logs warning messages with PII. */ warningPii(message, correlationId) { this.logMessage(message, { logLevel: LogLevel.Warning, containsPii: true, correlationId: correlationId, }); } /** * Logs info messages. */ info(message, correlationId) { this.logMessage(message, { logLevel: LogLevel.Info, containsPii: false, correlationId: correlationId, }); } /** * Logs info messages with PII. */ infoPii(message, correlationId) { this.logMessage(message, { logLevel: LogLevel.Info, containsPii: true, correlationId: correlationId, }); } /** * Logs verbose messages. */ verbose(message, correlationId) { this.logMessage(message, { logLevel: LogLevel.Verbose, containsPii: false, correlationId: correlationId, }); } /** * Logs verbose messages with PII. */ verbosePii(message, correlationId) { this.logMessage(message, { logLevel: LogLevel.Verbose, containsPii: true, correlationId: correlationId, }); } /** * Logs trace messages. */ trace(message, correlationId) { this.logMessage(message, { logLevel: LogLevel.Trace, containsPii: false, correlationId: correlationId, }); } /** * Logs trace messages with PII. */ tracePii(message, correlationId) { this.logMessage(message, { logLevel: LogLevel.Trace, containsPii: true, correlationId: correlationId, }); } /** * Returns whether PII Logging is enabled or not. */ isPiiLoggingEnabled() { return this.piiLoggingEnabled || false; } } /*! @azure/msal-common v16.6.2 2026-05-19 */ /** * Convert seconds to JS Date object. Seconds can be in a number or string format or undefined (will still return a date). * @param seconds */ function toDateFromSeconds(seconds) { if (seconds) { return new Date(Number(seconds) * 1000); } return new Date(); } /*! @azure/msal-common v16.6.2 2026-05-19 */ /** * Error thrown when user interaction is required. */ class InteractionRequiredAuthError extends AuthError { constructor(errorCode, errorMessage, subError, timestamp, traceId, correlationId, claims, errorNo) { super(errorCode, errorMessage, subError); Object.setPrototypeOf(this, InteractionRequiredAuthError.prototype); this.timestamp = timestamp || ""; this.traceId = traceId || ""; this.correlationId = correlationId || ""; this.claims = claims || ""; this.name = "InteractionRequiredAuthError"; this.errorNo = errorNo; } } /*! @azure/msal-common v16.6.2 2026-05-19 */ /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Error thrown when there is an error with the server code, for example, unavailability. */ class ServerError extends AuthError { constructor(errorCode, errorMessage, subError, errorNo, status) { super(errorCode, errorMessage, subError); this.name = "ServerError"; this.errorNo = errorNo; this.status = status; Object.setPrototypeOf(this, ServerError.prototype); } } /*! @azure/msal-common v16.6.2 2026-05-19 */ /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Converts a numeric tag from MSAL Runtime to a 5-character string representation. * Tags are encoded as 30-bit values (6 bits per character) using a custom symbol space. * @param tag - The numeric tag to convert * @returns The string representation of the tag */ function tagToString(tag) { if (tag === 0) { return "UNTAG"; } const tagSymbolSpace = "abcdefghijklmnopqrstuvwxyz0123456789****************************"; let tagBuffer = "*****"; const chars = [ tagSymbolSpace[(tag >> 24) & 0x3f], tagSymbolSpace[(tag >> 18) & 0x3f], tagSymbolSpace[(tag >> 12) & 0x3f], tagSymbolSpace[(tag >> 6) & 0x3f], tagSymbolSpace[(tag >> 0) & 0x3f], ]; tagBuffer = chars.join(""); return tagBuffer; } /** * Error class for MSAL Runtime errors that preserves detailed broker information */ class PlatformBrokerError extends AuthError { constructor(errorStatus, errorContext, errorCode, errorTag) { const tagString = tagToString(errorTag); const enhancedErrorContext = errorContext ? `${errorContext} (Error Code: ${errorCode}, Tag: ${tagString})` : `(Error Code: ${errorCode}, Tag: ${tagString})`; super(errorStatus, enhancedErrorContext); this.name = "PlatformBrokerError"; this.statusCode = errorCode; this.tag = tagString; Object.setPrototypeOf(this, PlatformBrokerError.prototype); } } /*! @azure/msal-common v16.6.2 2026-05-19 */ /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ const skuGroupSeparator = ","; const skuValueSeparator = "|"; function makeExtraSkuString(params) { const { skus, libraryName, libraryVersion, extensionName, extensionVersion, } = params; const skuMap = new Map([ [0, [libraryName, libraryVersion]], [2, [extensionName, extensionVersion]], ]); let skuArr = []; if (skus?.length) { skuArr = skus.split(skuGroupSeparator); // Ignore invalid input sku param if (skuArr.length < 4) { return skus; } } else { skuArr = Array.from({ length: 4 }, () => skuValueSeparator); } skuMap.forEach((value, key) => { if (value.length === 2 && value[0]?.length && value[1]?.length) { setSku({ skuArr, index: key, skuName: value[0], skuVersion: value[1], }); } }); return skuArr.join(skuGroupSeparator); } function setSku(params) { const { skuArr, index, skuName, skuVersion } = params; if (index >= skuArr.length) { return; } skuArr[index] = [skuName, skuVersion].join(skuValueSeparator); } /** @internal */ class ServerTelemetryManager { constructor(telemetryRequest, cacheManager) { this.cacheOutcome = CacheOutcome.NOT_APPLICABLE; this.cacheManager = cacheManager; this.apiId = telemetryRequest.apiId; this.correlationId = telemetryRequest.correlationId; this.wrapperSKU = telemetryRequest.wrapperSKU || ""; this.wrapperVer = telemetryRequest.wrapperVer || ""; this.telemetryCacheKey = SERVER_TELEM_CACHE_KEY + CACHE_KEY_SEPARATOR + telemetryRequest.clientId; } /** * API to add MSER Telemetry to request */ generateCurrentRequestHeaderValue() { const request = `${this.apiId}${SERVER_TELEM_VALUE_SEPARATOR}${this.cacheOutcome}`; const platformFieldsArr = [this.wrapperSKU, this.wrapperVer]; const nativeBrokerErrorCode = this.getNativeBrokerErrorCode(); if (nativeBrokerErrorCode?.length) { platformFieldsArr.push(`broker_error=${nativeBrokerErrorCode}`); } const platformFields = platformFieldsArr.join(SERVER_TELEM_VALUE_SEPARATOR); const regionDiscoveryFields = this.getRegionDiscoveryFields(); const requestWithRegionDiscoveryFields = [ request, regionDiscoveryFields, ].join(SERVER_TELEM_VALUE_SEPARATOR); return [ SERVER_TELEM_SCHEMA_VERSION, requestWithRegionDiscoveryFields, platformFields, ].join(SERVER_TELEM_CATEGORY_SEPARATOR); } /** * API to add MSER Telemetry for the last failed request */ generateLastRequestHeaderValue() { const lastRequests = this.getLastRequests(); const maxErrors = ServerTelemetryManager.maxErrorsToSend(lastRequests); const failedRequests = lastRequests.failedRequests .slice(0, 2 * maxErrors) .join(SERVER_TELEM_VALUE_SEPARATOR); const errors = lastRequests.errors .slice(0, maxErrors) .join(SERVER_TELEM_VALUE_SEPARATOR); const errorCount = lastRequests.errors.length; // Indicate whether this header contains all data or partial data const overflow = maxErrors < errorCount ? SERVER_TELEM_OVERFLOW_TRUE : SERVER_TELEM_OVERFLOW_FALSE; const platformFields = [errorCount, overflow].join(SERVER_TELEM_VALUE_SEPARATOR); return [ SERVER_TELEM_SCHEMA_VERSION, lastRequests.cacheHits, failedRequests, errors, platformFields, ].join(SERVER_TELEM_CATEGORY_SEPARATOR); } /** * API to cache token failures for MSER data capture * @param error */ cacheFailedRequest(error) { const lastRequests = this.getLastRequests(); if (lastRequests.errors.length >= SERVER_TELEM_MAX_CACHED_ERRORS) { // Remove a cached error to make room, first in first out lastRequests.failedRequests.shift(); // apiId lastRequests.failedRequests.shift(); // correlationId lastRequests.errors.shift(); } lastRequests.failedRequests.push(this.apiId, this.correlationId); if (error instanceof Error && !!error && error.toString()) { if (error instanceof AuthError) { if (error.subError) { lastRequests.errors.push(error.subError); } else if (error.errorCode) { lastRequests.errors.push(error.errorCode); } else { lastRequests.errors.push(error.toString()); } } else { lastRequests.errors.push(error.toString()); } } else { lastRequests.errors.push(SERVER_TELEM_UNKNOWN_ERROR); } this.cacheManager.setServerTelemetry(this.telemetryCacheKey, lastRequests, this.correlationId); return; } /** * Update server telemetry cache entry by incrementing cache hit counter */ incrementCacheHits() { const lastRequests = this.getLastRequests(); lastRequests.cacheHits += 1; this.cacheManager.setServerTelemetry(this.telemetryCacheKey, lastRequests, this.correlationId); return lastRequests.cacheHits; } /** * Get the server telemetry entity from cache or initialize a new one */ getLastRequests() { const initialValue = { failedRequests: [], errors: [], cacheHits: 0, }; const lastRequests = this.cacheManager.getServerTelemetry(this.telemetryCacheKey, this.correlationId); return lastRequests || initialValue; } /** * Remove server telemetry cache entry */ clearTelemetryCache() { const lastRequests = this.getLastRequests(); const numErrorsFlushed = ServerTelemetryManager.maxErrorsToSend(lastRequests); const errorCount = lastRequests.errors.length; if (numErrorsFlushed === errorCount) { // All errors were sent on last request, clear Telemetry cache this.cacheManager.removeItem(this.telemetryCacheKey, this.correlationId); } else { // Partial data was flushed to server, construct a new telemetry cache item with errors that were not flushed const serverTelemEntity = { failedRequests: lastRequests.failedRequests.slice(numErrorsFlushed * 2), errors: lastRequests.errors.slice(numErrorsFlushed), cacheHits: 0, }; this.cacheManager.setServerTelemetry(this.telemetryCacheKey, serverTelemEntity, this.correlationId); } } /** * Returns the maximum number of errors that can be flushed to the server in the next network request * @param serverTelemetryEntity */ static maxErrorsToSend(serverTelemetryEntity) { let i; let maxErrors = 0; let dataSize = 0; const errorCount = serverTelemetryEntity.errors.length; for (i = 0; i < errorCount; i++) { // failedRequests parameter contains pairs of apiId and correlationId, multiply index by 2 to preserve pairs const apiId = serverTelemetryEntity.failedRequests[2 * i] || ""; const correlationId = serverTelemetryEntity.failedRequests[2 * i + 1] || ""; const errorCode = serverTelemetryEntity.errors[i] || ""; // Count number of characters that would be added to header, each character is 1 byte. Add 3 at the end to account for separators dataSize += apiId.toString().length + correlationId.toString().length + errorCode.length + 3; if (dataSize < SERVER_TELEM_MAX_LAST_HEADER_BYTES) { // Adding this entry to the header would still keep header size below the limit maxErrors += 1; } else { break; } } return maxErrors; } /** * Get the region discovery fields * * @returns string */ getRegionDiscoveryFields() { const regionDiscoveryFields = []; regionDiscoveryFields.push(this.regionUsed || ""); regionDiscoveryFields.push(this.regionSource || ""); regionDiscoveryFields.push(this.regionOutcome || ""); return regionDiscoveryFields.join(","); } /** * Update the region discovery metadata * * @param regionDiscoveryMetadata * @returns void */ updateRegionDiscoveryMetadata(regionDiscoveryMetadata) { this.regionUsed = regionDiscoveryMetadata.region_used; this.regionSource = regionDiscoveryMetadata.region_source; this.regionOutcome = regionDiscoveryMetadata.region_outcome; } /** * Set cache outcome */ setCacheOutcome(cacheOutcome) { this.cacheOutcome = cacheOutcome; } setNativeBrokerErrorCode(errorCode) { const lastRequests = this.getLastRequests(); lastRequests.nativeBrokerErrorCode = errorCode; this.cacheManager.setServerTelemetry(this.telemetryCacheKey, lastRequests, this.correlationId); } getNativeBrokerErrorCode() { return this.getLastRequests().nativeBrokerErrorCode; } clearNativeBrokerErrorCode() { const lastRequests = this.getLastRequests(); delete lastRequests.nativeBrokerErrorCode; this.cacheManager.setServerTelemetry(this.telemetryCacheKey, lastRequests, this.correlationId); } static makeExtraSkuString(params) { return makeExtraSkuString(params); } } /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ class BasePersistence { async verifyPersistence() { // We are using a different location for the test to avoid overriding the functional cache const persistenceValidator = await this.createForPersistenceValidation(); try { await persistenceValidator.save(Constants.PERSISTENCE_TEST_DATA); const retrievedDummyData = await persistenceValidator.load(); if (!retrievedDummyData) { throw PersistenceError.createCachePersistenceError("Persistence check failed. Data was written but it could not be read. " + "Possible cause: on Linux, LibSecret is installed but D-Bus isn't running \ because it cannot be started over SSH."); } if (retrievedDummyData !== Constants.PERSISTENCE_TEST_DATA) { throw PersistenceError.createCachePersistenceError(`Persistence check failed. Data written ${Constants.PERSISTENCE_TEST_DATA} is different \ from data read ${retrievedDummyData}`); } await persistenceValidator.delete(); return true; } catch (e) { throw PersistenceError.createCachePersistenceError(`Verifing persistence failed with the error: ${e}`); } } } /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Reads and writes data to file specified by file location. File contents are not * encrypted. * * If file or directory has not been created, it FilePersistence.create() will create * file and any directories in the path recursively. */ class FilePersistence extends BasePersistence { constructor(fileLocation, loggerOptions) { super(); this.logger = new Logger(loggerOptions || FilePersistence.createDefaultLoggerOptions()); this.filePath = fileLocation; } static async create(fileLocation, loggerOptions) { const filePersistence = new FilePersistence(fileLocation, loggerOptions); await filePersistence.createCacheFile(); return filePersistence; } async save(contents) { try { await fs.promises.writeFile(this.getFilePath(), contents, "utf-8"); } catch (err) { if (isNodeError(err)) { throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message); } else { throw err; } } } async saveBuffer(contents) { try { await fs.promises.writeFile(this.getFilePath(), contents); } catch (err) { if (isNodeError(err)) { throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message); } else { throw err; } } } async load() { try { return await fs.promises.readFile(this.getFilePath(), "utf-8"); } catch (err) { if (isNodeError(err)) { throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message); } else { throw err; } } } async loadBuffer() { try { return await fs.promises.readFile(this.getFilePath()); } catch (err) { if (isNodeError(err)) { throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message); } else { throw err; } } } async delete() { try { await fs.promises.unlink(this.getFilePath()); return true; } catch (err) { if (isNodeError(err)) { if (err.code === Constants.ENOENT_ERROR) { // file does not exist, so it was not deleted this.logger.warning("Cache file does not exist, so it could not be deleted", ""); return false; } throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message); } else { throw err; } } } getFilePath() { return this.filePath; } async reloadNecessary(lastSync) { return lastSync < (await this.timeLastModified()); } getLogger() { return this.logger; } createForPersistenceValidation() { const testCacheFileLocation = `${path.dirname(this.filePath)}/test.cache`; return FilePersistence.create(testCacheFileLocation); } static createDefaultLoggerOptions() { return { loggerCallback: () => { // allow users to not set loggerCallback }, piiLoggingEnabled: false, logLevel: LogLevel.Info, }; } async timeLastModified() { try { const stats = await fs.promises.stat(this.filePath); return stats.mtime.getTime(); } catch (err) { if (isNodeError(err)) { if (err.code === Constants.ENOENT_ERROR) { // file does not exist, so it's never been modified this.logger.verbose("Cache file does not exist", ""); return 0; } throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message); } else { throw err; } } } async createCacheFile() { await this.createFileDirectory(); // File is created only if it does not exist const fileHandle = await fs.promises.open(this.filePath, "a"); await fileHandle.close(); this.logger.info(`File created at ${this.filePath}`, ""); } async createFileDirectory() { try { await fs.promises.mkdir(path.dirname(this.filePath), { recursive: true }); } catch (err) { if (isNodeError(err)) { if (err.code === Constants.EEXIST_ERROR) { this.logger.info(`Directory ${path.dirname(this.filePath)} already exists`, ""); } else { throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message); } } else { throw err; } } } } /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ class UnavailableDpapi { constructor(errorMessage) { this.errorMessage = errorMessage; } protectData() { throw new Error(this.errorMessage); } unprotectData() { throw new Error(this.errorMessage); } } let Dpapi; if (process.platform !== "win32") { Dpapi = new UnavailableDpapi("Dpapi is not supported on this platform"); } else { // In .mjs files, require is not defined. We need to use createRequire to get a require function const safeRequire = typeof require !== "undefined" ? require : module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('msal-node-extensions.cjs', document.baseURI).href))); try { Dpapi = safeRequire(`../bin/${process.arch}/dpapi`); } catch (e) { Dpapi = new UnavailableDpapi("Dpapi bindings unavailable"); } } /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Specifies the scope of the data protection - either the current user or the local * machine. * * You do not need a key to protect or unprotect the data. * If you set the Scope to CurrentUser, only applications running on your credentials can * unprotect the data; however, that means that any application running on your credentials * can access the protected data. If you set the Scope to LocalMachine, any full-trust * application on the computer can unprotect, access, and modify the data. * */ const DataProtectionScope = { CurrentUser: "CurrentUser", LocalMachine: "LocalMachine", }; /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Uses CryptProtectData and CryptUnprotectData on Windows to encrypt and decrypt file contents. * * scope: Scope of the data protection. Either local user or the current machine * optionalEntropy: Password or other additional entropy used to encrypt the data */ class FilePersistenceWithDataProtection extends BasePersistence { constructor(filePersistence, scope, optionalEntropy) { super(); this.scope = scope; this.optionalEntropy = optionalEntropy ? Buffer.from(optionalEntropy, "utf-8") : null; this.filePersistence = filePersistence; } static async create(fileLocation, scope, optionalEntropy, loggerOptions) { const filePersistence = await FilePersistence.create(fileLocation, loggerOptions); const persistence = new FilePersistenceWithDataProtection(filePersistence, scope, optionalEntropy); return persistence; } async save(contents) { try { const encryptedContents = Dpapi.protectData(Buffer.from(contents, "utf-8"), this.optionalEntropy, this.scope.toString()); await this.filePersistence.saveBuffer(encryptedContents); } catch (err) { if (isNodeError(err)) { throw PersistenceError.createFilePersistenceWithDPAPIError(err.message); } else { throw err; } } } async load() { try { const encryptedContents = await this.filePersistence.loadBuffer(); if (typeof encryptedContents === "undefined" || !encryptedContents || 0 === encryptedContents.length) { this.filePersistence .getLogger() .info("Encrypted contents loaded from file were null or empty", ""); return null; } return Dpapi.unprotectData(encryptedContents, this.optionalEntropy, this.scope.toString()).toString(); } catch (err) { if (isNodeError(err)) { throw PersistenceError.createFilePersistenceWithDPAPIError(err.message); } else {