CORS, AWS HTTP API Gateway, Lambdas, Serverless, and SuperTokens

Eric Hosick
The Opinionated Software Architect
8 min readJun 24, 2023

--

A story of confusing documentation, standards, and the misunderstanding of CORS.

Photo by Victor Lu.

Introduction

Understanding CORS (Cross-Origin Resource Sharing) might seem simple on the surface. It’s a mechanism that ensures secure cross-domain communication, thereby bypassing the same-origin policy, a vital security measure intended to safeguard user data and uphold website integrity against potential cross-domain cyber threats.

However, its implementation may present unforeseen challenges, even for those with a background in development. The aim of this guide is to streamline the process, providing clear and comprehensive insights to alleviate any complexities that may arise in the application of CORS.

This tutorial is specifically targeted at assisting developers in running SuperTokens middle tier in an AWS lambda while implementing CORS at the HTTP API Gateway layer. Please note that this post does not cover authorizers with JWT tokens.

The Opinionated TLDR

Please be aware: this guide is explicitly tailored for AWS’s HTTP API Gateway. The presented instructions and concepts might not be applicable to the REST API Gateway, or other services.

  • The Access-Control-Allow-Origin is not returned if the Access-Control-Allow-Headers defined in the HTTP API Gateway does not contain every header requested by Access-Control-Request-Headers sent by the client (browser). One reason why the error “No ‘Access-Control-Allow-Origin’ header is present on the requested resource” is returned.
  • The Access-Control-Allow-Origin is not returned if the origin of the request (example: http://localhost:2000) does not match exactly with any Access-Control-Allow-Origin entries in the HTTP API Gateway: even the port counts. One reason why the error “No ‘Access-Control-Allow-Origin’ header is present on the requested resource” is returned.
  • The Access-Control-Allow-Origin is not returned if the client requests an HTTP Method (POST, GET, PUT, etc.) that is not in the Access-Control-Allow-Methods defined in the HTTP API Gateway. One reason why the error “No ‘Access-Control-Allow-Origin’ header is present on the requested resource” is returned.
  • When CORS is configured in HTTP API Gateway then “API Gateway ignores CORS headers returned from your backend integration” (see HTTP API CORS).
  • There is no need to setup preflight OPTIONS routes in HTTP API Gateway because “API Gateway automatically sends a response to preflight OPTIONS requests, even if there isn’t an OPTIONS route configured for your API” (see HTTP API CORS).
  • “For a CORS request, API Gateway adds the configured CORS headers to the response from an integration.” (see HTTP API CORS).
  • This post isn’t about understanding the design decisions behind CORS but instead navigating around them. You gotta admit that when * isn’t always * things can get a little frustrating.
  • You can debug CORS using curl if you pass the Origin: ... header via -H "Origin: http://localhost". Without that header, the service will ignore any CORS logic and return the request. CORS is not an authorizer (that isn’t its purpose) so be careful not to assume it is one.
  • It’s easy to only see headers returned by a call bypassing the --head flag to curl.

The Dreaded “No Access-Control-Allow-Origin Header is Present”

This can happen for quite a few reasons and we will try to cover as many of them as we can below.

This error means that the browser, while in full on CORS mode, expected the response to have a header Access-Control-Allow-Origin but one was not returned. If everything isn’t exactly provided as expected by the HTTP API Gateway, this value will not be set. Further, no error messages are provided as to why.

For preflight, you simply get the error:

Access to fetch at ‘https://{gateway_id}.execute-api.{aws_region}.amazonaws.com/auth/session/refresh' from origin ‘{client_origin_url}' has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

This is understandable because you don’t want to give any attackers information on why a CORS request failed.

But it is kind of like a magic trick where the Magician asks “Is this your card” and you answer back with blue, sky, and house. That answer doesn’t make it easy for the magician. Just like no information on why we aren’t getting the headers makes debugging hard.

Mismatch Between Allow and Request Headers

When the client sends a request to the server, it sets the Access-Control-Request-Headers. Every one of the requested headers must also be listed in the Access-Control-Allow-Headers defined in the CORS HTTP API Gateway setting for the Access-Control-Allow-Origin to returned.

Client Origin Not In Access-Control-Allow-Origin

The origin of the request (the browser) such as https://localhost:2000 must be present in the list of possible origins within the CORS HTTP API Gateway setting. Only an exact match (even with the port) will lead to the HTTP API Gateway returning the Access-Control-Allow-Origin .

It’s Not Your Service Anymore: It’s The Gateway

When you enable CORS on HTTP API Gateway, you are moving the responsibility of defining CORS from your services to the gateway (See Http API Cors)!

If you configure CORS for an API, API Gateway ignores CORS headers returned from your backend integration.

This is great because CORS is a cross-cutting concern and as such should be handled outside of the service layer.

So, you should be able to remove any logic from your service around CORS if you are using CORS at the HTTP API Gateway level.

When the * Wildcard Is Not a Wildcard

In software engineering, we sometimes use the * to represent all or any. So, you would assume that setting Access-Control-Allow-Headers to * would result in any Access-Control-Request-Headers being accepted by the server. However, according to the documentation Access-Control-Request-Headers this isn’t always the case:

The value “*" only counts as a special wildcard value for requests without credentials (requests without HTTP cookies or HTTP authentication information). In requests with credentials, it is treated as the literal header name "*" without special semantics. Note that the Authorization header can't be wildcarded and always needs to be listed explicitly.

Setting Access-Control-Allow-Headers to * is possible in the HTTP API Gateway and it does get you past the preflight check because at this stage, apparently, the * is being treated as a wildcard. So, we are out of the wood, right?

NOPE

When we hit the actual route (post-preflight) we see the following error:

Access to fetch at ‘https://{gateway_id}.execute-api.us-east-1.amazonaws.com/auth/session/refresh' from origin ‘https://insights-dev.qloo.com:3000' has been blocked by CORS policy: Request header field fdi-version is not allowed by Access-Control-Allow-Headers in preflight response.

Darn. But it worked for the preflight but now the * is being treated without special semantics.

So, you will need to explicitly list every allowed header your application will ever need in the HTTP API Gateway’s Access-Control-Request-Headers . That means that you may think you are out of the woods because if you need to add more headers to your system, you’re going to get that error message.

Debugging Using CURL

curl can be a very useful tool for debugging but without the right headers, CORS will not run the server logic. The following curl command without authorizers will cause Supertokens to send an OTP email message to the user!

curl 'https://{gateway_id}.execute-api.{aws_region}.amazonaws.com/auth/signinup/code' \
--header 'Content-Type: text/plain' \
--data-raw '{"email":"someone@example.com"}'

To verify that CORS is working, and easily see what headers we get back, let’s add the appropriate flags:

  curl \
-H "Origin: https://your.webpage.com:3000" \
-H "Access-Control-Request-Method: OPTIONS" \
-H "Access-Control-Request-Headers: content-type,fdi-version,rid,st-auth-mode" \
-X OPTIONS --head \
https://{gateway_id}.execute-api.{aws_region}.amazonaws.com/auth/signinup/code

You should see a result like this:

HTTP/2 204 
date: Sat, 24 Jun 2023 22:58:02 GMT
access-control-allow-origin: https://insights-dev.qloo.com:3000
access-control-allow-methods: OPTIONS,POST
access-control-allow-headers: anti-csrf,authorization,content-type,fdi-version,rid,st-auth-mode
access-control-allow-credentials: true
access-control-max-age: 6000
apigw-requestid: {some_requestid}

What is important to note is that everything has to match correctly or you get no access-control-allow-* headers set. So for us:

  • Origin (https://your.webpage.com:3000) = access-control-allow-origin (https://your.webpage.com:3000)
  • Access-Control-Request-Method (POST) is contained in access-control-allow-methods (OPTIONS, POST)
  • All Access-Control-Request-Headers (content-type, fdi-version, rid, st-auth-mode) are in the access-control-allow-headers (anti-csrf, authorization, content-type, fdi-version, rid, st-auth-mode)

Note that if we even have one small difference, for example, we add derp to the Access-Control-Request-Headers, then the result we get has no Access-Control-Allow-* headers.

HTTP/2 204 
date: Sat, 24 Jun 2023 23:02:02 GMT
apigw-requestid: {some_requestid}ba

Serverless Configuration For HTTP API Gateway

The following is the basic serverless.ymlconfiguration file you will need to get Supertokens working with CORS handled by the HTTP API Gateway.

# See https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml

service: your-service
provider:
name: aws
# See https://www.serverless.com/framework/docs/providers/aws/guide/deploying#deployment-method
deploymentMethod: direct
stage: ${sls:stage}
region: us-east-1

# See https://www.serverless.com/framework/docs/providers/aws/guide/functions#vpc-configuration
vpc:
securityGroupIds:
- {sg_ids}
subnetIds:
- {sub_nets}

# See https://www.serverless.com/framework/docs/providers/aws/events/http-api#access-logs
logs:
httpApi: true

# function specific overloads
runtime: nodejs18.x
timeout: 29

httpApi:

# See https://www.serverless.com/framework/docs/providers/aws/events/http-api#detailed-metrics
metrics: false

# See https://www.serverless.com/framework/docs/providers/aws/events/http-api#tags
useProviderTags: true

# See https://www.serverless.com/framework/docs/providers/aws/events/http-api#cors-setup
cors:
allowedOrigins:
- 'https://your.webpage.com:3000'
allowedHeaders:
- Content-Type
- rid
- fdi-version
- anti-csrf
- authorization
- st-auth-mode
allowedMethods:
- OPTIONS
- POST
- GET
allowCredentials: true
exposedResponseHeaders:
- '*'
maxAge: 6000 # In seconds

package:
individually: true

functions:
- ${file(./src/functions/auth/serverless.yml)}

plugins:
- serverless-esbuild

Supertokens Handler

The Supertokens documentation provides a handler snippet for the AWS lambda. However, this documentation expects that the handler will be responsible for CORS. This is a basic handler without the CORS setup.

import supertokens from 'supertokens-node';
import { middleware } from 'supertokens-node/framework/awsLambda';
import middy from '@middy/core';
import getBackendConfig from './backend-config';

supertokens.init(getBackendConfig());

const postAuth = middy(
middleware(),
)
.onError((request) => {
throw request.error;
});

export default postAuth;

Conclusion

In conclusion, while the concepts underpinning CORS may initially seem simple, implementing it effectively, especially in AWS’s HTTP API Gateway, can present unexpected challenges.

However, understanding the nuances of ‘Access-Control-Allow-Origin’ and ‘Access-Control-Allow-Headers’, as well as the means to debug CORS issues, can help you navigate these complexities.

Remember, CORS is not an authorizer, but a mechanism designed to ensure secure cross-domain communication. It’s crucial to note that the configurations are specific to AWS’s HTTP API Gateway, and they may not apply to other services. With the insights provided in this post, you should now be better equipped to run SuperTokens middle tier in an AWS lambda while implementing CORS at the HTTP API Gateway layer.

--

--

Eric Hosick
The Opinionated Software Architect

Creator, entrepreneur, software architect, software engineer, lecturer, and technologist.