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:
Error on credentials-exchange: 500 Script execution time exceeded
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.
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.
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 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.
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?
I created an API which is able to retreive access token successfully using CLientID and ClientSecret (Client Credentials Flow)
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:
Any additional parameter needed to be added on request header while added an Action to the flow?
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?
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.
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.
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.
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);
};