Skip to content

Commit

Permalink
Merge pull request #7446 from AzureAD/refactor-storage-impl
Browse files Browse the repository at this point in the history
Creating distinct classes for LocalStorage, SessionStorage and Cookies
to prepare for upcoming changes to the LocalStorage implementation
related to KMSI
  • Loading branch information
tnorling authored Dec 11, 2024
2 parents 83b6a89 + 3e3616e commit b76e73d
Show file tree
Hide file tree
Showing 10 changed files with 395 additions and 270 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Refactor storage implementations #7446",
"packageName": "@azure/msal-browser",
"email": "[email protected]",
"dependentChangeType": "patch"
}
53 changes: 51 additions & 2 deletions lib/msal-browser/apiReview/msal-browser.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -561,10 +561,9 @@ export class BrowserPerformanceMeasurement implements IPerformanceMeasurement {
static supportsBrowserPerformance(): boolean;
}

// Warning: (ae-forgotten-export) The symbol "IWindowStorage" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "BrowserStorage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
// @public @deprecated (undocumented)
export class BrowserStorage implements IWindowStorage<string> {
constructor(cacheLocation: string);
// (undocumented)
Expand Down Expand Up @@ -1155,6 +1154,22 @@ export interface ITokenCache {
loadExternalTokens(request: SilentRequest, response: ExternalTokenResponse, options: LoadTokenOptions): AuthenticationResult;
}

// Warning: (ae-missing-release-tag) "IWindowStorage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface IWindowStorage<T> {
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
containsKey(key: string): boolean;
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
getItem(key: string): T | null;
getKeys(): string[];
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
removeItem(key: string): void;
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
setItem(key: string, value: T): void;
}

export { JsonWebTokenTypes }

// Warning: (ae-missing-release-tag) "LoadTokenOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand All @@ -1166,6 +1181,23 @@ export type LoadTokenOptions = {
extendedExpiresOn?: number;
};

// Warning: (ae-missing-release-tag) "LocalStorage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class LocalStorage implements IWindowStorage<string> {
constructor();
// (undocumented)
containsKey(key: string): boolean;
// (undocumented)
getItem(key: string): string | null;
// (undocumented)
getKeys(): string[];
// (undocumented)
removeItem(key: string): void;
// (undocumented)
setItem(key: string, value: string): void;
}

export { Logger }

export { LogLevel }
Expand Down Expand Up @@ -1579,6 +1611,23 @@ export { ServerError }

export { ServerResponseType }

// Warning: (ae-missing-release-tag) "SessionStorage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class SessionStorage implements IWindowStorage<string> {
constructor();
// (undocumented)
containsKey(key: string): boolean;
// (undocumented)
getItem(key: string): string | null;
// (undocumented)
getKeys(): string[];
// (undocumented)
removeItem(key: string): void;
// (undocumented)
setItem(key: string, value: string): void;
}

// Warning: (ae-missing-release-tag) "SignedHttpRequest" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down
168 changes: 27 additions & 141 deletions lib/msal-browser/src/cache/BrowserCacheManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ import {
InMemoryCacheKeys,
StaticCacheKeys,
} from "../utils/BrowserConstants.js";
import { BrowserStorage } from "./BrowserStorage.js";
import { LocalStorage } from "./LocalStorage.js";
import { SessionStorage } from "./SessionStorage.js";
import { MemoryStorage } from "./MemoryStorage.js";
import { IWindowStorage } from "./IWindowStorage.js";
import { extractBrowserRequestState } from "../utils/BrowserProtocolUtils.js";
Expand All @@ -64,6 +65,7 @@ import { RedirectRequest } from "../request/RedirectRequest.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { base64Decode } from "../encode/Base64Decode.js";
import { base64Encode } from "../encode/Base64Encode.js";
import { CookieStorage } from "./CookieStorage.js";

/**
* This class implements the cache storage interface for MSAL through browser local or session storage.
Expand All @@ -79,14 +81,13 @@ export class BrowserCacheManager extends CacheManager {
protected internalStorage: MemoryStorage<string>;
// Temporary cache
protected temporaryCacheStorage: IWindowStorage<string>;
// Cookie storage
protected cookieStorage: CookieStorage;
// Logger instance
protected logger: Logger;
// Telemetry perf client
protected performanceClient?: IPerformanceClient;

// Cookie life calculation (hours * minutes * seconds * ms)
protected readonly COOKIE_LIFE_MULTIPLIER = 24 * 60 * 60 * 1000;

constructor(
clientId: string,
cacheConfig: Required<CacheOptions>,
Expand All @@ -102,10 +103,10 @@ export class BrowserCacheManager extends CacheManager {
this.browserStorage = this.setupBrowserStorage(
this.cacheConfig.cacheLocation
);
this.temporaryCacheStorage = this.setupTemporaryCacheStorage(
this.cacheConfig.temporaryCacheLocation,
this.cacheConfig.cacheLocation
this.temporaryCacheStorage = this.setupBrowserStorage(
this.cacheConfig.temporaryCacheLocation
);
this.cookieStorage = new CookieStorage();

// Migrate cache entries from older versions of MSAL.
if (cacheConfig.cacheMigrationEnabled) {
Expand All @@ -123,51 +124,23 @@ export class BrowserCacheManager extends CacheManager {
protected setupBrowserStorage(
cacheLocation: BrowserCacheLocation | string
): IWindowStorage<string> {
switch (cacheLocation) {
case BrowserCacheLocation.LocalStorage:
case BrowserCacheLocation.SessionStorage:
try {
return new BrowserStorage(cacheLocation);
} catch (e) {
this.logger.verbose(e as string);
try {
switch (cacheLocation) {
case BrowserCacheLocation.LocalStorage:
return new LocalStorage();
case BrowserCacheLocation.SessionStorage:
return new SessionStorage();
case BrowserCacheLocation.MemoryStorage:
default:
break;
}
case BrowserCacheLocation.MemoryStorage:
default:
break;
}
} catch (e) {
this.logger.error(e as string);
}
this.cacheConfig.cacheLocation = BrowserCacheLocation.MemoryStorage;
return new MemoryStorage();
}

/**
* Returns a window storage class implementing the IWindowStorage interface that corresponds to the configured temporaryCacheLocation.
* @param temporaryCacheLocation
* @param cacheLocation
*/
protected setupTemporaryCacheStorage(
temporaryCacheLocation: BrowserCacheLocation | string,
cacheLocation: BrowserCacheLocation | string
): IWindowStorage<string> {
switch (cacheLocation) {
case BrowserCacheLocation.LocalStorage:
case BrowserCacheLocation.SessionStorage:
try {
// Temporary cache items will always be stored in session storage to mitigate problems caused by multiple tabs
return new BrowserStorage(
temporaryCacheLocation ||
BrowserCacheLocation.SessionStorage
);
} catch (e) {
this.logger.verbose(e as string);
return this.internalStorage;
}
case BrowserCacheLocation.MemoryStorage:
default:
return this.internalStorage;
}
}

/**
* Migrate all old cache entries to new schema. No rollback supported.
* @param storeAuthStateInCookie
Expand Down Expand Up @@ -1144,7 +1117,7 @@ export class BrowserCacheManager extends CacheManager {
getTemporaryCache(cacheKey: string, generateKey?: boolean): string | null {
const key = generateKey ? this.generateCacheKey(cacheKey) : cacheKey;
if (this.cacheConfig.storeAuthStateInCookie) {
const itemCookie = this.getItemCookie(key);
const itemCookie = this.cookieStorage.getItem(key);
if (itemCookie) {
this.logger.trace(
"BrowserCacheManager.getTemporaryCache: storeAuthStateInCookies set to true, retrieving from cookies"
Expand Down Expand Up @@ -1198,7 +1171,12 @@ export class BrowserCacheManager extends CacheManager {
this.logger.trace(
"BrowserCacheManager.setTemporaryCache: storeAuthStateInCookie set to true, setting item cookie"
);
this.setItemCookie(key, value);
this.cookieStorage.setItem(
key,
value,
undefined,
this.cacheConfig.secureCookies
);
}
}

Expand All @@ -1221,7 +1199,7 @@ export class BrowserCacheManager extends CacheManager {
this.logger.trace(
"BrowserCacheManager.removeItem: storeAuthStateInCookie is true, clearing item cookie"
);
this.clearItemCookie(key);
this.cookieStorage.removeItem(key);
}
}

Expand Down Expand Up @@ -1301,96 +1279,6 @@ export class BrowserCacheManager extends CacheManager {
}
}

/**
* Add value to cookies
* @param cookieName
* @param cookieValue
* @param expires
* @deprecated
*/
setItemCookie(
cookieName: string,
cookieValue: string,
expires?: number
): void {
let cookieStr = `${encodeURIComponent(cookieName)}=${encodeURIComponent(
cookieValue
)};path=/;SameSite=Lax;`;
if (expires) {
const expireTime = this.getCookieExpirationTime(expires);
cookieStr += `expires=${expireTime};`;
}

if (this.cacheConfig.secureCookies) {
cookieStr += "Secure;";
}

document.cookie = cookieStr;
}

/**
* Get one item by key from cookies
* @param cookieName
* @deprecated
*/
getItemCookie(cookieName: string): string {
const name = `${encodeURIComponent(cookieName)}=`;
const cookieList = document.cookie.split(";");
for (let i: number = 0; i < cookieList.length; i++) {
let cookie = cookieList[i];
while (cookie.charAt(0) === " ") {
cookie = cookie.substring(1);
}
if (cookie.indexOf(name) === 0) {
return decodeURIComponent(
cookie.substring(name.length, cookie.length)
);
}
}
return Constants.EMPTY_STRING;
}

/**
* Clear all msal-related cookies currently set in the browser. Should only be used to clear temporary cache items.
* @deprecated
*/
clearMsalCookies(): void {
const cookiePrefix = `${Constants.CACHE_PREFIX}.${this.clientId}`;
const cookieList = document.cookie.split(";");
cookieList.forEach((cookie: string): void => {
while (cookie.charAt(0) === " ") {
// eslint-disable-next-line no-param-reassign
cookie = cookie.substring(1);
}
if (cookie.indexOf(cookiePrefix) === 0) {
const cookieKey = cookie.split("=")[0];
this.clearItemCookie(cookieKey);
}
});
}

/**
* Clear an item in the cookies by key
* @param cookieName
* @deprecated
*/
clearItemCookie(cookieName: string): void {
this.setItemCookie(cookieName, Constants.EMPTY_STRING, -1);
}

/**
* Get cookie expiration time
* @param cookieLifeDays
* @deprecated
*/
getCookieExpirationTime(cookieLifeDays: number): string {
const today = new Date();
const expr = new Date(
today.getTime() + cookieLifeDays * this.COOKIE_LIFE_MULTIPLIER
);
return expr.toUTCString();
}

/**
* Prepend msal.<client-id> to each key; Skip for any JSON object as Key (defined schemas do not need the key appended: AccessToken Keys or the upcoming schema)
* @param key
Expand Down Expand Up @@ -1570,7 +1458,6 @@ export class BrowserCacheManager extends CacheManager {
);
this.resetRequestCache(cachedState || Constants.EMPTY_STRING);
}
this.clearMsalCookies();
}

/**
Expand Down Expand Up @@ -1609,7 +1496,6 @@ export class BrowserCacheManager extends CacheManager {
this.resetRequestCache(stateValue);
}
});
this.clearMsalCookies();
this.setInteractionInProgress(false);
}

Expand Down
22 changes: 11 additions & 11 deletions lib/msal-browser/src/cache/BrowserStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@ import {
} from "../error/BrowserConfigurationAuthError.js";
import { BrowserCacheLocation } from "../utils/BrowserConstants.js";
import { IWindowStorage } from "./IWindowStorage.js";
import { LocalStorage } from "./LocalStorage.js";
import { SessionStorage } from "./SessionStorage.js";

/**
* @deprecated This class will be removed in a future major version
*/
export class BrowserStorage implements IWindowStorage<string> {
private windowStorage: Storage;
private windowStorage: IWindowStorage<string>;

constructor(cacheLocation: string) {
this.validateWindowStorage(cacheLocation);
this.windowStorage = window[cacheLocation];
}

private validateWindowStorage(cacheLocation: string): void {
if (
(cacheLocation !== BrowserCacheLocation.LocalStorage &&
cacheLocation !== BrowserCacheLocation.SessionStorage) ||
!window[cacheLocation]
) {
if (cacheLocation === BrowserCacheLocation.LocalStorage) {
this.windowStorage = new LocalStorage();
} else if (cacheLocation === BrowserCacheLocation.SessionStorage) {
this.windowStorage = new SessionStorage();
} else {
throw createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.storageNotSupported
);
Expand Down
Loading

0 comments on commit b76e73d

Please sign in to comment.