Implementation of persistent token cache for msal-node inconsistent and incomplete - basic JSON file implementation not working as expected
See original GitHub issueCore Library
MSAL Node (@azure/msal-node)
Core Library Version
1.3.3
Wrapper Library
MSAL Node Extensions (@azure/msal-node-extensions)
Wrapper Library Version
1.0.0-alpha.12
Description
There is no clear documentation provided to achieve the following use case for msal-node
. The linked documentation is for dotnet. And from what I can gather there are a number of competing documents or resources on how to access token cache for the purpose of “allow background apps, APIs, and services to use the access token cache to continue to act on behalf of users in their absence.”.
It would be great to understand what we should or should not be implementing in order to achieve the behaviour below:
(Advanced) Accessing the user’s cached tokens in background apps and services
**You can use MSAL’s token cache implementation to allow background apps, APIs, and services to use the access token cache to continue to act on behalf of users in their absence. Doing so is especially useful if the background apps and services need to continue to work on behalf of the user after the user has exited the front-end web app.
Today, most background processes use application permissions when they need to work with a user’s data without them being present to authenticate or reauthenticate. Because application permissions often require admin consent, which requires elevation of privilege, unnecessary friction is encountered as the developer didn’t intend to obtain permission beyond that which the user originally consented to for their app.**
This code sample on GitHub shows how to avoid this unneeded friction by accessing MSAL’s token cache from background apps:
Accessing the logged-in user’s token cache from background apps, APIs, and services
What I have managed to find
- The Express Test App - which seems to say Redis is the answer, but again feels woefully under documented
- The Node Extensions Persistence Docs? - which seems perfect and then explicitly says to avoid “web app / web api scenarios”
- This caching.md - seems like it should be the source of truth
- And finally cache configuration - which in a basic setup of writing to a json file seems to not work as intended, this is also referenced here: https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-node-migration,
Error Message
token cache getAllAccounts() method returns empty array
Msal Logs
[Sun, 14 Nov 2021 06:29:19 GMT] : @azure/msal-node@1.3.3 : Info - getTokenCache called TokenCache { cacheHasChanged: false, storage: NodeStorage { clientId: ‘d1e92154-378c-4784-8214-acd958eedd1d’, cryptoImpl: CryptoProvider { pkceGenerator: PkceGenerator {} }, cache: {}, changeEmitters: [ [Function: bound handleChangeEvent] ], logger: Logger { level: 3, localCallback: [Function: loggerCallback], piiLoggingEnabled: false, correlationId: ‘’, packageName: ‘@azure/msal-node’, packageVersion: ‘1.3.3’ } }, persistence: { beforeCacheAccess: [AsyncFunction: beforeCacheAccess], afterCacheAccess: [AsyncFunction: afterCacheAccess] }, logger: Logger { level: 3, localCallback: [Function: loggerCallback], piiLoggingEnabled: false, correlationId: ‘’, packageName: ‘@azure/msal-node’, packageVersion: ‘1.3.3’ } } TokenCacheContext { cache: TokenCache { cacheHasChanged: false, storage: NodeStorage { clientId: ‘d1e92154-378c-4784-8214-acd958eedd1d’, cryptoImpl: [CryptoProvider], cache: {}, changeEmitters: [Array], logger: [Logger] }, persistence: { beforeCacheAccess: [AsyncFunction: beforeCacheAccess], afterCacheAccess: [AsyncFunction: afterCacheAccess] }, logger: Logger { level: 3, localCallback: [Function: loggerCallback], piiLoggingEnabled: false, correlationId: ‘’, packageName: ‘@azure/msal-node’, packageVersion: ‘1.3.3’ }, cacheSnapshot: undefined }, hasChanged: false } [] [Sun, 14 Nov 2021 06:29:19 GMT] : @azure/msal-node@1.3.3 : Verbose - initializeRequestScopes called
MSAL Configuration
const cachePath = "test.json"
// Call back APIs which automatically write and read into a .json file - example implementation
const beforeCacheAccess = async (cacheContext) => {
cacheContext.tokenCache.deserialize(fs.readFile(cachePath, "utf-8"));
};
const afterCacheAccess = async (cacheContext) => {
console.log(cacheContext)
if(cacheContext.cacheHasChanged){
fs.writeFile(cachePath, cacheContext.tokenCache.serialize());
}
};
const cachePlugin = {
beforeCacheAccess,
afterCacheAccess,
}
const config = {
auth: {
clientId: "XXX",
authority: "https://login.microsoftonline.com/XXX",
clientSecret: "XXX"
},
cache: {
cachePlugin
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose,
}
},
};
Relevant Code Snippets
const AWS = require("aws-sdk");
const express = require("express");
const serverless = require("serverless-http");
const msal = require('@azure/msal-node');
const fs = require('fs');
const cachePath = "test.json"
// Call back APIs which automatically write and read into a .json file - example implementation
const beforeCacheAccess = async (cacheContext) => {
cacheContext.tokenCache.deserialize(fs.readFile(cachePath, "utf-8"));
};
const afterCacheAccess = async (cacheContext) => {
console.log(cacheContext)
if(cacheContext.cacheHasChanged){
fs.writeFile(cachePath, cacheContext.tokenCache.serialize());
}
};
const cachePlugin = {
beforeCacheAccess,
afterCacheAccess,
}
const config = {
auth: {
clientId: "XXX",
authority: "https://login.microsoftonline.com/XXX",
clientSecret: "XXX"
},
cache: {
cachePlugin
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose,
}
},
};
const scopes = ["Calendars.Read.Shared", "Calendars.ReadWrite.Shared"]
const app = express();
const cca = new msal.ConfidentialClientApplication(config);
const msalCacheManager = cca.getTokenCache();
app.use(express.json());
app.get("/availability", async function (req, res) {
console.log(msalCacheManager);
// get Accounts
const accounts = await msalCacheManager.getAllAccounts();
console.log(accounts);
// Build silent request
const silentRequest = {
account: accounts[0], // You would filter accounts to get the account you want to get tokens for
scopes: scopes,
};
// Acquire Token Silently to be used in MS Graph call
cca.acquireTokenSilent(silentRequest).then((response) => {
console.log("\nSuccessful silent token acquisition:\nResponse: \n:", response);
res.json({ response });
// return msalCacheManager.writeToPersistence();
}).catch((error) => {
res.status(500).json({ error });
});
});
app.get('/authenticate', async function (req, res) {
const authCodeUrlParameters = {
scopes,
redirectUri: "http://localhost:3000/dev/redirect",
};
// get url to sign user in and consent to scopes needed for application
cca.getAuthCodeUrl(authCodeUrlParameters).then((response) => {
res.redirect(response);
}).catch((error) => {
console.log(JSON.stringify(error));
res.status(500).json({ error });
});
});
app.get('/redirect', async function (req, res) {
const tokenRequest = {
code: req.query.code,
scopes,
redirectUri: "http://localhost:3000/dev/redirect",
};
cca.acquireTokenByCode(tokenRequest).then(async function (response) {
console.log("\nResponse: \n:", response);
try {
res.json({ response });
} catch (error) {
console.log(error);
res.status(500).json({ error: "Could not create user" });
}
}).catch((error) => {
console.log(error);
res.status(500).send(error);
});
});
app.use((req, res, next) => {
return res.status(404).json({
error: "Not Found",
});
});
module.exports.handler = serverless(app);
Reproduction Steps
- Start the app
- Authenticate
- test.json will have the data written to the json file
- Visit the
/availability
location - The silent auth method fails
Expected Behavior
The app should be able to use the token cache stored in the json file
Identity Provider
Azure AD / MSA
Browsers Affected (Select all that apply)
None (Server)
Regression
No response
Source
External (Customer)
Issue Analytics
- State:
- Created 2 years ago
- Comments:7 (4 by maintainers)
Top GitHub Comments
Yes, @dillonbailey, in order to work with a JSON file the stated document will provide the correct approach.
We will work on updating the doc on configuration as well, thank you for that.
Amazing, thanks @samuelkubai
Will reopen if any issues. Also I never replied to your earlier question on environment. This will be deployed to a server less environment on AWS.
Thanks again!