Caching M2M access token in Actions

Hello, as it has already announced in this blog post Actions Caching Is Now Available, I was trying to create and integrate an Action to M2M flow which will cache the access token. Either fetch the cached token if available or create a new token.
I was following the post Caching Management API Access Tokens. In this post caching was done as post login step in onExecutePostLogin. But for M2M communication I was trying inside onExecuteCredentialsExchange. But I am bit confused which api method should be called in case of reading token from the cache or creating a new token.
Besides when I added the action inside the M2M flow it gived me some error:

  1. Error on credentials-exchange: 500 Script execution time exceeded
  2. Error on credentials-exchange: ETIMEDOUT Did you forgot to call the callback?

This is what I added in the action:

exports.onExecuteCredentialsExchange = async (event, api) => {
  const ManagementClient = require('auth0').ManagementClient;
  const jwt_decode = require('jwt-decode');

  //get current value for 'my-api-token'
  const record = api.cache.get('my-api-token');
  console.log("Record: ", record);
  let token = record?.value;
  let current_time = Date.now().valueOf() / 1000;

  console.log("Cached token: ", token);
  if (token != undefined) {
    var decoded = jwt_decode(token);
  }

  //Check if cached token exists/is not undefined and not expired
  if (token != undefined && decoded.exp > current_time) {
    //Initialize management client with existing token to use against Management API
    const managementWithOldToken = new ManagementClient({
      token: token,
      domain: event.secrets.domain,
    });
  
  } else if (token == undefined || decoded.exp < current_time) {
    //Initialize management client with credentials
    const management = new ManagementClient({
      domain: event.secrets.domain,
      clientId: event.secrets.clientID,
      clientSecret: event.secrets.clientSecret,
      audience: event.secrets.audience,
      tokenProvider: {
        enableCache: true,
        cacheTTLInSeconds: 86400
      }
    });

    //Get new access token and set it in cache
    const newToken = await management.getAccessToken();
    const result = api.cache.set('my-api-token', newToken);
    if (result.type === 'error') {
      console.log("failed to set the token in the cache with error code", result.code)      
    } else {
      console.log("successfully set access token in cache")
    }
      
    //Initialize management client with new token to use against Management API
    const managementWithNewToken = new ManagementClient({
      token: newToken,
      domain: event.secrets.domain,
    });      
  }
};

When I run the action it logs the successful creation of token but when i test second time it returns undefined, meaning nothing is returned from cache.

Would highly appreciate any help/information. Thanks.

1 Like

Hi @saiful1991,

Welcome to the Auth0 Community!

I have tested this myself and found that the Access Token value is too large, which is why you are experiencing the undefined error.

This is consistent with our Actions Limitations documentation, which states that Cache keys have a maximum size of 64 bytes and values have a maximum size of 2048 bytes.

Investigating further, I found that the Access Token is 4234 bytes which exceed the maximum size allowed for a value.

I am going to look further into this with my colleagues to get a second opinion.

I will follow up with you as soon as I can.

Thanks,
Rueben

Hi @saiful1991,

Thank you for your patience.

After carefully going through our Action Limitations documentation, I noticed that it makes note that the cumulative size of cached keys and their values must not exceed 8192 bytes.

Given that, one way to make this work is to use a few cache (key, value) pairs to store the Management API Token.

Because Cache Keys are limited to 2048 bytes, we can make this work with 3 Cache items.

I have tested this myself and got it to work with the following:

    const management = new ManagementClient({
      domain: event.secrets.domain,
      clientId: event.secrets.clientID,
      clientSecret: event.secrets.clientSecret,
      audience: event.secrets.audience,
      tokenProvider: {
        enableCache: true,
        cacheTTLInSeconds: 86400
      }
    });

    //Get new access token and set it in cache
    const newToken = await management.getAccessToken();
    api.cache.set('first',newToken.slice(0,2048))
    api.cache.set('second', newToken.slice(2048,4096))
    api.cache.set('third', newToken.slice(4096))
    
    //Get the new Cached Token
    const getNewToken = api.cache.get('first').value + api.cache.get('second').value + api.cache.get('third').value
    console.log(getNewToken)

I hope this helps!

Please reach out again if you have any additional questions.

Thank you,
Rueben

Hi @rueben.tiow,

Thank you for your response and detailed explanation.

I have tested your solution. It stores and fetch cached token successfully. However there is another use-case scenario I tried and faced unexpected behaviour. According to the Actions Limitations documentation cached items should persist upto 15 minutes. Now let’s say I have already cached a token which should be cached upto 15 minutes. Now if I make a second call winthin 15 minutes I should got back the cached key. I tested this scenario but I got undefined. Here is what I tried:

const ManagementClient = require('auth0').ManagementClient;
  
  // check if there is any old cached token < 15 minutes
  // because I would like to stop creating a token if
  // there is already a cached token (with in 15 minutes)
  console.log("First token: ", api.cache.get('first')?.value);
  console.log("Second token: ", api.cache.get('second')?.value);
  console.log("Third token: ", api.cache.get('third')?.value);

  // calling managemnt api for a new token
  const management = new ManagementClient({
      domain: event.secrets.domain,
      clientId: event.secrets.clientID,
      clientSecret: event.secrets.clientSecret,
      audience: event.secrets.audience,
      tokenProvider: {
        enableCache: true,
        cacheTTLInSeconds: 86400
      }
    });

    //Get new access token and set it in cache
    const newToken = await management.getAccessToken();
    api.cache.set('first',newToken.slice(0,2048))
    api.cache.set('second', newToken.slice(2048,4096))
    api.cache.set('third', newToken.slice(4096))
    
    //Get the new Cached Token
    // @ts-ignore
    const getNewToken = api.cache.get('first').value + api.cache.get('second').value + api.cache.get('third').value
    console.log(getNewToken);

I got the follwing return:

First token:  undefined
Second token:  undefined
Third token:  undefined

FYI: I tried this in the Test feature in the Actions editor window. Which I don’t really know if it runs a new instance every time we press the run button.

Thanks in advance.

Hi @saiful1991,

Thank you for your response.

I went ahead and tested the script in an actual flow, and it works as expected and persists the cache items for 15 minutes (checked the expires_at value).

It seems that the Actions built-in debugger is unable to simulate this behavior. However, in an Actual flow, I could get the cached items without issues.

With that, I can confirm that the script works successfully.

Could you please try the script in a genuine M2M flow and let me know how it goes?

Thanks,
Rueben

Hi @rueben.tiow,

Thank you very much for your reply.

Here is what I tried:

  1. I created an API which is able to retreive access token successfully using CLientID and ClientSecret (Client Credentials Flow)
  2. I added this Action(which I mentioned earlier) to either retreive the cached token or new token. But when I request for an access token from my API, it waits about 15 to 20 seconds and returns 500 error. I found the following error: Error on credentials-exchange: 500 Script execution time exceeded in the log, and my log is bloated with error messages. It seems my API request created a loop of triggers.

Based on the trial, I would to ask the following questions:

  1. Any additional parameter needed to be added on request header while added an Action to the flow?
  2. Action stays between the Machine-to-Machine flow: Token Requested → Action → Token Issued. If an access token is created in the Action using ManagementClient, then how and what an Action should trigger as a response? Which API Object should be triggered in this case?

Thank you in advance.

1 Like

Hi @saiful1991,

Thank you for your reply.

Yes, the execution timeout exceeded error can happen if your execution flow takes more than 20 seconds to run. See below:

(Reference: Action limitations)

No, there is no need for additional parameters in the request header.

I also recommend using the Real-time Webtask Logs Extension to debug your Action scripts if they are exceeding 20 seconds.

Could you please clarify what you would like to do with your cached token?

Note that the access token created in the Action is not the same token issued to the application performing the Client Credentials flow.

I look forward to your reply.

Thanks,
Rueben

Hi @rueben.tiow,

Thank you for your response.

Based on your reply, I conclude with the decision that I had a misunderstanding of access token created in Action with regard to token for Client Credential Flow. I intend to follow a different approach for our business use-case.

Thanks,
Saiful

1 Like

Hey there,

I just ran into the exact same issue with access token being too large to fit into the cache. Rueben’s solution works to scatter and gather the access token. But it brings up more friction on the usage and maintenance.

I’m wondering if this could raise the question on the cache size limitation. I’m no expert here, but based on our use case, access token is the only usage of the api cache. In addition, when launch the actions cache, access token is also the example usage.

Thanks

Hi @di1,

I understand that this may cause more friction to use and maintain, but these are the current recommendation.

Regarding the Cache Size Limitation, I will try my best to find out from our Engineers if there are any thoughts about changing the cache size.

Thanks,
Rueben

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.

Hi all.

The M2M client used for management API should have limited scopes to fit into cache value limit. Normally just read:users and update:users is enough to perform typical search/link activities in Actions, and that access_token does fit in 2k.

Here is an example for your reference showing cache/search/link operations in one Action.

exports.onExecutePostLogin = async (event, api) => {

    console.log('account-link action user: ', event?.user?.email);

    const {ManagementClient, AuthenticationClient} = require('auth0');

    if (event?.user?.email_verified !== true) { // no linking if email is not verified
        return;
    }

    /*
      if (event.user.identities.length > 1) { // no linking if user is already linked
          return;
      }
      */

    const {domain} = event.secrets || {};

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

    if (!token) {
        const {clientId, clientSecret} = event.secrets || {};

        const cc = new AuthenticationClient({domain, clientId, clientSecret});

        try {
            const {data} = await cc.oauth.clientCredentialsGrant({audience: `https://${domain}/api/v2/`});

            token = data?.access_token;

            if (!token) {
                console.log('failed get api v2 cc token');
                return;
            }
            console.log('cache MIS!');

            const result = api.cache.set('management-token', token, {ttl: data.expires_in * 1000});

            if (result?.type === 'error') {
                console.log('failed to set the token in the cache with error code', result.code);
            }
        } catch (err) {
            console.log('failed calling cc grant', err);
            return;
        }
    }

    // This will make an Authentication API call
    const client = new ManagementClient({domain, token});

    // Search for other candidate users
    const {data: candidateUsers} = await client.usersByEmail.getByEmail({email: event?.user?.email});

    if (!Array.isArray(candidateUsers) || !candidateUsers.length) { // didn't find anything
        return;
    }

    const firstCandidate = candidateUsers.find((c) =>
        c.user_id !== event.user.user_id &&         // not the current user
        //c.identities[0].provider === "auth0" &&   // DB user
        c.email_verified                            // make sure email is verified
    );

    if (!firstCandidate) { // didn't find any other user with the same email other than ourselves
        return;
    }

    const primaryChanged = firstCandidate.provider === 'auth0';

    let primaryUserId, secondaryProvider, secondaryUserId, primaryCustomerId, secondaryCustomerId;

    if (primaryChanged) {
        primaryUserId = firstCandidate.user_id;
        secondaryProvider = event.user.identities[0].provider;
        secondaryUserId = event.user.identities[0].user_id;

        primaryCustomerId = firstCandidate.app_metadata.customer_id;
        secondaryCustomerId = event.user.app_metadata.customer_id;
    } else {
        primaryUserId = event.user.user_id;
        secondaryProvider = firstCandidate.identities[0].provider;
        secondaryUserId = firstCandidate.user_id;

        primaryCustomerId = event.user.app_metadata?.customer_id;
        secondaryCustomerId = firstCandidate.app_metadata?.customer_id;
    }

    try {
        await client.users.link({id: primaryUserId}, {provider: secondaryProvider, user_id: secondaryUserId});
    } catch (err) {
        console.log('unable to link, no changes');
        return;
    }

    // -- customer_id(s) logic --
    if (secondaryCustomerId) {   // we have a secondary customer id, time to do some merge
        if (primaryCustomerId) { // both customer_ids remain and merge
            api.user.setAppMetadata('customer_id', [primaryCustomerId, secondaryCustomerId]);
        } else {
            api.user.setAppMetadata('customer_id', secondaryCustomerId);
        }
    }

    if (primaryChanged) api.authentication.setPrimaryUser(primaryUserId);

};

1 Like