The below code snippets work in Angular 9. This will ensure the implementation for the storage service is the same, regardless of whether we are persisting to Local or Session storage.
Setup injection tokensimport { InjectionToken } from '@angular/core'; export const LOCAL_STORAGE = new InjectionToken<Storage>('Browser Storage', { providedIn: 'root', factory: () => localStorage }); export const SESSION_STORAGE = new InjectionToken<Storage>('Browser Storage', { providedIn: 'root', factory: () => sessionStorage });
Define a storage interfaceexport interface IStore { get(key: string): any; set(key: string, value: string): void remove(key: string): void clear(): void }
Create a base storage serviceimport { IStore } from './istore'; export class StorageService implements IStore { // it is expected that implementors of this class will inject Storage constructor(public storage: Storage) { } get(key: string) { return this.storage.getItem(key); } set(key: string, value: string) { this.storage.setItem(key, value); } remove(key: string) { this.storage.removeItem(key); } clear() { this.storage.clear(); } }
Create a session storage service, injecting the session tokenimport { Injectable, Inject } from '@angular/core'; import { SESSION_STORAGE } from './session-storage.token'; import { StorageService } from './storage-service'; /** * A storage service. * * Can be used for local or session, depending on what you pass in. * * @export * @class StorageService */ @Injectable({ providedIn: 'root' }) export class SessionStorageService extends StorageService { constructor(@Inject(SESSION_STORAGE) public storage: Storage) { // inject our storage instance into the base class, in this case, session storage super(storage); } }
Create a local storage service, injecting the local tokenimport { Injectable, Inject } from '@angular/core'; import { LOCAL_STORAGE } from './local-storage.token'; import { StorageService } from './storage-service'; @Injectable({ providedIn: 'root' }) export class LocalStorageService extends StorageService { constructor(@Inject(LOCAL_STORAGE) public storage: Storage) { // inject our storage instance into the base class, in this case, local storage super(storage); } }
A service that uses SessionStorageService!import { Injectable } from '@angular/core'; import { SessionStorageService } from './session-storage.service'; @Injectable({ providedIn: 'root' }) export class PreventCSRFService { private _key : string = 'nonce'; public get key() : string { return this._key; } constructor( // use Session Storage for persistence private storage: SessionStorageService ) { const state = this.storage.get(this.key); if (state === null || state === undefined) { const nonce = this.randomString(16); this.storage.set(this.key, nonce); } } /** * Generate a cryptographically secure randoms string * https://auth0.com/docs/api-auth/tutorials/nonce * * @returns {string} * @memberof PreventCSRFService */ public randomString(length: number): string { const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._' let result: string = ''; while (length > 0) { const bytes = new Uint8Array(16); let random = window.crypto.getRandomValues(bytes); random.forEach(function(c) { if (length == 0) { return; } if (c < charset.length) { result += charset[c]; length--; } }); } return result; } /** * Check if the nonce for this session matches or not * * @param {string} actual What we're comparing against * @returns {boolean} Whether what we got matches what we stored * @memberof PreventCSRFService */ public nonceMatches(actual: string): boolean { const expected = this.storage.get(this.key); if (expected && actual) { return expected === actual; } return false; } /** * Get the nonce for this session * * @returns {string} A cryptographically strong random string, which is persisted in session storage * @memberof PreventCSRFService */ public getNonce(): string { return this.storage.get(this.key); } }