Signing requests with AWS SDK in Lambda functions
1. Scenarios when we must sign requests
When we call almost all AWS API endpoints, we must sign the request with our access key (access key id and secret access key). The signature verifies who we are, records the date and time we submitted the request, and protects the data in transit. Almost all endpoints require the Signature Version 4 signing process.
AWS CLI and the SDKs automatically sign the requests on behalf of us. They look for our access key on our computer or get it from the application’s role.
But in some scenarios, we have to manually sign the request. These cases include the use of a programming language for which no SDK exists. Or, we use a supported language but we want to call a Lambda function URL or an endpoint behind an API Gateway which is protected by AWS IAM.
2. The problem
Say we have a Lambda function, which invokes an endpoint created by an API Gateway, where we have protected the endpoint with AWS_IAM
. We can use this type of protection when one microservice has to call another.
In this case the Lambda function will use axios to make the HTTP request.
3. Using AWS SDK for JavaScript
AWS SDK for JavaScript v3 provides modules for SigV4 signing.
We should install at least these two AWS packages and, of course, axios
:
npm install @aws-sdk/signature-v4 @aws-crypto/sha256-js axios
The @aws-sdk/signature-v4
package implements the SigV4 request signing algorithm, while @aws-crypto/sha256-js
is the JavaScript implementation of SHA256
.
The Lambda function which should sign the request can have the following code:
import axios from 'axios';
import { SignatureV4 } from '@aws-sdk/signature-v4';
import { Sha256 } from '@aws-crypto/sha256-js';
const {
API_URL,
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY,
AWS_SESSION_TOKEN
} = process.env;
const apiUrl = new URL(API_URL);
const sigv4 = new SignatureV4({
service: 'execute-api',
region: 'us-east-1',
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
sessionToken: AWS_SESSION_TOKEN,
},
sha256: Sha256,
});
export const handler = async () => {
const signed = await sigv4.sign({
method: 'GET',
hostname: apiUrl.host,
path: apiUrl.pathname,
protocol: apiUrl.protocol,
headers: {
'Content-Type': 'application/json',
host: apiUrl.hostname, // compulsory
},
});
try {
const { data } = await axios({
...signed,
url: API_URL, // compulsory
});
console.log('Successfully received data: ', data);
return data;
} catch (error) {
console.log('An error occurred', error);
throw error;
}
};
We must specify some compulsory elements.
3.1. SignatureV4 class
credentials
in the SignatureV4
constructor contains the access key id, secret access key and session token of the Lambda function’s execution role. Because the function assumes the role, the access key id and secret access key are not enough. Roles are temporary credentials, so we will need to specify the session token too.
Credentials come from the AWS_ACCESS_KEY_ID
, AWS_SECRET_ACCESS_KEY
and AWS_SESSION_TOKEN
runtime environment variables. Their values come from the Lambda execution role and are available from the process.env
object without further setup.
service
and region
are straightforward. If we want to call an endpoint in API Gateway, like in this case, service
will be execute-api
. In the case of a Lambda function URL, we should set the value of service
to lambda
. region
is hard-coded here, but we can make it dynamic by adding it as an environment variable to the function.
Sha256
is a constructor which uses a cryptographic hash function. SignatureV4
will calculate a hash value from parts of the request, which AWS will compare to its own generated checksum. If they match, the request can proceed.
3.2. axios request
We use the sign
method on the SignatureV4
instance to sign the request.
The method accepts the HttpRequest
we want to sign. The code above lists the minimum compulsory properties. We must also specify the host
header, otherwise, we will receive a 403
error.
sign
resolves with the signed HttpRequest
, so we can pass it to the axios
instance. Don’t forget to specify the url
property in the axios
config object.
3.3. Invoking the function
We can now deploy and invoke the Lambda function. The request should be successful, and we should see the return value of the endpoint.
4. Other solutions
SignatureV4
in the SDK is not the only way to sign axios
requests.
We can create custom axios
clients for the requests we sign. Then we can intercept the requests using a package which builds on the popular (but apparently unmaintained) aws4 module.
5. Summary
AWS requires Signature Version 4 as a layer of protection for their API endpoints most of the time. The CLI and all SDKs automatically sign the requests, but we can encounter situations when an explicit signature process is necessary. One such scenario is when a Lambda function invokes an API that is protected by AWS_IAM
.
It’s best to use the signature-v4
package, which is available in the AWS SDKs to sign requests in Lambda functions.
6. References
Signature Version 4 documentation - Details about the SigV4 process and how the signature is created.
Module @aws-sdk/signature-v4 - Official (but a bit dry and less than informative) documentation on the SDK’s signature-v4
package.
Sign GraphQL Request with AWS IAM and Signature V4 - Great post about signing requests with AWS SDK for JavaScript v3. It uses fetch
instead of axios
.