How to setup CORS for Lambda Proxy integration

How to solve CORS errors with API Gateway and Lambda proxy integration

Author's image
Tamás Sallai
3 mins

CORS with Lambda

The URL of an API behind API Gateway is in the form of https://<id>.execute-api.<region>.amazonaws.com, but your users are usually using a different URL to get your frontend. This can be a website hosted from an S3 bucket, a CloudFront distribution, or something entirely different. But since the domains are different, calling the API is a cross-origin request that requires CORS headers, most apparent from this error:

Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

In this example, I'll use a website deployed to an S3 bucket. This is cross-origin as the bucket website URL is different than the API Gateway URL.

resource "aws_s3_bucket" "frontend_bucket" {
  website {
    index_document = "index.html"
  }
}

With Lambda proxy integration, the CORS headers are to be set on the Lambda-side instead of on the API Gateway. I'll assume you're using this setup in this article.

Credentials

Cross-origin requests can be made in two ways: with or without credentials. Credentials in this context are anything that makes the user identifiable, which usually means cookies. Without cookies sent, the backend gets an anonymous request. With credentials included, the server receives the cookies and can identify the logged-in user.

When a fetch is made, by default, it won't include credentials in cross-origin requests:

fetch("URL") // no credentials
fetch("URL", {credentials: "include"}) // with credentials

Without credentials

If you don't need to include cookies or other credentials, then set Access-Control-Allow-Origin: *:

return {
	statusCode: 200,
	headers: {
		"Access-Control-Allow-Origin": "*",
	},
	body: ...,
};

This way the API can be called from anywhere but without login information. If you need to, you can still use query parameters to enforce authorization, just like signed URLs do. You can pass a JWT token this way or use any other means of credentials.

With credentials

What if you need to use cookies?

In this case, the server needs to include the Access-Control-Allow-Credentials: true header and also it must not use the Access-Control-Allow-Origin: *.

Because the wildcard is not allowed in this case, you need a whitelist of domains and need to verify the Origin header. This list should come from the environment as it might not be known build-time:

resource "aws_lambda_function" "lambda" {
	# ...
  environment {
    variables = {
      ALLOWED_ORIGINS = join(" ", ["http://${aws_s3_bucket.frontend_bucket.website_endpoint}"])
    }
  }
}

With the whitelist, requests made from allowed domains will get the CORS headers, but those from other domains won't.

Then to check the Origin header and set the CORS response headers only for whitelisted domains present in this environment variable:

const corsHeaders = (headers) => {
	const origin = headers.origin || headers.Origin;

	if (process.env.ALLOWED_ORIGINS.split(" ").includes(origin)) {
		return {
			"Access-Control-Allow-Origin": origin,
			"Access-Control-Allow-Credentials": "true",
			"Vary": "Origin", // for proxies
		};
	}
};

// ...

return {
	statusCode: 200,
	headers: {
		...corsHeaders(event.headers),
	},
}

Conclusion

Using multiple domains always comes with cross-origin errors and it's easy to get lost in all the nuances and edge cases of the relevant specifications. I hope this guide will help you get an overview of CORS and solve CORS-related errors with Lambda and API Gateway.

November 19, 2019
In this article