/*! @azure/msal-node v2.5.1 2023-11-07 */ 'use strict'; 'use strict'; var msalCommon = require('@azure/msal-common'); var Deserializer = require('./serializer/Deserializer.cjs'); var Serializer = require('./serializer/Serializer.cjs'); /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ const defaultSerializedCache = { Account: {}, IdToken: {}, AccessToken: {}, RefreshToken: {}, AppMetadata: {}, }; /** * In-memory token cache manager * @public */ class TokenCache { constructor(storage, logger, cachePlugin) { this.cacheHasChanged = false; this.storage = storage; this.storage.registerChangeEmitter(this.handleChangeEvent.bind(this)); if (cachePlugin) { this.persistence = cachePlugin; } this.logger = logger; } /** * Set to true if cache state has changed since last time serialize or writeToPersistence was called */ hasChanged() { return this.cacheHasChanged; } /** * Serializes in memory cache to JSON */ serialize() { this.logger.trace("Serializing in-memory cache"); let finalState = Serializer.Serializer.serializeAllCache(this.storage.getInMemoryCache()); // if cacheSnapshot not null or empty, merge if (this.cacheSnapshot) { this.logger.trace("Reading cache snapshot from disk"); finalState = this.mergeState(JSON.parse(this.cacheSnapshot), finalState); } else { this.logger.trace("No cache snapshot to merge"); } this.cacheHasChanged = false; return JSON.stringify(finalState); } /** * Deserializes JSON to in-memory cache. JSON should be in MSAL cache schema format * @param cache - blob formatted cache */ deserialize(cache) { this.logger.trace("Deserializing JSON to in-memory cache"); this.cacheSnapshot = cache; if (this.cacheSnapshot) { this.logger.trace("Reading cache snapshot from disk"); const deserializedCache = Deserializer.Deserializer.deserializeAllCache(this.overlayDefaults(JSON.parse(this.cacheSnapshot))); this.storage.setInMemoryCache(deserializedCache); } else { this.logger.trace("No cache snapshot to deserialize"); } } /** * Fetches the cache key-value map */ getKVStore() { return this.storage.getCache(); } /** * API that retrieves all accounts currently in cache to the user */ async getAllAccounts() { this.logger.trace("getAllAccounts called"); let cacheContext; try { if (this.persistence) { cacheContext = new msalCommon.TokenCacheContext(this, false); await this.persistence.beforeCacheAccess(cacheContext); } return this.storage.getAllAccounts(); } finally { if (this.persistence && cacheContext) { await this.persistence.afterCacheAccess(cacheContext); } } } /** * Returns the signed in account matching homeAccountId. * (the account object is created at the time of successful login) * or null when no matching account is found * @param homeAccountId - unique identifier for an account (uid.utid) */ async getAccountByHomeId(homeAccountId) { const allAccounts = await this.getAllAccounts(); if (homeAccountId && allAccounts && allAccounts.length) { return (allAccounts.filter((accountObj) => accountObj.homeAccountId === homeAccountId)[0] || null); } else { return null; } } /** * Returns the signed in account matching localAccountId. * (the account object is created at the time of successful login) * or null when no matching account is found * @param localAccountId - unique identifier of an account (sub/obj when homeAccountId cannot be populated) */ async getAccountByLocalId(localAccountId) { const allAccounts = await this.getAllAccounts(); if (localAccountId && allAccounts && allAccounts.length) { return (allAccounts.filter((accountObj) => accountObj.localAccountId === localAccountId)[0] || null); } else { return null; } } /** * API to remove a specific account and the relevant data from cache * @param account - AccountInfo passed by the user */ async removeAccount(account) { this.logger.trace("removeAccount called"); let cacheContext; try { if (this.persistence) { cacheContext = new msalCommon.TokenCacheContext(this, true); await this.persistence.beforeCacheAccess(cacheContext); } await this.storage.removeAccount(msalCommon.AccountEntity.generateAccountCacheKey(account)); } finally { if (this.persistence && cacheContext) { await this.persistence.afterCacheAccess(cacheContext); } } } /** * Called when the cache has changed state. */ handleChangeEvent() { this.cacheHasChanged = true; } /** * Merge in memory cache with the cache snapshot. * @param oldState - cache before changes * @param currentState - current cache state in the library */ mergeState(oldState, currentState) { this.logger.trace("Merging in-memory cache with cache snapshot"); const stateAfterRemoval = this.mergeRemovals(oldState, currentState); return this.mergeUpdates(stateAfterRemoval, currentState); } /** * Deep update of oldState based on newState values * @param oldState - cache before changes * @param newState - updated cache */ mergeUpdates(oldState, newState) { Object.keys(newState).forEach((newKey) => { const newValue = newState[newKey]; // if oldState does not contain value but newValue does, add it if (!oldState.hasOwnProperty(newKey)) { if (newValue !== null) { oldState[newKey] = newValue; } } else { // both oldState and newState contain the key, do deep update const newValueNotNull = newValue !== null; const newValueIsObject = typeof newValue === "object"; const newValueIsNotArray = !Array.isArray(newValue); const oldStateNotUndefinedOrNull = typeof oldState[newKey] !== "undefined" && oldState[newKey] !== null; if (newValueNotNull && newValueIsObject && newValueIsNotArray && oldStateNotUndefinedOrNull) { this.mergeUpdates(oldState[newKey], newValue); } else { oldState[newKey] = newValue; } } }); return oldState; } /** * Removes entities in oldState that the were removed from newState. If there are any unknown values in root of * oldState that are not recognized, they are left untouched. * @param oldState - cache before changes * @param newState - updated cache */ mergeRemovals(oldState, newState) { this.logger.trace("Remove updated entries in cache"); const accounts = oldState.Account ? this.mergeRemovalsDict(oldState.Account, newState.Account) : oldState.Account; const accessTokens = oldState.AccessToken ? this.mergeRemovalsDict(oldState.AccessToken, newState.AccessToken) : oldState.AccessToken; const refreshTokens = oldState.RefreshToken ? this.mergeRemovalsDict(oldState.RefreshToken, newState.RefreshToken) : oldState.RefreshToken; const idTokens = oldState.IdToken ? this.mergeRemovalsDict(oldState.IdToken, newState.IdToken) : oldState.IdToken; const appMetadata = oldState.AppMetadata ? this.mergeRemovalsDict(oldState.AppMetadata, newState.AppMetadata) : oldState.AppMetadata; return { ...oldState, Account: accounts, AccessToken: accessTokens, RefreshToken: refreshTokens, IdToken: idTokens, AppMetadata: appMetadata, }; } /** * Helper to merge new cache with the old one * @param oldState - cache before changes * @param newState - updated cache */ mergeRemovalsDict(oldState, newState) { const finalState = { ...oldState }; Object.keys(oldState).forEach((oldKey) => { if (!newState || !newState.hasOwnProperty(oldKey)) { delete finalState[oldKey]; } }); return finalState; } /** * Helper to overlay as a part of cache merge * @param passedInCache - cache read from the blob */ overlayDefaults(passedInCache) { this.logger.trace("Overlaying input cache with the default cache"); return { Account: { ...defaultSerializedCache.Account, ...passedInCache.Account, }, IdToken: { ...defaultSerializedCache.IdToken, ...passedInCache.IdToken, }, AccessToken: { ...defaultSerializedCache.AccessToken, ...passedInCache.AccessToken, }, RefreshToken: { ...defaultSerializedCache.RefreshToken, ...passedInCache.RefreshToken, }, AppMetadata: { ...defaultSerializedCache.AppMetadata, ...passedInCache.AppMetadata, }, }; } } exports.TokenCache = TokenCache; //# sourceMappingURL=TokenCache.cjs.map