Introduction

A couple of days ago my team had a task to integrate our Java application with Amazon API Gateway endpoints. As you probably know the Amazon API Gateway requires signing requests. In our case, the authentication was based on IAM user access key and secret access key. Signing the request is a cryptographic hash function returning a hash value based on the input. Amazon API Gateway supports authentication using AWS Signature Version 4.

Welcome_image

Ready to start?

During the brainstorm session we considered following ideas:

  1. Generate AWS SDK for the specific API Gateway definition - the most sufficient way recommended by Amazon .
  2. Generate HTTP client from OpenAPI or Swagger definition (both definitions are ready to download form API Gateway).
  3. Use custom HTTP client, like RestTemplate and implement signer by ourselves based on the Python example .
  4. Use AWS SDK, AWS4Signer and pure JSON request/response to call API Gateway.

We also knew that this API is not stable and the API provider will change it. In these circumstances, we decided to not use pre-generated AWS SDK or HTTP client from OpenAPI (do you remember boilerplate code generated from WSDL and DTOs mappings?). Additionally, the attempt to generate dedicated AWS SDK gave us the error in AWS Console. Sick!.

Error in AWS console

The natural decision was to implement 3rd or 4th idea. I have organized an internal competition where the winning solution will be used in our project. My teammate has started to implement AWS signer and call API Gateway manually by using RestTemplate. I have started the research on how to do it by using AWS SDK and AWS4Signer implementation. During the implementation both of us complained. My colleague had a problem to debug why the checksum (hash code) is incorrect. I wasn’t able to find the example of using AWS SDK and AWS4Signer in official AWS documentation and StackOverflow as well. There was only one place in Apache NiFi project where similar implementation has been done.

Finally, the fastest solution was a custom implementation of API Gateway caller based on AWS SDK and AWS4Signer.

In this blog post, I would like to describe required steps to implement API Gateway caller by using raw JSON request and response. If you are impatient, feel free to jump directly to the GitHub example and check my customized implementation based on Apache NiFi.

How to do it?

1. Gather basic information.

First of all, you have to collect following data from your API Gateway provider:

  • AWS_IAM_ACCESS_KEY (IAM user),
  • AWS_IAM_SECRET_ACCESS_KEY (IAM password),
  • AWS_REGION (the region where your API Gateway is deployed),
  • AWS_API_GATEWAY_ENDPOINT (the URL to the API Gateway endpoint).

2. Include the AWS SDK to your project.

In my case I have added following Maven dependency to the code:

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-core</artifactId>
    <version>{version}</version>
</dependency>

3. Create the JsonApiGatewayCaller class extending AmazonWebServiceClient.

public class JsonApiGatewayCaller extends AmazonWebServiceClient

The AmazonWebServiceClient abstract base class is responsible for basic client capabilities that are the same across all AWS SDK Java clients, for example setting the client endpoint.

4. Create the JsonApiGatewayCaller constructor .

    public JsonApiGatewayCaller(String accessKey, String secretAccessKey, String apiKey, String region, URI endpoint) {

        super(new ClientConfiguration());

        this.credentials = new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretAccessKey));
        this.apiKey = apiKey;
        this.endpoint = endpoint;

        this.signer = new AWS4Signer();
        this.signer.setServiceName(API_GATEWAY_SERVICE_NAME);
        this.signer.setRegionName(region);

        final JsonOperationMetadata metadata = new JsonOperationMetadata().withHasStreamingSuccessResponse(false).withPayloadJson(false);
        final Unmarshaller<ApiGatewayResponse, JsonUnmarshallerContext> responseUnmarshaller = in -> new ApiGatewayResponse(in.getHttpResponse());
        this.responseHandler = SdkStructuredPlainJsonFactory.SDK_JSON_FACTORY.createResponseHandler(metadata, responseUnmarshaller);

        JsonErrorUnmarshaller defaultErrorUnmarshaller = new JsonErrorUnmarshaller(ApiGatewayException.class, null) {
            @Override
            public AmazonServiceException unmarshall(JsonNode jsonContent) throws Exception {
                return new ApiGatewayException(jsonContent.toString());
            }
        };

        this.errorResponseHandler = SdkStructuredPlainJsonFactory.SDK_JSON_FACTORY.createErrorResponseHandler(
                Collections.singletonList(defaultErrorUnmarshaller), null);
    }
  • The clientConfiguration object contains basic HTTP options such as proxy settings, user agent string, max retry attempts, etc.
  • The credentials object represents AWSCredentialsProvider. You have a couple of ways how to provide IAM credentials. to your code, see the official documentation. In our simplified example, we use static credentials provider.
  • The apiKey object represents the Authorization Setting in our API Gateway. See the official documentation .
  • The endpoint object is represented by URL to our API Gateway, for example https://234n34k5678k.execute-api.eu-west-1.amazonaws.com/TEST.
  • The next three lines create the AWS4Signer object to sign requests with the AWS4 signing protocol.
  • The last section in the constructor is responsible for the responseHandler and errorResponseHandler creation. This is the crucial part to retrieve response in pure JSON representation.

5. Create the ExecutionContext.

    private ExecutionContext createExecutionContext() {
        final ExecutionContext executionContext = ExecutionContext.builder().withSignerProvider(
                new DefaultSignerProvider(this, signer)).build();
        executionContext.setCredentialsProvider(credentials);
        return executionContext;
    }

The ExecutionContext comes from com.amazonws.http package and it aggregates the request handlers, web service client, signer provider, and AWS Request Metrics. Please note, that this class is not thread-safe.

6. Create the DefaultRequest.

    private DefaultRequest prepareRequest(HttpMethodName method, String resourcePath, InputStream content) {
        DefaultRequest request = new DefaultRequest(API_GATEWAY_SERVICE_NAME);
        request.setHttpMethod(method);
        request.setContent(content);
        request.setEndpoint(this.endpoint);
        request.setResourcePath(resourcePath);
        request.setHeaders(Collections.singletonMap("Content-type", "application/json"));
        return request;
    }

This method creates the DefaultRequest in a quite easy way. The crucial part here is to set Content-type: application/json header to properly ‘talk’ in JSON format.

7. HTTP method execution.

    public ApiGatewayResponse execute(HttpMethodName method, String resourcePath, InputStream content) {

        final ExecutionContext executionContext = createExecutionContext();
        DefaultRequest request = prepareRequest(method, resourcePath, content);
        RequestConfig requestConfig = new AmazonWebServiceRequestAdapter(request.getOriginalRequest());
        return this.client.execute(request, responseHandler, errorResponseHandler, executionContext, requestConfig).getAwsResponse();
    }

This method is the heart of our class. It is responsible to call the API Gateway endpoint by preparing the execution context, the request, the request config and invoking the execute method from the AmazonHttpClient. You can notice here, that this method requires to pass responseHandler and errorResponseandler to properly unmarshall results. The implementation uses our own ApiGatewayResponse and ApiGatewayException.

8. Final usage.

Let’s assume that we would like to send POST method to the /pets endpoint with following JSON request:

{
    "type": "dog",
    "price": 249.99
}

Our Java code can look like as following example:

    JsonApiGatewayCaller caller = new JsonApiGatewayCaller(
                    AWS_IAM_ACCESS_KEY,
                    AWS_IAM_SECRET_ACCESS_KEY,
                    null,
                    AWS_REGION,
                    new URI(AWS_API_GATEWAY_ENPOINT)
            );

    ApiGatewayResponse response = caller.execute(HttpMethodName.POST, "/pets", new ByteArrayInputStream(exampleJsonRequest.getBytes()));

Conclusion

  • Using AWS SDK is not very trivial and sometimes it is not well documented.
  • Signing requests by AWS4 protocol can cause troubles.
  • You have to produce a lot of boilerplate code to simply call the API Gateway.
  • The full Java code is available on the GitHub.

Updated:

Leave a comment