'Proper signing of requests to aws resources through http
I have a lambda function that is writing some data to an Elasticsearch domain I have also set up through AWS. Currently the access policy on my domain is just to allow my own IP address to work with the domain
{"Version": "2012-10-17", "Statement": [{
"Effect": "Allow", "Principal": {"AWS": "*"},
"Action": "es:*",
"Resource": "arn:aws:es:us-east-1:$ACCOUNT:domain/DOMAIN/*",
"Condition": { "IpAddress": { "aws:SourceIp": $MYIP } }
}]}
I found the aws4
library for signing http requests. I'm using it as such:
axios(aws4.sign({
host: process.env.ES_ENDPOINT,
method: "post",
url: `https://${process.env.ES_ENDPOINT}/foobot/foobot`,
data,
}))
This was actually working before without the aws4.sign
piece as I had the ES domain completely open, but now I've applied the IP address policy above.
Now, I continually get an error like this in response:
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.
Is there something else I need to do to properly sign the request?
Solution 1:[1]
This actually has to do with the two libraries, axios
and aws4
. aws4
will sign based off of a normal NodeJS http
request, and in a POST request with a body the body is required to properly sign the request.
This is fixed pretty simply by also passing in body
and path
axios(aws4.sign({
host: process.env.ES_ENDPOINT,
method: "POST",
url: `https://${process.env.ES_ENDPOINT}/foobot/foobot`,
data,
body: JSON.stringify(data),
path: "/foobot/foobot",
}))
Solution 2:[2]
We found that there are AWS libraries that can handle things smoothly without having to share your credentials with the lambda via environment variables.
Here is a full example that allows a lambda to call an appysync endpoint.
Adapting it to any other service should not be hard.
Hope it helps someone.
const { defaultProvider } = require('@aws-sdk/credential-provider-node');
const { SignatureV4 } = require('@aws-sdk/signature-v4');
const { Sha256 } = require('@aws-crypto/sha256-js');
const { HttpRequest } = require('@aws-sdk/protocol-http');
const axios = require('axios');
const signer = new SignatureV4({
credentials: defaultProvider(),
region: 'eu-west-1',
service: 'appsync',
sha256: Sha256,
});
/**
* Send a signed graphQl request via http to appsync
*
* @param {string} appsyncUrl URL to reach GraphQL
* @param {object} requestBody JSON stringified request object.
* @returns {Promise} Request that has been sent
*/
async function send(appsyncUrl, requestBody) {
const parsedUrl = new UrlParse(appsyncUrl);
const endpoint = parsedUrl.hostname.toString();
const path = parsedUrl.pathname.toString();
const req = new HttpRequest({
hostname: endpoint,
path,
method: 'POST',
body: requestBody,
headers: {
host: endpoint,
'Content-Type': 'application/json',
},
});
const signed = await signer.sign(req, { signingDate: new Date() });
return axios
.post(appsyncUrl, signed.body, { headers: signed.headers })
.then((response) => {
if (response.data && response.data.errors) {
console.error({ error: response.data.errors }, 'Updating data failed');
} else if (response.data) {
return response.data;
}
})
.catch((error) => console.error({ error, endpoint }, 'Failed to connect to graphQL server'));
}
usage:
const myGraphQlMutation = /* GraphQL */ `
mutation MyMutation($id: ID!, $status: String!) {
myMutation(result: { id: $id, status: $status }) {
id
status
}
}
`;
const sendToAppSync = async (id, status) => {
const requestBody = JSON.stringify({
query: myGraphQlMutation,
variables: {
id: id,
status: status,
},
});
try {
const response = await appsync.send(process.env.APPSYNC_ENDPOINT, requestBody);
} catch (error) {
logger.error(`[ERROR] Error calling appsync: ${JSON.stringify(error, null, 2)}`);
throw error;
}
Of course, you will need to have given the proper rights to your lambda IAM role. (this blog post gives good pointers)
Solution 3:[3]
Yes i use this same example and it run successfully. But each time its return different signature. Even for same values.
axios(aws4.sign({
host: process.env.ES_ENDPOINT,
method: "POST",
url: `https://${process.env.ES_ENDPOINT}/foobot/foobot`,
data,
body: JSON.stringify(data),
path: "/foobot/foobot",
}))
Solution 4:[4]
The above answers have been helpful in bringing it all in to my solution here for posting a WS message from a lambda to the Websocket API in the API Gateway.
The reason for this was in order to avoid using the aws-sdk, which adds at least another ~2500ms on first run (warmup).
Here is my code for this, hope it helps others:
const data = {'success': true};
const request = {
host: process.env.AWS_API_GATEWAY_ENDPOINT,
method: 'POST',
url: `https://${process.env.AWS_API_GATEWAY_ENDPOINT}/wss/@connections/${connectionId}`, // this is for axios
path: `/wss/@connections/${connectionId}`,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
data, // this is needed for axios
}
const signedRequest = aws4.sign(request,
{
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
sessionToken: process.env.AWS_SESSION_TOKEN // needed when sending from a lambda
});
delete signedRequest.headers['Host']; // delete Host to not potentially mess with axios
const response = await axios(signedRequest);
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Explosion Pills |
Solution 2 | |
Solution 3 | Boom |
Solution 4 | Lior Kupers |