Description
Core Library
MSAL Node (@azure/msal-node)
Core Library Version
3.5.2
Wrapper Library
Not Applicable
Wrapper Library Version
None
Public or Confidential Client?
Confidential
Description
When inspecting the MSAL Node persistent token cache in MongoDB Atlas, the Account
> tenantProfiles
property values seem to be getting 'double escaped'. I am not performing any modification on the cache values before inserting or retrieving them from persistent storage, so I think it might be a bug with either the deserialize
or serialize
methods that are used in the beforeCacheEvent
and afterCacheEvent
handlers.
I have tried the 'default' approach of:
-
In
beforeCacheAccess
, writing the persistent cache data (MongoDB Atlas) to the in-memory cache withcache_context.tokenCache.deserialize(cache_data.data)
-
In
afterCacheAccess
, writing the in-memory cache data to the persistent cache (MongoDB Atlas) with thiscache_context.tokenCache.serialize()
And also JSON parsing the string before adding it to persistent storage (so it is an object that is easier to 'view' in the database) and JSON stringifying the value when writing back to the in-memory cache.
Both approaches result in the Account
> tenantProfiles
property values being escaped differently to the other properties in the token cache.
It makes viewing the contents of the cache difficult because tenantProfiles
always seems to be escaped 'more' than the other properties, so its hard to get the contents into a consistent format.
Error Message
N/A
MSAL Logs
N/A
Network Trace (Preferrably Fiddler)
- Sent
- Pending
MSAL Configuration
{
auth: {
clientId: process.env.APP_CLIENT_ID_B2C_SignIn,
clientSecret: process.env.APP_CLIENT_SECRET_B2C_SignIn,
authority: process.env.POLICY_AUTHORITY_B2C_SIGN_IN,
knownAuthorities: [process.env.AUTHORITY_DOMAIN_B2C_SignIn], // this must be an array, and should not include the 'https://'
},
cache: {
cachePlugin: msal_node_persistant_token_cache_plugin(cache_collection_name),
}
}
Relevant Code Snippets
Contents of `msal_node_persistant_token_cache_plugin.js`:
import { mongodb_client_manager } from './mongodb_client_manager.js';
const msal_node_persistant_token_cache_plugin = (cache_collection_name) => {
let client;
let collection;
const get_collection = async () => {
// only create/get the MongoDB client if we haven't already
if (!client) {
// all plugin instances share the same MongoDB client (connection pooling)
client = await mongodb_client_manager.open();
// log first-time initialization of the MSAL token cache system
if (!mongodb_client_manager.msal_token_cache_initialized) {
console.log("msal token cache db connection established");
mongodb_client_manager.msal_token_cache_initialized = true;
}
// each plugin instance gets its own collection in the msal_node_token_cache database
// cache_collection_name is set when the plugin is created and remains fixed for this instance
collection = client.db('msal_node_token_cache').collection(cache_collection_name);
}
return collection;
};
// the plugin must return an object with these two methods that MSAL will call
return {
// called BEFORE MSAL needs to access the token cache
// this ensures the in-memory cache has the latest data from MongoDB
beforeCacheAccess: async (cache_context) => {
try {
console.log(`beforeCacheAccess triggered for collection "${cache_collection_name}"`);
const collection = await get_collection();
const cache_data = await collection.findOne({ _id: 'persistent_token_cache' });
if (cache_data) {
// option 01 - don't stringify if it was stored as a string
// cache_context.tokenCache.deserialize(cache_data.data);
// option 02 - stringify the object if it was stored as an object, before deserializing
const cache_string = JSON.stringify(cache_data.data);
cache_context.tokenCache.deserialize(cache_string);
} else {
console.log(`the "${cache_collection_name}" token cache does not exist yet...`);
}
} catch (error) {
console.error('error accessing cache before operation:', error);
}
},
// called AFTER MSAL modifies the token cache
// this persists any changes from the in-memory cache to MongoDB
afterCacheAccess: async (cache_context) => {
console.log(`afterCacheAccess triggered for collection "${cache_collection_name}"`);
// only write to MongoDB if MSAL modified the in-memory cache
if (cache_context.cacheHasChanged) {
try {
const collection = await get_collection();
// serialize converts the token cache object into a JSON string
// option 01 - save this string to MongoDB
const cache_data = cache_context.tokenCache.serialize();
// option 02 - parse the string before storing in MongoDB (so it's easier to view etc)
const cache_object = JSON.parse(cache_data);
// MongoDB has a 16MB document size limit
if (Buffer.byteLength(cache_data, 'utf8') > 16000000) {
throw new Error('cache data exceeds the maximum allowed size of 16MB');
}
// update or create the cache document in MongoDB
await collection.updateOne(
{ _id: 'persistent_token_cache' },
// add either the string or the object, depending on your choice
// in this example we are inserting it as an object
{ $set: { data: cache_object } },
{ upsert: true }
);
} catch (error) {
console.error('error accessing cache after operation:', error);
}
}
}
};
};
export { msal_node_persistant_token_cache_plugin };
Reproduction Steps
- Look at the contents of the persistent token cache in MongoDB
- If you stored the contents in the default JSON string format (i.e the result of
cache_context.tokenCache.serialize()
), you will see that thetenantProfiles
property looks something like this, i.e the values are escaped multiple times:
\"tenantProfiles\":[\"{\\\"tenantId\\\":\\\"B2C_1_signupsignin1\\\",\\\"localAccountId\\\":\\\"******\\\",\\\"name\\\":\\\"FirstName LastName\\\",\\\"isHomeTenant\\\":false}\"]
- If you used JSON parse to store the contents as a JSON object, you will see that the
tenantProfiles
property has escaped properties, but none of the other properties in the cache are escaped.
Expected Behavior
I expect all escaping of the token cache content to be consistent - the key:value pairs within the objects within the Account
> tenantProfiles
property seem to be getting escaped multiple times.
Identity Provider
Azure B2C Basic Policy
Browsers Affected (Select all that apply)
None (Server)
Regression
No response