In this blog I talk about API Gateway and S3. Specifically the static web site hosting feature available in S3 that allows developers to host their websites without the need to run a web server.

Enabling static web hosting in S3 is simple and requires no tutorial, you simply enable the feature under the Properties tab and your S3 bucket becomes publicly available. What I really want to address in this blog is the use of the provided response headers in S3 and important headers that are not available. Headers in S3 can be applied to an object by selecting the Change metadata option from the Actions dropdown. Doing so will present a side panel allowing you to select and set response headers and values from an autocomplete input.

S3 Change metadata dialog.

The autocomplete contains many common headers such as Cache-Control and Content-Type but does not contain some important headers that you may want to set to prevent security risks such as X-Frame-Options. X-Frame-Options allows the browser to determine whether the retrieved document can be used within an iframe which allows us to prevent Clickjacking attacks.

To get around this we can us the API Gateway to proxy all GET requests to the bucket which allows us to set custom headers on all responses. All RESTfull request method types such as GET, DELETE, PUSH etc can be proxied through the API Gateway but that requires further discussion around security because we don’t just want anyone to be able to push files to our S3 bucket. That discussion is out of scope for this blog. Instead, we are just going to go through what is required to allow public GET access to S3 objects. Lets get started.

So first we create a standard bucket with the default private access and attach a bucket policy that only allows the API Gateway to access the objects stored in the bucket.

S3 Create bucket dialog
S3 Bucket policy allowing API Gateway access

Next, create a simple index.html file and place it into the root of your new bucket.

Simple index.html file
Simple index.html file in the root of the S3 bucket

Now we need to create a new API Gateway as well as a new IAM role that will allow our API Gateway to connect to our S3 bucket.

Create API Gateway dialog
Our new IAM role allowing API Gateway to connect to S3.

Under the root resource of your new API Gateway, create a new resource with a name of “item” and a path of {item}. The path will be used to determine the path of the object you want to retrieve. The {item} section of the path will be mapped to a provided url param.

New API Gateway resource with a path of {item}

Next, under the new item resource, create a new GET method with the following configuration.

Item resource GET method configuration

Here we select the Simple Storage Service (S3) as the service to proxy requests to and specify the desired region. The arn (amazon reference name) of the IAM role created earlier that allows the API Gateway to connect to S3 is also set as the execution role. Lastly, we select that the action type should be set to “Use path override”. This allows us to specify the root path of the bucket ensuring that any object request starts at the path of the override value. We also reference the URL param {item} so any GET request will retrieve the object at “bucket-name/{item}”. Once the GET method is created, we are presented with the API Gateway Method Execution display. This outlines the steps of the request and response consisting of the original request and the integration request.

API Gateway Method Execution display

Our next step is to map the item path param of the original request to the item param of the integration request. We do this by clicking on the integration request, expanding the URL Path Parameters section and adding a new path parameter as shown below.

Adding the item url parameter mapping.

Don’t forget to click the tick button to save once the parameter values have been added. Now return back to the method execution display, click the action button and deploy the api. Doing so will display the Deploy Api dialog where you can create a new stage to deploy to. Call the stage “test”, then click deploy.

Creation of the test stage and deployment of the API Gateway S3 Proxy

Now that the API has been deployed you will be presented with the test stage editor display showing the URL of the newly created test stage. You can use this URL to execute a GET request to the API Gateway.

Test stage display showing the api-s3 proxy API URL.

Open a browser and paste the api-s3 proxy API URL appended with index.html

api-s3 proxy API GET request index.html response

You will notice that the content-type of the response is set as “Content-Type:application/json” which prevents the browser from rendering the response as html. We fix this by mapping the content-type and content-disposition headers from the original request/response to and from the integration request/response. Click back on the Resources section of the api s3 proxy API Gateway and select the GET method. Click on the Method Execution and add the Content-Type and Content-Disposition headers to the HTTP Headers section.

Adding the Content-Type and Content-Disposition headers to the Method Execution

Now click on the request Method Integration and add the Content-Type and Content-Disposition headers. We set the value for both headers by referencing the headers of the original request via “method.request.header.Content-Type” and “method.request.header.Content-Disposition”. Shown below.

Mapping the Integration request headers to the original request headers

If you now inspect the Response Method Execution, you will notice that by default the response body for an empty response is set to have a Content-Type of application/json. We need to add a Content-Type header for 200 responses.

Setting the Content-Type and Content-Disposition of the Method response

Now we can map the Integration response headers to the original response headers. Open the Integration response display and map both headers to their corresponding integration response values via “integration.request.header.Content-Type” and “integration.request.header.Content-Disposition”.

Setting the Integration response Content-Type and Content-Disposition values

If you deploy the api gateway to the test environment again and refresh the browser, the index.html file will be rendered by the browser correctly as text/html.

Index.html rendered in the browser as text/html

Our final step is to achieve what we originally set out to do which is to implement the X-Frame-Options header to prevent the index.html page from being used within an iframe. We do this by adding the X-Frame-Options header to the Method Response as below

Setting the X-Frame-Options header to the Method Response

We then need to map the X-Frame-Options header to the Integration response. You will notice that rather than mapping the header to a value in the “integration.response”, we hard code it instead as ‘deny’ in single quotes. The X-Frame-Options header can be set as one of three values:

  • deny: Do not allow the page to be used in an iframe.
  • sameorigin: Only allow the page to be used in an iframe on the same origin.
  • allow-from: Only allow the page to be used in an iframe from the specified origin.
Setting the X-Frame-Options header to ‘deny’

If you redeploy the api to the test stage, refresh the browser and inspect the network tab. You will find that the response headers for the index.html network request contain the X-Frame-Options header with a value of ‘deny’.

We now have a fully working API Gateway S3 proxy allowing us to set custom headers.

Senior Full Stack Developer at MoneySupermarket.