How to implement “Remember Me” functionality using Token Based Authentication and localStorage in a Web Application

OK, so you’ve jumped aboard the new wagon and ditched cookie based authentication. You want to save authentication data/token in the all shiny localStorage or sessionStorage (rather than in cookies) of the browser. But you've soon realized that implementing the "Remember Me" functionality you took for granted in cookie authentication is not as straight forward with Token Based Authentication and localStorage/sessionStorage.

I also faced this challenge and had to spend time getting around it. I’m putting this here to spare you the time of having to implement this basic requirement yourself.

How I solved this is to implement a storageManager over the localStorage and sessionStorage. So with this you can do something like:

if (rememberMe)
    storageManager.savePermanentData('data''key');
else     storageManager.saveSyncedSessionData('data''key');

To retrieve data you use var myData = storageManager.getData('key'); It doesn’t matter where the underlying data was saved.
To delete you use storageManager.deleteData('key');

The storage manager offers many conveniences to client-side data storage and manipulation. But the primary benefit is the synchronization of your data across multiple opened browser tabs; you don't get this with the traditional sessionStorage implementation.

See below for the full API for this library.

Download the storageManager code from here:

//Author: Ebenezer Monney
//Email: i@ebenmonney.com
 
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
 
 
 
@Injectable()
export class LocalStoreManager {
 
    private static syncListenerInitialised = false;
    private syncKeys: string[] = [];
    private _dbEvent = new Subject<string>();
 
    private reservedKeys: string[] = ['sync_keys''addToSyncKeys''removeFromSyncKeys',
        'getSessionStorage''setSessionStorage''addToSessionStorage''removeFromSessionStorage''clearAllSessionsStorage''raiseDBEvent'];
 
    public static readonly DBKEY_USER_DATA = "user_data";
    private static readonly DBKEY_SYNC_KEYS = "sync_keys";
 
 
    //Todo: Implement EventListeners for the various event operations and a SessionStorageEvent for specific data keys
 
    public initialiseStorageSyncListener() {
        if (LocalStoreManager.syncListenerInitialised == true)
            return;
 
        LocalStoreManager.syncListenerInitialised = true;
        window.addEventListener("storage"this.sessionStorageTransferHandler, false);
        this.syncSessionStorage();
    }
 
 
 
    public deinitialiseStorageSyncListener() {
 
        window.removeEventListener("storage"this.sessionStorageTransferHandler, false);
 
        LocalStoreManager.syncListenerInitialised = false;
    }
 
 
 
 
    private sessionStorageTransferHandler = (event: StorageEvent) => {
 
        if (!event.newValue)
            return;
 
        if (event.key == 'getSessionStorage') {
 
            if (sessionStorage.length) {
                localStorage.setItem('setSessionStorage', JSON.stringify(sessionStorage));
                localStorage.removeItem('setSessionStorage');
            }
        }
        else if (event.key == 'setSessionStorage') {
 
            if (!this.syncKeys.length)
                this.loadSyncKeys();
 
            let data = JSON.parse(event.newValue);
 
            for (let key in data) {
 
                if (this.syncKeysContains(key))
                    sessionStorage.setItem(key, data[key]);
            }
 
        }
        else if (event.key == 'addToSessionStorage') {
 
            let data = JSON.parse(event.newValue);
            this.addToSessionStorageHelper(data["data"], data["key"]);
        }
        else if (event.key == 'removeFromSessionStorage') {
 
            this.removeFromSessionStorageHelper(event.newValue);
        }
        else if (event.key == 'clearAllSessionsStorage' && sessionStorage.length) {
 
            this.clearInstanceSessionStorage();
        }
        else if (event.key == 'addToSyncKeys') {
 
            this.addToSyncKeysHelper(event.newValue);
        }
        else if (event.key == 'removeFromSyncKeys') {
 
            this.removeFromSyncKeysHelper(event.newValue);
        }
        else if (event.key == 'raiseDBEvent') {
 
            this.raiseDBEventHelper(event.newValue);
        }
    }
 
 
 
 
 
    private syncSessionStorage() {
 
        localStorage.setItem('getSessionStorage''_dummy');
        localStorage.removeItem('getSessionStorage');
    }
 
 
    public clearAllStorage() {
 
        this.clearAllSessionsStorage();
        this.clearLocalStorage();
    }
 
 
    public clearAllSessionsStorage() {
 
        this.clearInstanceSessionStorage();
        localStorage.removeItem(LocalStoreManager.DBKEY_SYNC_KEYS);
 
        localStorage.setItem('clearAllSessionsStorage''_dummy');
        localStorage.removeItem('clearAllSessionsStorage');
    }
 
 
    public clearInstanceSessionStorage() {
 
        sessionStorage.clear();
        this.syncKeys = [];
    }
 
 
    public clearLocalStorage() {
        localStorage.clear();
    }
 
 
 
 
    private addToSessionStorage(data: string, key: string) {
 
        this.addToSessionStorageHelper(data, key);
        this.addToSyncKeysBackup(key);
 
        localStorage.setItem('addToSessionStorage', JSON.stringify({ key: key, data: data }));
        localStorage.removeItem('addToSessionStorage');
    }
 
    private addToSessionStorageHelper(data: string, key: string) {
 
        this.addToSyncKeysHelper(key);
        sessionStorage.setItem(key, data);
    }
 
 
    private removeFromSessionStorage(keyToRemove: string) {
 
        this.removeFromSessionStorageHelper(keyToRemove);
        this.removeFromSyncKeysBackup(keyToRemove);
 
        localStorage.setItem('removeFromSessionStorage', keyToRemove);
        localStorage.removeItem('removeFromSessionStorage');
    }
 
 
    private removeFromSessionStorageHelper(keyToRemove: string) {
 
        sessionStorage.removeItem(keyToRemove);
        this.removeFromSyncKeysHelper(keyToRemove);
    }
 
 
    private testForInvalidKeys(key: string) {
 
        if (!key)
            throw new Error("key cannot be empty")
 
        if (this.reservedKeys.some(x => x == key))
            throw new Error(`The storage key "${key}" is reserved and cannot be used. Please use a different key`);
    }
 
 
    private syncKeysContains(key: string) {
 
        return this.syncKeys.some(x => x == key);
    }
 
 
    private loadSyncKeys() {
 
        if (this.syncKeys.length)
            return;
 
        this.syncKeys = this.getSyncKeysFromStorage();
    }
 
 
    private getSyncKeysFromStorage(defaultValue: string[] = []): string[] {
 
        let data = localStorage.getItem(LocalStoreManager.DBKEY_SYNC_KEYS);
 
        if (data == null)
            return defaultValue;
        else
            return <string[]>JSON.parse(data);
    }
 
 
    private addToSyncKeys(key: string) {
 
        this.addToSyncKeysHelper(key);
        this.addToSyncKeysBackup(key);
 
        localStorage.setItem('addToSyncKeys', key);
        localStorage.removeItem('addToSyncKeys');
    }
 
 
    private addToSyncKeysBackup(key: string) {
 
        let storedSyncKeys = this.getSyncKeysFromStorage();
 
        if (!storedSyncKeys.some(x => x == key)) {
 
            storedSyncKeys.push(key);
            localStorage.setItem(LocalStoreManager.DBKEY_SYNC_KEYS, JSON.stringify(storedSyncKeys));
        }
    }
 
    private removeFromSyncKeysBackup(key: string) {
 
        let storedSyncKeys = this.getSyncKeysFromStorage();
 
        let index = storedSyncKeys.indexOf(key);
 
        if (index > -1) {
            storedSyncKeys.splice(index, 1);
            localStorage.setItem(LocalStoreManager.DBKEY_SYNC_KEYS, JSON.stringify(storedSyncKeys));
        }
    }
 
 
    private addToSyncKeysHelper(key: string) {
 
        if (!this.syncKeysContains(key))
            this.syncKeys.push(key);
    }
 
 
 
    private removeFromSyncKeys(key: string) {
 
        this.removeFromSyncKeysHelper(key);
        this.removeFromSyncKeysBackup(key);
 
        localStorage.setItem('removeFromSyncKeys', key);
        localStorage.removeItem('removeFromSyncKeys');
    }
 
 
    private removeFromSyncKeysHelper(key: string) {
 
        let index = this.syncKeys.indexOf(key);
 
        if (index > -1) {
            this.syncKeys.splice(index, 1);
        }
    }
 
 
    public saveSessionData(data: string, key = LocalStoreManager.DBKEY_USER_DATA) {
 
        this.testForInvalidKeys(key);
 
        this.removeFromSyncKeys(key);
        localStorage.removeItem(key);
        sessionStorage.setItem(key, data);
    }
 
 
    public saveSyncedSessionData(data: string, key = LocalStoreManager.DBKEY_USER_DATA) {
 
        this.testForInvalidKeys(key);
 
        localStorage.removeItem(key);
        this.addToSessionStorage(data, key);
    }
 
 
    public savePermanentData(data: string, key = LocalStoreManager.DBKEY_USER_DATA) {
 
        this.testForInvalidKeys(key);
 
        this.removeFromSessionStorage(key);
        localStorage.setItem(key, data);
    }
 
 
 
    public moveDataToSessionStorage(key = LocalStoreManager.DBKEY_USER_DATA) {
 
        this.testForInvalidKeys(key);
 
        let data = this.getData(key);
 
        if (data == null)
            return;
 
        this.saveSessionData(data, key);
    }
 
 
    public moveDataToSyncedSessionStorage(key = LocalStoreManager.DBKEY_USER_DATA) {
 
        this.testForInvalidKeys(key);
 
        let data = this.getData(key);
 
        if (data == null)
            return;
 
        this.saveSyncedSessionData(data, key);
    }
 
 
    public moveDataToPermanentStorage(key = LocalStoreManager.DBKEY_USER_DATA) {
 
        this.testForInvalidKeys(key);
 
        let data = this.getData(key);
 
        if (data == null)
            return;
 
        this.savePermanentData(data, key);
    }
 
 
    public getData(key = LocalStoreManager.DBKEY_USER_DATA) {
 
        this.testForInvalidKeys(key);
 
        let data = sessionStorage.getItem(key);
 
        if (data == null)
            data = localStorage.getItem(key);
 
        return data;
    }
 
 
    public getDataObject<T>(key = LocalStoreManager.DBKEY_USER_DATA): T {
 
        let data = this.getData(key);
 
        if (data != null)
            return <T>JSON.parse(data);
        else
            return null;
    }
 
 
    public deleteData(key = LocalStoreManager.DBKEY_USER_DATA) {
 
        this.testForInvalidKeys(key);
 
        this.removeFromSessionStorage(key);
        localStorage.removeItem(key);
    }
 
 
 
    raiseDBEvent(event: string) {
        this.raiseDBEventHelper(event);
 
        localStorage.setItem('raiseDBEvent', event);
        localStorage.removeItem('raiseDBEvent');
    }
 
    private raiseDBEventHelper(event: string) {
        this._dbEvent.next(event);
    }
 
 
    getDBEventListener(): Observable<string> {
        return this._dbEvent.asObservable();
    }
}
//Author: Ebenezer Monney
//Email: i@ebenmonney.com
define(["require""exports"], function (require, exports) {
    "use strict";
    var LocalStoreManager = (function () {
        function LocalStoreManager() {
            var _this = this;
            this.syncKeys = [];
            this.reservedKeys = ['sync_keys''addToSyncKeys''removeFromSyncKeys',
                'getSessionStorage''setSessionStorage''addToSessionStorage''removeFromSessionStorage''clearAllSessionsStorage''raiseDBEvent'];
            this.sessionStorageTransferHandler = function (event) {
                if (!event.newValue)
                    return;
                if (event.key == 'getSessionStorage') {
                    if (sessionStorage.length) {
                        localStorage.setItem('setSessionStorage', JSON.stringify(sessionStorage));
                        localStorage.removeItem('setSessionStorage');
                    }
                }
                else if (event.key == 'setSessionStorage') {
                    if (!_this.syncKeys.length)
                        _this.loadSyncKeys();
                    var data = JSON.parse(event.newValue);
                    for (var key in data) {
                        if (_this.syncKeysContains(key))
                            sessionStorage.setItem(key, data[key]);
                    }
                }
                else if (event.key == 'addToSessionStorage') {
                    var data = JSON.parse(event.newValue);
                    _this.addToSessionStorageHelper(data["data"], data["key"]);
                }
                else if (event.key == 'removeFromSessionStorage') {
                    _this.removeFromSessionStorageHelper(event.newValue);
                }
                else if (event.key == 'clearAllSessionsStorage' && sessionStorage.length) {
                    _this.clearInstanceSessionStorage();
                }
                else if (event.key == 'addToSyncKeys') {
                    _this.addToSyncKeysHelper(event.newValue);
                }
                else if (event.key == 'removeFromSyncKeys') {
                    _this.removeFromSyncKeysHelper(event.newValue);
                }
            };
        }
        //Todo: Implement EventListeners for the various event operations and a SessionStorageEvent for specific data keys
        LocalStoreManager.prototype.initialiseStorageSyncListener = function () {
            if (LocalStoreManager.syncListenerInitialised == true)
                return;
            LocalStoreManager.syncListenerInitialised = true;
            window.addEventListener("storage"this.sessionStorageTransferHandler, false);
            this.syncSessionStorage();
        };
        LocalStoreManager.prototype.deinitialiseStorageSyncListener = function () {
            window.removeEventListener("storage"this.sessionStorageTransferHandler, false);
            LocalStoreManager.syncListenerInitialised = false;
        };
        LocalStoreManager.prototype.syncSessionStorage = function () {
            localStorage.setItem('getSessionStorage''_dummy');
            localStorage.removeItem('getSessionStorage');
        };
        LocalStoreManager.prototype.clearAllStorage = function () {
            this.clearAllSessionsStorage();
            this.clearLocalStorage();
        };
        LocalStoreManager.prototype.clearAllSessionsStorage = function () {
            this.clearInstanceSessionStorage();
            localStorage.removeItem(LocalStoreManager.DBKEY_SYNC_KEYS);
            localStorage.setItem('clearAllSessionsStorage''_dummy');
            localStorage.removeItem('clearAllSessionsStorage');
        };
        LocalStoreManager.prototype.clearInstanceSessionStorage = function () {
            sessionStorage.clear();
            this.syncKeys = [];
        };
        LocalStoreManager.prototype.clearLocalStorage = function () {
            localStorage.clear();
        };
        LocalStoreManager.prototype.addToSessionStorage = function (data, key) {
            this.addToSessionStorageHelper(data, key);
            this.addToSyncKeysBackup(key);
            localStorage.setItem('addToSessionStorage', JSON.stringify({ key: key, data: data }));
            localStorage.removeItem('addToSessionStorage');
        };
        LocalStoreManager.prototype.addToSessionStorageHelper = function (data, key) {
            this.addToSyncKeysHelper(key);
            sessionStorage.setItem(key, data);
        };
        LocalStoreManager.prototype.removeFromSessionStorage = function (keyToRemove) {
            this.removeFromSessionStorageHelper(keyToRemove);
            this.removeFromSyncKeysBackup(keyToRemove);
            localStorage.setItem('removeFromSessionStorage', keyToRemove);
            localStorage.removeItem('removeFromSessionStorage');
        };
        LocalStoreManager.prototype.removeFromSessionStorageHelper = function (keyToRemove) {
            sessionStorage.removeItem(keyToRemove);
            this.removeFromSyncKeysHelper(keyToRemove);
        };
        LocalStoreManager.prototype.testForInvalidKeys = function (key) {
            if (!key)
                throw new Error("key cannot be empty");
            if (this.reservedKeys.some(function (x) { return x == key; }))
                throw new Error("The storage key \"" + key + "\" is reserved and cannot be used. Please use a different key");
        };
        LocalStoreManager.prototype.syncKeysContains = function (key) {
            return this.syncKeys.some(function (x) { return x == key; });
        };
        LocalStoreManager.prototype.loadSyncKeys = function () {
            if (this.syncKeys.length)
                return;
            this.syncKeys = this.getSyncKeysFromStorage();
        };
        LocalStoreManager.prototype.getSyncKeysFromStorage = function (defaultValue) {
            if (defaultValue === void 0) { defaultValue = []; }
            var data = localStorage.getItem(LocalStoreManager.DBKEY_SYNC_KEYS);
            if (data == null)
                return defaultValue;
            else
                return JSON.parse(data);
        };
        LocalStoreManager.prototype.addToSyncKeys = function (key) {
            this.addToSyncKeysHelper(key);
            this.addToSyncKeysBackup(key);
            localStorage.setItem('addToSyncKeys', key);
            localStorage.removeItem('addToSyncKeys');
        };
        LocalStoreManager.prototype.addToSyncKeysBackup = function (key) {
            var storedSyncKeys = this.getSyncKeysFromStorage();
            if (!storedSyncKeys.some(function (x) { return x == key; })) {
                storedSyncKeys.push(key);
                localStorage.setItem(LocalStoreManager.DBKEY_SYNC_KEYS, JSON.stringify(storedSyncKeys));
            }
        };
        LocalStoreManager.prototype.removeFromSyncKeysBackup = function (key) {
            var storedSyncKeys = this.getSyncKeysFromStorage();
            var index = storedSyncKeys.indexOf(key);
            if (index > -1) {
                storedSyncKeys.splice(index, 1);
                localStorage.setItem(LocalStoreManager.DBKEY_SYNC_KEYS, JSON.stringify(storedSyncKeys));
            }
        };
        LocalStoreManager.prototype.addToSyncKeysHelper = function (key) {
            if (!this.syncKeysContains(key))
                this.syncKeys.push(key);
        };
        LocalStoreManager.prototype.removeFromSyncKeys = function (key) {
            this.removeFromSyncKeysHelper(key);
            this.removeFromSyncKeysBackup(key);
            localStorage.setItem('removeFromSyncKeys', key);
            localStorage.removeItem('removeFromSyncKeys');
        };
        LocalStoreManager.prototype.removeFromSyncKeysHelper = function (key) {
            var index = this.syncKeys.indexOf(key);
            if (index > -1) {
                this.syncKeys.splice(index, 1);
            }
        };
        LocalStoreManager.prototype.saveSessionData = function (data, key) {
            if (key === void 0) { key = LocalStoreManager.DBKEY_USER_DATA; }
            this.testForInvalidKeys(key);
            this.removeFromSyncKeys(key);
            localStorage.removeItem(key);
            sessionStorage.setItem(key, data);
        };
        LocalStoreManager.prototype.saveSyncedSessionData = function (data, key) {
            if (key === void 0) { key = LocalStoreManager.DBKEY_USER_DATA; }
            this.testForInvalidKeys(key);
            localStorage.removeItem(key);
            this.addToSessionStorage(data, key);
        };
        LocalStoreManager.prototype.savePermanentData = function (data, key) {
            if (key === void 0) { key = LocalStoreManager.DBKEY_USER_DATA; }
            this.testForInvalidKeys(key);
            this.removeFromSessionStorage(key);
            localStorage.setItem(key, data);
        };
        LocalStoreManager.prototype.moveDataToSessionStorage = function (key) {
            if (key === void 0) { key = LocalStoreManager.DBKEY_USER_DATA; }
            this.testForInvalidKeys(key);
            var data = this.getData(key);
            if (data == null)
                return;
            this.saveSessionData(data, key);
        };
        LocalStoreManager.prototype.moveDataToSyncedSessionStorage = function (key) {
            if (key === void 0) { key = LocalStoreManager.DBKEY_USER_DATA; }
            this.testForInvalidKeys(key);
            var data = this.getData(key);
            if (data == null)
                return;
            this.saveSyncedSessionData(data, key);
        };
        LocalStoreManager.prototype.moveDataToPermanentStorage = function (key) {
            if (key === void 0) { key = LocalStoreManager.DBKEY_USER_DATA; }
            this.testForInvalidKeys(key);
            var data = this.getData(key);
            if (data == null)
                return;
            this.savePermanentData(data, key);
        };
        LocalStoreManager.prototype.getData = function (key) {
            if (key === void 0) { key = LocalStoreManager.DBKEY_USER_DATA; }
            this.testForInvalidKeys(key);
            var data = sessionStorage.getItem(key);
            if (data == null)
                data = localStorage.getItem(key);
            return data;
        };
        LocalStoreManager.prototype.getDataObject = function (key) {
            if (key === void 0) { key = LocalStoreManager.DBKEY_USER_DATA; }
            var data = this.getData(key);
            if (data != null)
                return JSON.parse(data);
            else
                return null;
        };
        LocalStoreManager.prototype.deleteData = function (key) {
            if (key === void 0) { key = LocalStoreManager.DBKEY_USER_DATA; }
            this.testForInvalidKeys(key);
            this.removeFromSessionStorage(key);
            localStorage.removeItem(key);
        };
        return LocalStoreManager;
    }());
    LocalStoreManager.syncListenerInitialised = false;
    LocalStoreManager.DBKEY_USER_DATA = "user_data";
    LocalStoreManager.DBKEY_SYNC_KEYS = "sync_keys";
    exports.LocalStoreManager = LocalStoreManager;
});

Below is a detailed list of convenient methods you get with this library:

  • saveSessionData: Save data into a single tab. Stuff you save with this is not available in other tabs
  • saveSyncedSessionData: Whatever you save with this function is available in all opened tabs. This is what you’ll use to save a user's Authentication Token when the user chooses not to "Remember Me"
  • savePermanentData: Whatever you save with this is permanently saved on disk and is available in all opened tabs. This is what you’ll use to save the Authentication token of a user who chooses to "Remember Me"
  • moveDataToSessionStorage: Moves data that is in other storage locations (i.e. permanent storage, synced session storage) to session storage. In session storage each tab has its data independent of other tabs
  • moveDataToSyncedSessionStorage: Moves data in other storage locations (i.e. permanent storage, session storage) to SyncedSessionStorage. Whatever is saved here is available in all opened tabs
  • moveDataToPermanentStorage: Moves data in other storage locations (i.e. session storage, synced session storage) to permanent storage
  • getData: Used to retrieve data from the data store
  • getDataObject<T>: Used to retrieve data from the data store. This method returns an object of type T. Use this when you saved an object to the data store, to have it return the object back to you
  • deleteData: Deletes data from the data store
  • raiseDBEvent: Ummm I'm not gonna talk about this. Just ignore it for the most part
  • getDBEventListener: Same as above. Ignore, or let me know in the comments if you’re interested in raising events in other tabs on data save
  • Critical! initialiseStorageSyncListener: This is the first thing you have to call before you use any functionality in this library. You can call this on Page Load. This hooks tab synchronization up
  • deinitialiseStorageSyncListener : You don’t really have to call this method, but call this when you want to unhook tab synchronization for some reason

IMPORTANT: Call initialiseStorageSyncListener once to setup this library. This will hook up synchronizing your data between multiple opened browser tabs. One place to do this is when the page loads for the first time.

Extra utility methods. Note that using the core methods above is enough, you don’t have to call any of these yourself

  • clearAllStorage: Clears everything in all data stores
  • clearAllSessionsStorage: Clears all session storage in all opened browser tabs. Permanent storage is not affected
  • clearInstanceSessionStorageClears all storage in the current browser tab only
  • clearLocalStorage: Clear permanent storage only. Session storage are not affected

Hope this helps someone out there.

  • Hello,

    Great helper class, and I love that it's written in TypeScript too. I'm having trouble getting it to work however, if it does what I think it is supposed to do. :( When I open a separate tab and copy and paste the same URL from the initial tab it doesn't contain the data I saved in the first tab using saveSyncedSessionData. Please see code below.

    var mgr = null;
    $j(document).ready(function(){
    mgr = new LocalStoreManager();
    mgr.initialiseStorageSyncListener();
    var data = mgr.getData("key");
    if(data){
    alert(data);
    }
    mgr.saveSyncedSessionData("test","key");
    });

    I would think that the second tab would have "test" for key "key", but mgr.getData("key") on second tab is returning null.

    Do you have a working demo? Any suggestions?

    Thanks,
    Matt

  • dear Ebenezer Monney , you did a great job and I really like your approach. one thing can you explain raiseDBEvent

  • where should I call the initialiseStorageSyncListener because login-resolver is called before it and persist the user to login and also token is available in session storage.

  • I see you don't monetize your website, don't waste your traffic, you can earn additional cash
    every month because you've got high quality content.
    If you want to know what is the best adsense alternative, type in google: adsense alternative Mertiso's
    tips

Add a Comment

As it will appear on the website

Not displayed

Your website