microsoft-authentication-library-for-js icon indicating copy to clipboard operation
microsoft-authentication-library-for-js copied to clipboard

acquireTokenSilent : ERROR InteractionRequiredAuthError: no_tokens_found: No refresh token found in the cache. Please sign-in

Open AdzeB opened this issue 1 year ago • 2 comments

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

  1. Successful login using the following scope ["openid","profile","Calendars.Read","Calendars.ReadWrite", "email","user.read","offline_access"]
  2. 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)

AdzeB avatar Sep 10 '24 19:09 AdzeB

cc @Robbie-Microsoft @bgavrilMS

sameerag avatar Sep 19 '24 04:09 sameerag

@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?

altinokdarici avatar Sep 26 '24 20:09 altinokdarici

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.

altinokdarici avatar Sep 26 '24 21:09 altinokdarici

InteractiveBrowserCredential is a public client scenario and @Robbie-Microsoft and I don't own this scenario.

Updating the issue.

CC @peterzenz

bgavrilMS avatar Oct 01 '24 11:10 bgavrilMS

Why was this clsoe?

AdzeB avatar Oct 13 '24 13:10 AdzeB

I'm sickened by the awful developer experience in dealing with Microsoft and Azure.

mryraghi avatar Oct 17 '24 00:10 mryraghi

@AdzeB - how do you get the first set of tokens? Do you use AcquireTokenByAuthCode?

bgavrilMS avatar Oct 17 '24 18:10 bgavrilMS

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));

}

AdzeB avatar Oct 17 '24 18:10 AdzeB

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: image

WiktorHeimroth avatar Oct 24 '24 13:10 WiktorHeimroth

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.

Robbie-Microsoft avatar Oct 25 '24 20:10 Robbie-Microsoft

It would be helpful to a have a minimal bug repro to work on.

bgavrilMS avatar Oct 28 '24 11:10 bgavrilMS

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

  1. Successful login using acquireTokenByCode function with following scope const scope = ['openid', 'api://client-id/app-name']

  2. Call AcquireTokenSilent

Expected Behavior we should be able to get a new token.

Identity Provider Entra ID

WiktorHeimroth avatar Oct 28 '24 12:10 WiktorHeimroth

Hi, any updates/fixes on the issue above?

WiktorHeimroth avatar Nov 05 '24 14:11 WiktorHeimroth

Sorry, the bot keeps closing it this.

bgavrilMS avatar Nov 06 '24 16:11 bgavrilMS

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 avatar Nov 06 '24 16:11 bgavrilMS

@bgavrilMS How you like the sample to work, would you like it with supabase etc set up…you just need to enter the keys?

AdzeB avatar Nov 09 '24 10:11 AdzeB

We can provision our own Entra ID app, redirect URI, secret etc.

bgavrilMS avatar Nov 11 '24 11:11 bgavrilMS

@bgavrilMS Apologies for the late reply but you can use this project to reproduce the error

AdzeB avatar Nov 23 '24 13:11 AdzeB

@bgavrilMS Any update on reproducing the error

AdzeB avatar Dec 12 '24 09:12 AdzeB

@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;
        }
    }

KarishmaGhiya avatar Jan 03 '25 07:01 KarishmaGhiya

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:

  1. start redis
  2. run index.ts, as part of a typescript project
  3. navigate to http://localhost:3000
  4. click the sign in button to get a token via auth-code flow (it's stored in the redis cache)
  5. navigate to http://localhost:3000/silent as many times as you want, to get the token via the silent flow
  6. you can use flushall to 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 avatar Jan 09 '25 23:01 Robbie-Microsoft

@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?

KarishmaGhiya avatar Jan 10 '25 10:01 KarishmaGhiya

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;

  1. You are creating a new instance of the msal client like const msalInstance = new ConfidentialClientApplication(msalConfig(supabase, userId));
  2. When you call msalInstance.acquireTokenSilent(), the msalInstance.tokenCache does 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.

Kvnyu avatar Jan 24 '25 07:01 Kvnyu

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 avatar Jan 30 '25 19:01 Robbie-Microsoft

@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?

MohHamoud avatar Feb 05 '25 09:02 MohHamoud

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.

Robbie-Microsoft avatar Feb 05 '25 17:02 Robbie-Microsoft

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

MohHamoud avatar Feb 10 '25 13:02 MohHamoud

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

KarishmaGhiya avatar Feb 11 '25 22:02 KarishmaGhiya

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

oshihirii avatar Apr 29 '25 11:04 oshihirii

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.

oshihirii avatar Apr 30 '25 10:04 oshihirii