acquireTokenSilent : ERROR InteractionRequiredAuthError: no_tokens_found: No refresh token found in the cache. Please sign-in
Core Library
MSAL Node (@azure/msal-node)
Core Library Version
2.13.1
Wrapper Library
Not Applicable
Wrapper Library Version
N/A
Public or Confidential Client?
Confidential
Description
We are calling acquireTokenSilent to get a new token without needing the user to give permissions again, but the function throws the error acquireTokenSilent : ERROR InteractionRequiredAuthError: no_tokens_found: No refresh token found in the cache. Please sign-in
Error Message
Error silently: InteractionRequiredAuthError: no_tokens_found: No refresh token found in the cache. Please sign-in.
MSAL Logs
[Tue, 10 Sep 2024 19:11:02 GMT] : [] : @azure/[email protected] : Info - CacheManager:getIdToken - Returning ID token
[Tue, 10 Sep 2024 19:11:02 GMT] : [edf00ed8-1a30-462a-b1da-04e9ac1bb8e5] : @azure/[email protected] : Info - Building oauth client configuration with the following authority: https://login.microsoftonline.com/TENANT_I/oauth2/v2.0/token.
[Tue, 10 Sep 2024 19:11:02 GMT] : [edf00ed8-1a30-462a-b1da-04e9ac1bb8e5] : @azure/[email protected] : Info - Token refresh is required due to cache outcome: 1
[Tue, 10 Sep 2024 19:11:02 GMT] : [] : @azure/[email protected] : Info - CacheManager:getRefreshToken - No refresh token found.
Error silently: InteractionRequiredAuthError: no_tokens_found: No refresh token found in the cache. Please sign-in.
Network Trace (Preferrably Fiddler)
- [ ] Sent
- [X] Pending
MSAL Configuration
export const msalConfig = (
supabase: SupabaseClient,
userId: string,
): Configuration => {
return {
auth: {
clientId: process.env.OUTLOOK_CLIENT_ID || "",
clientSecret: process.env.OUTLOOK_CLIENT_SECRET,
authority:
`https://login.microsoftonline.com/${process.env.OUTLOOK_TENANT_ID}`,
},
cache: {
cachePlugin: new SupabaseCachePlugin(supabase, userId),
},
system: {
loggerOptions: {
loggerCallback(
loglevel: LogLevel,
message: string,
containsPii: boolean,
) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: LogLevel.Info,
},
},
};
};
Relevant Code Snippets
export class SupabaseCachePlugin implements ICachePlugin {
private supabase: SupabaseClient;
private userId: string;
constructor(supabase: SupabaseClient, userId: string) {
this.supabase = supabase;
this.userId = userId;
}
async beforeCacheAccess(cacheContext: TokenCacheContext): Promise<void> {
// Load the cache from Supabase for the specific user
console.log("beforeCacheAccess", cacheContext);
const { data, error } = await this.supabase
.from("msal_cache")
.select("cache_data")
.eq("user_id", this.userId)
.single();
if (data && !error) {
cacheContext.tokenCache.deserialize(data.cache_data);
}
}
async afterCacheAccess(cacheContext: TokenCacheContext): Promise<void> {
if (cacheContext.cacheHasChanged) {
// Save the updated cache to Supabase for the specific user
const serializedCache = cacheContext.tokenCache.serialize();
await this.supabase
.from("msal_cache")
.upsert({
user_id: this.userId,
cache_data: serializedCache,
});
}
}
}
Reproduction Steps
- Successful login using the following scope ["openid","profile","Calendars.Read","Calendars.ReadWrite", "email","user.read","offline_access"]
- Call AcquireTokenSilent function
Expected Behavior
we should be able to get a new token.
Identity Provider
Entra ID (formerly Azure AD) / MSA
Browsers Affected (Select all that apply)
None (Server)
Regression
N/A
Source
External (Customer)
cc @Robbie-Microsoft @bgavrilMS
@Robbie-Microsoft @bgavrilMS could you help here? We are hitting the same issue. (not sure if the root causes are the same or different) I wonder if it could be related to a recent code change or something on msal side?
In our setup, We have a nodejs app and we use InteractiveBrowserCredential from @azure/identity along with @azure/identity-cache-persistence. We don't implement any custom cache plugin in our code.
Here is my call stack (it might be irrelevant since it's bundled but it might help with the class/fn names.)
InteractionRequiredAuthError: no_tokens_found: No refresh token found in the cache. Please sign-in.
at createInteractionRequiredAuthError (getCredential-QYKZSYTT.js:6046:10)
at _RefreshTokenClient.acquireTokenWithCachedRefreshToken (getCredential-QYKZSYTT.js:7082:17)
at getCredential-QYKZSYTT.js:2019:16
at _RefreshTokenClient.acquireTokenByRefreshToken (getCredential-QYKZSYTT.js:7071:211)
at async withSilentAuthentication (getCredential-QYKZSYTT.js:17186:22)
at async (getCredential-QYKZSYTT.js:18751:11)
at async Object.withSpan (chunk-FUS4SMZ3.js:552:26)
at async getCredential (getCredential-QYKZSYTT.js:19923:32)
error Command failed with exit code 1.
InteractiveBrowserCredential is a public client scenario and @Robbie-Microsoft and I don't own this scenario.
Updating the issue.
CC @peterzenz
Why was this clsoe?
I'm sickened by the awful developer experience in dealing with Microsoft and Azure.
@AdzeB - how do you get the first set of tokens? Do you use AcquireTokenByAuthCode?
Hi @bgavrilMS I used getAuthCodeUrl
const authCodeUrlParameters: AuthorizationUrlRequest = {
scopes: OUTLOOK\_SCOPES,
redirectUri: redirectUri,
// prompt: "consent", // Force a new consent prompt
// extraQueryParameters: {
// response\_mode: "query", // Ensures compatibility with various OAuth flows
// },
responseMode: "query",
prompt: "consent", // Force a new consent prompt
extraQueryParameters: {
response\_mode: "query",
access: "offline", // Explicitly request offline access
},
};
// Generate the authorization URL
const authUrl = await getMsalClient(supabase, userId).getAuthCodeUrl(
authCodeUrlParameters,
);
console.log("alok", authUrl);
return authUrl;
export function getMsalClient(supabase: SupabaseClient, userId: string) {
return new ConfidentialClientApplication(msalConfig(supabase, userId));
}
Seeing the same issue when using msal-node.
Acquiring first token with acquireTokenByCode is successful but when i try to use acquireTokenSilent with the same scope and account object that i recieved from acquireTokenByCode response im seeing this error:
Folks, I'm not able to reproduce this error. Via our msal-node Silent Flow sample, I plugged in my own clientId and used a clientCertificate (thumbprint + private key) instead of clientSecret on lines 236-240 in index.js. I used msal-node v2.13.1 like specified above.
It would be helpful to a have a minimal bug repro to work on.
Core Library
MSAL Node (@azure/msal-node)
Core Library Version 2.15.0
Wrapper Library Not Applicable
Wrapper Library Version N/A
Public or Confidential Client? Confidential
Description Acquiring first token with acquireTokenByCode is successful but when trying to use acquireTokenSilent with the same scope and account object that i recieved from acquireTokenByCode response im seeing this error.
Error Message
[InteractionRequiredAuthError: no_tokens_found: No refresh token found in the cache. Please sign-in.] { errorCode: 'no_tokens_found', errorMessage: 'No refresh token found in the cache. Please sign-in.', subError: '', name: 'InteractionRequiredAuthError', timestamp: '', traceId: '', correlationId: '31fccaf9-c819-4c7f-8514-1cb3462ce8fb', claims: '', errorNo: undefined }
MSAL Logs
[Mon, 28 Oct 2024 12:16:01 GMT] : [] : @azure/[email protected] : Verbose - initializeRequestScopes called [Mon, 28 Oct 2024 12:16:01 GMT] : [31fccaf9-c819-4c7f-8514-1cb3462ce8fb] : @azure/[email protected] : Verbose - buildOauthClientConfiguration called [Mon, 28 Oct 2024 12:16:01 GMT] : [31fccaf9-c819-4c7f-8514-1cb3462ce8fb] : @azure/[email protected] : Verbose - createAuthority called [Mon, 28 Oct 2024 12:16:01 GMT] : [] : @azure/[email protected] : Verbose - Attempting to get cloud discovery metadata from authority configuration [Mon, 28 Oct 2024 12:16:01 GMT] : [] : @azure/[email protected] : Verbose - Did not find cloud discovery metadata in the config... Attempting to get cloud discovery metadata from the hardcoded values.
[Mon, 28 Oct 2024 12:16:01 GMT] : [] : @azure/[email protected] : Verbose - Found cloud discovery metadata from hardcoded values. [Mon, 28 Oct 2024 12:16:01 GMT] : [] : @azure/[email protected] : Verbose - Attempting to get endpoint metadata from authority configuration [Mon, 28 Oct 2024 12:16:01 GMT] : [] : @azure/[email protected] : Verbose - Did not find endpoint metadata in the config... Attempting to get endpoint metadata from the hardcoded values. [Mon, 28 Oct 2024 12:16:01 GMT] : [] : @azure/[email protected] : Verbose - Replacing tenant domain name with id {tenantid} [Mon, 28 Oct 2024 12:16:01 GMT] : [31fccaf9-c819-4c7f-8514-1cb3462ce8fb] : @azure/[email protected] : Info - Building oauth client configuration with the following authority: https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token. [Mon, 28 Oct 2024 12:16:01 GMT] : [31fccaf9-c819-4c7f-8514-1cb3462ce8fb] : @azure/[email protected] : Verbose - Silent flow client created [Mon, 28 Oct 2024 12:16:01 GMT] : [] : @azure/[email protected] : Info - CacheManager:getAccessToken - No token found [Mon, 28 Oct 2024 12:16:01 GMT] : [31fccaf9-c819-4c7f-8514-1cb3462ce8fb] : @azure/[email protected] : Info - Token refresh is required due to cache outcome: 2 [Mon, 28 Oct 2024 12:16:01 GMT] : [] : @azure/[email protected] : Info - CacheManager:getRefreshToken - No refresh token found.
MSAL Configuration
const msalConfig = { auth: { clientId: process.env.AZURE_AD_CLIENT_ID, authority: https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}, clientSecret: process.env.AZURE_AD_SECRET, }, cache: { cacheLocation: "localStorage" }, system: { loggerOptions: { logLevel: msal.LogLevel.Verbose, loggerCallback: (level: any, message: any, containsPii: any) => { if (containsPii) { return; } switch (level) { case msal.LogLevel.Error: console.error(message); return; case msal.LogLevel.Info: console.info(message); return; case msal.LogLevel.Verbose: console.debug(message); return; case msal.LogLevel.Warning: console.warn(message); return; } }, piiLoggingEnabled: false, }, }, };
Reproduction Steps This is happening inside next.js 13 API Routes
-
Successful login using acquireTokenByCode function with following scope
const scope = ['openid', 'api://client-id/app-name'] -
Call AcquireTokenSilent
Expected Behavior we should be able to get a new token.
Identity Provider Entra ID
Hi, any updates/fixes on the issue above?
Sorry, the bot keeps closing it this.
Folks, we have not been able to reproduce this issue. Can someone please provide a minimal repo? I can see in the original post a custom cache, it's not enough to repro. Could someone pls create a small sample that reproduces the issue?
@bgavrilMS How you like the sample to work, would you like it with supabase etc set up…you just need to enter the keys?
We can provision our own Entra ID app, redirect URI, secret etc.
@bgavrilMS Apologies for the late reply but you can use this project to reproduce the error
@bgavrilMS Any update on reproducing the error
@sameerag @tnorling @bgavrilMS
I have been working on InteractiveBrowserCredential and I am running into the same bug that the user has pasted above. Apparently the bug is that the tokenCache() is not called anywhere during the silent token authentication. So whenever there is an account passed in for the silent authentication, it doesn't find the record because the tokenCache needs to be loaded in each call.
I was trying to debug this locally in the compiled version of the code and found the fix.
This can be fixed with the addition of the following code in acquireTokenSilent just before the return statement.
https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/src/client/ClientApplication.ts#L268
await this.getTokenCache().getAllAccounts()
So the code for this function should look like -
async acquireTokenSilent(request) {
const validRequest = {
...request,
...(await this.initializeBaseRequest(request)),
forceRefresh: request.forceRefresh || false,
};
const serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.acquireTokenSilent, validRequest.correlationId, validRequest.forceRefresh);
try {
const silentFlowClientConfig = await this.buildOauthClientConfiguration(validRequest.authority, validRequest.correlationId, validRequest.redirectUri || "", serverTelemetryManager, undefined, request.azureCloudOptions);
const silentFlowClient = new SilentFlowClient(silentFlowClientConfig);
this.logger.verbose("Silent flow client created", validRequest.correlationId);
await this.getTokenCache().getAllAccounts()
return await silentFlowClient.acquireToken(validRequest);
}
catch (e) {
if (e instanceof AuthError) {
e.setCorrelationId(validRequest.correlationId);
}
serverTelemetryManager.cacheFailedRequest(e);
throw e;
}
}
I'm still unable to reproduce this issue in msal-node. I've pasted the code I'm using, below. Is someone able to tweak what I've written to get a repro?
To use:
- start redis
- run index.ts, as part of a typescript project
- navigate to
http://localhost:3000 - click the sign in button to get a token via auth-code flow (it's stored in the redis cache)
- navigate to
http://localhost:3000/silentas many times as you want, to get the token via the silent flow - you can use
flushallto clear the redis cache at any time
index.ts:
(replace values for: client_id, tenant_id, client_secret)
import {
ConfidentialClientApplication,
AuthorizationCodeRequest,
LogLevel,
Configuration,
} from "@azure/msal-node";
import { RedisClientType, createClient } from "redis";
import { RedisCachePlugin } from "./RedisCachePlugin";
import express from "express";
// Initialize Express server
const app = express();
const port = 3000;
// Initialize Redis client
const redisClient: RedisClientType = createClient({
url: "redis://localhost:6379", // default Redis URL
});
// MSAL Configuration for Confidential Client
const config: Configuration = {
auth: {
clientId: "<client_id>",
clientSecret: "<client_secret>,
authority:
"https://login.microsoftonline.com/<tenant_id>",
},
cache: {
// Pass the Redis client and partitionKey to the cache plugin
cachePlugin: new RedisCachePlugin(
redisClient,
"<client_id>.<tenant_id>" // clientId.tenentId
),
},
system: {
loggerOptions: {
logLevel: LogLevel.Verbose,
},
},
};
const cca = new ConfidentialClientApplication(config);
// Store account info (in-memory) to use later for silent requests
let currentAccount: any = null;
async function getAuthorizationUrl() {
await redisClient.connect().catch((err) => {
console.error("Error connecting to Redis:", err);
throw new Error("Failed to connect to Redis");
});
// Generate the authorization URL
const authCodeRequest = {
scopes: ["https://graph.microsoft.com/.default"], // Scopes you want access to
redirectUri: "http://localhost:3000/redirect", // Replace with your redirect URI
};
try {
const authUrl = await cca.getAuthCodeUrl(authCodeRequest);
console.log("Auth URL:", authUrl);
return authUrl;
} catch (error) {
console.error("Error generating authorization URL:", error);
}
}
// Redirect URI handler: here the user is redirected after authentication
app.get("/redirect", async (req, res) => {
const { code } = req.query; // Authorization code from the redirect URL
if (!code || typeof code !== "string") {
res.status(400).send("Authorization code not found or invalid.");
return;
}
// Exchange authorization code for tokens
const tokenRequest: AuthorizationCodeRequest = {
code: code,
scopes: ["https://graph.microsoft.com/.default"], // Same scopes as before
redirectUri: "http://localhost:3000/redirect", // Same redirect URI as before
};
try {
const response = await cca.acquireTokenByCode(tokenRequest);
console.log("Access token acquired:", response.accessToken);
// Save account for future silent requests
currentAccount = response.account;
res.send(
"Authentication successful. Token acquired. You can now make silent requests."
);
} catch (error) {
console.error("Error acquiring token by code:", error);
res.status(500).send("Failed to acquire token.");
}
});
// Use acquireTokenSilent (once the user has authenticated and we have their account object)
async function acquireTokenSilent(account: any) {
const silentRequest = {
scopes: ["https://graph.microsoft.com/.default"], // Same scopes as before
account: account, // Pass the account object here (this is from the first authorization code flow)
};
try {
const silentResponse = await cca.acquireTokenSilent(silentRequest);
console.log(
"Token acquired silently from cache:",
silentResponse.accessToken
);
return silentResponse.accessToken;
} catch (silentError) {
console.error("Error acquiring token silently:", silentError);
// Optionally, if silent acquisition fails, you can fallback to interactive flow again:
if (
silentError instanceof Error &&
silentError.message.includes("interaction_required")
) {
console.log(
"Silent token acquisition failed. Re-authenticating..."
);
// You could ask the user to log in again.
}
return null;
}
}
// Silent Route to manually acquire token silently
app.get("/silent", async (req, res) => {
if (!currentAccount) {
res.status(400).send("No account found. Please login first.");
return;
}
// Call acquireTokenSilent with the stored account
const accessToken = await acquireTokenSilent(currentAccount);
if (accessToken) {
res.send(`Silent token acquired: ${accessToken}`);
} else {
res.status(500).send("Failed to acquire token silently.");
}
});
// Start the server and redirect the user to the login page
app.get("/login", async (req, res) => {
const authUrl = await getAuthorizationUrl();
res.redirect(authUrl as string); // Redirect user to Microsoft login page
});
// Add a route handler for the root path
app.get("/", (req, res) => {
res.send("Welcome! <a href='/login'>Click here to log in</a>");
});
// Start Express server
app.listen(port, () => {
console.log(`App listening at http://localhost:${port}`);
});
RedisCachePlugin.ts:
import { ICachePlugin, TokenCacheContext } from "@azure/msal-node";
import { RedisClientType } from "redis";
/**
* Custom Redis cache plugin for MSAL Node to handle token cache operations using Redis.
* Implements ICachePlugin to integrate with MSAL's token cache system.
*/
export class RedisCachePlugin implements ICachePlugin {
private client: RedisClientType;
private partitionKey: string;
/**
* Initializes the RedisCachePlugin with a Redis client and partition key.
* @param client The Redis client used for connecting to the Redis database.
* @param partitionKey A unique partition key used to differentiate this cache from others (e.g., client ID or tenant).
*/
constructor(client: RedisClientType, partitionKey: string) {
this.client = client;
this.partitionKey = partitionKey;
}
/**
* Method invoked before any cache access. This is where the cache is checked to see if data is available.
* If data exists in Redis, it is deserialized and added to MSAL's token cache for further use.
*
* @param context The context object containing MSAL's token cache and cache operation metadata.
*
* @returns A promise that resolves when the cache access operation is complete.
*/
async beforeCacheAccess(context: TokenCacheContext): Promise<void> {
try {
// Attempt to fetch the serialized token cache data from Redis using the partitionKey
const data = await this.client.get(this.partitionKey); // Use partitionKey as the cache key
if (data) {
// Deserialize the data into MSAL's token cache and store it in context.tokenCache
context.tokenCache.deserialize(data);
console.log("Cache hit: Data retrieved from Redis");
} else {
console.log("Cache miss: No data found in Redis");
}
} catch (error) {
// If there is any issue while retrieving data from Redis, log the error and rethrow it
console.error("Error fetching from cache", error);
throw error;
}
}
/**
* Method invoked after a cache access operation has occurred. If the cache has been modified,
* the updated cache is serialized and stored back into Redis.
*
* @param context The context object containing MSAL's token cache and cache operation metadata.
*
* @returns A promise that resolves when the cache save operation is complete.
*/
async afterCacheAccess(context: TokenCacheContext): Promise<void> {
// Check if the cache has been modified (i.e., has any changes)
if (context.hasChanged) {
try {
// Access the token cache from the context and serialize it into a format suitable for storage (JSON)
const serializedData = context.tokenCache.serialize();
// Store the serialized data back into Redis with no expiration time
await this.client.set(this.partitionKey, serializedData); // Use partitionKey as the cache key
console.log("Cache data saved to Redis");
} catch (error) {
// If there is any issue while saving data to Redis, log the error and rethrow it
console.error("Error saving to cache", error);
throw error;
}
} else {
console.log("No changes in the cache, skipping save operation.");
}
}
}
@Robbie-Microsoft You need to use PersistenceCache plugin from msal-node-extensions as well. Please refer to the private gist i sent you over Teams. The repro i have in the internal gist is with @azure/identity, but you should be able to repro it with the underlying msal libraries for that scenario.
Consider the case where a user has just signed in and saved the authentication record on the disk/ file. Now the user passes in that authentication record, which is saved on the disk, to attempt a consecutive login, there is an error thrown by msal saying - "no_tokens_found: No refresh token found in the cache. Please sign-in."
In your code snippet above, you use the RedisCachePlugin. Can you try 'PersistenceCachePlugin'? Also is the silent authentication successful for you?
Posting in case this helps anyone else.
I was also running into the error
ERROR InteractionRequiredAuthError: no_tokens_found: No refresh token found in the cache.
Along the lines of @KarishmaGhiya's solution above, I did some debugging and found that this is happens under the following circumstance;
- You are creating a new instance of the msal client like
const msalInstance = new ConfidentialClientApplication(msalConfig(supabase, userId)); - When you call
msalInstance.acquireTokenSilent(), themsalInstance.tokenCachedoes not automatically hydrate from the provided cache (e.g Redis).
As @KarishmaGhiya's fix has not been merged, a workaround I'm using is just to have a method like
export const getMsalInstance = async (userId) => {
const msalInstance = new ConfidentialClientApplication(getMsalConfig(userId))
await msalInstance.tokenCache.getAllAccounts()
return msalInstance;
};
This is to be used anytime an msalInstance is required. Calling getAllAccounts() here forces the in memory cache to be updated from the persistent storage.
We're still unable to reproduce this issue, and Karishma has been out-of-office most of January. What she wrote is sort of a band-aid fix, and we're looking for the root of the problem.
Separately, Shyla just merged #7469, and we're hoping that this could fix this issue you're all seeing. I'll ping here after our next release, which will include #7469.
@Robbie-Microsoft
I am using this package and facing the same issue
When is this expected to be released? And what can I do till it is?
We're still unable to reproduce this issue, and Karishma has been out-of-office most of January. What she wrote is sort of a band-aid fix, and we're looking for the root of the problem.
Separately, Shyla just merged #7469, and we're hoping that this could fix this issue you're all seeing. I'll ping here after our next release, which will include #7469.
We released msal-node v3.2.1 yesterday evening. Please try that and let us know if you're still seeing the issue.
We're still unable to reproduce this issue, and Karishma has been out-of-office most of January. What she wrote is sort of a band-aid fix, and we're looking for the root of the problem. Separately, Shyla just merged #7469, and we're hoping that this could fix this issue you're all seeing. I'll ping here after our next release, which will include #7469.
We released msal-node v3.2.1 yesterday evening. Please try that and let us know if you're still seeing the issue.
It seems it is now fixed, thanks Will wait sometime to make sure it vanished permanently
- I am seeing a different behavior for authentication using persistent caching for msft tenant accounts v/s the non msft tenant accounts. I am getting the same error for non-msft tenant accounts. It works for msft tenant accounts.
Posting in case this helps anyone else.
I was also running into the error
ERROR InteractionRequiredAuthError: no_tokens_found: No refresh token found in the cache........................ a workaround I'm using is just to have a method like
export const getMsalInstance = async (userId) => { const msalInstance = new ConfidentialClientApplication(getMsalConfig(userId)) await msalInstance.tokenCache.getAllAccounts() return msalInstance; };This is to be used anytime an msalInstance is required. Calling
getAllAccounts()here forces the in memory cache to be updated from the persistent storage.
=========================
Thanks for posting your solution @Kvnyu .
I upgraded MSAL Node several days ago and it broke my acquireTokenSilent logic in my confidential client app.
I have established that my code works fine in MSAL Node 3.1.0 and then breaks in 3.2.0.
I tested the latest version 3.5.2 and it is still broken there for me as well.
I get the error:
InteractionRequiredAuthError: no_tokens_found: No refresh token found in the cache. Please sign-in.
When I added your code:
await confidential_client_application.tokenCache.getAllAccounts();
I could see in my logs that atleast the in-memory cache wasn't empty anymore.
Prior to adding your code, in 3.2.0, it was just an empty object.
However, I still get the same error.
I'm guessing it relates to this commit:
https://github.com/AzureAD/microsoft-authentication-library-for-js/commit/9ae4c0ef2926e389f3637c260ea4d4de3e294517
but after several days of intensive troubleshooting, I still haven't found a solution.
I look at the diff between 3.1.0 and 3.2.0 and can't pinpoint where the problem is:
https://github.com/AzureAD/microsoft-authentication-library-for-js/compare/msal-node-v3.1.0...msal-node-v3.2.0
My basic checklist:
01) Same config object used for initial MSAL instance and subsequent MSAL instance to call acquireTokenSilent on ✅
msal_config = {
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
redirectUri: process.env.APP_REDIRECT_URI_B2C_SignIn,
validateAuthority: false,
},
cache: {
cachePlugin: msal_node_persistant_token_cache_plugin(cache_collection_name),
}
};
02) Passing an account object and scopes to acquireTokenSilent ✅
msal_method_parameter = {
account: account_object,
scopes: ["openid", "profile", "offline_access"]
};
03) Using MongoDB persistent cache, pretty much exactly as specified (here) ✅
import { mongodb_client_manager } from './mongodb_client_manager.js';
/*
this plugin runs whenever MSAL node interacts with the in-memory token cache.
before the interaction, the beforeCacheAccess event is triggered - and the contents of the persistent cache are read into the in-memory cache.
after the interaction, the afterCacheAccess event is triggered - and the contents of the in-memory cache are written to the persistent cache.
*/
const msal_node_persistant_token_cache_plugin = (cache_collection_name) => {
let client;
let collection;
const get_collection = async () => {
if (!client) {
client = await mongodb_client_manager.open();
collection = client.db('msal_node_token_cache').collection(cache_collection_name);
}
return collection;
};
return {
// before the in-memory cache is accessed, we need to read the contents of the persistent cache
beforeCacheAccess: async (cache_context) => {
try {
console.log(`beforeCacheAccess triggered, cache collection name: "${cache_collection_name}"`);
const collection = await get_collection();
const cache_data = await collection.findOne({ _id: 'persistent_token_cache' });
if (cache_data) {
// deserialize takes the JSON string in cache_data.data and converts it back into a token cache object
cache_context.tokenCache.deserialize(cache_data.data);
console.log(`token cache read from persistent cache and written to in-memory cache`);
} else {
console.log(`the "${cache_collection_name}" token cache does not exist`);
}
} catch (error) {
console.error('error accessing cache before operation:', error);
}
},
// after the in-memory cache is written to, we need to write the contents of the in-memory cache to the persistent cache
afterCacheAccess: async (cache_context) => {
console.log(`afterCacheAccess triggered, cache collection name: "${cache_collection_name}"`);
// only write to the persistent cache if the in-memory cache has changed
if (cache_context.cacheHasChanged) {
try {
const collection = await get_collection();
// serialize takes the token cache object and converts it into a JSON string
const cache_data = cache_context.tokenCache.serialize();
if (Buffer.byteLength(cache_data, 'utf8') > 16000000) {
throw new Error('cache data exceeds the maximum allowed size of 16MB');
}
// write the in-memory cache to the persistent cache
await collection.updateOne(
{ _id: 'persistent_token_cache' },
{ $set: { data: cache_data } },
{ upsert: true }
);
console.log(`in-memory cache written to persistent cache`);
} catch (error) {
console.error('error accessing cache after operation:', error);
}
}
}
};
};
export { msal_node_persistant_token_cache_plugin };
04) When I log the account object I am passing through to acquireTokenSilent and the contents of the token cache, it looks like acquireTokenSilent should be finding a match - the homeAccountId value in the account object is the same as the homeAccountId value in the idToken and refreshToken in the token cache (and the cacheSnapshot) ✅
I tried both using the account object that is returned from the original B2C signin authentication result (which I was storing in the session object) and also just using the homeAccountId from that account object and using account = await confidential_client_application.tokenCache.getAccountByHomeId(home_account_id);, neither made a difference.
getAccountByHomeId() returns something like this:
{
"homeAccountId": "111111111111111111111111111111111111-b2c_1_signupsignin1.222222222222222222222222222222222222",
"environment": "<b2c-tenant-name>.b2clogin.com",
"tenantId": "B2C_1_signupsignin1",
"username": "info@<some-domain>.com",
"localAccountId": "111111111111111111111111111111111111",
"name": "FirstName LastName (External)",
"authorityType": "MSSTS",
"tenantProfiles": {}
}
Not sure what I will try next 🫤.
If it helps anyone, 7 days of pain was caused by including the https:// protocol in the knownAuthorities property value in the MSAL instance Configuration object:
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 the domain should not include the 'https://' prefix!!!!!
}
For some reason, prior to MSAL Node 3.2.0 this did not cause a problem when calling acquireTokenSilent, from 3.2.0 onwards it resulted in:
InteractionRequiredAuthError: no_tokens_found: No refresh token found in the cache. Please sign-in.
So maybe the incorrect value messed up acquireTokenSilent being able to find a match between the account object it was passed and the token cache.
I also had a deprecated property in the config which I removed, not sure if that made a difference though:
validateAuthority: false
and I removed the redirectUri property, as it doesn't belong in the MSAL instance Configuration object.