Skip to content

Values in Account > tenantProfiles in a persisted MSAL Node token cache are 'double escaped' #7732

Open
@oshihirii

Description

@oshihirii

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 with cache_context.tokenCache.deserialize(cache_data.data)

  • In afterCacheAccess, writing the in-memory cache data to the persistent cache (MongoDB Atlas) with this cache_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

  1. Look at the contents of the persistent token cache in MongoDB
  2. If you stored the contents in the default JSON string format (i.e the result of cache_context.tokenCache.serialize()), you will see that the tenantProfiles property looks something like this, i.e the values are escaped multiple times:
\"tenantProfiles\":[\"{\\\"tenantId\\\":\\\"B2C_1_signupsignin1\\\",\\\"localAccountId\\\":\\\"******\\\",\\\"name\\\":\\\"FirstName LastName\\\",\\\"isHomeTenant\\\":false}\"]
  1. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs: Attention 👋Awaiting response from the MSAL.js teamb2cRelated to Azure B2C library-specific issuesbug-unconfirmedA reported bug that needs to be investigated and confirmedconfidential-clientIssues regarding ConfidentialClientApplicationsmsal-nodeRelated to msal-node packagequestionCustomer is asking for a clarification, use case or information.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions