Skip to content

Instantly share code, notes, and snippets.

@agutoli
Last active October 10, 2024 23:35
Show Gist options
  • Save agutoli/28b1d134bddeb42600032861c1b7feb6 to your computer and use it in GitHub Desktop.
Save agutoli/28b1d134bddeb42600032861c1b7feb6 to your computer and use it in GitHub Desktop.
AWS Signature Version 4 in Google Apps Script

AWS Signature Version 4 in Google Apps Script

This Gist provides a Google Apps Script implementation of AWS Signature Version 4 (AWS SigV4) for signing API requests to AWS services. The script includes functions for creating canonical requests, signing them with HMAC-SHA256, and generating the necessary authorization headers to authenticate requests to services like API Gateway (execute-api).

The code is organized within a namespace (AWS.SignatureV4), making it reusable and modular. It allows you to sign requests by passing your AWS credentials (accessKeyId and secretAccessKey) along with request options (method, host, path, region, service).

Key Features:

  • Fully compliant with AWS Signature Version 4 process.
  • Supports GET requests with empty payload (can be extended for POST/PUT).
  • Calculates canonical request, string-to-sign, and signature.
  • Includes HMAC-SHA256 signing for secure API request authentication.
  • Easily integrates with Google Apps Script's UrlFetchApp for making signed requests.

Example Usage:

function main() {
  const path = '/v1/my-api'; // Replace with your API endpoint path
  const host = 'api.example.com'; // Replace with your API Gateway host
  const endpoint = `https://${host}${path}`;

  const opts = {
    host,
    path,
    method: "GET",
    service: "execute-api", // AWS service name
    region: "ap-southeast-2", // AWS region
  };

  AWS.SignatureV4.sign(opts, {
    accessKeyId: 'FAKEACCESSKEYID12345', // Replace with your AWS Access Key ID
    secretAccessKey: 'FAKESECRETACCESSKEY67890' // Replace with your AWS Secret Access Key
  });

  const response = UrlFetchApp.fetch(endpoint, opts);
  Logger.log(response.getContentText());
}

How It Works:

  1. Canonical Request: The script constructs a canonical request based on the HTTP method, path, and headers.
  2. String to Sign: This string includes the AWS algorithm, request date, credential scope, and hash of the canonical request.
  3. Signature Generation: The string to sign is hashed with a derived key, resulting in the signature that is added to the request headers.

This code is especially useful for integrating Google Apps Script with AWS API Gateway or other AWS services that require SigV4 signed requests.

Feel free to use and modify as needed!

// @see https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
var AWS = AWS || {}; // Create the namespace
AWS.SignatureV4 = {
/**
* Sign the request using AWS Signature Version 4
* @param {Object} opts - Request options containing method, host, path, region, and service
* @param {Object} credentials - AWS credentials containing accessKeyId and secretAccessKey
*/
sign: function(opts, credentials) {
const payload = ""; // Empty payload for GET request (signing is required for any method)
const now = new Date();
const amzDate = Utilities.formatDate(now, "UTC", "yyyyMMdd'T'HHmmss'Z'"); // Full timestamp for x-amz-date
const dateStamp = Utilities.formatDate(now, "UTC", "yyyyMMdd"); // Date in YYYYMMDD format for signing key
// Step 1: Create the canonical request
const canonicalUri = opts.path; // URI encoded path (e.g., /v1/my-api)
const canonicalQuerystring = ""; // No query parameters for this request
const canonicalHeaders =
"host:" + opts.host + "\n" + "x-amz-date:" + amzDate + "\n"; // Required headers: Host and x-amz-date
const signedHeaders = "host;x-amz-date"; // Headers that are part of the signature
const payloadHash = sha256(payload); // SHA-256 hash of the request payload
// The canonical request combines method, URI, query string, headers, signed headers, and payload hash
const canonicalRequest = [
opts.method, // HTTP Verb: GET, POST, PUT, etc.
canonicalUri, // Resource path, e.g., /v1/my-path/
canonicalQuerystring, // Query string (none in this case)
canonicalHeaders, // Canonical headers
signedHeaders, // Headers being signed
payloadHash, // SHA256 hash of the payload (empty in case of GET)
].join("\n");
// Step 2: Create the string to sign
const algorithm = "AWS4-HMAC-SHA256"; // Hashing algorithm
const credentialScope =
dateStamp + "/" + opts.region + "/" + opts.service + "/aws4_request"; // Credential scope (date, region, service, AWS4 request)
const canonicalRequestHash = sha256(canonicalRequest); // Hash of the canonical request
// String to sign includes the algorithm, request time, scope, and hash of the canonical request
const stringToSign = [
algorithm,
amzDate,
credentialScope,
canonicalRequestHash,
].join("\n");
// Step 3: Calculate the signature
const signature = getSignatureKey(
credentials.secretAccessKey, // Secret access key
dateStamp, // Date stamp (YYYYMMDD)
opts.region, // AWS region (e.g., ap-southeast-2)
opts.service, // AWS service (e.g., execute-api)
stringToSign // String to be signed
);
// Step 4: Add the signature to the request headers
const authorizationHeader = [
`${algorithm} Credential=` + credentials.accessKeyId + "/" + credentialScope, // Credential string
"SignedHeaders=" + signedHeaders, // Signed headers
"Signature=" + signature, // The calculated signature
].join(", ");
// Adding necessary headers to the request options
const headers = {
"X-Amz-Date": amzDate, // x-amz-date header with the current timestamp
Authorization: authorizationHeader, // Authorization header with the signature
};
opts.headers = headers; // Update the request with signed headers
}
};
/**
* Utility function to compute the SHA-256 hash of a given input
* @param {string} input - The input string
* @returns {string} - The hex-encoded hash of the input
*/
function sha256(input) {
const bytes = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, input);
return bytesToHex(bytes); // Convert the result to hexadecimal
}
/**
* Utility function to calculate HMAC-SHA256
* @param {string} input - The data to sign
* @param {string|ByteArray} key - The secret key used to sign the data
* @returns {ByteArray} - The HMAC-SHA256 signature as byte array
*/
function hmac(input, key) {
return Utilities.computeHmacSha256Signature(input, key); // Generate HMAC-SHA256 signature
}
/**
* Function to calculate the signing key for AWS Signature V4
* @param {string} secretKey - The AWS secret access key
* @param {string} dateStamp - The date in YYYYMMDD format
* @param {string} regionName - AWS region (e.g., ap-southeast-2)
* @param {string} serviceName - AWS service name (e.g., execute-api)
* @param {string} stringToSign - The string to be signed
* @returns {string} - The signature as hex-encoded string
*/
function getSignatureKey(secretKey, dateStamp, regionName, serviceName, stringToSign) {
const kDate = hmac(dateStamp, "AWS4" + secretKey); // HMAC of the date stamp
const kRegion = hmac(Utilities.newBlob(regionName).getBytes(), kDate); // HMAC of the region
const kService = hmac(Utilities.newBlob(serviceName).getBytes(), kRegion); // HMAC of the service
const signingKey = hmac(Utilities.newBlob("aws4_request").getBytes(), kService); // HMAC of the "aws4_request"
// Return the signature
return bytesToHex(hmac(Utilities.newBlob(stringToSign).getBytes(), signingKey));
}
/**
* Utility function to convert a byte array to a hex-encoded string
* @param {ByteArray} byteArray - The byte array to convert
* @returns {string} - Hexadecimal representation of the byte array
*/
function bytesToHex(byteArray) {
return byteArray
.map(function (byte) {
return ("0" + (byte & 0xff).toString(16)).slice(-2); // Convert each byte to hex
})
.join("");
}
function main() {
const path = '/v1/my-api'; // Replace with your API endpoint path
const host = 'api.example.com'; // Replace with your API Gateway host
const endpoint = `https://${host}${path}`;
const opts = {
host,
path,
method: "GET",
service: "execute-api", // AWS service name
region: "ap-southeast-2", // AWS region
};
AWS.SignatureV4.sign(opts, {
accessKeyId: 'FAKEACCESSKEYID12345', // Replace with your AWS Access Key ID
secretAccessKey: 'FAKESECRETACCESSKEY67890' // Replace with your AWS Secret Access Key
});
const response = UrlFetchApp.fetch(endpoint, opts);
Logger.log(response.getContentText());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment