AWS API Gateway IAM Authorization - Generating signature using crypto.js

882 Views Asked by At

I am working on an app for Jira Cloud platform using forge framework. I created an HTTP endpoint using AWS API Gateway. This endpoint triggers a lambda function that does some operation on DynamoDB. I employed IAM authorization for the endpoint. After failing trials to use aws4 library with forge, I used the following function that is taken from AWS documentation to create signing key. However, while sending the request using javascript, I always get "{message: Forbidden}".:

export function getAWSHeaders(){
  const accessKey = ""
  const secretKey =  ""
  const regionName = "us-east-1"
  const serviceName = "execute-api"


  var date = new Date().toISOString().split('.')[0] + 'Z';
  date = date.split("-").join("").split(":").join("")
  var dateWithoutTime = date.split("T")[0]

  var myHeaders = {}
  myHeaders["X-Amz-Date"] = date;

  var crypto = require("crypto-js");

  var kDate = crypto.HmacSHA256(dateWithoutTime, "AWS4" + secretKey);
  var kRegion = crypto.HmacSHA256(regionName, kDate);
  var kService = crypto.HmacSHA256(serviceName, kRegion);
  var kSigning = crypto.HmacSHA256("aws4_request", kService);

  myHeaders["Authorization"] = "AWS4-HMAC-SHA256 Credential=" + accessKey + "/" + dateWithoutTime + "/us-east-1/execute-api/aws4_request, SignedHeaders=host;x-amz-date, Signature=" + kSigning

  return myHeaders;
}

This is how I send the request:

resolver.define("test", async ({context}) => {
  var url = ""
  var myHeaders = getAWSHeaders()
  var requestOptions = {
    method: 'GET',
    headers: myHeaders,
    redirect: 'follow'
  };

  const result = await fetch(url, requestOptions)

I cannot figure out what is wrong with my signing key generation. I checked several posts but could not find a sample request. Thanks for the help in advance.

PS: I tested it using Postman, it works with the "AWS Signature" authorization in Postman.

1

There are 1 best solutions below

1
On BEST ANSWER

Here is the file I'm using to generate SigV4 requests with node. The method 'getSignedHeaders()' is likely what you're looking for.

import crypto from 'crypto-js';
import moment from 'moment';
import axios from "axios";

const credentials = {
    accessKeyId: 'REDACTED',
    secretAccessKey: 'REDACTED'
}
const region = 'us-east-1'

const apiUrl ={
    hostname: 'REDACTED.execute-api.us-east-1.amazonaws.com',
    url: 'https://REDACTED.execute-api.us-east-1.amazonaws.com'
}

const defaultHeaders = {
    "Content-Type": "application/json",
  };

const client = axios.create({
    baseUrl: apiUrl.url,
    headers: {
        common: {
            host: apiUrl.hostname, // AWS signature V4 requires this header
        },
        post: defaultHeaders,
        put: defaultHeaders,
        patch: defaultHeaders,
    },
});

client.interceptors.request.use(async (config) => {
    // This is mainly for typescript's benefit; we expect method and url to be present
    if (!(config.method && config.url)) {
      throw new Error("Incomplete request");
    }
    // Axios somewhat annoyingly separates headers by request method.
    // We need to merge them so we can include them in the signature.
    const headers = getSignedHeaders(credentials, region, apiUrl.hostname, config)
    const outputConfig = {
      ...config,
      headers: { [config.method]: headers },
    };
    return outputConfig;
  });

client.get(apiUrl.url, {
    "query": "test query",
    "operationName": "opName",
    "variables": {
        "foo": "bar",
        "achoo": "why"
    }
}).then(res => console.log('done: ', res.data)).catch(err => console.log('errror! ', err))

export default function getSignedHeaders(credentials, region, host, axiosConfig) {
    // Task 1: Create a canonical request for Signature Version 4
    // Arrange the contents of your request (host, action, headers, etc.) into a standard (canonical) format. The canonical request is one of the inputs used to create a string to sign.
    const t = moment().utc()
    const { accessKeyId, secretAccessKey } = credentials

    const amzDate = t.format("YYYYMMDDTHHmmss") + 'Z'
    const httpRequestMethod = axiosConfig.method.toUpperCase()
    const canonicalURI = '/'
    const canonicalQueryString = ''
    const canonicalHeaders= 'host:' + host + '\n' + 'x-amz-date:' + amzDate + '\n'
    const signedHeaders = 'host;x-amz-date'
    const payload = axiosConfig.data ? JSON.stringify(axiosConfig.data) : ''
    const hashedPayload = createHash(payload)

    const canonicalRequest =
    httpRequestMethod + '\n' +
    canonicalURI + '\n' +
    canonicalQueryString + '\n' +
    canonicalHeaders + '\n' +
    signedHeaders + '\n' +
    hashedPayload

    const hashedCanonicalRequest = createHash(canonicalRequest);

    //   if you used SHA256, you will specify AWS4-HMAC-SHA256 as the signing algorithm

    // Task 2: Create a string to sign for Signature Version 4
    // Create a string to sign with the canonical request and extra information such as the algorithm, request date, credential scope, and the digest (hash) of the canonical request.
    const algorithm = 'AWS4-HMAC-SHA256'
    const requestDateTime = amzDate
    const dateStamp = t.format('YYYYMMDD') // Date w/o time, used in credential scope
    const service = 'execute-api'
    const credentialScope = dateStamp + '/' + region + '/' + service + '/' + 'aws4_request'

    const stringToSign =
        algorithm + '\n' +
        requestDateTime + '\n' +
        credentialScope + '\n' +
        hashedCanonicalRequest

    // Task 3: Calculate the signature for AWS Signature Version 4
    // Derive a signing key by performing a succession of keyed hash operations (HMAC operations) on the request date, Region, and service, with your AWS secret access key as the key for the initial hashing operation. After you derive the signing key, you then calculate the signature by performing a keyed hash operation on the string to sign. Use the derived signing key as the hash key for this operation.

    var kDate = crypto.HmacSHA256(dateStamp, "AWS4" + secretAccessKey);
    var kRegion = crypto.HmacSHA256(region, kDate);
    var kService = crypto.HmacSHA256(service, kRegion);
    var kSigning = crypto.HmacSHA256("aws4_request", kService);
    console.log('kSigning: ', crypto.enc.Hex.stringify(kSigning))

    const signature = crypto.enc.Hex.stringify(crypto.HmacSHA256(stringToSign, kSigning));

    // Task 4: Add the signature to the HTTP request
    // After you calculate the signature, add it to an HTTP header or to the query string of the request.
    const authorizationHeader = algorithm + ' Credential=' + accessKeyId + '/' + credentialScope + ', SignedHeaders=' + signedHeaders + ', Signature=' + signature

    const headers = {
        'X-Amz-Date': amzDate,
        'Authorization': authorizationHeader,
        'Host': host
    }

    return headers
}

function createHash(input) {
    return crypto.enc.Hex.stringify(crypto.SHA256(input))
}