Node.js - Calling AWS service endpoints and signing with AWS Signature Version 4
Example of using Node.js to call AWS service endpoints and signing the API request with AWS Signature Version 4.
Note that this example uses the temporary AWS credentials provided by an IAM role for Amazon EC2.
Create a file example.js with the following content and replace the example values at the top.
// Example of listing all S3 buckets:
const HOST = "s3.amazonaws.com";
const REGION = "us-east-1";
const SERVICE_NAME = "s3";
const METHOD = "GET";
const PATH = "/";
const PAYLOAD = ``;
// Other example of listing all container products in your AWS Marketplace seller account:
/*
const HOST = "catalog.marketplace.us-east-1.amazonaws.com";
const REGION = "us-east-1";
const SERVICE_NAME = "aws-marketplace";
const METHOD = "POST";
const PATH = "/ListEntities";
const PAYLOAD = `{
"Catalog": "AWSMarketplace",
"EntityType": "ContainerProduct"
}`;
*/
// Other example of updating a container product information in your AWS Marketplace seller account:
/*
const HOST = "catalog.marketplace.us-east-1.amazonaws.com";
const REGION = "us-east-1";
const SERVICE_NAME = "aws-marketplace";
const METHOD = "POST";
const PATH = "/StartChangeSet";
const PAYLOAD = `{
"Catalog": "AWSMarketplace",
"ChangeSet": [
{
"ChangeType": "UpdateInformation",
"Entity": {
"Identifier": "prod-example123",
"Type": "[email protected]"
},
"DetailsDocument": {
"ProductTitle": "My new title",
"ShortDescription": "My new description.",
"LongDescription": "My new long description."
}
}
]
}`;
*/
const sendRequest = async function(url, opt) {
opt = opt ? opt : {};
const parsedUrl = require("url").parse(url);
let headers = opt.headers ? opt.headers : {};
headers["Content-length"] = opt.body ? opt.body.length : 0;
const options = {
hostname: parsedUrl.hostname,
port: opt.port ? opt.port : (parsedUrl.protocol == "https:" ? 443 : 80),
path: parsedUrl.path,
method: opt.method ? opt.method : "GET",
headers: headers,
timeout: typeof opt.timeout == "number" ? opt.timeout : 30000
};
let response = await new Promise(function(res, err) {
let completed = false;
let request = require(parsedUrl.protocol == "https:" ? "https" : "http").request(options, function(response) {
let responseText = [];
response.on("data", function(d) {
responseText.push(d);
});
response.on("end", function() {
completed = true;
response.responseText = responseText.join("");
res(response);
});
});
request.on("error", function(error) {
completed = true;
console.error(`sendRequest Error: ${error.message}`);
err(error);
});
request.write(opt.body ? opt.body : "");
request.end();
if (options.timeout > 0) {
setTimeout(function() {
if (completed) {
return;
}
request.destroy(new Error(`Timed out (${options.timeout} ms).`));
}, options.timeout);
}
});
if (response.statusCode == 301 || response.statusCode == 302) {
let redirectUrl;
const locationParsed = require("url").parse(response.headers.location);
if (response.headers.location) {
if (locationParsed.protocol && locationParsed.host) {
redirectUrl = locationParsed.href;
} else if (locationParsed.path) {
if (locationParsed.path[0] == "/") {
redirectUrl = `${parsedUrl.protocol}//${parsedUrl.host}${locationParsed.path}`;
} else {
redirectUrl = `${parsedUrl.protocol}//${parsedUrl.host}${parsedUrl.path.replace(/^([^?]*\/).*$/,"$1")}${locationParsed.path}`;
}
} else {
throw new Error(`Redirect location header invalid: ${response.headers.location}`);
}
} else {
throw new Error(`Redirect location header missing: ${response.headers.location}`);
}
const newOpt = {...JSON.parse(JSON.stringify(opt)),...{method: "GET"}};
delete newOpt.body;
return await sendRequest(redirectUrl, newOpt);
}
return response;
};
/*
Return:
{
"AccessKeyId": "ASIAQVEXAMPLE12345",
"SecretAccessKey": "example12345",
"Token": "exampleToken12345"
}
Or false if none.
*/
const getEnvVarCredentials = function() {
if (
!process.env.AWS_ACCESS_KEY_ID ||
!process.env.AWS_SECRET_ACCESS_KEY ||
!process.env.AWS_SESSION_TOKEN
) {
return false;
}
return {
"AccessKeyId": process.env.AWS_ACCESS_KEY_ID,
"SecretAccessKey": process.env.AWS_SECRET_ACCESS_KEY,
"Token": process.env.AWS_SESSION_TOKEN
};
};
/*
Return:
{
"Code": "Success",
"LastUpdated": "2022-09-26T19:14:08Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAQVEXAMPLE12345",
"SecretAccessKey": "example12345",
"Token": "exampleToken12345",
"Expiration": "2022-09-27T01:37:12Z"
}
Or false if none.
*/
const getInstanceRoleV2 = async function() {
// IMDSv2
const tokenEndpoint = "http://169.254.169.254/latest/api/token";
let token;
try {
token = (await sendRequest(tokenEndpoint, {
method: "PUT",
headers: {
"X-aws-ec2-metadata-token-ttl-seconds": 900
}
})).responseText;
if (!token) {
throw new Error(`No response from IMDSv2 token endpoint: ${tokenEndpoint}`);
}
} catch (e) {
console.error(`Could not get token for IMDSv2: ${e.message}`);
return false;
}
const endpoint = "http://169.254.169.254/latest/meta-data/iam/security-credentials/";
let roleName;
let json;
try {
roleName = (await sendRequest(endpoint, {
headers: {
"X-aws-ec2-metadata-token": token
}
})).responseText;
if (!roleName) {
throw new Error(`No response from IMDSv2 endpoint: ${endpoint}`);
}
json = (await sendRequest(`${endpoint}${roleName}`, {
headers: {
"X-aws-ec2-metadata-token": token
}
})).responseText;
if (!json) {
throw new Error(`No response from IMDSv2 endpoint: ${endpoint}${roleName}`);
}
const creds = JSON.parse(json);
return creds;
} catch (e) {
console.error(`Could not get IMDSv2 role credentials: ${e.message}`);
return false;
}
};
/*
Return:
{
"Code": "Success",
"LastUpdated": "2022-09-26T19:14:08Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAQVEXAMPLE12345",
"SecretAccessKey": "example12345",
"Token": "exampleToken12345",
"Expiration": "2022-09-27T01:37:12Z"
}
Or false if none.
*/
const getInstanceRole = async function() {
// IMDSv1
const endpoint = "http://169.254.169.254/latest/meta-data/iam/security-credentials/";
let roleName;
let json;
try {
roleName = (await sendRequest(endpoint)).responseText;
if (!roleName) {
throw new Error(`No response from IMDSv1 endpoint: ${endpoint}`);
}
json = (await sendRequest(`${endpoint}${roleName}`)).responseText;
if (!json) {
throw new Error(`No response from IMDSv1 endpoint: ${endpoint}${roleName}`);
}
const creds = JSON.parse(json);
return creds;
} catch (e) {
console.error(`Could not get IMDSv1 role credentials: ${e.message}`);
return false;
}
};
/*
headers =
{
"Host": "iam.amazonaws.com",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"My-header1": " a b c",
"X-Amz-Date": "20150830T123600Z",
"My-Header2": " \"a b c\" "
}
Return:
{
"content-type": "application/x-www-form-urlencoded; charset=utf-8",
"host": "iam.amazonaws.com",
"my-header1": "a b c",
"my-header2": "\"a b c\"",
"x-amz-date": "20150830T123600Z"
}
*/
const getCanonicalHeaders = function(headers) {
let unsorted = {};
for (const k in headers) {
unsorted[k.trim().toLowerCase()] = headers[k].trim().replace(/\s+/g, " ");
}
const sortedKeys = Object.keys(unsorted).sort();
let sorted = {};
for (const k of sortedKeys) {
sorted[k] = unsorted[k];
}
return sorted;
};
/*
path =
"/DescribeEntity?entityId=EntityId&catalog=Catalog"
Return:
"catalog=Catalog&entityId=EntityId"
path =
"/ListEntities"
Return:
""
*/
const getCanonicalQueryStringFromPath = function(path) {
const ii = path.trim().indexOf("?");
if (ii == -1 || ii + 1 == path.length) {
return "";
}
const params = path.substr(ii + 1).split("&");
params.sort();
return params.join("&");
};
const getSha256Signature = function(str) {
const crypto = require("crypto");
return crypto.createHash("sha256").update(str).digest("hex").toLowerCase();
};
// https://docs.aws.amazon.com/general/latest/gr/sigv4_elements.html
const createCanonicalRequest = function(method, path, host, headers, payload) {
const pathNoQueryString = path.split("?")[0];
const queryString = getCanonicalQueryStringFromPath(path);
const canonicalHeaders = getCanonicalHeaders(headers);
const canonicalHeadersString = Object.keys(canonicalHeaders).map(function(k) {
return `${k}:${canonicalHeaders[k]}`;
}).join("\n") + "\n";
const signedHeaders = Object.keys(canonicalHeaders).join(";");
const payloadHashHex = getSha256Signature(payload);
const canonicalRequest =
method + "\n" +
pathNoQueryString + "\n" +
queryString + "\n" +
canonicalHeadersString + "\n" +
signedHeaders + "\n" +
payloadHashHex;
return canonicalRequest;
};
// https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
const createStringToSign = function(date, region, serviceName, hashedCanonicalRequest) {
const algo = "AWS4-HMAC-SHA256";
const yyyymmddthhmmssz = date.toISOString().split(".")[0].replace(/[\-:]/g, "") + "Z";
const yyyymmdd = date.toISOString().split("T")[0].replace(/-/g, "");
const credentialScope = `${yyyymmdd}/${region}/${serviceName}/aws4_request`;
const str =
algo + "\n" +
yyyymmddthhmmssz + "\n" +
credentialScope + "\n" +
hashedCanonicalRequest;
return str;
};
// https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
const calculateSignature = function(secretKey, date, region, serviceName, stringToSign) {
const crypto = require("crypto");
const yyyymmdd = date.toISOString().split("T")[0].replace(/-/g, "");
const kDate = crypto.createHmac("sha256", "AWS4" + secretKey).update(yyyymmdd).digest();
const kRegion = crypto.createHmac("sha256", kDate).update(region).digest();
const kService = crypto.createHmac("sha256", kRegion).update(serviceName).digest();
const kSigning = crypto.createHmac("sha256", kService).update("aws4_request").digest();
const signature = crypto.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
return signature;
};
// https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
const createAuthorizationHeader = function(accessKeyId, date, region, serviceName, headers, signature) {
const algo = "AWS4-HMAC-SHA256";
const yyyymmdd = date.toISOString().split("T")[0].replace(/-/g, "");
const canonicalHeaders = getCanonicalHeaders(headers);
const signedHeaders = Object.keys(canonicalHeaders).join(";");
const authorization = `${algo} Credential=${accessKeyId}/${yyyymmdd}/${region}/${serviceName}/aws4_request, SignedHeaders=${signedHeaders}, Signature=${signature}`;
return authorization;
};
(async function(){
let creds;
creds = getEnvVarCredentials();
if (!creds) {
creds = await getInstanceRoleV2();
}
if (!creds) {
creds = await getInstanceRole();
}
if (!creds) {
console.error("No AWS credentials found in environment variables or instance metadata service.");
return;
}
const date = new Date();
const yyyymmddthhmmssz = date.toISOString().split(".")[0].replace(/[\-:]/g, "") + "Z";
let headers = {
"Host": HOST,
"X-Amz-Date": yyyymmddthhmmssz,
"X-Amz-Security-Token": creds.Token,
"x-amz-content-sha256": getSha256Signature(PAYLOAD) // For s3: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
};
const canonicalRequest = createCanonicalRequest(METHOD, PATH, HOST, headers, PAYLOAD);
const hashedCanonicalRequest = getSha256Signature(canonicalRequest);
const stringToSign = createStringToSign(date, REGION, SERVICE_NAME, hashedCanonicalRequest);
const signature = calculateSignature(creds.SecretAccessKey, date, REGION, SERVICE_NAME, stringToSign);
const authorization = createAuthorizationHeader(creds.AccessKeyId, date, REGION, SERVICE_NAME, headers, signature);
headers["Authorization"] = authorization;
const res = await sendRequest(`https://${HOST}${PATH}`, {
method: METHOD,
body: PAYLOAD,
headers: headers
});
try {
const json = JSON.parse(res.responseText);
console.log(JSON.stringify(json, undefined, 2));
} catch (e) {
console.log(e.message, res);
}
process.exit();
})();
Execute using Node.js in BASH.
node example.js