From bd0e92936f82d3cd4563cd45ebf1f73fd1db9f01 Mon Sep 17 00:00:00 2001 From: Tobias Oitzinger <42447585+toitzi@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:26:42 +0100 Subject: [PATCH] feat(openapi): HTTP Authentication Support for Swagger UI (#6665) * feat(openapi): http authentication support add support for http authentication (for example http basic or bearer tokens) Closes: #6664 * add symfony support --------- Co-authored-by: soyuka --- src/Laravel/ApiPlatformProvider.php | 1 + src/Laravel/config/api-platform.php | 6 ++++++ src/OpenApi/Factory/OpenApiFactory.php | 5 +++++ src/OpenApi/Options.php | 7 ++++++- .../Tests/Factory/OpenApiFactoryTest.php | 12 +++++++++++ .../ApiPlatformExtension.php | 1 + .../DependencyInjection/Configuration.php | 18 ++++++++++++++++ .../Bundle/Resources/config/openapi.xml | 1 + .../ApiPlatformExtensionTest.php | 1 + .../DependencyInjection/ConfigurationTest.php | 21 +++++++++++++++++++ 10 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index e90d2687d2d..a13fb63b0e5 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -753,6 +753,7 @@ public function register(): void contactEmail: $config->get('api-platform.swagger_ui.contact.email', ''), licenseName: $config->get('api-platform.swagger_ui.license.name', ''), licenseUrl: $config->get('api-platform.swagger_ui.license.url', ''), + httpAuth: $config->get('api-platform.swagger_ui.http_auth', []), ); }); diff --git a/src/Laravel/config/api-platform.php b/src/Laravel/config/api-platform.php index 1531d0ed6d4..f24a85a9a13 100644 --- a/src/Laravel/config/api-platform.php +++ b/src/Laravel/config/api-platform.php @@ -98,6 +98,12 @@ // 'url' => 'https://www.example.com/support', // 'email' => 'support@example.com', //], + //'http_auth' => [ + // 'Personal Access Token' => [ + // 'scheme' => 'bearer', + // 'bearerFormat' => 'JWT' + // ] + //] ], 'url_generation_strategy' => UrlGeneratorInterface::ABS_PATH, diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 435cd4f133a..30fe6cb29bb 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -744,6 +744,11 @@ private function getSecuritySchemes(): array $securitySchemes[$key] = new SecurityScheme('apiKey', $description, $apiKey['name'], $apiKey['type']); } + foreach ($this->openApiOptions->getHttpAuth() as $key => $httpAuth) { + $description = \sprintf('Value for the http %s parameter.', $httpAuth['scheme']); + $securitySchemes[$key] = new SecurityScheme('http', $description, null, null, $httpAuth['scheme'], $httpAuth['bearerFormat'] ?? null); + } + return $securitySchemes; } diff --git a/src/OpenApi/Options.php b/src/OpenApi/Options.php index 5683c7c2178..a229b4d36ec 100644 --- a/src/OpenApi/Options.php +++ b/src/OpenApi/Options.php @@ -15,7 +15,7 @@ final class Options { - public function __construct(private readonly string $title, private readonly string $description = '', private readonly string $version = '', private readonly bool $oAuthEnabled = false, private readonly ?string $oAuthType = null, private readonly ?string $oAuthFlow = null, private readonly ?string $oAuthTokenUrl = null, private readonly ?string $oAuthAuthorizationUrl = null, private readonly ?string $oAuthRefreshUrl = null, private readonly array $oAuthScopes = [], private readonly array $apiKeys = [], private readonly ?string $contactName = null, private readonly ?string $contactUrl = null, private readonly ?string $contactEmail = null, private readonly ?string $termsOfService = null, private readonly ?string $licenseName = null, private readonly ?string $licenseUrl = null, private bool $overrideResponses = true) + public function __construct(private readonly string $title, private readonly string $description = '', private readonly string $version = '', private readonly bool $oAuthEnabled = false, private readonly ?string $oAuthType = null, private readonly ?string $oAuthFlow = null, private readonly ?string $oAuthTokenUrl = null, private readonly ?string $oAuthAuthorizationUrl = null, private readonly ?string $oAuthRefreshUrl = null, private readonly array $oAuthScopes = [], private readonly array $apiKeys = [], private readonly ?string $contactName = null, private readonly ?string $contactUrl = null, private readonly ?string $contactEmail = null, private readonly ?string $termsOfService = null, private readonly ?string $licenseName = null, private readonly ?string $licenseUrl = null, private bool $overrideResponses = true, private readonly array $httpAuth = []) { } @@ -74,6 +74,11 @@ public function getApiKeys(): array return $this->apiKeys; } + public function getHttpAuth(): array + { + return $this->httpAuth; + } + public function getContactName(): ?string { return $this->contactName; diff --git a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php index d95b74b1973..648ca63ad8c 100644 --- a/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php +++ b/src/OpenApi/Tests/Factory/OpenApiFactoryTest.php @@ -521,6 +521,14 @@ public function testInvoke(): void 'type' => 'query', 'name' => 'key', ], + ], null, null, null, null, null, null, true, [ + 'bearer' => [ + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT', + ], + 'basic' => [ + 'scheme' => 'basic', + ], ]), new PaginationOptions(true, 'page', true, 'itemsPerPage', true, 'pagination') ); @@ -622,12 +630,16 @@ public function testInvoke(): void 'oauth' => new SecurityScheme('oauth2', 'OAuth 2.0 authorization code Grant', null, null, null, null, new OAuthFlows(null, null, null, new OAuthFlow('/oauth/v2/auth', '/oauth/v2/token', '/oauth/v2/refresh', new \ArrayObject(['scope param'])))), 'header' => new SecurityScheme('apiKey', 'Value for the Authorization header parameter.', 'Authorization', 'header'), 'query' => new SecurityScheme('apiKey', 'Value for the key query parameter.', 'key', 'query'), + 'bearer' => new SecurityScheme('http', 'Value for the http bearer parameter.', null, null, 'bearer', 'JWT'), + 'basic' => new SecurityScheme('http', 'Value for the http basic parameter.', null, null, 'basic', null), ])); $this->assertEquals([ ['oauth' => []], ['header' => []], ['query' => []], + ['bearer' => []], + ['basic' => []], ], $openApi->getSecurity()); $paths = $openApi->getPaths(); diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 5c83b24b099..cfeabc1d642 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -500,6 +500,7 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.enable_swagger_ui', $config['enable_swagger_ui']); $container->setParameter('api_platform.enable_re_doc', $config['enable_re_doc']); $container->setParameter('api_platform.swagger.api_keys', $config['swagger']['api_keys']); + $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 f31ab980e97..c8619006d2e 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -330,6 +330,24 @@ private function addSwaggerSection(ArrayNodeDefinition $rootNode): void ->end() ->end() ->end() + ->arrayNode('http_auth') + ->info('Creates http security schemes for OpenAPI.') + ->useAttributeAsKey('key') + ->validate() + ->ifTrue(static fn ($v): bool => (bool) array_filter(array_keys($v), fn ($item) => !preg_match('/^[a-zA-Z0-9._-]+$/', $item))) + ->thenInvalid('The api keys "key" is not valid according to the pattern enforced by OpenAPI 3.1 ^[a-zA-Z0-9._-]+$.') + ->end() + ->prototype('array') + ->children() + ->scalarNode('scheme') + ->info('The OpenAPI HTTP auth scheme, for example "bearer"') + ->end() + ->scalarNode('bearerFormat') + ->info('The OpenAPI HTTP bearer format') + ->end() + ->end() + ->end() + ->end() ->variableNode('swagger_ui_extra_configuration') ->defaultValue([]) ->validate() diff --git a/src/Symfony/Bundle/Resources/config/openapi.xml b/src/Symfony/Bundle/Resources/config/openapi.xml index 2f1a7391ac1..91a2d449d60 100644 --- a/src/Symfony/Bundle/Resources/config/openapi.xml +++ b/src/Symfony/Bundle/Resources/config/openapi.xml @@ -59,6 +59,7 @@ %api_platform.openapi.license.name% %api_platform.openapi.license.url% %api_platform.openapi.overrideResponses% + %api_platform.swagger.http_auth% diff --git a/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 2a2fe0f197c..f2eb7690853 100644 --- a/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -284,5 +284,6 @@ public function testEventListenersConfiguration(): void ]; $this->assertContainerHas($services, $aliases); + $this->container->hasParameter('api_platform.swagger.http_auth'); } } diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 84a1ed5066c..572de7d7960 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -149,6 +149,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'swagger' => [ 'versions' => [3], 'api_keys' => [], + 'http_auth' => [], 'swagger_ui_extra_configuration' => [], ], 'eager_loading' => [ @@ -391,4 +392,24 @@ public function testEnableElasticsearch(): void $this->assertTrue($config['elasticsearch']['enabled']); } + + /** + * Test config for http auth. + */ + public function testHttpAuth(): void + { + $config = $this->processor->processConfiguration($this->configuration, [ + 'api_platform' => [ + 'swagger' => [ + 'http_auth' => ['PAT' => [ + 'scheme' => 'bearer', + 'bearerFormat' => 'JWT', + ]], + ], + ], + ]); + + $this->assertArrayHasKey('http_auth', $config['swagger']); + $this->assertSame(['scheme' => 'bearer', 'bearerFormat' => 'JWT'], $config['swagger']['http_auth']['PAT']); + } }