reactjsoauth-2.0viteopenid-connectpkce

Avoid instantiating multiple objects ReactJS + Vite


I'm building a sample ReactJS + Vite app implementing OAuth2/OIDC flows and DPoP using the oidc-client-ts library and from the documentation I see that to instantiate a UserManager object I'd have to do something along the lines of

import { UserManager } from 'oidc-client-ts';

const settings = {
    authority: 'https://demo.identityserver.io',
    client_id: 'interactive.public',
    redirect_uri: 'http://localhost:8080',
    response_type: 'code',
    scope: 'openid profile email api',
    post_logout_redirect_uri: 'http://localhost:8080',
    userStore: new WebStorageStateStore({ store: window.localStorage }),
    dpop: {
        bind_authorization_code: true,
        store: new IndexedDbDPoPStore()
    }
};

const userManager = new UserManager(settings);

Problem is that every time a component needs to use the UserManager object a new IndexedDbDPoPStore is generated. This, plus the fact that i need to programmatically generate a Private/Public key pair to bind an access token to the public key, a new key pair is also generated along side the new store. This leads to problems when someone refreshes the page on a component that needs to use the UserManager and a REST API call is made because the DPoPproof will be generated using the newly created private key and not the original one. Is there a way to prevent UserManager from being created multiple times? I was thinking about a global variable but I don't know if it is the correct solution. Thanks


Solution

  • This is the workaround adopted to avoid creating a new key pair every time the page is reloaded and the component is rendered again.

    Disclaimer: this works only because the oidc-client-ts library uses the values specified in the code ('oidc' and 'dpop'). Any change to them in a future release might lead to loss of functionality of the provided code. This also applies if the usage of IndexedDB to rely on Private/Public key storage is dropped.

    const dbName = 'oidc'
    const objectName = 'dpop'
    const clientID = 'yourClientID'
    
    async function checkIndexedDbDPoPStore() {
        return new Promise((resolve, reject) => {
            const req = indexedDB.open(dbName);
    
            req.onsuccess = (event) => {
                const db = event.target.result;
                if (db.objectStoreNames.contains(objectName)) {
                    const transaction = db.transaction([objectName], 'readonly');
                    const objectStore = transaction.objectStore(objectName);
                    const getRequest = objectStore.get(clientID);
    
                    getRequest.onsuccess = () => {
                        if (getRequest.result) {
                            resolve(db);
                        } else {
                            db.close();
                            resolve(null);
                        }
                    };
    
                    getRequest.onerror = () => {
                        db.close();
                        reject('Error retrieving ' + clientID + ' from IndexedDB');
                    };
                } else {
                    db.close();
                    resolve(null);
                }
            };
    
            req.onerror = () => {
                reject('Error opening IndexedDB');
            };
    
            req.onupgradeneeded = (event) => {
                event.target.transaction.abort();
                resolve(null);
            };
        });
    }
    

    Is important to abort the transaction if the onupgradeneeded event occurs.