diff --git a/docs/utilities/cors.md b/docs/utilities/cors.md
new file mode 100644
index 000000000..0a1061fea
--- /dev/null
+++ b/docs/utilities/cors.md
@@ -0,0 +1,199 @@
+---
+title: Cors
+description: Utility
+---
+
+The Cors utility helps configuring [CORS (Cross-Origin Resource Sharing)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) for your Lambda function when used with API Gateway and Lambda proxy integration.
+When configured as Lambda proxy integration, the function must set CORS HTTP headers in the response ([doc](https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html#:~:text=Enabling%20CORS%20support%20for%20Lambda%20or%20HTTP%20proxy%20integrations)).
+
+**Key features**
+
+* Automatically set the CORS HTTP headers in the response.
+* Multi-origins is supported, only the origin matching the request origin will be returned.
+* Support environment variables or programmatic configuration
+
+## Install
+
+To install this utility, add the following dependency to your project.
+
+=== "Maven"
+
+ ```xml
+
+ software.amazon.lambda
+ powertools-cors
+ {{ powertools.version }}
+
+
+
+
+
+ ...
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.14.0
+
+ 1.8
+ 1.8
+ 1.8
+
+
+ software.amazon.lambda
+ powertools-cors
+
+ ...
+
+
+
+
+
+ compile
+
+
+
+
+ ...
+
+
+ ```
+=== "Gradle"
+
+ ```groovy
+ plugins{
+ id 'java'
+ id 'io.freefair.aspectj.post-compile-weaving' version '6.3.0'
+ }
+
+ dependencies {
+ ...
+ aspect 'software.amazon.lambda:powertools-cors:{{ powertools.version }}'
+ }
+ ```
+
+## Cors Annotation
+
+You can use the `@CrossOrigin` annotation on the `handleRequest` method of a class that implements `RequestHandler`
+
+=== "Cross Origin Annotation"
+
+```java hl_lines="3"
+public class FunctionProxy implements RequestHandler {
+
+ @CrossOrigin
+ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
+ // ...
+ return new APIGatewayProxyResponseEvent()
+ .withStatusCode(200)
+ .withHeaders(headers)
+ .withBody(body);
+ }
+}
+```
+
+## Configuration
+
+### Using the annotation
+
+=== "FunctionProxy.java"
+
+The annotation provides all the parameters required to set up the different CORS headers:
+
+| parameter | Default | Description |
+|----------------------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
+| **allowedHeaders** | `Authorization, *` ([*](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers#directives)) | [`Access-Control-Allow-Headers`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) header |
+| **exposedHeaders** | `*` | [`Access-Control-Expose-Headers`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers) header |
+| **origins** | `*` | [`Access-Control-Allow-Origin`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) header |
+| **methods** | `*` | [`Access-Control-Allow-Methods`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) header |
+| **allowCredentials** | `true` | [`Access-Control-Allow-Credentials`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) header |
+| **maxAge** | `29` | [`Access-Control-Max-Age`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) header |
+
+**Example:**
+
+=== "FunctionProxy.java"
+
+```java hl_lines="3-7"
+public class FunctionProxy implements RequestHandler {
+
+ @CrossOrigin(
+ origins = "http://origin.com, https://other.origin.com",
+ allowedHeaders = "Authorization, Content-Type, X-API-Key",
+ methods = "POST, OPTIONS"
+ )
+ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
+ // ...
+ return new APIGatewayProxyResponseEvent()
+ .withStatusCode(200)
+ .withHeaders(headers)
+ .withBody(body);
+ }
+}
+```
+
+
+### Using Environment variables
+
+You can configure the CORS header values in the environment variables of your Lambda function. They will have precedence over the annotation configuration.
+This way you can externalize the configuration and change it without changing the code.
+
+| Environment variable | Default | Description |
+|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
+| **ACCESS_CONTROL_ALLOW_HEADERS** | `Authorization, *` ([*](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers#directives)) | [`Access-Control-Allow-Headers`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) header |
+| **ACCESS_CONTROL_EXPOSE_HEADERS** | `*` | [`Access-Control-Expose-Headers`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers) header |
+| **ACCESS_CONTROL_ALLOW_ORIGIN** | `*` | [`Access-Control-Allow-Origin`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) header |
+| **ACCESS_CONTROL_ALLOW_METHODS** | `*` | [`Access-Control-Allow-Methods`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) header |
+| **ACCESS_CONTROL_ALLOW_CREDENTIALS** | `true` | [`Access-Control-Allow-Credentials`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) header |
+| **ACCESS_CONTROL_MAX_AGE** | `29` | [`Access-Control-Max-Age`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) header |
+
+**Example:**
+
+=== "SAM template"
+
+```yaml hl_lines="12-17"
+ CorsFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ CodeUri: Function
+ Handler: CorsFunction::handleRequest
+ Runtime: java11
+ Architectures:
+ - x86_64
+ MemorySize: 512
+ Environment:
+ Variables:
+ ACCESS_CONTROL_ALLOW_HEADERS: 'Authorization, Content-Type, X-API-Key'
+ ACCESS_CONTROL_EXPOSE_HEADERS: '*'
+ ACCESS_CONTROL_ALLOW_ORIGIN: 'https://mydomain.com'
+ ACCESS_CONTROL_ALLOW_METHODS: 'OPTIONS, POST'
+ ACCESS_CONTROL_MAX_AGE: '300'
+ ACCESS_CONTROL_ALLOW_CREDENTIALS: 'true'
+ Events:
+ MyApi:
+ Type: Api
+ Properties:
+ Path: /cors
+ Method: post
+```
+
+## Advanced information about origin configuration
+Browsers do no support multiple origins (comma-separated) in the `Access-Control-Allow-Origin` header. Only one must be provided.
+If your backend can be reached by multiple frontend applications (with different origins),
+the function must return the `Access-Control-Allow-Origin` header that matches the `origin` header in the request.
+
+Lambda Powertools handles this for you. You can configure multiple origins, separated by commas:
+
+=== "Multiple origins"
+
+```java hl_lines="2"
+ @CrossOrigin(
+ origins = "http://origin.com, https://other.origin.com"
+ )
+```
+
+!!! warning "Origins must be well-formed"
+ Origins must be well-formed URLs: `{protocol}://{host}[:{port}]` where protocol can be `http` or `https` and port is optional. [Mode details](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#definition_of_an_origin).
+
+ `*`, `http://*` and `https://*` are also valid origins.
+
+!!! info "`Vary` header"
+ Note that when returning a specific value (rather than `*`), the `Vary` header must be set to `Origin` ([doc](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin#cors_and_caching)). Lambda Powertools handles this for you.
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
index e16d01450..db0e7b9d2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -17,6 +17,7 @@ nav:
- utilities/validation.md
- utilities/custom_resources.md
- utilities/serialization.md
+ - utilities/cors.md
theme:
name: material
diff --git a/pom.xml b/pom.xml
index c38099545..2271a909f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,7 @@
powertools-test-suite
powertools-cloudformation
powertools-idempotency
+ powertools-cors
diff --git a/powertools-cors/pom.xml b/powertools-cors/pom.xml
new file mode 100644
index 000000000..7a77e931b
--- /dev/null
+++ b/powertools-cors/pom.xml
@@ -0,0 +1,117 @@
+
+
+ 4.0.0
+
+ powertools-parent
+ software.amazon.lambda
+ 1.12.0
+
+ powertools-cors
+ AWS Lambda Powertools for Java library CORS Configuration
+
+ https://aws.amazon.com/lambda/
+
+ GitHub Issues
+ https://github.com/awslabs/aws-lambda-powertools-java/issues
+
+
+ https://github.com/awslabs/aws-lambda-powertools-java.git
+
+
+
+ AWS Lambda Powertools team
+ Amazon Web Services
+ https://aws.amazon.com/
+
+
+
+
+
+ ossrh
+ https://aws.oss.sonatype.org/content/repositories/snapshots
+
+
+
+
+
+ software.amazon.lambda
+ powertools-core
+
+
+ com.amazonaws
+ aws-lambda-java-events
+
+
+ org.aspectj
+ aspectjrt
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-inline
+ test
+
+
+ com.amazonaws
+ aws-lambda-java-tests
+ test
+
+
+ org.junit-pioneer
+ junit-pioneer
+ 1.6.2
+ test
+
+
+
+
+
+ jdk16
+
+ [16,)
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M5
+
+
+ --add-opens java.base/java.util=ALL-UNNAMED
+ --add-opens java.base/java.lang=ALL-UNNAMED
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/Constants.java b/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/Constants.java
new file mode 100644
index 000000000..6628b4f71
--- /dev/null
+++ b/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/Constants.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package software.amazon.lambda.powertools.cors;
+
+public interface Constants {
+ String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
+ String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
+ String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
+ String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
+ String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
+ String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
+
+ String VARY = "Vary";
+ String VARY_ORIGIN = "Origin";
+
+ String ENV_ACCESS_CONTROL_ALLOW_HEADERS = "ACCESS_CONTROL_ALLOW_HEADERS";
+ String ENV_ACCESS_CONTROL_EXPOSE_HEADERS = "ACCESS_CONTROL_EXPOSE_HEADERS";
+ String ENV_ACCESS_CONTROL_ALLOW_ORIGIN = "ACCESS_CONTROL_ALLOW_ORIGIN";
+ String ENV_ACCESS_CONTROL_ALLOW_METHODS = "ACCESS_CONTROL_ALLOW_METHODS";
+ String ENV_ACCESS_CONTROL_ALLOW_CREDENTIALS = "ACCESS_CONTROL_ALLOW_CREDENTIALS";
+ String ENV_ACCESS_CONTROL_MAX_AGE = "ACCESS_CONTROL_MAX_AGE";
+
+ String WILDCARD = "*";
+
+ String DEFAULT_ACCESS_CONTROL_ALLOW_HEADERS = "Authorization, *";
+ String DEFAULT_ACCESS_CONTROL_EXPOSE_HEADERS = WILDCARD;
+ String DEFAULT_ACCESS_CONTROL_ALLOW_METHODS = WILDCARD;
+ String DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN = WILDCARD;
+ boolean DEFAULT_ACCESS_CONTROL_ALLOW_CREDENTIALS = true;
+ int DEFAULT_ACCESS_CONTROL_MAX_AGE = 29;
+
+}
diff --git a/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/CrossOrigin.java b/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/CrossOrigin.java
new file mode 100644
index 000000000..7db320680
--- /dev/null
+++ b/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/CrossOrigin.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package software.amazon.lambda.powertools.cors;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static software.amazon.lambda.powertools.cors.Constants.*;
+
+/**
+ * CrossOrigin annotation to be placed on a Lambda function configured with API Gateway as proxy.
+ * Your function must implement RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent>
+ * It will automatically add the Cross-Origins Resource Sharing (CORS) headers in the response sent to API Gateway & the client.
+ * By default, it allows everything (all methods, headers and origins). Make sure to restrict to your need.
+ *
You can use the annotation alone and keep the default setup (or use environment variables instead, see below):
+ *
+ * @CrossOrigin
+ * public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context ctx) {
+ * // ...
+ * return response;
+ * }
+ *
+ *
+ * You can use the annotation and customize the parameters:
+ *
+ * @CrossOrigin(
+ * origins = "http://origin.com",
+ * allowedHeaders = "Content-Type",
+ * methods = "POST, OPTIONS"
+ * )
+ * public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context ctx) {
+ * // ...
+ * return response;
+ * }
+ *
+ *
+ * You can also use the following environment variables if you wish to externalize the configuration:
+ * ACCESS_CONTROL_ALLOW_HEADERS
+ * ACCESS_CONTROL_EXPOSE_HEADERS
+ * ACCESS_CONTROL_ALLOW_ORIGIN
+ * ACCESS_CONTROL_ALLOW_METHODS
+ * ACCESS_CONTROL_ALLOW_CREDENTIALS
+ * ACCESS_CONTROL_MAX_AGE
+ *
+ * Note that if you configure cross-origin both in environment variables and programmatically with the annotation,
+ * environment variables take the precedence over the annotation.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface CrossOrigin {
+ /**
+ * Allowed methods (Access-Control-Request-Methods). You can set several methods separated by a comma (',').
+ * Default: * (allow all methods)
+ */
+ String methods() default DEFAULT_ACCESS_CONTROL_ALLOW_METHODS;
+ /**
+ * Allowed origins (Access-Control-Allow-Origin). You can set several origins separated by a comma (',').
+ * If you do so, only the origin matching the client origin will be sent in the response.
+ * An origin must be a well-formed url: {protocol}://{host}[:{port}]
+ * Default: * (allow all origins)
+ */
+ String origins() default DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN;
+ /**
+ * Allowed headers (Access-Control-Request-Headers). You can set several headers separated by a comma (',').
+ * Note that Authorization is not part of the wildcard and must be set explicitly
+ * Default: Authorization, * (allow all headers)
+ */
+ String allowedHeaders() default DEFAULT_ACCESS_CONTROL_ALLOW_HEADERS;
+ /**
+ * Exposed headers (Access-Control-Expose-Headers). You can set several headers separated by a comma (',').
+ * Default: * (expose all headers)
+ */
+ String exposedHeaders() default DEFAULT_ACCESS_CONTROL_EXPOSE_HEADERS;
+ /**
+ * If credential mode is allowed (ACCESS_CONTROL_ALLOW_CREDENTIALS)
+ * Default: true
+ */
+ boolean allowCredentials() default DEFAULT_ACCESS_CONTROL_ALLOW_CREDENTIALS;
+ /**
+ * How long (in seconds) the preflight request is cached (default: 29)
+ */
+ int maxAge() default DEFAULT_ACCESS_CONTROL_MAX_AGE;
+}
diff --git a/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/internal/CrossOriginAspect.java b/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/internal/CrossOriginAspect.java
new file mode 100644
index 000000000..8e1d1257e
--- /dev/null
+++ b/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/internal/CrossOriginAspect.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package software.amazon.lambda.powertools.cors.internal;
+
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import software.amazon.lambda.powertools.cors.CrossOrigin;
+
+import java.lang.reflect.Method;
+
+import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.isHandlerMethod;
+import static software.amazon.lambda.powertools.core.internal.LambdaHandlerProcessor.placedOnRequestHandler;
+
+@Aspect
+public class CrossOriginAspect {
+
+ private static final Logger LOG = LogManager.getLogger(CrossOriginAspect.class);
+
+ @Pointcut("@annotation(crossOrigin)")
+ public void callAt(CrossOrigin crossOrigin) {
+ }
+
+ @Around(value = "callAt(crossOrigin) && execution(@CrossOrigin * *.*(..))", argNames = "pjp,crossOrigin")
+ public Object around(ProceedingJoinPoint pjp,
+ CrossOrigin crossOrigin) throws Throwable {
+ Object result = pjp.proceed(pjp.getArgs());
+
+ if (!isHandlerMethod(pjp) || !placedOnRequestHandler(pjp) || !isApiGatewayRequest(pjp)) {
+ LOG.warn("@Cors annotation must be used on a Lambda handler that receives APIGatewayProxyRequestEvent and return APIGatewayProxyResponseEvent");
+ return result;
+ }
+
+ CrossOriginHandler crossOriginHandler = new CrossOriginHandler(crossOrigin);
+ return proceed(pjp, result, crossOriginHandler);
+ }
+
+ private Object proceed(ProceedingJoinPoint pjp, Object result, CrossOriginHandler crossOriginHandler) {
+ try {
+ APIGatewayProxyRequestEvent request = (APIGatewayProxyRequestEvent) pjp.getArgs()[0];
+ APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result;
+
+ return crossOriginHandler.process(request, response);
+ } catch (Exception e) {
+ // should not happen, but we don't want to fail because of this
+ LOG.error("Error while setting CORS headers. If you think this is an issue in PowerTools, please open an issue on GitHub.", e);
+ }
+ return result;
+ }
+
+ private boolean isApiGatewayRequest(ProceedingJoinPoint pjp) {
+ Method method = ((MethodSignature) pjp.getSignature()).getMethod();
+ return method.getReturnType().equals(APIGatewayProxyResponseEvent.class) &&
+ pjp.getArgs()[0] instanceof APIGatewayProxyRequestEvent;
+ }
+}
diff --git a/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/internal/CrossOriginHandler.java b/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/internal/CrossOriginHandler.java
new file mode 100644
index 000000000..dea09f10d
--- /dev/null
+++ b/powertools-cors/src/main/java/software/amazon/lambda/powertools/cors/internal/CrossOriginHandler.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package software.amazon.lambda.powertools.cors.internal;
+
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import software.amazon.lambda.powertools.cors.CrossOrigin;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+
+import static software.amazon.lambda.powertools.cors.Constants.*;
+import static software.amazon.lambda.powertools.cors.Constants.ACCESS_CONTROL_MAX_AGE;
+
+public class CrossOriginHandler {
+ private static final Logger LOG = LogManager.getLogger(CrossOriginHandler.class);
+
+ private final String allowHeaders;
+ private final String exposedHeaders;
+ private final String allowMethods;
+ private final String allowOrigins;
+ private final String allowCredentials;
+ private final String maxAge;
+
+ CrossOriginHandler(CrossOrigin crossOrigin) {
+ allowHeaders = Optional.ofNullable(System.getenv(ENV_ACCESS_CONTROL_ALLOW_HEADERS)).orElse(crossOrigin.allowedHeaders());
+ exposedHeaders = Optional.ofNullable(System.getenv(ENV_ACCESS_CONTROL_EXPOSE_HEADERS)).orElse(crossOrigin.exposedHeaders());
+ allowMethods = Optional.ofNullable(System.getenv(ENV_ACCESS_CONTROL_ALLOW_METHODS)).orElse(crossOrigin.methods());
+ allowOrigins = Optional.ofNullable(System.getenv(ENV_ACCESS_CONTROL_ALLOW_ORIGIN)).orElse(crossOrigin.origins());
+ allowCredentials = Optional.ofNullable(System.getenv(ENV_ACCESS_CONTROL_ALLOW_CREDENTIALS)).orElse(String.valueOf(crossOrigin.allowCredentials()));
+ maxAge = Optional.ofNullable(System.getenv(ENV_ACCESS_CONTROL_MAX_AGE)).orElse(String.valueOf(crossOrigin.maxAge()));
+ }
+
+ public APIGatewayProxyResponseEvent process(APIGatewayProxyRequestEvent request, APIGatewayProxyResponseEvent response) {
+ try {
+ String origin = request.getHeaders().get("origin");
+ LOG.debug("origin=" + origin);
+
+ if (response.getHeaders() == null) {
+ response.setHeaders(new HashMap<>());
+ }
+ processOrigin(response, origin);
+ response.getHeaders().put(ACCESS_CONTROL_ALLOW_HEADERS, allowHeaders);
+ response.getHeaders().put(ACCESS_CONTROL_EXPOSE_HEADERS, exposedHeaders);
+ response.getHeaders().put(ACCESS_CONTROL_ALLOW_METHODS, allowMethods);
+ response.getHeaders().put(ACCESS_CONTROL_ALLOW_CREDENTIALS, allowCredentials);
+ response.getHeaders().put(ACCESS_CONTROL_MAX_AGE, maxAge);
+ } catch (Exception e) {
+ // should not happen, but we don't want to fail because of this
+ LOG.error("Error while setting CORS headers. If you think this is an issue in PowerTools, please open an issue on GitHub", e);
+ }
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("====== CORS Headers ======");
+ response.getHeaders().forEach((key, value) -> {if (key.startsWith("Access-Control")) { LOG.debug(key + " -> " + value); }} );
+ }
+ return response;
+ }
+
+ private void processOrigin(APIGatewayProxyResponseEvent response, String origin) throws MalformedURLException {
+ if (allowOrigins != null) {
+ List allowOriginList = Arrays.asList(allowOrigins.split("\\s*,\\s*"));
+ if (allowOriginList.stream().anyMatch(WILDCARD::equals)) {
+ response.getHeaders().put(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
+ response.getHeaders().put(VARY, VARY_ORIGIN);
+ } else {
+ URL url = new URL(origin);
+ allowOriginList.stream().filter(o -> {
+ try {
+ URL allowUrl = new URL(o);
+ return url.getProtocol().equals(allowUrl.getProtocol()) &&
+ url.getPort() == allowUrl.getPort() &&
+ (url.getHost().equals(allowUrl.getHost()) || allowUrl.getHost().equals(WILDCARD));
+ } catch (MalformedURLException e) {
+ LOG.warn("Allowed origin '"+o+"' is malformed. It should contain a protocol, a host and eventually a port");
+ return false;
+ }
+ }).findAny().ifPresent(validOrigin -> {
+ response.getHeaders().put(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
+ response.getHeaders().put(VARY, VARY_ORIGIN);
+ });
+ }
+ }
+ }
+}
diff --git a/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/CrossOriginTest.java b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/CrossOriginTest.java
new file mode 100644
index 000000000..016b6668b
--- /dev/null
+++ b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/CrossOriginTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package software.amazon.lambda.powertools.cors;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+import com.amazonaws.services.lambda.runtime.tests.EventLoader;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.SetEnvironmentVariable;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import software.amazon.lambda.powertools.cors.handlers.CorsFunction;
+import software.amazon.lambda.powertools.cors.handlers.CorsNotOnHandlerFunction;
+import software.amazon.lambda.powertools.cors.handlers.CorsOnSqsFunction;
+import software.amazon.lambda.powertools.cors.handlers.DefaultCorsFunction;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static software.amazon.lambda.powertools.cors.Constants.*;
+
+public class CrossOriginTest {
+ @Mock
+ private Context context;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void functionWithCustomAndDefaultCorsConfig_shouldHaveCorsHeaders() {
+ CorsFunction function = new CorsFunction();
+ APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
+ APIGatewayProxyResponseEvent response = function.handleRequest(event, context);
+
+ Map headers = response.getHeaders();
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_ORIGIN, "http://origin.com");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_METHODS, "POST, OPTIONS");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_MAX_AGE, String.valueOf(DEFAULT_ACCESS_CONTROL_MAX_AGE));
+ assertThat(headers).containsEntry(ACCESS_CONTROL_EXPOSE_HEADERS, DEFAULT_ACCESS_CONTROL_EXPOSE_HEADERS);
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_CREDENTIALS, String.valueOf(DEFAULT_ACCESS_CONTROL_ALLOW_CREDENTIALS));
+ }
+
+ @Test
+ @SetEnvironmentVariable.SetEnvironmentVariables(value = {
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_ALLOW_HEADERS, value = "Content-Type, X-Amz-Date"),
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_ALLOW_ORIGIN, value = "https://example.com, http://origin.com"),
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_ALLOW_METHODS, value = "OPTIONS, POST"),
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_EXPOSE_HEADERS, value = "Content-Type, X-Amz-Date"),
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_ALLOW_CREDENTIALS, value = "true"),
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_MAX_AGE, value = "42"),
+ })
+ public void functionWithCustomCorsEnvVars_shouldHaveCorsHeaders() {
+ DefaultCorsFunction function = new DefaultCorsFunction();
+ APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
+ APIGatewayProxyResponseEvent response = function.handleRequest(event, context);
+
+ Map headers = response.getHeaders();
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_ORIGIN, "http://origin.com");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, X-Amz-Date");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, POST");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_MAX_AGE, "42");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_EXPOSE_HEADERS, "Content-Type, X-Amz-Date");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
+ }
+
+ @Test
+ public void functionWithCorsOnSqsEvent() {
+ CorsOnSqsFunction function = new CorsOnSqsFunction();
+ SQSEvent sqsEvent = EventLoader.loadSQSEvent("sqs_event.json");
+ SQSBatchResponse response = function.handleRequest(sqsEvent, context);
+ assertThat(response).isNotNull();
+ }
+
+ @Test
+ public void functionWithCorsNotOnHandler_shouldNotContainCorsHeaders() {
+ CorsNotOnHandlerFunction function = new CorsNotOnHandlerFunction();
+ APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
+ APIGatewayProxyResponseEvent response = function.handleRequest(event, context);
+ assertThat(response.getHeaders()).isNull();
+ }
+
+}
diff --git a/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/CorsFunction.java b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/CorsFunction.java
new file mode 100644
index 000000000..56a4d549a
--- /dev/null
+++ b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/CorsFunction.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package software.amazon.lambda.powertools.cors.handlers;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import software.amazon.lambda.powertools.cors.CrossOrigin;
+
+public class CorsFunction implements RequestHandler {
+
+ @CrossOrigin(
+ origins = "http://origin.com",
+ allowedHeaders = "Content-Type",
+ methods = "POST, OPTIONS"
+ )
+ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
+ return new APIGatewayProxyResponseEvent().withBody("OK");
+ }
+
+}
diff --git a/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/CorsNotOnHandlerFunction.java b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/CorsNotOnHandlerFunction.java
new file mode 100644
index 000000000..5a0bbb43b
--- /dev/null
+++ b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/CorsNotOnHandlerFunction.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package software.amazon.lambda.powertools.cors.handlers;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import software.amazon.lambda.powertools.cors.CrossOrigin;
+
+public class CorsNotOnHandlerFunction implements RequestHandler {
+
+ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
+ return new APIGatewayProxyResponseEvent().withBody(doSomething());
+ }
+
+ @CrossOrigin
+ private String doSomething() {
+ return "something";
+ }
+
+}
diff --git a/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/CorsOnSqsFunction.java b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/CorsOnSqsFunction.java
new file mode 100644
index 000000000..af6782711
--- /dev/null
+++ b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/CorsOnSqsFunction.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package software.amazon.lambda.powertools.cors.handlers;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+import software.amazon.lambda.powertools.cors.CrossOrigin;
+
+public class CorsOnSqsFunction implements RequestHandler {
+
+ @CrossOrigin
+ public SQSBatchResponse handleRequest(final SQSEvent input, final Context context) {
+ return new SQSBatchResponse();
+ }
+
+}
diff --git a/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/DefaultCorsFunction.java b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/DefaultCorsFunction.java
new file mode 100644
index 000000000..8f8778c76
--- /dev/null
+++ b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/handlers/DefaultCorsFunction.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package software.amazon.lambda.powertools.cors.handlers;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import software.amazon.lambda.powertools.cors.CrossOrigin;
+
+public class DefaultCorsFunction implements RequestHandler {
+
+ @CrossOrigin
+ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
+ return new APIGatewayProxyResponseEvent().withBody("OK");
+ }
+
+}
diff --git a/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/internal/CrossOriginHandlerTest.java b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/internal/CrossOriginHandlerTest.java
new file mode 100644
index 000000000..5fbe55e38
--- /dev/null
+++ b/powertools-cors/src/test/java/software/amazon/lambda/powertools/cors/internal/CrossOriginHandlerTest.java
@@ -0,0 +1,229 @@
+package software.amazon.lambda.powertools.cors.internal;
+
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import com.amazonaws.services.lambda.runtime.tests.EventLoader;
+import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.SetEnvironmentVariable;
+import software.amazon.lambda.powertools.cors.CrossOrigin;
+
+import java.lang.annotation.Annotation;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.*;
+import static software.amazon.lambda.powertools.cors.Constants.*;
+
+public class CrossOriginHandlerTest {
+
+ CrossOrigin defaultCors = new CrossOrigin() {
+
+ @Override
+ public Class extends Annotation> annotationType() {
+ return CrossOrigin.class;
+ }
+
+ @Override
+ public String methods() {
+ return DEFAULT_ACCESS_CONTROL_ALLOW_METHODS;
+ }
+
+ @Override
+ public String origins() {
+ return DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN;
+ }
+
+ @Override
+ public String allowedHeaders() {
+ return DEFAULT_ACCESS_CONTROL_ALLOW_HEADERS;
+ }
+
+ @Override
+ public String exposedHeaders() {
+ return DEFAULT_ACCESS_CONTROL_EXPOSE_HEADERS;
+ }
+
+ @Override
+ public boolean allowCredentials() {
+ return DEFAULT_ACCESS_CONTROL_ALLOW_CREDENTIALS;
+ }
+
+ @Override
+ public int maxAge() {
+ return DEFAULT_ACCESS_CONTROL_MAX_AGE;
+ }
+ };
+
+ @Test
+ public void defaultCorsConfiguration_shouldReturnDefaultHeaders() {
+ CrossOriginHandler handler = new CrossOriginHandler(defaultCors);
+ APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
+ APIGatewayProxyResponseEvent response = handler.process(event, new APIGatewayProxyResponseEvent().withBody("OK"));
+
+ assertThat(response.getBody()).isEqualTo("OK");
+ Map headers = response.getHeaders();
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_ORIGIN, "http://origin.com");
+ assertThat(headers).containsEntry(VARY, VARY_ORIGIN);
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_HEADERS, DEFAULT_ACCESS_CONTROL_ALLOW_HEADERS);
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_METHODS, DEFAULT_ACCESS_CONTROL_ALLOW_METHODS);
+ assertThat(headers).containsEntry(ACCESS_CONTROL_MAX_AGE, String.valueOf(DEFAULT_ACCESS_CONTROL_MAX_AGE));
+ assertThat(headers).containsEntry(ACCESS_CONTROL_EXPOSE_HEADERS, DEFAULT_ACCESS_CONTROL_EXPOSE_HEADERS);
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_CREDENTIALS, String.valueOf(DEFAULT_ACCESS_CONTROL_ALLOW_CREDENTIALS));
+ }
+
+ @Test
+ public void defaultCorsConfiguration_withExistingHeaders_shouldReturnDefaultAndExistingHeaders() {
+ CrossOriginHandler handler = new CrossOriginHandler(defaultCors);
+ APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
+ Map initialHeaders = new HashMap<>();
+ initialHeaders.put("Content-Type", "application/json");
+ APIGatewayProxyResponseEvent response = handler.process(event, new APIGatewayProxyResponseEvent().withBody("OK").withHeaders(initialHeaders));
+
+ assertThat(response.getBody()).isEqualTo("OK");
+ Map headers = response.getHeaders();
+ assertThat(headers).containsEntry("Content-Type", "application/json");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_ORIGIN, "http://origin.com");
+ assertThat(headers).containsEntry(VARY, VARY_ORIGIN);
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_HEADERS, DEFAULT_ACCESS_CONTROL_ALLOW_HEADERS);
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_METHODS, DEFAULT_ACCESS_CONTROL_ALLOW_METHODS);
+ assertThat(headers).containsEntry(ACCESS_CONTROL_MAX_AGE, String.valueOf(DEFAULT_ACCESS_CONTROL_MAX_AGE));
+ assertThat(headers).containsEntry(ACCESS_CONTROL_EXPOSE_HEADERS, DEFAULT_ACCESS_CONTROL_EXPOSE_HEADERS);
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_CREDENTIALS, String.valueOf(DEFAULT_ACCESS_CONTROL_ALLOW_CREDENTIALS));
+ }
+
+ @Test
+ public void defaultCorsConfig_withDifferentOrigin_shouldNotReturnAllowOriginHeader() {
+ CrossOrigin cors = new CrossOrigin() {
+
+ @Override
+ public Class extends Annotation> annotationType() {
+ return CrossOrigin.class;
+ }
+
+ @Override
+ public String methods() {
+ return DEFAULT_ACCESS_CONTROL_ALLOW_METHODS;
+ }
+
+ @Override
+ public String origins() {
+ return "http://other_origin.com, origin.com";
+ }
+
+ @Override
+ public String allowedHeaders() {
+ return DEFAULT_ACCESS_CONTROL_ALLOW_HEADERS;
+ }
+
+ @Override
+ public String exposedHeaders() {
+ return DEFAULT_ACCESS_CONTROL_EXPOSE_HEADERS;
+ }
+
+ @Override
+ public boolean allowCredentials() {
+ return DEFAULT_ACCESS_CONTROL_ALLOW_CREDENTIALS;
+ }
+
+ @Override
+ public int maxAge() {
+ return DEFAULT_ACCESS_CONTROL_MAX_AGE;
+ }
+ };
+ CrossOriginHandler handler = new CrossOriginHandler(cors);
+ APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
+ APIGatewayProxyResponseEvent response = handler.process(event, new APIGatewayProxyResponseEvent().withBody("OK"));
+
+ Map headers = response.getHeaders();
+ assertThat(headers).doesNotContainKey(ACCESS_CONTROL_ALLOW_ORIGIN);
+ assertThat(headers).doesNotContainKey(VARY);
+ }
+
+ @Test
+ public void defaultCorsConfig_withMalformedOrigin_shouldNotReturnAllowOriginHeader() {
+ CrossOrigin cors = new CrossOrigin() {
+
+ @Override
+ public Class extends Annotation> annotationType() {
+ return CrossOrigin.class;
+ }
+
+ @Override
+ public String methods() {
+ return DEFAULT_ACCESS_CONTROL_ALLOW_METHODS;
+ }
+
+ @Override
+ public String origins() {
+ return "http://origin.com";
+ }
+
+ @Override
+ public String allowedHeaders() {
+ return DEFAULT_ACCESS_CONTROL_ALLOW_HEADERS;
+ }
+
+ @Override
+ public String exposedHeaders() {
+ return DEFAULT_ACCESS_CONTROL_EXPOSE_HEADERS;
+ }
+
+ @Override
+ public boolean allowCredentials() {
+ return DEFAULT_ACCESS_CONTROL_ALLOW_CREDENTIALS;
+ }
+
+ @Override
+ public int maxAge() {
+ return DEFAULT_ACCESS_CONTROL_MAX_AGE;
+ }
+ };
+ CrossOriginHandler handler = new CrossOriginHandler(cors);
+ APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event_malformed_origin.json");
+ APIGatewayProxyResponseEvent response = handler.process(event, new APIGatewayProxyResponseEvent().withBody("OK"));
+
+ Map headers = response.getHeaders();
+ assertThat(headers).doesNotContainKey(ACCESS_CONTROL_ALLOW_ORIGIN);
+ assertThat(headers).doesNotContainKey(VARY);
+ }
+
+ @Test
+ @SetEnvironmentVariable.SetEnvironmentVariables(value = {
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_ALLOW_HEADERS, value = "Content-Type, X-Amz-Date"),
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_ALLOW_ORIGIN, value = "http://origin.com"),
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_ALLOW_METHODS, value = "OPTIONS, POST"),
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_EXPOSE_HEADERS, value = "Content-Type, X-Amz-Date"),
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_ALLOW_CREDENTIALS, value = "true"),
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_MAX_AGE, value = "42"),
+ })
+ public void corsConfigWithEnvVars_shouldReturnCorrectHeaders() {
+ CrossOriginHandler handler = new CrossOriginHandler(defaultCors);
+ APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
+ APIGatewayProxyResponseEvent response = handler.process(event, new APIGatewayProxyResponseEvent().withBody("OK"));
+
+ assertThat(response.getBody()).isEqualTo("OK");
+ Map headers = response.getHeaders();
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_ORIGIN, "http://origin.com");
+ assertThat(headers).containsEntry(VARY, VARY_ORIGIN);
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, X-Amz-Date");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_METHODS, "OPTIONS, POST");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_MAX_AGE, "42");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_EXPOSE_HEADERS, "Content-Type, X-Amz-Date");
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
+ }
+
+ @Test
+ @SetEnvironmentVariable.SetEnvironmentVariables(value = {
+ @SetEnvironmentVariable(key = ENV_ACCESS_CONTROL_ALLOW_ORIGIN, value = "http://*")
+ })
+ public void corsWithWildcardOrigin_shouldReturnCorrectOrigin() {
+ CrossOriginHandler handler = new CrossOriginHandler(defaultCors);
+ APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_event.json");
+ APIGatewayProxyResponseEvent response = handler.process(event, new APIGatewayProxyResponseEvent().withBody("OK"));
+
+ assertThat(response.getBody()).isEqualTo("OK");
+ Map headers = response.getHeaders();
+ assertThat(headers).containsEntry(ACCESS_CONTROL_ALLOW_ORIGIN, "http://origin.com");
+ assertThat(headers).containsEntry(VARY, VARY_ORIGIN);
+ }
+}
diff --git a/powertools-cors/src/test/resources/apigw_event.json b/powertools-cors/src/test/resources/apigw_event.json
new file mode 100644
index 000000000..9c9e69a15
--- /dev/null
+++ b/powertools-cors/src/test/resources/apigw_event.json
@@ -0,0 +1,62 @@
+{
+ "body": "{\"id\":1234, \"name\":\"product\", \"price\":42}",
+ "resource": "/{proxy+}",
+ "path": "/path/to/resource",
+ "httpMethod": "POST",
+ "isBase64Encoded": false,
+ "queryStringParameters": {
+ "foo": "bar"
+ },
+ "pathParameters": {
+ "proxy": "/path/to/resource"
+ },
+ "stageVariables": {
+ "baz": "qux"
+ },
+ "headers": {
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Encoding": "gzip, deflate, sdch",
+ "Accept-Language": "en-US,en;q=0.8",
+ "Cache-Control": "max-age=0",
+ "CloudFront-Forwarded-Proto": "https",
+ "CloudFront-Is-Desktop-Viewer": "true",
+ "CloudFront-Is-Mobile-Viewer": "false",
+ "CloudFront-Is-SmartTV-Viewer": "false",
+ "CloudFront-Is-Tablet-Viewer": "false",
+ "CloudFront-Viewer-Country": "US",
+ "origin": "http://origin.com",
+ "Upgrade-Insecure-Requests": "1",
+ "User-Agent": "Custom User Agent String",
+ "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
+ "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
+ "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
+ "X-Forwarded-Port": "443",
+ "X-Forwarded-Proto": "https"
+ },
+ "requestContext": {
+ "accountId": "123456789012",
+ "resourceId": "123456",
+ "stage": "prod",
+ "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
+ "requestTime": "09/Apr/2015:12:34:56 +0000",
+ "requestTimeEpoch": 1428582896000,
+ "identity": {
+ "cognitoIdentityPoolId": null,
+ "accountId": null,
+ "cognitoIdentityId": null,
+ "caller": null,
+ "accessKey": null,
+ "sourceIp": "127.0.0.1",
+ "cognitoAuthenticationType": null,
+ "cognitoAuthenticationProvider": null,
+ "userArn": null,
+ "userAgent": "Custom User Agent String",
+ "user": null
+ },
+ "path": "/prod/path/to/resource",
+ "resourcePath": "/{proxy+}",
+ "httpMethod": "POST",
+ "apiId": "1234567890",
+ "protocol": "HTTP/1.1"
+ }
+}
diff --git a/powertools-cors/src/test/resources/apigw_event_malformed_origin.json b/powertools-cors/src/test/resources/apigw_event_malformed_origin.json
new file mode 100644
index 000000000..602203f17
--- /dev/null
+++ b/powertools-cors/src/test/resources/apigw_event_malformed_origin.json
@@ -0,0 +1,62 @@
+{
+ "body": "{\"id\":1234, \"name\":\"product\", \"price\":42}",
+ "resource": "/{proxy+}",
+ "path": "/path/to/resource",
+ "httpMethod": "POST",
+ "isBase64Encoded": false,
+ "queryStringParameters": {
+ "foo": "bar"
+ },
+ "pathParameters": {
+ "proxy": "/path/to/resource"
+ },
+ "stageVariables": {
+ "baz": "qux"
+ },
+ "headers": {
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Encoding": "gzip, deflate, sdch",
+ "Accept-Language": "en-US,en;q=0.8",
+ "Cache-Control": "max-age=0",
+ "CloudFront-Forwarded-Proto": "https",
+ "CloudFront-Is-Desktop-Viewer": "true",
+ "CloudFront-Is-Mobile-Viewer": "false",
+ "CloudFront-Is-SmartTV-Viewer": "false",
+ "CloudFront-Is-Tablet-Viewer": "false",
+ "CloudFront-Viewer-Country": "US",
+ "origin": "malformed-origin-should-not-happen.com",
+ "Upgrade-Insecure-Requests": "1",
+ "User-Agent": "Custom User Agent String",
+ "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
+ "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
+ "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
+ "X-Forwarded-Port": "443",
+ "X-Forwarded-Proto": "https"
+ },
+ "requestContext": {
+ "accountId": "123456789012",
+ "resourceId": "123456",
+ "stage": "prod",
+ "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
+ "requestTime": "09/Apr/2015:12:34:56 +0000",
+ "requestTimeEpoch": 1428582896000,
+ "identity": {
+ "cognitoIdentityPoolId": null,
+ "accountId": null,
+ "cognitoIdentityId": null,
+ "caller": null,
+ "accessKey": null,
+ "sourceIp": "127.0.0.1",
+ "cognitoAuthenticationType": null,
+ "cognitoAuthenticationProvider": null,
+ "userArn": null,
+ "userAgent": "Custom User Agent String",
+ "user": null
+ },
+ "path": "/prod/path/to/resource",
+ "resourcePath": "/{proxy+}",
+ "httpMethod": "POST",
+ "apiId": "1234567890",
+ "protocol": "HTTP/1.1"
+ }
+}
diff --git a/powertools-cors/src/test/resources/sqs_event.json b/powertools-cors/src/test/resources/sqs_event.json
new file mode 100644
index 000000000..d33db4b53
--- /dev/null
+++ b/powertools-cors/src/test/resources/sqs_event.json
@@ -0,0 +1,40 @@
+{
+ "Records": [
+ {
+ "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b4b54",
+ "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==",
+ "body": "{\n \"id\": 1234,\n \"name\": \"product\",\n \"price\": 42\n}",
+ "attributes": {
+ "ApproximateReceiveCount": "1",
+ "SentTimestamp": "1601975706495",
+ "SenderId": "AROAIFU437PVZ5L2J53F5",
+ "ApproximateFirstReceiveTimestamp": "1601975706499"
+ },
+ "messageAttributes": {
+
+ },
+ "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72",
+ "eventSource": "aws:sqs",
+ "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda",
+ "awsRegion": "eu-central-1"
+ },
+ {
+ "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b4b54",
+ "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==",
+ "body": "{\n \"id\": 12345,\n \"name\": \"product5\",\n \"price\": 45\n}",
+ "attributes": {
+ "ApproximateReceiveCount": "1",
+ "SentTimestamp": "1601975706495",
+ "SenderId": "AROAIFU437PVZ5L2J53F5",
+ "ApproximateFirstReceiveTimestamp": "1601975706499"
+ },
+ "messageAttributes": {
+
+ },
+ "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72",
+ "eventSource": "aws:sqs",
+ "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda",
+ "awsRegion": "eu-central-1"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/powertools-idempotency/pom.xml b/powertools-idempotency/pom.xml
index 0332bf8e4..dd5348dde 100644
--- a/powertools-idempotency/pom.xml
+++ b/powertools-idempotency/pom.xml
@@ -122,6 +122,7 @@
com.amazonaws
aws-lambda-java-tests
+ test
com.amazonaws