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.
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();
});
};