Testing API Gateway Request Validation
This lab references the code in the aws-connectedcar-dotnet-serverless repository. If you're new to this course, see the introduction for information about setting up your workstation and getting the sample code.
Next, we’re going to switch back to testing API endpoints over the wire using Postman. Our goal in this lab is to see the OpenAPI data schemas and request validation in action. We’ll see how this API-level validation protects the backend services from both malformed and malicious requests.
Testing Request Body & Parameter Validation
Step 1: Update the global variables in Postman
In these labs, you’ve deployed a new stack in AWS, and as a result you need to set updated values for the “adminApi” and “apiKey” global variables in Postman. You can refer to the last lab in the CloudFormation section for information about running the query-outputs.zsh and query-attributes.zsh scripts, to get the values you need. As covered previously, you can also get these values from the console.
Once you have these two values at hand, open the editable global variables tab in Postman and enter them in the “Initial value” and “Current value” columns, as shown below. Once the values are entered, click the “Save” button at the top-right, and close the tab.
Before moving to the next step, it’s worth sending a request for one of the tests just to confirm the configuration. If you send a request for the “Get Dealers” test, you should see a “200 OK” response, like this:
Step 2: Send a “Create Dealer” request with a missing field
Now we’re ready to send some invalid requests to exercise the API-level validation. Let’s review the constraints that the data schemas enforce for the Dealer JSON that’s sent for the “Create Dealer” request. We’ll start with the Dealer object, on lines 95-113 of the schemas.openapil.yaml file:
For the Dealer request body, the name, address, and stateCode are all required fields. You can test this by sending a request with one of these fields removed. In Postman, remove the “stateCode” field for the “Create Dealer” body, and try sending a request. You should get a “400 Bad Request” response, as shown below:
Having tried the invalid request above, go back to the console and navigate to the X-Ray traces in CloudWatch. There will probably only be this one trace displayed, at the bottom of the page, which you can click to open. The segments timeline for this trace should show that the invalid request you just sent was stopped by the API, and did not execute any further:
Compare the trace shown above with another trace that we captured in the CloudWatch labs for an invalid request:
In the earlier trace for the “Get Timeslots” test, shown above, the query parameters were missing. This invalid request was ultimately caught by the receiving Lambda. It’s much better to stop invalid requests when they are first received by the API like this, rather than pass them through to the services downstream.
Step 3: Send a “Create Dealer” request with an invalid field value
The data schemas in the sample code don’t just enforce mandatory fields within objects. They also enforce value constraints within fields. Digging further into the schemas.openapi.yaml file, you can see the Address object that’s defined on lines 49-64:
With this Address object, there’s a field-level schema for the “streetAddress” field, which looks like this:
This pattern allows upper- and lower-case text, numbers, and some additional limited characters. The intent is to prevent submission of characters that could be used for code injection, such as curly braces or semi-colons. To test this value constraint, you can try sending a request with invalid characters in the streetAddress field. Back in Postman, restore the “stateCode” field, and then for the “streetAddress” field, add a pair of curly braces to the value. When you submit this request, you should again get a “400 Bad Request” response:
Step 4: Send a “Get Dealers” request with an invalid query parameter value
The endpoints defined in OpenAPI can also enforce data value constraints for query and path parameters. For the GET method of the endpoint shown below (for the “Get Dealers” operation), the “stateCode” query parameter is constrained by the StateCode schema that’s referenced on line 43:
The referenced StateCode field in the schema file, as shown below, includes enumerated values for the 50 US states and the 10 Canadian provinces:
To test the validation for this endpoint, switch back to Postman, select the “Get Dealers” test, and change the “stateCode” query parameter value from “OR” to “XX”, which of course is not one of the enumerated values. When you send this request again, you should once more get a “400 Bad Request” response:
Testing Request Size Validation
Look again at all the field-level data schemas in the schemas.openapi.yaml file:
What you’ll notice is that all of these fields define a maximum data size. This might be defined through the data type format, such as an integer that has an int64 format. Or it might be defined through a maxLength property for string types.
Step 5: Send a “Create Customer” request with a username that exceeds the size limit
To show these size limits in action, we’ll send a request for the “Create Customer” test with a username value that exceeds the size limit.
However, at this point, we need to explain this operation, since it’s different from most of the other POST operations in the sample code. In most cases, these operations use the same request body format as the target DynamoDB table, because that’s where the data is going. For the “Create Customer” operation, an item is created in the Customer table in DynamoDB, but also, a user account for that customer is created in the Cognito User Pool.
As a result, the request body for this operation is an aggregation of the fields required for both of these destinations, which is defined by the CustomerProvision object. The endpoint for this operation receives this data type, and the target Lambda maps the fields to separate data types for the table and the user account before calling their respective services. It helps to explain the working of this “Create Customer” operation because it makes clear the need for the username property in the CustomerProvision request body to be unique for each call. If this field is not unique, the call to create the user account in Cognito will fail.
With this background information in mind, navigate to the “Admin_API” collection in Postman, and select the “Pre-request script” option at the top of the screen. You should see the code that sets some of the global variables before each test in the collection executes:
The code in line 12, shown above, performs a value assignment for the username variable. This assignment combines the “USER” prefix with a pseudo-random timestamp value. This is how we’re able to run the test collection repeatedly without generating errors in Cognito.
Let’s now check the OpenAPI schema for the CustomerProvision object:
As you can see, the username field has a reference to the AlphaNumeric field-level schema, which is shown below. This schema limits the length of the field to a maximum of 50 characters:
Switching back to Postman, on the line of code that sets the username variable, add a sequence of fifty “X” characters, as highlighted below:
Apart from the length, adding these fifty characters does not violate the regex pattern in the AlphaNumeric field. Now open the “Admin_API” collection, then the “Customer” folder, and select the “Create Customer” test. After sending the request, you should once more get a “400 Bad Request” response:
After running this test, make sure you remove the extra “X” characters from the “pre-request script”. We’ll need this “Create Customer” test to be in a working state for the next lab.
Protecting Downstream Services
To recap: in the OpenAPI schemas all the object data types that define incoming request bodies also have their fields defined by data schemas. And each of these field-level data schemas has a size limit, which means that there’s an aggregate size limit for each of the object-level schemas.
When you combine these request body size limits with the configurable request throttling provided in the API stages, you set limits to the overall traffic load that can be placed on the services that are downstream of the APIs. You can have misbehaving client applications that send overly large or excessive volumes of requests. These requests could be the result of faulty or maybe even malicious software. In either case, the excess traffic will not be passed beyond the API.