Skip to content

Commit

Permalink
refactor storage implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
tnorling committed Dec 4, 2024
1 parent df26209 commit 6f916e5
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 208 deletions.
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
79 changes: 79 additions & 0 deletions lib/msal-browser/src/cache/CookieStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { IWindowStorage } from "./IWindowStorage.js";

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

export class CookieStorage implements IWindowStorage<string> {
getItem(key: string): string | null {
const name = `${encodeURIComponent(key)}`;
const cookieList = document.cookie.split(";");
for (let i=0; i < cookieList.length; i++) {
const cookie = cookieList[i];
let [key, ...rest] = decodeURIComponent(cookie).trim().split("=");
const value = rest.join("=");

if (key === name) {
return value;
}
}
return "";
}

setItem(
key: string,
value: string,
cookieLifeDays?: number,
secure: boolean = true
): void {
let cookieStr = `${encodeURIComponent(key)}=${encodeURIComponent(
value
)};path=/;SameSite=Lax;`;

if (cookieLifeDays) {
const expireTime = getCookieExpirationTime(cookieLifeDays);
cookieStr += `expires=${expireTime};`;
}

if (secure) {
cookieStr += "Secure;";
}

document.cookie = cookieStr;
}

removeItem(key: string): void {
// Setting expiration to -1 removes it
this.setItem(key, "", -1);
}

getKeys(): string[] {
const cookieList = document.cookie.split(";");
let keys = [];
for (let cookie in cookieList) {
let cookieParts = decodeURIComponent(cookie).trim().split("=");
keys.push(cookieParts[0]);
}
return keys;
}

containsKey(key: string): boolean {
return this.getKeys().includes(key);
}
}

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

0 comments on commit 6f916e5

Please sign in to comment.