AWS Signature Version 4 (SigV4) is the recommended method for authenticating requests to AWS services. It ensures that requests are tamper-proof and securely signed. By leveraging SigV4, you can add an extra layer of security to your requests while interacting with AWS services, such as API Gateway.
Table of Content
AWS SigV4
With sigV4, user’s secrets never appear API requests directly, and with its design meaning every request is finely scoped down and signed with a token that is valid only for a given AWS service, in a given AWS region, on a given day. All of these are checked and verified at the authenticated stage, before AWS tries to authorise a request.
Facts and Figures
- Universal Application: SigV4 is the standard for all AWS regions and services.
- Components of SigV4: The signature includes the AWS access key, a date stamp, region, service name, and the request itself. This comprehensive inclusion ensures robust security.
- Scope of Signature: The signature is specific to each request and includes a timestamp, making it valid only for a short period. This helps prevent replay attacks.
- Payload Hashing: SigV4 includes a hash of the payload in the request. This ensures that the payload has not been tampered with during transit.
- Credential Scope: The credential scope in SigV4 includes the date of the request, the AWS region, the service name, and a special “aws4_request” tag, adding layers to the security context.
Here is the code for lambda (or any other AWS Service) to signs the requests which is intended for AWS OpenSearch Serverless:-
Example using Node.js
const aws4 = require("aws4"); // npm i aws4
const emptyHash =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
const AOSS_HOST = "1ad917d3d5dc.eu-west-1.aoss.amazonaws.com";
const request = {
host: AOSS_HOST,
method: "GET",
path: "/my-index",
service: "aoss", // AWS Service
region: "eu-west-1",
headers: {
"Content-Type": "application/json",
"X-Amz-Content-Sha256": emptyHash,
},
//body: JSON.stringify({ /* Request body */ }), // For POST, PUT etc
};
const signedRequest = aws4.sign(request);
(async () => {
const requestOptions = {
method: request.method,
headers: signedRequest.headers,
};
console.log({ ...requestOptions, url: AOSS_HOST });
const response = await fetch(
`https://${AOSS_HOST}/${request.path}`,
requestOptions
);
const result = await response.text();
})();
Example using Java
import com.amazonaws.DefaultRequest;
import com.amazonaws.Request;
import com.amazonaws.auth.AWS4Signer;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.http.HttpMethodName;
import com.amazonaws.regions.Regions;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
public class AwsSearch {
private static final String HOST = "https://1ad917d3d5dc.eu-west-1.aoss.amazonaws.com";
private static final String RESOURCE_PATH = "/prefix/_search";
private static final String SHA256_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
public static String search() {
try {
RequestParams requestParams = new RequestParams("aoss", HOST, RESOURCE_PATH,HttpMethodName.POST, Regions.EU_WEST_1);
Request<?> request = signRequest(requestParams);
HttpPost postRequest = new HttpPost(HOST + RESOURCE_PATH);
for (Map.Entry<String, String> headerEntry : request.getHeaders().entrySet()) {
postRequest.addHeader(headerEntry.getKey(), headerEntry.getValue());
}
StringEntity entity = new StringEntity("{\"from\": 0, \"size\": 1000, \"query\":{\"match_all\": {} }}");
postRequest.setEntity(entity);
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpResponse response = httpClient.execute(postRequest);
return EntityUtils.toString(response.getEntity());
}
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
private static Request<?> signRequest(RequestParams params) throws URISyntaxException {
AwsCredentials awsCredentials = new AwsCredentials();
BasicAWSCredentials credentials = new BasicAWSCredentials(awsCredentials.awsAccessKeyId, awsCredentials.awsSecretAccessKey);
Request<?> request = new DefaultRequest<>(params.serviceName);
request.setHttpMethod(params.methodName);
request.setEndpoint(new URI(params.host));
request.setResourcePath(params.path);
request.addHeader("Content-Type", "application/json");
request.addHeader("X-Amz-Security-Token", awsCredentials.awsSessionToken);
request.addHeader("X-Amz-Content-Sha256", SHA256_HASH);
AWS4Signer signer = new AWS4Signer();
signer.setRegionName(params.region.getName());
signer.setServiceName(request.getServiceName());
signer.sign(request, credentials);
return request;
}
@Data
@AllArgsConstructor
private static class RequestParams {
String serviceName;
String host;
String path;
HttpMethodName methodName;
Regions region;
}
@Getter
private static class AwsCredentials{
private final String awsAccessKeyId;
private final String awsSecretAccessKey;
private final String awsSessionToken;
public AwsCredentials() {
this.awsAccessKeyId = System.getenv("AWS_ACCESS_KEY_ID");
this.awsSecretAccessKey = System.getenv("AWS_SECRET_ACCESS_KEY");
this.awsSessionToken = System.getenv("AWS_SESSION_TOKEN");
}
}
}
// Usage
search();