Categories
Software development

DRY Angular Storage

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.

import { 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
});
export interface IStore {
    get(key: string): any;
    set(key: string, value: string): void
    remove(key: string): void
    clear(): void
}
import { 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();
    }
}
import { 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);
  }

}
import { 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);
  }

}
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);
  }
}