node.jstypescriptaws-lambda

Issue Creating Signed Request in JavaScript to AWS for AWS managed Prometheus


I'm working on creating a lambda to query and dynamically stand up AWS managed Prometheus alarms. To do so, I need to make a signed http request and I'm getting this error:

""statusCode": 403, "headers": "{\n "date": "Mon, 24 Jun 2024 12:00:23 GMT",\n "content-type": "application/json",\n "content-length": "1867",\n "connection": "keep-alive",\n "x-amzn-requestid": "7a6bc218-9f69-42ac-b2f9-cfb0b0832acf, 7a6bc218-9f69-42ac-b2f9-cfb0b0832acf",\n "server": "amazon",\n "x-amzn-errortype": "InvalidSignatureException"\n}", "body": "{"message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\n\nThe Canonical String for this request should have been"

The logging is very verbose and I'll remove the sensitive logging once I move outside my test env.I'm also using a custom logging lib so you can ignore that part. I thought I had this configured correctly but now I'm getting stuck.

That being said, am I formatting this request wrong? Any ideas?

Here my code:

import {SignatureV4} from '@smithy/signature-v4';
import {defaultProvider} from '@aws-sdk/credential-provider-node';
import {HttpRequest} from '@smithy/protocol-http';
import {Sha256} from '@aws-crypto/sha256-browser';


const makeSignedRequest = async (
  path: string,
  region: string
): Promise<any> => {
  const hostname = `aps-workspaces.${region}.amazonaws.com`;
  const request = new HttpRequest({
    method: 'GET',
    protocol: 'https:',
    hostname,
    path,
    headers: {
      'Content-Type': 'application/json',
      Host: hostname, // Explicitly adding the Host header
    },
  });
  
  request.headers.Host = hostname; // Explicitly adding the Host header
  log
    .info()
    .str('initialRequest', JSON.stringify(request, null, 2))
    .msg('Initial request object');
  const signer = new SignatureV4({
    credentials: defaultProvider(),
    region,
    service: 'aps',
    sha256: Sha256,
  });
  const signedRequest = await signer.sign(request);
  log
    .info()
    .str(
      'signedRequest',
      JSON.stringify(
        {
          method: signedRequest.method,
          protocol: signedRequest.protocol,
          hostname: signedRequest.hostname,
          path: signedRequest.path,
          headers: signedRequest.headers,
        },
        null,
        2
      )
    )
    .msg('Signed request object');
  return new Promise((resolve, reject) => {
    const req = https.request(
      {
        hostname: signedRequest.hostname,
        path: signedRequest.path,
        method: signedRequest.method,
        headers: signedRequest.headers,
      },
      res => {
        let data = '';
        res.on('data', chunk => {
          data += chunk;
        });
        res.on('end', () => {
          log
            .info()
            .num('statusCode', res.statusCode || 0)
            .str('headers', JSON.stringify(res.headers, null, 2))
            .str('body', data)
            .msg('Response received');
          try {
            const parsedData = JSON.parse(data);
            resolve(parsedData);
          } catch (error) {
            log
              .error()
              .err(error)
              .str('rawData', data)
              .msg('Failed to parse response data');
            reject(error);
          }
        });
      }
    );
    req.on('error', error => {
      log.error().err(error).msg('Request error');
      reject(error);
    });
    req.end();
  });
};const makeSignedRequest = async (
  path: string,
  region: string
): Promise<any> => {
  const hostname = `aps-workspaces.${region}.amazonaws.com`;
  const request = new HttpRequest({
    method: 'GET',
    protocol: 'https:',
    hostname,
    path,
    headers: {
      'Content-Type': 'application/json',
      Host: hostname, 
    },
  });

 
  request.headers.Host = hostname; 

  log
    .info()
    .str('initialRequest', JSON.stringify(request, null, 2))
    .msg('Initial request object');

  const signer = new SignatureV4({
    credentials: defaultProvider(),
    region,
    service: 'aps',
    sha256: Sha256,
  });

  const signedRequest = await signer.sign(request);

  log
    .info()
    .str(
      'signedRequest',
      JSON.stringify(
        {
          method: signedRequest.method,
          protocol: signedRequest.protocol,
          hostname: signedRequest.hostname,
          path: signedRequest.path,
          headers: signedRequest.headers,
        },
        null,
        2
      )
    )
    .msg('Signed request object');

  return new Promise((resolve, reject) => {
    const req = https.request(
      {
        hostname: signedRequest.hostname,
        path: signedRequest.path,
        method: signedRequest.method,
        headers: signedRequest.headers,
      },
      res => {
        let data = '';
        res.on('data', chunk => {
          data += chunk;
        });
        res.on('end', () => {
          log
            .info()
            .num('statusCode', res.statusCode || 0)
            .str('headers', JSON.stringify(res.headers, null, 2))
            .str('body', data)
            .msg('Response received');

          try {
            const parsedData = JSON.parse(data);
            resolve(parsedData);
          } catch (error) {
            log
              .error()
              .err(error)
              .str('rawData', data)
              .msg('Failed to parse response data');
            reject(error);
          }
        });
      }
    );

    req.on('error', error => {
      log.error().err(error).msg('Request error');
      reject(error);
    });

    req.end();
  });
};

The signed request should be pulling the correct creds and signing the req but for the life of me I can't see where I'm going wrong.


Solution

  • So after doing some further digging, found some new code examples from AWS dating back 3 months ago—fairly recent.

    https://github.com/aws-samples/sigv4-signing-examples/blob/main/sdk/nodejs/main.js

    I made some easy adjustments to my code and was able to successfully sign the request. Here's what I did, for posterity:

    import * as aws4 from 'aws4';
    import {defaultProvider} from '@aws-sdk/credential-provider-node';
    
    const makeSignedRequest = async (
      path: string,
      region: string
    ): Promise<any> => {
      const hostname = `aps-workspaces.${region}.amazonaws.com`;
    
      // Fetch credentials using the default provider
      const credentials = await defaultProvider()();
    
      // Define the request options
      const options = {
        hostname,
        path,
        method: 'GET',
        headers: {
          host: hostname,
          'Content-Type': 'application/json',
        },
      };
    
      // Sign the request using aws4
      const signer = aws4.sign(
        {
          service: 'aps',
          region: region,
          path: path,
          headers: options.headers,
          method: options.method,
          body: '',
        },
        {
          accessKeyId: credentials.accessKeyId,
          secretAccessKey: credentials.secretAccessKey,
          sessionToken: credentials.sessionToken,
        }
      );
    
      // Add signed headers to the request options
      Object.assign(options.headers, signer.headers);
    
      return new Promise((resolve, reject) => {
        const req = https.request(options, res => {
          let data = '';
          res.on('data', chunk => {
            data += chunk;
          });
          res.on('end', () => {
            log
              .info()
              .num('statusCode', res.statusCode || 0)
              .str('headers', JSON.stringify(res.headers, null, 2))
              .str('body', data)
              .msg('Response received');
    
            try {
              const parsedData = JSON.parse(data);
              resolve(parsedData);
            } catch (error) {
              log
                .error()
                .err(error)
                .str('rawData', data)
                .msg('Failed to parse response data');
              reject(error);
            }
          });
        });
    
        req.on('error', error => {
          log.error().err(error).msg('Request error');
          reject(error);
        });
    
        req.end();
      });
    };