Signing AWS requests with Signature Version 4 within a Pre-Flight Script

Hi!

I have an AppSync API and I am tried of using the AppSync console because it is quite lacking in features and it is VERY buggy.

I am happy to have found Apollo Studio! Pretty awesome so far.

My API has 3 auth modes:

  • IAM (admin all access)
  • API Key (public access)
  • OIDC (Users JWTs)

API Key and OIDC work great out of the box. But I am having trouble with IAM auth.

I found out about Preflight scripts and thought it would be the perfect place to sign my requests and create the relevant header data needed to authorize my requests. I noticed it even comes with CryptoJS! Perfect!

I wrote a small script in node to test out the mechanisms to sign my query with crypto-js and was successful.

However, when I ported that code into the pre-flight script, I was not able to query my API. AWS tells me there is something wrong my signature.

I thought someone might help me out here.

Here is my node code that works:

const crypto = require("crypto-js");
const moment = require("moment");
const axios = require("axios");

const apiUrl =
  "https://<MY_API>.appsync-api.ca-central-1.amazonaws.com/graphql";

const endpoint = new URL(apiUrl).hostname.toString();

const request = `mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
    }
  }`;
async function send() {
  const access_key = "";
  const secret_key = "";
  const method = "POST";
  const service = "appsync";
  const host = endpoint;
  const region = "ca-central-1";
  const base = "https://";
  const content_type = "application/json";
  const canonical_uri = "/graphql";

  function getSignatureKey(key, dateStamp, regionName, serviceName) {
    var kDate = crypto.HmacSHA256(dateStamp, "AWS4" + key);
    var kRegion = crypto.HmacSHA256(regionName, kDate);
    var kService = crypto.HmacSHA256(serviceName, kRegion);
    var kSigning = crypto.HmacSHA256("aws4_request", kService);
    return kSigning;
  }

  const amz_date = moment().utc().format("yyyyMMDDTHHmmss") + "Z";
  const date_stamp = moment().utc().format("yyyyMMDD");

  const canonical_querystring = "";

  const request_parameters = JSON.stringify({
    query: request,
    variables: { input: {} },
  });
  const payload_hash = crypto.SHA256(request_parameters);

  const canonical_headers =
    ":authority:" +
    host +
    "\n" +
    "x-amz-content-sha256:" +
    payload_hash +
    "\n" +
    "x-amz-date:" +
    amz_date +
    "\n";
  const signed_headers = ":authority;x-amz-content-sha256;x-amz-date";

  const fs = require("fs");

  const canonical_request =
    method +
    "\n" +
    canonical_uri +
    "\n" +
    canonical_querystring +
    "\n" +
    canonical_headers +
    "\n" +
    signed_headers +
    "\n" +
    payload_hash;

  fs.writeFileSync("canonical_request.txt", canonical_request);

  const algorithm = "AWS4-HMAC-SHA256";
  const credential_scope =
    date_stamp + "/" + region + "/" + service + "/" + "aws4_request";
  const string_to_sign =
    algorithm +
    "\n" +
    amz_date +
    "\n" +
    credential_scope +
    "\n" +
    crypto.SHA256(canonical_request);

  console.log(string_to_sign);

  const signing_key = getSignatureKey(secret_key, date_stamp, region, service);
  const signature = crypto.HmacSHA256(string_to_sign, signing_key);
  const authorization_header =
    algorithm +
    " " +
    "Credential=" +
    access_key +
    "/" +
    credential_scope +
    ", " +
    "SignedHeaders=" +
    signed_headers +
    ", " +
    "Signature=" +
    signature;

  const headers = {
    "X-Amz-Content-Sha256": payload_hash.toString(),
    "X-Amz-Date": amz_date,
    Authorization: authorization_header,
    "Content-Type": content_type,
  };
  const response = await axios({
    method: method,
    baseURL: base + host,
    url: canonical_uri,
    data: request_parameters,
    headers: headers,
  });
}

send()

My ported pre-flight script:

const crypto = explorer.CryptoJS


function getAmzDate() {
const utcYear = new Date().getUTCFullYear()
// const utcMonth = new Date().getUTCMonth()
const utcMonth = 11
const utcDay = new Date().getUTCDate()
const utcHour = new Date().getUTCHours()
const utcMinutes = new Date().getUTCMinutes()
const utcSeconds = new Date().getUTCSeconds()

const amz_date_v = `${utcYear}${utcMonth}${utcDay}T${utcHour}${utcMinutes}${utcSeconds}Z`
return amz_date_v
}

function setRequestHeaders() {
  const access_key = "";
  const secret_key = ""
  const method = "POST";
  const service = "appsync";
  const host = "<MY_API>.ca-central-1.amazonaws.com";
  const region = "ca-central-1";
  const canonical_uri = "/graphql";

  function getSignatureKey(key, dateStamp, regionName, serviceName) {
    console.log(key, dateStamp, regionName, serviceName)
    var kDate = crypto.HmacSHA256(dateStamp, "AWS4" + key);
    var kRegion = crypto.HmacSHA256(regionName, kDate);
    var kService = crypto.HmacSHA256(serviceName, kRegion);
    var kSigning = crypto.HmacSHA256("aws4_request", kService);
    return kSigning;
  }

  const amz_date = "20221111T010829Z" //temp until getAmzDate is fixed
  const date_stamp = "20221111"

  const canonical_querystring = "";

  const request_parameters = JSON.stringify({
    query: explorer.request.body.query,
    variables: explorer.request.body.variables,
  });
  const payload_hash = crypto.SHA256(request_parameters);

  const canonical_headers =
    ":authority" +
    host +
    "\n" +
    "x-amz-content-sha256:" +
    payload_hash +
    "\n" +
    "x-amz-date:" +
    amz_date +
    "\n";

  const signed_headers = ":authority;x-amz-content-sha256;x-amz-date";

  const canonical_request =
    method +
    "\n" +
    canonical_uri +
    "\n" +
    canonical_querystring +
    "\n" +
    canonical_headers +
    "\n" +
    signed_headers +
    "\n" +
    payload_hash;

  const algorithm = "AWS4-HMAC-SHA256";
  const credential_scope =
    date_stamp + "/" + region + "/" + service + "/" + "aws4_request";
  const string_to_sign =
    algorithm +
    "\n" +
    amz_date +
    "\n" +
    credential_scope +
    "\n" +
    crypto.SHA256(canonical_request);

  const signing_key = getSignatureKey(secret_key, date_stamp, region, service);

  const signature = crypto.HmacSHA256(string_to_sign, signing_key);

  const authorization_header =
    algorithm +
    " " +
    "Credential=" +
    access_key +
    "/" +
    credential_scope +
    ", " +
    "SignedHeaders=" +
    signed_headers +
    ", " +
    "Signature=" +
    signature;

  explorer.environment.set('authorization', authorization_header)
  explorer.environment.set('date', amz_date)
  explorer.environment.set('sha', payload_hash.toString())
}

setRequestHeaders()

Hi Alex,
sorry, I don’t have answer to your question, but what catch my eyes is:

Can I ask you how do you integrate with OIDC SSO server? Do you integrate somehow complete login flow with access token obtaining and refresh? I was not able to achieve this, I was only able to use JWT access token copied manually from the SSO server and put in Authorization header, which is not optimal/user friendly.