Search code examples
node.jsamazon-web-servicespuppeteeramazon-cloudwatch-synthetics

Cloudwatch Canary - Multi step API check, passing HTTP response between steps


I am attempting to set up a Cloudwatch Synthetics Canary that can Query my API with a POST action to make an Authentication attempt, return a token then do a second request using that token in the header.

My Code for the multi step Canary looks like this (removed the bit at the top that talks to secrets manager as that's irrelevant for this question):

const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const syntheticsConfiguration = synthetics.getConfiguration();


const apiCanaryBlueprint = async function () {

    const [ key, cert ] = await getKeyCert();

    syntheticsConfiguration.setConfig({
        restrictedHeaders: [], // Value of these headers will be redacted from logs and reports
        restrictedUrlParameters: [] // Values of these url parameters will be redacted from logs and reports
    });

    // Handle validation for positive scenario
    const validateSuccessful = async function(res) {
        return new Promise((resolve, reject) => {
            if (res.statusCode < 200 || res.statusCode > 299) {
                throw new Error(res.statusCode + ' ' + res.statusMessage);
            }

            let responseBody = '';
            res.on('data', (d) => {
                responseBody += d;
            });

            res.on('end', () => {
                // Add validation on 'responseBody' here if required.
                resolve();
            });
        });
    };


    // Set request option for Verify mywebsite.com
    let requestOptionsStep1 = {
        hostname: 'mywebsite.com',
        method: 'POST',
        path: '/v1/oauth/token',
        port: '443',
        protocol: 'https:',
        body: "{\n\"client_id\":\"xxx\",\n\"client_secret\":\"yyy\",\n\"audience\":\"https://mywebsite.com\",\n\"grant_type\":\"client_credentials\"\n}",
        headers: {"Content-Type":"application/json"},
        key: key,
        cert: cert
    };
    requestOptionsStep1['headers']['User-Agent'] = [synthetics.getCanaryUserAgentString(), requestOptionsStep1['headers']['User-Agent']].join(' ');

    // Set step config option for Verify mywebsite.com
   let stepConfig1 = {
        includeRequestHeaders: true,
        includeResponseHeaders: true,
        includeRequestBody: true,
        includeResponseBody: true,
        continueOnHttpStepFailure: true
    };

    await synthetics.executeHttpStep('Verify mywebsite.com', requestOptionsStep1, validateSuccessful, stepConfig1);

    // Set request option for LP Lookup mywebsite.com-2
    let requestOptionsStep2 = {
        hostname: 'mywebsite.com',
        method: 'GET',
        path: '/my/api/request/path',
        port: '443',
        protocol: 'https:',
        body: "",
        headers: {"content-type":"application/json","authorization:":"bearer: VALUE FROM FIRST REQEST"}
    };
    requestOptionsStep2['headers']['User-Agent'] = [synthetics.getCanaryUserAgentString(), requestOptionsStep2['headers']['User-Agent']].join(' ');

    // Set step config option for LP Lookup mywebsite.com-2
   let stepConfig2 = {
        includeRequestHeaders: true,
        includeResponseHeaders: true,
        includeRequestBody: true,
        includeResponseBody: true,
        continueOnHttpStepFailure: true
    };

    await synthetics.executeHttpStep('LP Lookup mywebsite.com-2', requestOptionsStep2, validateSuccessful, stepConfig2);

};

exports.handler = async () => {
    return await apiCanaryBlueprint();
};

The response body from the first query looks like:

{"access_token": "MYTOKEN", "scope": "vds rc", "expires_in": 51719, "token_type": "Bearer"}

So I basically need to get the "MYTOKEN" value and use it in my second request where I have "VALUE FROM FIRST REQEST"

Thanks in Advance


Solution

  • I was able to answer my own question so sharing here in case it helps someone else, my final code looks like this:

    const synthetics = require('Synthetics');
    const log = require('SyntheticsLogger');
    const syntheticsConfiguration = synthetics.getConfiguration();
    
    
    const apiCanaryBlueprint = async function () {
    
        const [ key, cert, clientid, clientkey ] = await getSecrets();
    
        syntheticsConfiguration.setConfig({
            restrictedHeaders: [], // Value of these headers will be redacted from logs and reports
            restrictedUrlParameters: [] // Values of these url parameters will be redacted from logs and reports
        });
    
        // Handle validation for positive scenario
        const validateSuccessful = async function(res) {
            return new Promise((resolve, reject) => {
                if (res.statusCode < 200 || res.statusCode > 299) {
                    throw new Error(res.statusCode + ' ' + res.statusMessage);
                }
    
                global.responseBody = '';
                res.on('data', (d) => {
                    responseBody += d;
                });
    
                res.on('end', () => {
                    // Add validation on 'responseBody' here if required.
                    resolve();
                });
            });
        };
    
    
        // Set request option for Verify mywebsite.com
        let requestOptionsStep1 = {
            hostname: process.env.MYWEBSITE_URL,
            method: 'POST',
            path: '/v1/oauth/token',
            port: '443',
            protocol: 'https:',
            body: `{\n\"client_id\":\"${clientid}\",\n\"client_secret\":\"${clientkey}\",\n\"audience\":\"https://mywebsite.com\",\n\"grant_type\":\"client_credentials\"\n}`,
            headers: {"Content-Type":"application/json"},
            key: key,
            cert: cert
        };
        requestOptionsStep1['headers']['User-Agent'] = [synthetics.getCanaryUserAgentString(), requestOptionsStep1['headers']['User-Agent']].join(' ');
    
        // Set step config option for Verify mywebsite.com
       let stepConfig1 = {
            includeRequestHeaders: true,
            includeResponseHeaders: true,
            includeRequestBody: true,
            includeResponseBody: true,
            continueOnHttpStepFailure: true
        };
    
        await synthetics.executeHttpStep('Verify mywebsite.com', requestOptionsStep1, validateSuccessful, stepConfig1);
    
        var jsonauthtoken = JSON.parse(responseBody)
        const authToken = jsonauthtoken.access_token;
        
        // Set request option for Verify mywebsite.com
        let requestOptionsStep2 = {
            hostname: process.env.MYWEBSITE_URL,
            method: 'GET',
            path: '/my/api/request/path',
            port: '443',
            protocol: 'https:',
            headers: {"Content-Type":"application/json", "authorization": `Bearer ${authToken}`},
            body: "",
            key: key,
            cert: cert
        };
        requestOptionsStep2['headers']['User-Agent'] = [synthetics.getCanaryUserAgentString(), requestOptionsStep2['headers']['User-Agent']].join(' ');
    
        // Set step config option for Verify mywebsite.com
        let stepConfig2 = {
            includeRequestHeaders: true,
            includeResponseHeaders: true,
            includeRequestBody: true,
            includeResponseBody: true,
            continueOnHttpStepFailure: true
        };
    
        await synthetics.executeHttpStep('QUERY mywebsite.com', requestOptionsStep2, validateSuccessful, stepConfig2);
    
    };
    
    exports.handler = async () => {
        return await apiCanaryBlueprint();
    };
    

    The most important things to note were:

    When it executes the first step It is sending it's output up to the "validateSuccessful" section so in that section I set the content of the responsebody to be a global variable

    global.responseBody = '';
    
    

    Then I was able to access it before setting the options for step 2 like this and parse the JSON:

        var jsonauthtoken = JSON.parse(responseBody)
        const authToken = jsonauthtoken.access_token;
    

    then I could use the auth token in my header for the second request:

    headers: {"Content-Type":"application/json", "authorization": `Bearer ${authToken}`},
    

    #note the backticks allow us to read the variable into our string

    and that was it. My final example has some other changes where I made use of Environment variables instead of hard coding things, hence the changes to the body section of the first request and the hostname