diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php
new file mode 100644
index 0000000000000..ba2e995d4f704
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string
+ */
+ public function getValue(): string
+ {
+ return "1";
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->corsConfiguration->isCredentialsAllowed();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php
new file mode 100644
index 0000000000000..68760de543daa
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return $this->corsConfiguration->getAllowedHeaders();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php
new file mode 100644
index 0000000000000..233839b9deb74
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return $this->corsConfiguration->getAllowedMethods();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php
new file mode 100644
index 0000000000000..21850f18db1f2
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return $this->corsConfiguration->getAllowedOrigins();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php
new file mode 100644
index 0000000000000..e30209ae25e68
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return (string) $this->corsConfiguration->getMaxAge();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Model/Cors/Configuration.php b/app/code/Magento/GraphQl/Model/Cors/Configuration.php
new file mode 100644
index 0000000000000..dd5a0b426e22d
--- /dev/null
+++ b/app/code/Magento/GraphQl/Model/Cors/Configuration.php
@@ -0,0 +1,96 @@
+scopeConfig = $scopeConfig;
+ }
+
+ /**
+ * Are CORS headers enabled
+ *
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_HEADERS_ENABLED);
+ }
+
+ /**
+ * Get allowed origins or null if stored configuration is empty
+ *
+ * @return string|null
+ */
+ public function getAllowedOrigins(): ?string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_ORIGINS);
+ }
+
+ /**
+ * Get allowed headers or null if stored configuration is empty
+ *
+ * @return string|null
+ */
+ public function getAllowedHeaders(): ?string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_HEADERS);
+ }
+
+ /**
+ * Get allowed methods or null if stored configuration is empty
+ *
+ * @return string|null
+ */
+ public function getAllowedMethods(): ?string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_METHODS);
+ }
+
+ /**
+ * Get max age header value
+ *
+ * @return int
+ */
+ public function getMaxAge(): int
+ {
+ return (int) $this->scopeConfig->getValue(self::XML_PATH_CORS_MAX_AGE);
+ }
+
+ /**
+ * Are credentials allowed
+ *
+ * @return bool
+ */
+ public function isCredentialsAllowed(): bool
+ {
+ return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_ALLOW_CREDENTIALS);
+ }
+}
diff --git a/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php
new file mode 100644
index 0000000000000..b40b64f48e51f
--- /dev/null
+++ b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+ service
+ Magento_Integration::config_oauth
+
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+
+ The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin. Fill this field with one or more origins (comma separated) or use '*' to allow access from all origins.
+
+ 1
+
+
+
+
+
+ The Access-Control-Allow-Methods response header specifies the method or methods allowed when accessing the resource in response to a preflight request. Use comma separated methods (e.g. GET,POST)
+
+ 1
+
+
+
+
+
+
+
+ validate-digits
+ The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.
+
+ 1
+
+
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+ The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to frontend code when the request's credentials mode is include.
+
+ 1
+
+
+
+
+
+
diff --git a/app/code/Magento/GraphQl/etc/config.xml b/app/code/Magento/GraphQl/etc/config.xml
new file mode 100644
index 0000000000000..39caacbec42d2
--- /dev/null
+++ b/app/code/Magento/GraphQl/etc/config.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ 0
+
+
+
+ 86400
+ 0
+
+
+
+
diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml
index b356f33c4f4bf..fca6c425e2507 100644
--- a/app/code/Magento/GraphQl/etc/di.xml
+++ b/app/code/Magento/GraphQl/etc/di.xml
@@ -98,4 +98,31 @@
300
+
+
+
+
+ Access-Control-Max-Age
+
+
+
+
+ Access-Control-Allow-Credentials
+
+
+
+
+ Access-Control-Allow-Headers
+
+
+
+
+ Access-Control-Allow-Methods
+
+
+
+
+ Access-Control-Allow-Origin
+
+
diff --git a/app/code/Magento/GraphQl/etc/graphql/di.xml b/app/code/Magento/GraphQl/etc/graphql/di.xml
index 77fce336374dd..23d49124d1a02 100644
--- a/app/code/Magento/GraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/GraphQl/etc/graphql/di.xml
@@ -30,4 +30,15 @@
+
+
+
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider
+
+
+
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php
new file mode 100644
index 0000000000000..25c808a549e80
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php
@@ -0,0 +1,126 @@
+resourceConfig = $objectManager->get(Config::class);
+ $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class);
+ $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class);
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0);
+ $this->reinitConfig->reinit();
+ }
+
+ public function testNoCorsHeadersWhenCorsIsDisabled(): void
+ {
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0);
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, 'Origin');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '1');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, 'GET,POST');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'magento.local');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400');
+ $this->reinitConfig->reinit();
+
+ $headers = $this->getHeadersFromIntrospectionQuery();
+
+ self::assertArrayNotHasKey('Access-Control-Max-Age', $headers);
+ self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers);
+ self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers);
+ self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers);
+ self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers);
+ }
+
+ public function testCorsHeadersWhenCorsIsEnabled(): void
+ {
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1);
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, 'Origin');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '1');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, 'GET,POST');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'magento.local');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400');
+ $this->reinitConfig->reinit();
+
+ $headers = $this->getHeadersFromIntrospectionQuery();
+
+ self::assertEquals('Origin', $headers['Access-Control-Allow-Headers']);
+ self::assertEquals('1', $headers['Access-Control-Allow-Credentials']);
+ self::assertEquals('GET,POST', $headers['Access-Control-Allow-Methods']);
+ self::assertEquals('magento.local', $headers['Access-Control-Allow-Origin']);
+ self::assertEquals('86400', $headers['Access-Control-Max-Age']);
+ }
+
+ public function testEmptyCorsHeadersWhenCorsIsEnabled(): void
+ {
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1);
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, '');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, '');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, '');
+ $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '');
+ $this->reinitConfig->reinit();
+
+ $headers = $this->getHeadersFromIntrospectionQuery();
+
+ self::assertArrayNotHasKey('Access-Control-Max-Age', $headers);
+ self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers);
+ self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers);
+ self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers);
+ self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers);
+ }
+
+ private function getHeadersFromIntrospectionQuery(): array
+ {
+ $query
+ = <<graphQlQueryWithResponseHeaders($query)['headers'] ?? [];
+ }
+}