There are many cases where you need an access token for an integration API. Since access tokens usually have a limited time span you will need to refresh those tokens occasionally. Depending on the API configuration you may be able to simply generate a new access token for each new Conversation. However, if that is not feasible you are left with the option to manually update the token or to use Cognigy Functions to automatically refresh it, this article will show you how.
Timeouts
There are a few things regarding timing that you must keep in mind:
- Functions have an execution time-limit, the default value is 15 minutes (900 seconds)
- Every Function will take some time to fully execute, how long exactly depends on the code
- The access token lifetime is probably longer than the Function execution time-limit
Based on these points the Function will have to re-trigger itself until the access token refresh time is within the current Function execution time-limit. Below you can see the life cycle:
Preparing the Endpoint
For this example, we will use a customized REST Endpoint that is storing the access token for channel integration API calls, you can find the REST Endpoint Transformer code below:
const INITIAL_TOKEN = {
"token": {
"access_token": "asdf",
"refresh_token": "asdf",
"expires_in": 2591999
},
"timestamp": 1625216009
}
const ENDPOINT_SECRET = "testTEST"
createRestTransformer({
/**
* This transformer is executed when receiving a message
* from the user, before executing the Flow.
*
* @param endpoint The configuration object for the used Endpoint.
* @param request The Express request object with a JSON parsed body.
* @param response The Express response object.
*
* @returns A valid userId, sessionId, as well as text and/or data,
* which has been extracted from the request body.
*/
handleInput: async ({ endpoint, request, response }) => {
/**
* Extract the userId, sessionId and text from the request. Example:
*
* const { userId, sessionId, text, data } = request.body;
*
* Note that the format of the request body will be different for
* every Endpoint, and the example above needs to be adjusted
* accordingly.
*
* If you return undefined for any of the variables below,
* the already computed value will be used.
*/
const accessTokenStorage = await getSessionStorage("accessToken","accessToken")
if (!accessTokenStorage.token) {
//first time initialization
console.log("initializing access token")
accessTokenStorage.token = INITIAL_TOKEN.token
accessTokenStorage.timestamp = INITIAL_TOKEN.timestamp
}
if (request.body.token &&
request.body.timestamp &&
request.body.endpointSecret == ENDPOINT_SECRET) {
//valid access token update
accessTokenStorage.token = request.body.token
accessTokenStorage.timestamp = request.body.timestamp
console.log("updating access token " + JSON.stringify(accessTokenStorage.token) + ", timestamp: " + accessTokenStorage.timestamp)
response.sendStatus(200)
return null
}
console.log("access token: " + accessTokenStorage.token.access_token)
console.log("refresh token: " + accessTokenStorage.token.refresh_token)
//this uses the default behavior if we don't receive the special POST request for the access token update
const userId = undefined;
const sessionId = undefined;
const text = undefined;
const data = undefined;
/**
* Return a valid object or a falsy value (e.g. null) to stop execution
*/
return {
userId,
sessionId,
text,
data
};
},
/**
* This transformer is executed on every output from the Flow.
*
* @param output The raw output from the Flow. It is possible to manipulate
* and return every distinct output before they get formatted in the 'handleExecutionFinished'
* transformer.
*
* @param endpoint The configuration object for the used Endpoint.
* @param userId The unique ID of the user.
* @param sessionId The unique ID for this session. Can be used together with the userId
* to retrieve the sessionStorage object.
*
* @returns The output that will be formatted into the final response in the 'handleExecutionFinished' transformer.
*/
handleOutput: async ({ output, endpoint, userId, sessionId }) => {
return output;
},
/**
* This transformer is executed when the Flow execution has finished.
* For REST based transformers, the final output will be sent to
* the user.
*
* @param processedOutput This is the final object that will be sent to the user.
* It is therefore structured according to the Endpoint channel of the transformer.
*
* @param outputs This is an array of all of the outputs that were output by the Flow.
* These will be merged to create the 'processedOutput' object.
*
* @param userId The unique ID of the user.
* @param sessionId The unique ID for this session. Can be used together with the userId
* to retrieve the sessionStorage object.
* @param endpoint The configuration object for the used Endpoint.
* @param response The express response object that can be used to send a custom response back to the user.
*
* @returns An object that will be sent to the user, unchanged. It therefore has to have the
* correct format according to the documentation of the specific Endpoint channel.
*/
handleExecutionFinished: async ({ processedOutput, outputs, userId, sessionId, endpoint, response }) => {
return processedOutput;
}
});
As you can see in the example REST Transformer, we are checking whether the payload has the refreshed access token together with the timestamp and the Endpoint secret (to prevent unauthorized token refresh) and if so, we replace the access token in the access token storage. By using “response.sendStatus” and “return null” we respond that the request was correct and prevent the Flow Execution.
The code can also be added to the Inject/Notify Transformer for Endpoints that are not based on REST, the principle stays the same.
The Function
For your function you will need to gather two prerequisites:
- A valid Cognigy API key for re-triggering the Function. You can generate one in your profile, as long as you have the necessary rights
- The Function ID. This can be found in the URL once you have created your Function:
Below you can find the whole Function code with inline comments that should explain each step:
const API_KEY = "YOUR_COGNIGY_API_KEY"
//THIS NEEDS TO USE THE ID THAT IS IN THE FUNCTION URL (https://trial.cognigy.ai/agent/600ae018ae44e40b645d3e43/60bf558bf236024a1d027a4f/function/HERE_IS_THE_ID/editor)
const FUNCTION_ID = "URL_FUNCTION_ID_OF_THIS_FUNCTION"
//a value of 90 means that we refresh the token when it is at 90% of its lifetime, it should always be below 100
const TOKEN_REFRESH_PERCENT = 90
//the function time limit in seconds on this environment, default is 15 minutes or 900 seconds
const FUNCTION_TIME_LIMIT = 900
//the number of seconds to have as a buffer for the Function execution
const FUNCTION_BUFFER = 5
export default async ({ parameters, api }: IFunctionExecutionArguments) => {
const {
token,
//waitingTime is in milliseconds
waitingTime
} = parameters;
if (waitingTime >= ((FUNCTION_TIME_LIMIT + FUNCTION_BUFFER) * 1000)) {
//no need for token refresh in this function, wait and retrigger Function
const waitMilliseconds = (FUNCTION_TIME_LIMIT - FUNCTION_BUFFER) * 1000
const waiter = await new Promise(r => setTimeout(r, waitMilliseconds))
const response = await api.httpRequest({
method: "post",
url: "https://api-trial.cognigy.ai/new/v2.0/functions/" + FUNCTION_ID + "/trigger",
headers: {
"Accept": "application/json",
"X-API-Key": API_KEY,
"Content-Type": "application/json"
},
data: {
"parameters": {
"token": token,
"waitingTime": waitingTime - (FUNCTION_TIME_LIMIT * 1000)
}
}
})
} else {
//save current timestamp
const beforeTimestamp = moment(new Date()).unix()
//refresh Token mockup request
//THIS NEEDS TO BE COMPLETELY REPLACED WITH THE PROPER REFRESH TOKEN API CALL
const tokenResponse = await api.httpRequest({
method: "post",
url: "YOUR_REFRESH_TOKEN_URL",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
}
})
//update Endpoint request
const updateEndpointResponse = await api.httpRequest({
method: "post",
//PUT YOUR REST ENDPOINT OR INJECT/NOTIFY API CALL HERE
url: "REST_ENDPOINT_OR_INJECT/NOTIFY_API_URL",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
data: {
"token": tokenResponse.data.token,
"timestamp": beforeTimestamp,
"endpointSecret": "testTEST"
}
})
const afterTimestamp = moment(new Date()).unix()
//calculate waiting time: (seconds) * percent * 10 = (seconds) * 1000 * percentage
const waitMilliseconds = (beforeTimestamp - afterTimestamp + token.expires_in) * TOKEN_REFRESH_PERCENT * 10
console.log("waitingTime: " + waitMilliseconds)
const response = await api.httpRequest({
method: "post",
url: "https://api-trial.cognigy.ai/new/v2.0/functions/" + FUNCTION_ID + "/trigger",
headers: {
"Accept": "application/json",
"X-API-Key": API_KEY,
"Content-Type": "application/json"
},
data: {
"parameters": {
"token": tokenResponse.data.token,
"waitingTime": waitMilliseconds
}
}
})
}
}
Comments
0 comments