Caching M2M tokens (not ManagementClient)

Hello!

The goal:

My goal is to reduce usage of M2M tokens. We have some internal APIs that communicate using M2M tokens and we have problems with the monthly quota (it is exceeded every month now). My initial thought was that I have to take a look on client side of m2m token utilization (use until expires, cache etc) but I discovered that m2m tokens caching was introduced yeey! (See here)

That was the answer for me, just create new Action and we are good but seems like this is not the truth.

The problem:

According to the linked blogpost what should be available is caching m2m token to “avoid exceeding their quotas on machine-to-machine exchanges” but it seems like it is not possible.

Every topic on this forum I saw was about caching Management API token, not regular M2M token generated by client credentials grant. Management API tokens are not included in quota (see here) so the only benefit is the performance.

I tried to do it by myself by checking API docs. By looking into m2m flow I assumed that what I need is:

  • create an Action for credentials-exchange trigger. (M2M / Client-Credentials action, onExecuteCredentialsExchange function)
  • check if there is a cached token for current client_id or is expired, if not then request a token and add to cache
  • get token from cache and return it

To do it I should be able to somehow prevent default behavior of this flow (returning from function) or use available API to change the token the flow will return. Checked event object, API object and auth0 nodejs SDK docs and I don’t see how this could be possible.

What worries me the most is this post and response. Looks like this is not possible which is opposite to the official blog post so I’m super confused.

Please help with this and let’s answer the big question: is caching m2m token generated by /oauth/token endpoint with client_credentials grant type possible.

Unless I am misunderstanding what you’re trying to do, of course it’s possible to cache M2M tokens. M2M tokens are long lived (24 hours I think). You have multiple options for caching the tokens: distributed cache memory, database, json file, etc. I do the following for this scenario:

  1. Store the token
  2. Track the timestamp of token
  3. If token is available and not expired, use it
  4. If token is available and expired, get a new token
  5. Go to step 1

Just remember to safeguard your access tokens when caching. Use any encryption, secure file store, etc. as needed.

Hope this helps!

Thanks for the suggestion but this is not the answer to the question. I want to use Auth0 Action and its cache to store M2M token (as linked blog post says it should be possible now). You proposal would work but I want to avoid changing code base in multiple m2m clients if this is possible. Is this a good approach? No, but it is cheaper in my case :slight_smile:

Anyway, thanks for Your response!

@jdbahnick an important fact and something that I can say with veracity because I checked it, the token that one keeps in cache only lives during the flow and not for request, it does not have more time of life, now, do you have some way to make it last between requests the cache? and if the token is still available (not expired) how do I return it to the application if the action is triggered before returning the token, I would appreciate if you show us example code please.

I keep my tokens within the bounds of my applications and do not rely on Auth0 resources for maintaining my tokens. Honestly, I hadn’t heard of Actions until OPs post. Based on the linked post, I’m not sure how they even work. Currently, I have background processes that integrate with public facing APIs and authenticate with M2M tokens. Those apps are responsible for maintaining the M2M tokens retrieved.

We also will have customers that will integrate their applications with our APIs and we will facilitate obtaining a token for them based on their account information. Then we can cache and control the tokens on our end in a database or distributed cache and not have our clients bother with it.

I understand, and that’s fine, but what you comment in the post is not related to the open topic, when you answered I thought you had found a way to make it work with auth0 resources, but everything indicates that you use third parties to reuse the generated token, if you have code or something that works to cache token using action please would you do a favor to the community by sharing it, there is very little to m2m

1 Like

Hey Marcin,
This sounds like the issue we were having. We went from using 8,000 authentication calls to 500 by implementing caching of the token in the action

snippets of code|

was
const lForm = qs.stringify({
client_id: event.secrets.CLIENT_ID,
client_secret: event.secrets.CLIENT_SECRET,
grant_type: “client_credentials”,
audience: “xxxx-api”

now includes
let { value: token } = api.cache.get(‘CachedToken’) || {};

  // Check if token is available
  if (!token) {
    // get access token
    const lForm = qs.stringify({
      client_id: event.secrets.CLIENT_ID,
      client_secret: event.secrets.CLIENT_SECRET,
      grant_type: "client_credentials",
      audience: "xxxx-api"
	  
  const cacheResult = api.cache.set('CachedToken', token);
    if (cacheResult.type === 'error') {
      console.log("failed to set the token in the cache with error code", cacheResult.code)
    } else {
      console.log("successfully set access token in cache:")
    }
  }

I appreciate this is not in context but this is the key changes to the action from pre to post caching

HI @rob_holtham1

is this a code snippet from your Auth0 Action? What You pasted here is easy and straightforward. I know how to generate token, use cache, use Auth0 Actions/APIs/SKDs etc. Has been coding actions for some time. What is unclear to me is how to return cached token. What exactly is causing action to return token from cache, not generate a new one. To create action for caching M2M tokens what I need is (step by step):

  • check cache key (or keys, if the token size is grater than cache storage for single variable) if token for client_id for current event is there
  • if token is not there then generate new token and update cache key (or keys) value, go to last step
  • if token is there then check if not expired, if expired then the same as step two (generate new token and update cache), go to last step
  • return the token from cache

I know how to develop all of the logic except the last step. How to tell Auth0 Action (M2M / Client-Credentials action, onExecuteCredentialsExchange function) to use token from my cache? By default this action allows to execute some code before the actual token is generated. I need to somehow prevent default behavior. Your code only shows how use cache. Did you managed to return cached M2M token instead of generating new one?

OK so this is the full action before and after change. The token is cached for the default 15 minutes and looking at the stats it’s caching maybe 5 tokens at a time (depends on threads)

BEFORE
exports.onExecutePostLogin = async (event, api) => {
const axios = require(‘axios’);
const qs = require(“qs”);

if (event.user.email && event.client.name === 'xxxx') {
  try {

     const firstname = event.user.firstname || event.user.given_name; 
     const lastname = event.user.lastname || event.user.family_name; 

    // get access token
    const lForm = qs.stringify({
      client_id: event.secrets.CLIENT_ID,
      client_secret: event.secrets.CLIENT_SECRET,
      grant_type: "client_credentials",
      audience: "xxxx"
    });
    const lConfig = {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
    };
    const lResponse = await axios.post(
      `${event.secrets.AUTH_BASE_URL}/oauth/token`,
      lForm,
      lConfig
    );

    console.log(lResponse)
    if (lResponse.status != 200 && !lResponse.data) {
      throw new Error("Unable to authenticate with IdP!")
    }

    // call BI endpoint
    const accessToken = lResponse.data.access_token;

    const config = {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    };

      const res = await axios.post(
      `${event.secrets.BI_BASE_URL}/v1/users/getuserclaims`,
      { email: event.user.email, userId: event.user.user_id, firstname:firstname , lastname:lastname }, 
      config
    );

    res.data.forEach(i => {
      api.accessToken.setCustomClaim(i.type, i.value)
      api.idToken.setCustomClaim(i.type, i.value)
    })

  }
  catch (ex) {
    console.log(ex)
    throw ex;
  }

}

};

AFTER
exports.onExecutePostLogin = async (event, api) => {
const axios = require(‘axios’);
const qs = require(“qs”);

if (event.user.email && event.client.name === ‘xxxx’) {
try {

  const firstname = event.user.firstname || event.user.given_name;
  const lastname = event.user.lastname || event.user.family_name;

  let { value: token } = api.cache.get('CachedToken') || {};

  // Check if token is available
  if (!token) {
    // get access token
    const lForm = qs.stringify({
      client_id: event.secrets.CLIENT_ID,
      client_secret: event.secrets.CLIENT_SECRET,
      grant_type: "client_credentials",
      audience: "xxxx"
    });
    const lConfig = {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
    };
    const lResponse = await axios.post(
      `${event.secrets.AUTH_BASE_URL}/oauth/token`,
      lForm,
      lConfig
    );


    if (lResponse.status != 200 && !lResponse.data) {
      throw new Error("Unable to authenticate with IdP!")
    }

    // call BI endpoint
    token = String(lResponse.data.access_token);

    const cacheResult = api.cache.set('CachedToken', token);
    if (cacheResult.type === 'error') {
      console.log("failed to set the token in the cache with error code", cacheResult.code)
    } else {
      console.log("successfully set access token in cache:")
    }
  }

  const config = {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  };

  const res = await axios.post(
    `${event.secrets.BI_BASE_URL}/v1/users/getuserclaims`,
    { email: event.user.email, userId: event.user.user_id, firstname: firstname, lastname: lastname },
    config
  );

  res.data.forEach(i => {
    api.accessToken.setCustomClaim(i.type, i.value)
    api.idToken.setCustomClaim(i.type, i.value)
  })

}
catch (ex) {
  console.log(ex)
  throw ex;
}

}
};

This code was provided to me through an Auth0 support ticket.

I checked Your code and it looks like this works for you because your Action is also your client for M2M tokens when someone logs in. In my case my own services are clients. You have to send request to ${event.secrets.BI_BASE_URL}/v1/users/getuserclaims so You need M2M token. Here you can cache before generating. This is login flow so it is different situation than mine. I have multiple APIs connecting to each other usign M2M tokens. Tokens are generated using /oauth/token endpoint but there will never be a login action. You are not caching inside onExecuteCredentialsExchange flow.

Thanks for the suggestion but it will not help in my case

Hello again!

I have created a ticket in auth0 support and got the response. Turns out this is not possible. You can cache only tokens (data in general) that was created within Action methods (by your custom code). You cannot change the default behavior of the entire flow. I will handle token caching on my side then.

Leaving this here and marking as a solution, I feel I was not the only one looking for this answer :slight_smile: