CORS, AWS HTTP API Gateway, Lambdas, Serverless, and SuperTokens
A story of confusing documentation, standards, and the misunderstanding of CORS.
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 theAccess-Control-Allow-Headers
defined in the HTTP API Gateway does not contain every header requested byAccess-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 anyAccess-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 theAccess-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 theOrigin: ...
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 tocurl
.
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 theAuthorization
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 inaccess-control-allow-methods
(OPTIONS
,POST
)- All
Access-Control-Request-Headers
(content-type
,fdi-version
,rid
,st-auth-mode
) are in theaccess-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.yml
configuration 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.