diff --git a/DependencyInjection/Compiler/ApiPlatformOpenApiPass.php b/DependencyInjection/Compiler/ApiPlatformOpenApiPass.php new file mode 100644 index 00000000..bab7eecc --- /dev/null +++ b/DependencyInjection/Compiler/ApiPlatformOpenApiPass.php @@ -0,0 +1,52 @@ +hasDefinition('lexik_jwt_authentication.api_platform.openapi.factory') || !$container->hasParameter('security.firewalls')) { + return; + } + + $checkPath = null; + $usernamePath = null; + $passwordPath = null; + $firewalls = $container->getParameter('security.firewalls'); + foreach ($firewalls as $firewallName) { + if ($container->hasDefinition('security.authenticator.json_login.' . $firewallName)) { + $firewallOptions = $container->getDefinition('security.authenticator.json_login.' . $firewallName)->getArgument(4); + $checkPath = $firewallOptions['check_path']; + $usernamePath = $firewallOptions['username_path']; + $passwordPath = $firewallOptions['password_path']; + + break; + } + } + + $openApiFactoryDefinition = $container->getDefinition('lexik_jwt_authentication.api_platform.openapi.factory'); + $checkPathArg = $openApiFactoryDefinition->getArgument(1); + $usernamePathArg = $openApiFactoryDefinition->getArgument(2); + $passwordPathArg = $openApiFactoryDefinition->getArgument(3); + + if (!$checkPath && !$checkPathArg) { + $container->removeDefinition('lexik_jwt_authentication.api_platform.openapi.factory'); + + return; + } + + if (!$checkPathArg) { + $openApiFactoryDefinition->replaceArgument(1, $checkPath); + } + if (!$usernamePathArg) { + $openApiFactoryDefinition->replaceArgument(2, $usernamePath ?? 'username'); + } + if (!$passwordPathArg) { + $openApiFactoryDefinition->replaceArgument(3, $passwordPath ?? 'password'); + } + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 66aa1a71..72136211 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -116,11 +116,19 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->arrayNode('api_platform') - ->info('API Platform compatibility: add check_path in OpenApi documentation.') + ->info('API Platform compatibility: add check_path in OpenAPI documentation.') ->children() ->scalarNode('check_path') ->defaultNull() - ->info('The login check path to document on OpenApi.') + ->info('The login check path to add in OpenAPI.') + ->end() + ->scalarNode('username_path') + ->defaultNull() + ->info('The path to the username in the JSON body.') + ->end() + ->scalarNode('password_path') + ->defaultNull() + ->info('The path to the password in the JSON body.') ->end() ->end() ->end() diff --git a/DependencyInjection/LexikJWTAuthenticationExtension.php b/DependencyInjection/LexikJWTAuthenticationExtension.php index 0ad7ba87..11e9e89a 100644 --- a/DependencyInjection/LexikJWTAuthenticationExtension.php +++ b/DependencyInjection/LexikJWTAuthenticationExtension.php @@ -144,15 +144,18 @@ public function load(array $configs, ContainerBuilder $container) ->replaceArgument(4, $encoderConfig['signature_algorithm']); } - if (isset($config['api_platform']['check_path'])) { - if (!class_exists(ApiPlatformBundle::class)) { - throw new LogicException('API Platform cannot be detected. Try running "composer require api-platform/core".'); - } + if (!class_exists(ApiPlatformBundle::class) && (isset($config['api_platform']['check_path']) || isset($config['api_platform']['username_path']) || isset($config['api_platform']['password_path']))) { + throw new LogicException('API Platform cannot be detected. Try running "composer require api-platform/core".'); + } + if (class_exists(ApiPlatformBundle::class)) { $loader->load('api_platform.xml'); + $container ->getDefinition('lexik_jwt_authentication.api_platform.openapi.factory') - ->replaceArgument(1, $config['api_platform']['check_path']); + ->replaceArgument(1, $config['api_platform']['check_path'] ?? null) + ->replaceArgument(2, $config['api_platform']['username_path'] ?? null) + ->replaceArgument(3, $config['api_platform']['password_path'] ?? null); } } diff --git a/LexikJWTAuthenticationBundle.php b/LexikJWTAuthenticationBundle.php index cacdfa2e..2f7b9ee1 100644 --- a/LexikJWTAuthenticationBundle.php +++ b/LexikJWTAuthenticationBundle.php @@ -2,6 +2,7 @@ namespace Lexik\Bundle\JWTAuthenticationBundle; +use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\ApiPlatformOpenApiPass; use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\DeprecateLegacyGuardAuthenticatorPass; use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\RegisterLegacyGuardAuthenticatorPass; use Lexik\Bundle\JWTAuthenticationBundle\DependencyInjection\Compiler\WireGenerateTokenCommandPass; @@ -32,6 +33,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new WireGenerateTokenCommandPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); $container->addCompilerPass(new DeprecateLegacyGuardAuthenticatorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); + $container->addCompilerPass(new ApiPlatformOpenApiPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); /** @var SecurityExtension $extension */ $extension = $container->getExtension('security'); diff --git a/OpenApi/OpenApiFactory.php b/OpenApi/OpenApiFactory.php index ed0386fb..c214236a 100644 --- a/OpenApi/OpenApiFactory.php +++ b/OpenApi/OpenApiFactory.php @@ -25,11 +25,15 @@ class OpenApiFactory implements OpenApiFactoryInterface private $decorated; private $checkPath; + private $usernamePath; + private $passwordPath; - public function __construct(OpenApiFactoryInterface $decorated, string $checkPath) + public function __construct(OpenApiFactoryInterface $decorated, string $checkPath, string $usernamePath, string $passwordPath) { $this->decorated = $decorated; $this->checkPath = $checkPath; + $this->usernamePath = $usernamePath; + $this->passwordPath = $passwordPath; } /** @@ -41,7 +45,8 @@ public function __invoke(array $context = []): OpenApi $openApi ->getPaths() - ->addPath($this->checkPath, (new PathItem())->withPost((new Operation()) + ->addPath($this->checkPath, (new PathItem())->withPost( + (new Operation()) ->withOperationId('login_check_post') ->withTags(['Login Check']) ->withResponses([ @@ -65,22 +70,14 @@ public function __invoke(array $context = []): OpenApi ], ]) ->withSummary('Creates a user token.') - ->withRequestBody((new RequestBody()) + ->withRequestBody( + (new RequestBody()) ->withDescription('The login data') ->withContent(new \ArrayObject([ 'application/json' => new MediaType(new \ArrayObject(new \ArrayObject([ 'type' => 'object', - 'properties' => [ - '_username' => [ - 'type' => 'string', - 'nullable' => false, - ], - '_password' => [ - 'type' => 'string', - 'nullable' => false, - ], - ], - 'required' => ['_username', '_password'], + 'properties' => $properties = array_merge_recursive($this->getJsonSchemaFromPathParts(explode('.', $this->usernamePath)), $this->getJsonSchemaFromPathParts(explode('.', $this->passwordPath))), + 'required' => array_keys($properties), ]))), ])) ->withRequired(true) @@ -89,4 +86,28 @@ public function __invoke(array $context = []): OpenApi return $openApi; } + + private function getJsonSchemaFromPathParts(array $pathParts): array + { + $jsonSchema = []; + + if (count($pathParts) === 1) { + $jsonSchema[array_shift($pathParts)] = [ + 'type' => 'string', + 'nullable' => false, + ]; + + return $jsonSchema; + } + + $pathPart = array_shift($pathParts); + $properties = $this->getJsonSchemaFromPathParts($pathParts); + $jsonSchema[$pathPart] = [ + 'type' => 'object', + 'properties' => $properties, + 'required' => array_keys($properties), + ]; + + return $jsonSchema; + } } diff --git a/Resources/config/api_platform.xml b/Resources/config/api_platform.xml index 80119029..d3061b37 100644 --- a/Resources/config/api_platform.xml +++ b/Resources/config/api_platform.xml @@ -5,9 +5,11 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - + + + diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index 8b636593..13b9d46b 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -145,11 +145,12 @@ Configure application routing api_login_check: path: /api/login_check -Enable API Platform compatibility -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +API Platform compatibility +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If `API Platform `__ is detected, the integration will be done with your security configuration. -To enable the `API Platform `__ compatibility, add the -``lexik_jwt_authentication.api_platform.check_path`` configuration option as following: +If you wish to change some parameters, you can do it with this configuration: .. code-block:: yaml @@ -158,6 +159,8 @@ To enable the `API Platform `__ compatibility, add th # ... api_platform: check_path: /api/login_check + username_path: email + password_path: security.credentials.password Usage ----- diff --git a/Tests/Functional/Command/ApiPlatformOpenApiExportCommandTest.php b/Tests/Functional/Command/ApiPlatformOpenApiExportCommandTest.php index ea29ece8..38c3a26b 100644 --- a/Tests/Functional/Command/ApiPlatformOpenApiExportCommandTest.php +++ b/Tests/Functional/Command/ApiPlatformOpenApiExportCommandTest.php @@ -2,7 +2,6 @@ namespace Lexik\Bundle\JWTAuthenticationBundle\Tests\Functional\Command; -use ApiPlatform\Symfony\Bundle\ApiPlatformBundle; use Lexik\Bundle\JWTAuthenticationBundle\Tests\Functional\TestCase; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -12,7 +11,7 @@ * * @author Vincent Chalamon * - * @requires function ApiPlatformBundle::build + * @requires function ApiPlatform\Symfony\Bundle\ApiPlatformBundle::build */ class ApiPlatformOpenApiExportCommandTest extends TestCase { @@ -43,10 +42,7 @@ public function testCheckOpenApiExportCommand() "paths": { "/login_check": { "post": { - "deprecated": false, - "description": "", "operationId": "login_check_post", - "parameters": [], "requestBody": { "description": "The login data", "required": true, @@ -55,18 +51,30 @@ public function testCheckOpenApiExportCommand() "schema": { "type": "object", "properties": { - "_username": { + "email": { "nullable": false, "type": "string" }, - "_password": { - "nullable": false, - "type": "string" + "security": { + "type": "object", + "properties": { + "credentials": { + "type": "object", + "properties": { + "password": { + "nullable": false, + "type": "string" + } + }, + "required": ["password"] + } + }, + "required": ["credentials"] } }, "required": [ - "_username", - "_password" + "email", + "security" ] } } diff --git a/Tests/Functional/app/AppKernel.php b/Tests/Functional/app/AppKernel.php index b2ff4e31..6ce14f75 100644 --- a/Tests/Functional/app/AppKernel.php +++ b/Tests/Functional/app/AppKernel.php @@ -119,6 +119,8 @@ public function registerContainerConfiguration(LoaderInterface $loader) $container->prependExtensionConfig('lexik_jwt_authentication', [ 'api_platform' => [ 'check_path' => '/login_check', + 'username_path' => 'email', + 'password_path' => 'security.credentials.password', ], ]); $router['resource'] = '%kernel.root_dir%/config/routing_api_platform.yml';