Caching of API token in action does not work

When a user logs in, I am using an action to contact two APIs: the Auth0 management API and a custom-made API. Both APIs require tokens for authentication. To avoid hitting rate limits (and to make the process faster) I cache these tokens using the caching mechanism in actions as described in e.g. Caching Management API Access Tokens in Login Action

However, the token usage for our custom API sky-rocketed when this was installed into production. I have now tested in a separate tenant, and it seems like the token is almost always missing in cache. If I cause a new silent login within a minute from the previous one, the token can be found in cache, but otherwise it is often (though not always) missing in cache and a new one is fetched.

  • I have checked that both tokens are well under the 2048B limit (they are at the most 1164B)
  • The cache expiration is set to a little less than 24 hours from now, using the expires_at options property
  • Sometimes fetching the token from cache succeeds and the time period during which it succeeds can be many minutes
  • Setting the cache value succeeds and if I add an extra step to fetch the token from the cache right after I set it, I can see that it is fetched properly, so the problem is not the setting or getting of the cache value, but rather that the cache is not persisted between consecutive runs
  • I am not running this in the test tool. I deploy the action on the Auth0 tenant and run real login/silent log in events in my real SPA-application, and monitor the logs in the Real-time Webtask Logs extension.

What can cause this behaviour and more importantly, how could it be fixed? E.g. is the action run on a plentitude of different hosts that each have their own cache which of course is empty the first time this host is used?

The token cache is set to cache with the following code

  const saveTokenToCache = (cacheKey, token) => {
    const decodedNew = jwtDecode(token);
    // Deduce the cache expiry time: Five minutes before the token expires (to make sure the token is valid throughout the action).
    const expiresAtSeconds = decodedNew?.exp ?? 0;
    let cacheExpiresMs = (expiresAtSeconds * 1000) - 5*60*1000;
    if (cacheExpiresMs < 0){
      cacheExpiresMs = 1000;
    }

  console.log('Setting to cache with expires_at: ', cacheExpiresMs);
  console.log('Blob size of the token is: ',(new Blob([token]))?.size);
   const result = api.cache.set(cacheKey, token, {expires_at: cacheExpiresMs});
   if (result.type === 'error') {
    console.log(
      'failed to set the token in the cache with error code',
      result.code,
    );
    } else {
      console.log(`successfully cached token.`);
    }
  };

Hi @linda,

Welcome to the Auth0 Community!

When I previously cached API tokens with Actions, I managed to do it with this script.

Would you be able to give that a try and let me know if you are still experiencing issues with the cache items not persisting?

And if so, could you provide me an example of your action script code with sensitive data redacted?

Thanks,
Rueben

1 Like

Hello @rueben.tiow,

I did read your script and try it previously, but it did not mitigate the issue. First, I did have the issue with one of the tokens being larger than 2048 and tried your solution to this, but as this did not remove the problem, I instead reduced the size of the token which was possible, as there were permissions that could be removed. While watching the logs, I can see that sometimes the token is found in cache, and sometimes not and it is therefore refetched. I attach my code below.


/**
 * Handler that will be called during the execution of a PostLogin flow.
 *
 * @param {Event} event - Details about the user and the context in which they are logging in.
 * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
 */
exports.onExecutePostLogin = async (event, api) => {

  const ManagementClient = require('auth0').ManagementClient;
  const axios = require('axios').default;
  const { jwtDecode } = require('jwt-decode');
  const domain = 'REMOVED';
  const managementAuth0 = 'auth0';
  const customAPI = 'REMOVED';
  // cache key for API explorer for Auth0 Management API
  const cacheKeyManagementToken = 'management-auth0-token';
  // cache key for custom api
  const cacheKeyAuthorizationToken = 'REMOVED';

  // -------------------------------
  // Helper functions - start
  // -------------------------------

  const queryToken = async settings => {
    const options = {
      method: 'POST',
      url: `https://${domain}/oauth/token`,
      headers: { 'content-type': 'application/x-www-form-urlencoded' },
      data: new URLSearchParams({
        grant_type: 'client_credentials',
        audience: settings.audience,
        client_id: settings.client_id,
        client_secret: settings.client_secret,
      }),
    };

    try {
      const response = await axios.request(options);
      return response.data;
    } catch (error) {
      console.error(error);
    }
  };

  const getAccessToken = async manager => {
    let settings;
    // to call Auth0 Management
    if (manager === managementAuth0)
      settings = {
        audience: 'REMOVED',
        client_id: 'REMOVED',
        client_secret: 'REMOVED',
      };
    // to call custom API
    else {
      settings = {
        audience: 'REMOVED',
        client_id: 'REMOVED',
        client_secret: 'REMOVED',
      };
    }

    const result = await queryToken(settings);

    return result.access_token;
  };

  const saveTokenToCache = (cacheKey, token) => {
    const decodedNew = jwtDecode(token);
    // Deduce the cache expiry time: Five minutes before the token expires (to make sure the token is valid throughout the action).
    const expiresAtSeconds = decodedNew?.exp ?? 0;
    let cacheExpiresMs = (expiresAtSeconds * 1000) - 5*60*1000;
    if (cacheExpiresMs < 0){
      cacheExpiresMs = 1000;
    }

  //console.log('Setting to cache with expires_at: ', cacheExpiresMs);
  //console.log('Blob size of the token is: ',(new Blob([token]))?.size);
   const result = api.cache.set(cacheKey, token, {expires_at: cacheExpiresMs});
   if (result.type === 'error') {
    console.log(
      'failed to set the token in the cache with error code',
      result.code,
    );
    } else {
      console.log(`successfully cached token.`);
    }
  };

  const getTokenFromCache = (cacheKey) => {
    return api.cache.get(cacheKey)?.value;
  };


  /**
   * Get an acccess token, primarily from cache, but if it is not found in cache or isn't valid anymore,
   * fetch a new one.
   * @param {*} manager
   * @param {*} cacheKey
   * @returns
   */
  const getToken = async (manager, cacheKey) => {

    let token = getTokenFromCache(cacheKey);
    //console.log('Trying first to get from cache, got:', token);

    let current_time = Date.now().valueOf() / 1000;
    let decoded = null;

    if (token) decoded = jwtDecode(token);
    //console.log('The exp of the token is', decoded?.exp);

    if (token != undefined && decoded?.exp > current_time) {
      console.log('Found token in cache.');
      return token;
    } else {
      token = await getAccessToken(manager);
      if (token) {
        saveTokenToCache(cacheKey, token);
        return token;
      }
      // Never throw errors in actions, deny access instead in error situations.
      api.access.deny(`Couldn't get or create access token for: ${manager}!`);
    }
  };

  // query data from custom API
  const getDataFromCustomApi = async (/*REMOVED PARAMETERS */) => {
    let token = await getToken(customAPI, cacheKeyAuthorizationToken);
    let url = 'REMOVED';

    const options = {
      method: 'GET',
      url: url,
      headers: {
        'content-type': 'application/json',
        'cache-control': 'no-cache',
        Authorization: `Bearer ${token}`,
      },
      data: {
        myfield: 'myvalue'
      },
    };

    // request custom data
    try {
      const response = await axios.request(options);
      return response.data;
    } catch (error) {
      console.error(`request error: ${error}`);
    }
  };

  const handleStuff = async (myvar = []) => {
    const token = await getToken(managementAuth0, cacheKeyManagementToken);
    const management = new ManagementClient({ domain: domain, token: token });
    const roles = (await management.roles.getAll()) || [];

    if (2 > 1 /* Removed condition */) {
      try {
        await management.users.deleteRoles(params, { roles: [/* removed */] });
      } catch (error) {
        console.log(error);
      }
    }

  };

  // -------------------------------
  // Helper functions - end
  // -------------------------------

  const customData = await getDataFromCustomApi(
    event.user.user_id,
    event.client.name,
    'removed' || null,
  );

  await handleStuff([/*removed*/]);

  api.accessToken.setCustomClaim(
    `${namespace}/removedId`,
    customData?.removedId,
  );
  api.idToken.setCustomClaim(
    `${namespace}/removedId`,
    customData?.removedId,
  );

  // add rest of needed token data
  api.accessToken.setCustomClaim(`${namespace}/removed`, 'removed');
};