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
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.