UNPKG

@okta-dfuhriman/okta-auth-js

Version:
275 lines (247 loc) 8.57 kB
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /*! * Copyright (c) 2015-present, Okta, Inc. and/or its affiliates. All rights reserved. * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") * * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and limitations under the License. * */ import Cookies from 'js-cookie'; import AuthSdkError from '../errors/AuthSdkError'; import { StorageOptions, CookieOptions, SimpleStorage, StorageType, StorageUtil, } from '../storage/types'; import { warn } from '../util'; import { isIE11OrLess } from '../features'; export interface CookieStorage extends SimpleStorage { setItem(key: string, value: any, expiresAt?: string | null): void; // can customize expiresAt getItem(key?: string): any; // if no key is passed, all cookies are returned removeItem(key: string); // remove a cookie } export interface BrowserStorageUtil extends StorageUtil { browserHasLocalStorage(): boolean; browserHasSessionStorage(): boolean; getStorageByType(storageType: StorageType, options: StorageOptions): SimpleStorage; getLocalStorage(): Storage; getSessionStorage(): Storage; getInMemoryStorage(): SimpleStorage; getCookieStorage(options?: StorageOptions): CookieStorage; testStorage(storage: any): boolean; storage: Cookies; inMemoryStore: Record<string, unknown>; } // Building this as an object allows us to mock the functions in our tests var storageUtil: BrowserStorageUtil = { // IE11 bug that Microsoft doesn't plan to fix // https://connect.microsoft.com/IE/Feedback/Details/1496040 browserHasLocalStorage: function() { try { var storage = this.getLocalStorage(); return this.testStorage(storage); } catch (e) { return false; } }, browserHasSessionStorage: function() { try { var storage = this.getSessionStorage(); return this.testStorage(storage); } catch (e) { return false; } }, testStorageType: function(storageType: StorageType): boolean { var supported = false; switch (storageType) { case 'sessionStorage': supported = this.browserHasSessionStorage(); break; case 'localStorage': supported = this.browserHasLocalStorage(); break; case 'cookie': case 'memory': supported = true; break; default: supported = false; break; } return supported; }, getStorageByType: function(storageType: StorageType, options?: StorageOptions): SimpleStorage { let storageProvider; switch (storageType) { case 'sessionStorage': storageProvider = this.getSessionStorage(); break; case 'localStorage': storageProvider = this.getLocalStorage(); break; case 'cookie': storageProvider = this.getCookieStorage(options); break; case 'memory': storageProvider = this.getInMemoryStorage(); break; default: throw new AuthSdkError(`Unrecognized storage option: ${storageType}`); break; } return storageProvider; }, findStorageType: function(types: StorageType[]) { let curType; let nextType; types = types.slice(); // copy array curType = types.shift(); nextType = types.length ? types[0] : null; if (!nextType) { return curType; } if (this.testStorageType(curType)) { return curType; } // preferred type was unsupported. warn(`This browser doesn't support ${curType}. Switching to ${nextType}.`); // fallback to the next type. this is a recursive call return this.findStorageType(types); }, getLocalStorage: function() { // Workaound for synchronization issue of LocalStorage cross tabs in IE11 if (isIE11OrLess() && !window.onstorage) { window.onstorage = function() {}; } return localStorage; }, getSessionStorage: function() { return sessionStorage; }, // Provides webStorage-like interface for cookies getCookieStorage: function(options): CookieStorage { const secure = options!.secure; const sameSite = options!.sameSite; const sessionCookie = options!.sessionCookie; if (typeof secure === 'undefined' || typeof sameSite === 'undefined') { throw new AuthSdkError('getCookieStorage: "secure" and "sameSite" options must be provided'); } const storage: CookieStorage = { getItem: this.storage.get, setItem: (key, value, expiresAt = '2200-01-01T00:00:00.000Z') => { // By defauilt, cookie shouldn't expire expiresAt = (sessionCookie ? null : expiresAt) as string; this.storage.set(key, value, expiresAt, { secure: secure, sameSite: sameSite, }); }, removeItem: (key) => { this.storage.delete(key); }, }; if (!options!.useSeparateCookies) { return storage; } // Tokens are stored separately because cookies have size limits. // Can only be used when storing an object value. Object properties will be saved to separate cookies. // Each property of the object must also be an object. return { getItem: function(key) { var data = storage.getItem(); // read all cookies var value = {}; Object.keys(data).forEach(k => { if (k.indexOf(key!) === 0) { // filter out unrelated cookies value[k.replace(`${key}_`, '')] = JSON.parse(data[k]); // populate with cookie data } }); return JSON.stringify(value); }, setItem: function(key, value) { var existingValues = JSON.parse(this.getItem(key)); value = JSON.parse(value); // Set key-value pairs from input to cookies Object.keys(value).forEach(k => { var storageKey = key + '_' + k; var valueToStore = JSON.stringify(value[k]); storage.setItem(storageKey, valueToStore); delete existingValues[k]; }); // Delete unmatched keys from existing cookies Object.keys(existingValues).forEach(k => { storage.removeItem(key + '_' + k); }); }, removeItem: function(key) { var existingValues = JSON.parse(this.getItem(key)); Object.keys(existingValues).forEach(k => { storage.removeItem(key + '_' + k); }); }, }; }, // Provides an in-memory solution inMemoryStore: {}, // override this for a unique memory store per instance getInMemoryStorage: function() { return { getItem: (key) => { return this.inMemoryStore[key]; }, setItem: (key, value) => { this.inMemoryStore[key] = value; }, }; }, testStorage: function(storage) { var key = 'okta-test-storage'; try { storage.setItem(key, key); storage.removeItem(key); return true; } catch (e) { return false; } }, storage: { set: function(name: string, value: string, expiresAt: string, options: CookieOptions): string { const { sameSite, secure } = options; if (typeof secure === 'undefined' || typeof sameSite === 'undefined') { throw new AuthSdkError('storage.set: "secure" and "sameSite" options must be provided'); } var cookieOptions: CookieOptions = { path: options.path || '/', secure, sameSite }; // eslint-disable-next-line no-extra-boolean-cast if (!!(Date.parse(expiresAt))) { // Expires value can be converted to a Date object. // // If the 'expiresAt' value is not provided, or the value cannot be // parsed as a Date object, the cookie will set as a session cookie. cookieOptions.expires = new Date(expiresAt); } Cookies.set(name, value, cookieOptions); return this.get(name); }, get: function(name?: string): string { // return all cookies when no args is provided if (!arguments.length) { return Cookies.get(); } return Cookies.get(name); }, delete: function(name: string): string { return Cookies.remove(name, { path: '/' }); } } }; export default storageUtil;