diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index 12c176f54b..9477d5c8a1 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -767,7 +767,8 @@ public function register(): void httpAuth: $config->get('api-platform.swagger_ui.http_auth', []), tags: $config->get('api-platform.openapi.tags', []), errorResourceClass: Error::class, - validationErrorResourceClass: ValidationError::class + validationErrorResourceClass: ValidationError::class, + withCredentials: $config->get('api-platform.swagger_ui.with_credentials', false), ); }); diff --git a/src/Laravel/State/SwaggerUiProcessor.php b/src/Laravel/State/SwaggerUiProcessor.php index 7ba643cb80..a29ea79a8c 100644 --- a/src/Laravel/State/SwaggerUiProcessor.php +++ b/src/Laravel/State/SwaggerUiProcessor.php @@ -83,6 +83,7 @@ public function process(mixed $openApi, Operation $operation, array $uriVariable 'clientSecret' => $this->oauthClientSecret, 'pkce' => $this->oauthPkce, ], + 'withCredentials' => $this->openApiOptions->getWithCredentials(), ]; $status = 200; diff --git a/src/Laravel/Tests/DocsTest.php b/src/Laravel/Tests/DocsTest.php index f8449f80ad..8ceb4e0aae 100644 --- a/src/Laravel/Tests/DocsTest.php +++ b/src/Laravel/Tests/DocsTest.php @@ -85,4 +85,10 @@ public function testHtmlDocsRendersScalarWithoutFooterWhenRequested(): void $this->assertStringContainsString('init-scalar-ui.js', $content); $this->assertStringNotContainsString('id="formats"', $content); } + + public function testSwaggerDataDoesNotContainWithCredentialsByDefault(): void + { + $res = $this->get('/api/docs', headers: ['accept' => 'text/html']); + $this->assertStringNotContainsString('"withCredentials":true', (string) $res->getContent()); + } } diff --git a/src/Laravel/Tests/DocsWithCredentialsTest.php b/src/Laravel/Tests/DocsWithCredentialsTest.php new file mode 100644 index 0000000000..a3d52e939c --- /dev/null +++ b/src/Laravel/Tests/DocsWithCredentialsTest.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Config\Repository; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class DocsWithCredentialsTest extends TestCase +{ + use ApiTestAssertionsTrait; + use WithWorkbench; + + protected function defineEnvironment($app): void + { + tap($app['config'], static function (Repository $config): void { + $config->set('api-platform.swagger_ui.with_credentials', true); + }); + } + + public function testSwaggerDataContainsWithCredentialsTrueWhenEnabled(): void + { + $res = $this->get('/api/docs', headers: ['accept' => 'text/html']); + $res->assertOk(); + $content = (string) $res->getContent(); + $this->assertStringContainsString('"withCredentials":true', $content); + } +} diff --git a/src/Laravel/config/api-platform.php b/src/Laravel/config/api-platform.php index 2db701be66..d07dbadaa8 100644 --- a/src/Laravel/config/api-platform.php +++ b/src/Laravel/config/api-platform.php @@ -143,6 +143,8 @@ // 'bearerFormat' => 'JWT', // ], // ], + // + // 'with_credentials' => true, ], // 'openapi' => [ diff --git a/src/Laravel/public/init-swagger-ui.js b/src/Laravel/public/init-swagger-ui.js index 101d4fc83b..794b86ae34 100644 --- a/src/Laravel/public/init-swagger-ui.js +++ b/src/Laravel/public/init-swagger-ui.js @@ -41,7 +41,8 @@ window.onload = function() { }).observe(document, {childList: true, subtree: true}); const data = JSON.parse(document.getElementById('swagger-data').innerText); - const ui = SwaggerUIBundle(Object.assign({ + + const config = { spec: data.spec, dom_id: '#swagger-ui', validatorUrl: null, @@ -55,7 +56,18 @@ window.onload = function() { SwaggerUIBundle.plugins.DownloadUrl, ], layout: 'StandaloneLayout', - }, data.extraConfiguration)); + }; + + if (data.withCredentials) { + // Cloudflare Access fix: ensure cookies are sent on token / CORS calls + config.requestInterceptor = (req) => { + req.credentials = 'include'; + return req; + }; + } + + const withExtraConfig = Object.assign(config, data.extraConfiguration); + const ui = SwaggerUIBundle(withExtraConfig); if (data.oauth.enabled) { ui.initOAuth({ diff --git a/src/OpenApi/Options.php b/src/OpenApi/Options.php index e91976aa92..a22904bd15 100644 --- a/src/OpenApi/Options.php +++ b/src/OpenApi/Options.php @@ -47,6 +47,7 @@ public function __construct( private ?string $errorResourceClass = null, private ?string $validationErrorResourceClass = null, private ?string $licenseIdentifier = null, + private bool $withCredentials = false, ) { } @@ -178,4 +179,9 @@ public function getLicenseIdentifier(): ?string { return $this->licenseIdentifier; } + + public function getWithCredentials(): bool + { + return $this->withCredentials; + } } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 0164d273aa..abbdd83ec9 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -684,6 +684,7 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.enable_scalar', $config['enable_scalar']); $container->setParameter('api_platform.swagger.api_keys', $config['swagger']['api_keys']); $container->setParameter('api_platform.swagger.persist_authorization', $config['swagger']['persist_authorization']); + $container->setParameter('api_platform.swagger.with_credentials', $config['swagger']['with_credentials']); $container->setParameter('api_platform.swagger.http_auth', $config['swagger']['http_auth']); if ($config['openapi']['swagger_ui_extra_configuration'] && $config['swagger']['swagger_ui_extra_configuration']) { throw new RuntimeException('You can not set "swagger_ui_extra_configuration" twice - in "openapi" and "swagger" section.'); diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index 911de422fd..7e33073796 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -324,6 +324,7 @@ private function addSwaggerSection(ArrayNodeDefinition $rootNode): void ->addDefaultsIfNotSet() ->children() ->booleanNode('persist_authorization')->defaultValue(false)->info('Persist the SwaggerUI Authorization in the localStorage.')->end() + ->booleanNode('with_credentials')->defaultValue(false)->info('Send credentials (cookies, authorization headers) on Swagger UI cross-origin requests (e.g. when running behind Cloudflare Access).')->end() ->arrayNode('versions') ->info('The active versions of OpenAPI to be exported or used in Swagger UI. The first value is the default.') ->defaultValue($supportedVersions) diff --git a/src/Symfony/Bundle/Resources/config/openapi.php b/src/Symfony/Bundle/Resources/config/openapi.php index b68eb55ed4..d5b9517a20 100644 --- a/src/Symfony/Bundle/Resources/config/openapi.php +++ b/src/Symfony/Bundle/Resources/config/openapi.php @@ -70,6 +70,7 @@ '%api_platform.openapi.errorResourceClass%', '%api_platform.openapi.validationErrorResourceClass%', '%api_platform.openapi.license.identifier%', + '%api_platform.swagger.with_credentials%', ]); $services->alias(Options::class, 'api_platform.openapi.options'); diff --git a/src/Symfony/Bundle/Resources/public/init-swagger-ui.js b/src/Symfony/Bundle/Resources/public/init-swagger-ui.js index bdf9bb3a8c..0e8059f7d7 100644 --- a/src/Symfony/Bundle/Resources/public/init-swagger-ui.js +++ b/src/Symfony/Bundle/Resources/public/init-swagger-ui.js @@ -41,7 +41,7 @@ window.onload = function() { }).observe(document, {childList: true, subtree: true}); const data = JSON.parse(document.getElementById('swagger-data').innerText); - const ui = SwaggerUIBundle(Object.assign({ + const config = { spec: data.spec, dom_id: '#swagger-ui', validatorUrl: null, @@ -56,7 +56,16 @@ window.onload = function() { SwaggerUIBundle.plugins.DownloadUrl, ], layout: 'StandaloneLayout', - }, data.extraConfiguration)); + }; + + if (data.withCredentials) { + config.requestInterceptor = (req) => { + req.credentials = 'include'; + return req; + }; + } + + const ui = SwaggerUIBundle(Object.assign(config, data.extraConfiguration)); if (data.oauth.enabled) { ui.initOAuth({ diff --git a/src/Symfony/Bundle/SwaggerUi/SwaggerUiProcessor.php b/src/Symfony/Bundle/SwaggerUi/SwaggerUiProcessor.php index eba9d89fed..065ada1ea1 100644 --- a/src/Symfony/Bundle/SwaggerUi/SwaggerUiProcessor.php +++ b/src/Symfony/Bundle/SwaggerUi/SwaggerUiProcessor.php @@ -64,6 +64,7 @@ public function process(mixed $openApi, Operation $operation, array $uriVariable 'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']), 'spec' => $this->normalizer->normalize($openApi, 'json', []), 'persistAuthorization' => $this->openApiOptions->hasPersistAuthorization(), + 'withCredentials' => $this->openApiOptions->getWithCredentials(), 'oauth' => [ 'enabled' => $this->openApiOptions->getOAuthEnabled(), 'type' => $this->openApiOptions->getOAuthType(), diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 9004ecb665..dc392b2c1d 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -161,6 +161,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'http_auth' => [], 'swagger_ui_extra_configuration' => [], 'persist_authorization' => false, + 'with_credentials' => false, ], 'eager_loading' => [ 'enabled' => true,