From 1ab4da9377512810ebce0976c6f8c81d2045af03 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Fri, 23 Apr 2021 14:43:19 +0200 Subject: [PATCH] Allow user to use his own Client entity --- .psalm.baseline.xml | 9 + docs/basic-setup.md | 2 + docs/index.md | 5 + docs/using-custom-client.md | 37 ++++ src/Command/CreateClientCommand.php | 42 ++-- src/Command/DeleteClientCommand.php | 12 +- src/Command/ListClientsCommand.php | 7 +- src/Command/UpdateClientCommand.php | 26 ++- src/Controller/AuthorizationController.php | 4 +- .../RegisterDoctrineOrmMappingPass.php | 26 +++ src/DependencyInjection/Configuration.php | 27 +++ .../LeagueOAuth2ServerExtension.php | 28 ++- .../AuthorizationRequestResolveEvent.php | 8 +- src/Event/ScopeResolveEvent.php | 8 +- src/Event/UserResolveEvent.php | 8 +- src/LeagueOAuth2ServerBundle.php | 21 +- src/Manager/ClientManagerInterface.php | 10 +- src/Manager/Doctrine/ClientManager.php | 31 ++- src/Manager/InMemory/ClientManager.php | 14 +- src/Model/AbstractClient.php | 186 ++++++++++++++++++ src/Model/AccessToken.php | 6 +- src/Model/AuthorizationCode.php | 6 +- src/Model/Client.php | 175 +--------------- src/Persistence/Mapping/Driver.php | 135 +++++++++++++ src/Repository/AccessTokenRepository.php | 4 +- src/Repository/AuthCodeRepository.php | 4 +- src/Repository/ClientRepository.php | 6 +- src/Repository/ScopeRepository.php | 17 +- src/Repository/UserRepository.php | 8 +- .../config/doctrine/model/AccessToken.orm.xml | 21 -- .../doctrine/model/AuthorizationCode.orm.xml | 21 -- .../config/doctrine/model/Client.orm.xml | 21 -- .../doctrine/model/RefreshToken.orm.xml | 19 -- src/Resources/config/services.php | 1 + src/Resources/config/storage/doctrine.php | 10 + src/Resources/config/storage/in_memory.php | 3 - .../DoctrineCredentialsRevoker.php | 22 ++- src/Service/CredentialsRevokerInterface.php | 4 +- tests/Acceptance/CreateClientCommandTest.php | 16 +- tests/Acceptance/DeleteClientCommandTest.php | 4 +- .../Acceptance/DoctrineClientManagerTest.php | 6 +- .../DoctrineCredentialsRevokerTest.php | 5 +- tests/Acceptance/UpdateClientCommandTest.php | 43 +++- .../Acceptance/resource/list-client-empty.txt | 6 +- ...t-clients-with-client-having-no-secret.txt | 10 +- tests/Acceptance/resource/list-clients.txt | 10 +- .../resource/list-filters-clients.txt | 10 +- 47 files changed, 681 insertions(+), 423 deletions(-) create mode 100644 docs/using-custom-client.md create mode 100644 src/DependencyInjection/CompilerPass/RegisterDoctrineOrmMappingPass.php create mode 100644 src/Model/AbstractClient.php create mode 100644 src/Persistence/Mapping/Driver.php delete mode 100644 src/Resources/config/doctrine/model/AccessToken.orm.xml delete mode 100644 src/Resources/config/doctrine/model/AuthorizationCode.orm.xml delete mode 100644 src/Resources/config/doctrine/model/Client.orm.xml delete mode 100644 src/Resources/config/doctrine/model/RefreshToken.orm.xml diff --git a/.psalm.baseline.xml b/.psalm.baseline.xml index 24af4936..7956736c 100644 --- a/.psalm.baseline.xml +++ b/.psalm.baseline.xml @@ -1,5 +1,14 @@ + + + $metadata + $metadata + $metadata + $metadata + $metadata + + ['league.oauth2_server.controller.authorization', 'indexAction'] diff --git a/docs/basic-setup.md b/docs/basic-setup.md index 317b7647..14721632 100644 --- a/docs/basic-setup.md +++ b/docs/basic-setup.md @@ -16,6 +16,7 @@ Usage: league:oauth2-server:create-client [options] [--] [ []] Arguments: + name The client name identifier The client identifier secret The client secret @@ -46,6 +47,7 @@ Options: --redirect-uri[=REDIRECT-URI] Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs. (multiple values allowed) --grant-type[=GRANT-TYPE] Sets allowed grant type for client. Use this option multiple times to set multiple grant types. (multiple values allowed) --scope[=SCOPE] Sets allowed scope for client. Use this option multiple times to set multiple scopes. (multiple values allowed) + --name=[=NAME] Sets name for client. --deactivated If provided, it will deactivate the given client. ``` diff --git a/docs/index.md b/docs/index.md index 73327a8f..edd4f4ee 100644 --- a/docs/index.md +++ b/docs/index.md @@ -93,6 +93,10 @@ For implementation into Symfony projects, please see [bundle documentation](docs # Set a custom prefix that replaces the default 'ROLE_OAUTH2_' role prefix role_prefix: ROLE_OAUTH2_ + + client: + # Set a custom client class. Must be a League\Bundle\OAuth2ServerBundle\Model\Client + classname: League\Bundle\OAuth2ServerBundle\Model\Client ``` 1. Enable the bundle in `config/bundles.php` by adding it to the array: @@ -138,6 +142,7 @@ security: * [Basic setup](basic-setup.md) * [Controlling token scopes](controlling-token-scopes.md) * [Implementing custom grant type](implementing-custom-grant-type.md) +* [Using custom client](using-custom-client.md) ## Contributing diff --git a/docs/using-custom-client.md b/docs/using-custom-client.md new file mode 100644 index 00000000..9aa41cb6 --- /dev/null +++ b/docs/using-custom-client.md @@ -0,0 +1,37 @@ +# Using custom client + +1. Create a class that extends the `\League\Bundle\OAuth2ServerBundle\Model\AbstractClient` class. + + Example: + + ```php + clientManager = $clientManager; + $this->clientFqcn = $clientFqcn; } protected function configure(): void @@ -57,6 +63,18 @@ protected function configure(): void 'Sets allowed scope for client. Use this option multiple times to set multiple scopes.', [] ) + ->addOption( + 'public', + null, + InputOption::VALUE_NONE, + 'Create a public client.' + ) + ->addOption( + 'allow-plain-text-pkce', + null, + InputOption::VALUE_NONE, + 'Create a client who is allowed to use plain challenge method for PKCE.' + ) ->addArgument( 'name', InputArgument::REQUIRED, @@ -72,18 +90,6 @@ protected function configure(): void InputArgument::OPTIONAL, 'The client secret' ) - ->addOption( - 'public', - null, - InputOption::VALUE_NONE, - 'Create a public client.' - ) - ->addOption( - 'allow-plain-text-pkce', - null, - InputOption::VALUE_NONE, - 'Create a client who is allowed to use plain challenge method for PKCE.' - ) ; } @@ -100,7 +106,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $this->clientManager->save($client); - $io->success('New oAuth2 client created successfully.'); + $io->success('New OAuth2 client created successfully.'); $headers = ['Identifier', 'Secret']; $rows = [ @@ -111,9 +117,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - private function buildClientFromInput(InputInterface $input): Client + private function buildClientFromInput(InputInterface $input): AbstractClient { $name = $input->getArgument('name'); + /** @var string $identifier */ $identifier = $input->getArgument('identifier') ?? hash('md5', random_bytes(16)); @@ -126,7 +133,8 @@ private function buildClientFromInput(InputInterface $input): Client /** @var string $secret */ $secret = $isPublic ? null : $input->getArgument('secret') ?? hash('sha512', random_bytes(32)); - $client = new Client($name, $identifier, $secret); + /** @var AbstractClient $client */ + $client = new $this->clientFqcn($name, $identifier, $secret); $client->setActive(true); $client->setAllowPlainTextPkce($input->getOption('allow-plain-text-pkce')); diff --git a/src/Command/DeleteClientCommand.php b/src/Command/DeleteClientCommand.php index fa1b9408..874630b1 100644 --- a/src/Command/DeleteClientCommand.php +++ b/src/Command/DeleteClientCommand.php @@ -30,7 +30,7 @@ public function __construct(ClientManagerInterface $clientManager) protected function configure(): void { $this - ->setDescription('Deletes an oAuth2 client') + ->setDescription('Deletes an OAuth2 client') ->addArgument( 'identifier', InputArgument::REQUIRED, @@ -42,15 +42,15 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $identifier = $input->getArgument('identifier'); - $client = $this->clientManager->find($identifier); - if (null === $client) { - $io->error(sprintf('oAuth2 client identified as "%s" does not exist', $identifier)); + + if (null === $client = $this->clientManager->find($input->getArgument('identifier'))) { + $io->error(sprintf('OAuth2 client identified as "%s" does not exist.', $input->getArgument('identifier'))); return 1; } + $this->clientManager->remove($client); - $io->success('Given oAuth2 client deleted successfully.'); + $io->success('OAuth2 client deleted successfully.'); return 0; } diff --git a/src/Command/ListClientsCommand.php b/src/Command/ListClientsCommand.php index b556a68f..0cbbec95 100644 --- a/src/Command/ListClientsCommand.php +++ b/src/Command/ListClientsCommand.php @@ -6,7 +6,7 @@ use League\Bundle\OAuth2ServerBundle\Manager\ClientFilter; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; -use League\Bundle\OAuth2ServerBundle\Model\Client; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\Bundle\OAuth2ServerBundle\Model\Grant; use League\Bundle\OAuth2ServerBundle\Model\RedirectUri; use League\Bundle\OAuth2ServerBundle\Model\Scope; @@ -18,7 +18,7 @@ final class ListClientsCommand extends Command { - private const ALLOWED_COLUMNS = ['identifier', 'secret', 'scope', 'redirect uri', 'grant type']; + private const ALLOWED_COLUMNS = ['name', 'identifier', 'secret', 'scope', 'redirect uri', 'grant type']; protected static $defaultName = 'league:oauth2-server:list-clients'; @@ -112,8 +112,9 @@ private function drawTable(InputInterface $input, OutputInterface $output, array private function getRows(array $clients, array $columns): array { - return array_map(static function (Client $client) use ($columns): array { + return array_map(static function (AbstractClient $client) use ($columns): array { $values = [ + 'name' => $client->getName(), 'identifier' => $client->getIdentifier(), 'secret' => $client->getSecret(), 'scope' => implode(', ', $client->getScopes()), diff --git a/src/Command/UpdateClientCommand.php b/src/Command/UpdateClientCommand.php index 05fe9710..a1756e78 100644 --- a/src/Command/UpdateClientCommand.php +++ b/src/Command/UpdateClientCommand.php @@ -5,7 +5,7 @@ namespace League\Bundle\OAuth2ServerBundle\Command; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; -use League\Bundle\OAuth2ServerBundle\Model\Client; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\Bundle\OAuth2ServerBundle\Model\Grant; use League\Bundle\OAuth2ServerBundle\Model\RedirectUri; use League\Bundle\OAuth2ServerBundle\Model\Scope; @@ -35,7 +35,7 @@ public function __construct(ClientManagerInterface $clientManager) protected function configure(): void { $this - ->setDescription('Updates an oAuth2 client') + ->setDescription('Updates an OAuth2 client') ->addOption( 'redirect-uri', null, @@ -57,6 +57,13 @@ protected function configure(): void 'Sets allowed scope for client. Use this option multiple times to set multiple scopes.', [] ) + ->addOption( + 'name', + null, + InputOption::VALUE_REQUIRED, + 'Sets name for client.', + [] + ) ->addOption( 'deactivated', null, @@ -76,19 +83,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); if (null === $client = $this->clientManager->find($input->getArgument('identifier'))) { - $io->error(sprintf('oAuth2 client identified as "%s"', $input->getArgument('identifier'))); + $io->error(sprintf('OAuth2 client identified as "%s" does not exist.', $input->getArgument('identifier'))); return 1; } $client = $this->updateClientFromInput($client, $input); $this->clientManager->save($client); - $io->success('Given oAuth2 client updated successfully.'); + + $io->success('OAuth2 client updated successfully.'); return 0; } - private function updateClientFromInput(Client $client, InputInterface $input): Client + private function updateClientFromInput(AbstractClient $client, InputInterface $input): AbstractClient { $client->setActive(!$input->getOption('deactivated')); @@ -99,7 +107,7 @@ private function updateClientFromInput(Client $client, InputInterface $input): C /** @var list $scopeStrings */ $scopeStrings = $input->getOption('scope'); - return $client + $client ->setRedirectUris(...array_map(static function (string $redirectUri): RedirectUri { return new RedirectUri($redirectUri); }, $redirectUriStrings)) @@ -110,5 +118,11 @@ private function updateClientFromInput(Client $client, InputInterface $input): C return new Scope($scope); }, $scopeStrings)) ; + + if ($name = $input->getOption('name')) { + $client->setName($name); + } + + return $client; } } diff --git a/src/Controller/AuthorizationController.php b/src/Controller/AuthorizationController.php index 39c29ad6..d76a4238 100644 --- a/src/Controller/AuthorizationController.php +++ b/src/Controller/AuthorizationController.php @@ -8,7 +8,7 @@ use League\Bundle\OAuth2ServerBundle\Event\AuthorizationRequestResolveEvent; use League\Bundle\OAuth2ServerBundle\Event\AuthorizationRequestResolveEventFactory; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; -use League\Bundle\OAuth2ServerBundle\Model\Client; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\Bundle\OAuth2ServerBundle\OAuth2Events; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; @@ -90,7 +90,7 @@ public function indexAction(Request $request): Response $authRequest = $this->server->validateAuthorizationRequest($serverRequest); if ('plain' === $authRequest->getCodeChallengeMethod()) { - /** @var Client $client */ + /** @var AbstractClient $client */ $client = $this->clientManager->find($authRequest->getClient()->getIdentifier()); if (!$client->isPlainTextPkceAllowed()) { throw OAuthServerException::invalidRequest('code_challenge_method', 'Plain code challenge method is not allowed for this client'); diff --git a/src/DependencyInjection/CompilerPass/RegisterDoctrineOrmMappingPass.php b/src/DependencyInjection/CompilerPass/RegisterDoctrineOrmMappingPass.php new file mode 100644 index 00000000..92c0d8a1 --- /dev/null +++ b/src/DependencyInjection/CompilerPass/RegisterDoctrineOrmMappingPass.php @@ -0,0 +1,26 @@ + + */ +class RegisterDoctrineOrmMappingPass extends DoctrineOrmMappingsPass +{ + public function __construct() + { + parent::__construct( + new Reference(Driver::class), + ['League\Bundle\OAuth2ServerBundle\Model'], + ['league.oauth2_server.persistence.doctrine.manager'], + 'league.oauth2_server.persistence.doctrine.enabled', + ['LeagueOAuth2ServerBundle' => 'League\Bundle\OAuth2ServerBundle\Model'] + ); + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 264f1e50..5244c1a5 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -5,6 +5,8 @@ namespace League\Bundle\OAuth2ServerBundle\DependencyInjection; use Defuse\Crypto\Key; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; +use League\Bundle\OAuth2ServerBundle\Model\Client; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -23,6 +25,7 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode->append($this->createResourceServerNode()); $rootNode->append($this->createScopesNode()); $rootNode->append($this->createPersistenceNode()); + $rootNode->append($this->createClientNode()); $rootNode ->children() @@ -171,4 +174,28 @@ private function createPersistenceNode(): NodeDefinition return $node; } + + private function createClientNode(): NodeDefinition + { + $treeBuilder = new TreeBuilder('client'); + $node = $treeBuilder->getRootNode(); + + $node + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('classname') + ->info(sprintf('Set a custom client class. Must be a %s', AbstractClient::class)) + ->defaultValue(Client::class) + ->validate() + ->ifTrue(function ($v) { + return !is_a($v, AbstractClient::class, true); + }) + ->thenInvalid(sprintf('%%s must be a %s', AbstractClient::class)) + ->end() + ->end() + ->end() + ; + + return $node; + } } diff --git a/src/DependencyInjection/LeagueOAuth2ServerExtension.php b/src/DependencyInjection/LeagueOAuth2ServerExtension.php index a817cd61..87278215 100644 --- a/src/DependencyInjection/LeagueOAuth2ServerExtension.php +++ b/src/DependencyInjection/LeagueOAuth2ServerExtension.php @@ -7,6 +7,7 @@ use Defuse\Crypto\Key; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use League\Bundle\OAuth2ServerBundle\AuthorizationServer\GrantTypeInterface; +use League\Bundle\OAuth2ServerBundle\Command\CreateClientCommand; use League\Bundle\OAuth2ServerBundle\DBAL\Type\Grant as GrantType; use League\Bundle\OAuth2ServerBundle\DBAL\Type\RedirectUri as RedirectUriType; use League\Bundle\OAuth2ServerBundle\DBAL\Type\Scope as ScopeType; @@ -15,7 +16,9 @@ use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\ClientManager; use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\RefreshTokenManager; use League\Bundle\OAuth2ServerBundle\Manager\ScopeManagerInterface; +use League\Bundle\OAuth2ServerBundle\Model\Client; use League\Bundle\OAuth2ServerBundle\Model\Scope as ScopeModel; +use League\Bundle\OAuth2ServerBundle\Persistence\Mapping\Driver; use League\Bundle\OAuth2ServerBundle\Security\Authenticator\OAuth2Authenticator; use League\Bundle\OAuth2ServerBundle\Service\CredentialsRevoker\DoctrineCredentialsRevoker; use League\OAuth2\Server\AuthorizationServer; @@ -53,7 +56,7 @@ public function load(array $configs, ContainerBuilder $container) $config = $this->processConfiguration(new Configuration(), $configs); - $this->configurePersistence($loader, $container, $config['persistence']); + $this->configurePersistence($loader, $container, $config); $this->configureAuthorizationServer($container, $config['authorization_server']); $this->configureResourceServer($container, $config['resource_server']); $this->configureScopes($container, $config['scopes']); @@ -63,6 +66,11 @@ public function load(array $configs, ContainerBuilder $container) $container->registerForAutoconfiguration(GrantTypeInterface::class) ->addTag('league.oauth2_server.authorization_server.grant'); + + $container + ->findDefinition(CreateClientCommand::class) + ->replaceArgument(1, $config['client']['classname']) + ; } /** @@ -205,12 +213,12 @@ private function configureGrants(ContainerBuilder $container, array $config): vo */ private function configurePersistence(LoaderInterface $loader, ContainerBuilder $container, array $config): void { - if (\count($config) > 1) { + if (\count($config['persistence']) > 1) { throw new \LogicException('Only one persistence method can be configured at a time.'); } - $persistenceConfiguration = current($config); - $persistenceMethod = key($config); + $persistenceConfig = current($config['persistence']); + $persistenceMethod = key($config['persistence']); switch ($persistenceMethod) { case 'in_memory': @@ -219,14 +227,14 @@ private function configurePersistence(LoaderInterface $loader, ContainerBuilder break; case 'doctrine': $loader->load('storage/doctrine.php'); - $this->configureDoctrinePersistence($container, $persistenceConfiguration); + $this->configureDoctrinePersistence($container, $config, $persistenceConfig); break; } } - private function configureDoctrinePersistence(ContainerBuilder $container, array $config): void + private function configureDoctrinePersistence(ContainerBuilder $container, array $config, array $persistenceConfig): void { - $entityManagerName = $config['entity_manager']; + $entityManagerName = $persistenceConfig['entity_manager']; $entityManager = new Reference( sprintf('doctrine.orm.%s_entity_manager', $entityManagerName) @@ -240,6 +248,7 @@ private function configureDoctrinePersistence(ContainerBuilder $container, array $container ->findDefinition(ClientManager::class) ->replaceArgument(0, $entityManager) + ->replaceArgument(1, $config['client']['classname']) ; $container @@ -257,6 +266,11 @@ private function configureDoctrinePersistence(ContainerBuilder $container, array ->replaceArgument(0, $entityManager) ; + $container + ->findDefinition(Driver::class) + ->replaceArgument(0, Client::class !== $config['client']['classname']) + ; + $container->setParameter('league.oauth2_server.persistence.doctrine.enabled', true); $container->setParameter('league.oauth2_server.persistence.doctrine.manager', $entityManagerName); } diff --git a/src/Event/AuthorizationRequestResolveEvent.php b/src/Event/AuthorizationRequestResolveEvent.php index 91042fa2..30146f10 100644 --- a/src/Event/AuthorizationRequestResolveEvent.php +++ b/src/Event/AuthorizationRequestResolveEvent.php @@ -4,7 +4,7 @@ namespace League\Bundle\OAuth2ServerBundle\Event; -use League\Bundle\OAuth2ServerBundle\Model\Client; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\Bundle\OAuth2ServerBundle\Model\Scope; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use Symfony\Component\HttpFoundation\Response; @@ -27,7 +27,7 @@ final class AuthorizationRequestResolveEvent extends Event private $scopes; /** - * @var Client + * @var AbstractClient */ private $client; @@ -49,7 +49,7 @@ final class AuthorizationRequestResolveEvent extends Event /** * @param Scope[] $scopes */ - public function __construct(AuthorizationRequest $authorizationRequest, array $scopes, Client $client) + public function __construct(AuthorizationRequest $authorizationRequest, array $scopes, AbstractClient $client) { $this->authorizationRequest = $authorizationRequest; $this->scopes = $scopes; @@ -94,7 +94,7 @@ public function getGrantTypeId(): string /** * @psalm-mutation-free */ - public function getClient(): Client + public function getClient(): AbstractClient { return $this->client; } diff --git a/src/Event/ScopeResolveEvent.php b/src/Event/ScopeResolveEvent.php index 09fd3cd5..244c23f8 100644 --- a/src/Event/ScopeResolveEvent.php +++ b/src/Event/ScopeResolveEvent.php @@ -4,7 +4,7 @@ namespace League\Bundle\OAuth2ServerBundle\Event; -use League\Bundle\OAuth2ServerBundle\Model\Client; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\Bundle\OAuth2ServerBundle\Model\Grant; use League\Bundle\OAuth2ServerBundle\Model\Scope; use Symfony\Contracts\EventDispatcher\Event; @@ -22,7 +22,7 @@ final class ScopeResolveEvent extends Event private $grant; /** - * @var Client + * @var AbstractClient */ private $client; @@ -34,7 +34,7 @@ final class ScopeResolveEvent extends Event /** * @param list $scopes */ - public function __construct(array $scopes, Grant $grant, Client $client, ?string $userIdentifier) + public function __construct(array $scopes, Grant $grant, AbstractClient $client, ?string $userIdentifier) { $this->scopes = $scopes; $this->grant = $grant; @@ -63,7 +63,7 @@ public function getGrant(): Grant return $this->grant; } - public function getClient(): Client + public function getClient(): AbstractClient { return $this->client; } diff --git a/src/Event/UserResolveEvent.php b/src/Event/UserResolveEvent.php index a2a44260..cbdaba8a 100644 --- a/src/Event/UserResolveEvent.php +++ b/src/Event/UserResolveEvent.php @@ -4,7 +4,7 @@ namespace League\Bundle\OAuth2ServerBundle\Event; -use League\Bundle\OAuth2ServerBundle\Model\Client; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\Bundle\OAuth2ServerBundle\Model\Grant; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Contracts\EventDispatcher\Event; @@ -27,7 +27,7 @@ final class UserResolveEvent extends Event private $grant; /** - * @var Client + * @var AbstractClient */ private $client; @@ -36,7 +36,7 @@ final class UserResolveEvent extends Event */ private $user; - public function __construct(string $username, string $password, Grant $grant, Client $client) + public function __construct(string $username, string $password, Grant $grant, AbstractClient $client) { $this->username = $username; $this->password = $password; @@ -59,7 +59,7 @@ public function getGrant(): Grant return $this->grant; } - public function getClient(): Client + public function getClient(): AbstractClient { return $this->client; } diff --git a/src/LeagueOAuth2ServerBundle.php b/src/LeagueOAuth2ServerBundle.php index f09990b8..d1f6a364 100644 --- a/src/LeagueOAuth2ServerBundle.php +++ b/src/LeagueOAuth2ServerBundle.php @@ -4,8 +4,8 @@ namespace League\Bundle\OAuth2ServerBundle; -use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass; use League\Bundle\OAuth2ServerBundle\DependencyInjection\CompilerPass\EncryptionKeyPass; +use League\Bundle\OAuth2ServerBundle\DependencyInjection\CompilerPass\RegisterDoctrineOrmMappingPass; use League\Bundle\OAuth2ServerBundle\DependencyInjection\LeagueOAuth2ServerExtension; use League\Bundle\OAuth2ServerBundle\DependencyInjection\Security\OAuth2Factory; use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; @@ -44,24 +44,7 @@ private function configureSecurityExtension(ContainerBuilder $container): void private function configureDoctrineExtension(ContainerBuilder $container): void { - /** @var string $modelDirectory */ - $modelDirectory = realpath(__DIR__ . '/Resources/config/doctrine/model'); - - $container->addCompilerPass( - DoctrineOrmMappingsPass::createXmlMappingDriver( - [ - $modelDirectory => 'League\Bundle\OAuth2ServerBundle\Model', - ], - [ - 'league.oauth2_server.persistence.doctrine.manager', - ], - 'league.oauth2_server.persistence.doctrine.enabled', - [ - 'LeagueOAuth2ServerBundle' => 'League\Bundle\OAuth2ServerBundle\Model', - ] - ) - ); - + $container->addCompilerPass(new RegisterDoctrineOrmMappingPass()); $container->addCompilerPass(new EncryptionKeyPass()); } } diff --git a/src/Manager/ClientManagerInterface.php b/src/Manager/ClientManagerInterface.php index ec76f8a4..3e9da70a 100644 --- a/src/Manager/ClientManagerInterface.php +++ b/src/Manager/ClientManagerInterface.php @@ -4,18 +4,18 @@ namespace League\Bundle\OAuth2ServerBundle\Manager; -use League\Bundle\OAuth2ServerBundle\Model\Client; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; interface ClientManagerInterface { - public function save(Client $client): void; + public function save(AbstractClient $client): void; - public function remove(Client $client): void; + public function remove(AbstractClient $client): void; - public function find(string $identifier): ?Client; + public function find(string $identifier): ?AbstractClient; /** - * @return list + * @return list */ public function list(?ClientFilter $clientFilter): array; } diff --git a/src/Manager/Doctrine/ClientManager.php b/src/Manager/Doctrine/ClientManager.php index c5574288..bae4346d 100644 --- a/src/Manager/Doctrine/ClientManager.php +++ b/src/Manager/Doctrine/ClientManager.php @@ -7,45 +7,60 @@ use Doctrine\ORM\EntityManagerInterface; use League\Bundle\OAuth2ServerBundle\Manager\ClientFilter; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; -use League\Bundle\OAuth2ServerBundle\Model\Client; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\Bundle\OAuth2ServerBundle\Model\Grant; use League\Bundle\OAuth2ServerBundle\Model\RedirectUri; use League\Bundle\OAuth2ServerBundle\Model\Scope; final class ClientManager implements ClientManagerInterface { + /** + * @var EntityManagerInterface + */ private $entityManager; - public function __construct(EntityManagerInterface $entityManager) + /** + * @var class-string + */ + private $clientFqcn; + + /** + * @param class-string $clientFqcn + */ + public function __construct(EntityManagerInterface $entityManager, string $clientFqcn) { $this->entityManager = $entityManager; + $this->clientFqcn = $clientFqcn; } - public function find(string $identifier): ?Client + public function find(string $identifier): ?AbstractClient { - return $this->entityManager->find(Client::class, $identifier); + $repository = $this->entityManager->getRepository($this->clientFqcn); + + return $repository->findOneBy(['identifier' => $identifier]); } - public function save(Client $client): void + public function save(AbstractClient $client): void { $this->entityManager->persist($client); $this->entityManager->flush(); } - public function remove(Client $client): void + public function remove(AbstractClient $client): void { $this->entityManager->remove($client); $this->entityManager->flush(); } /** - * @return list + * @return list */ public function list(?ClientFilter $clientFilter): array { - $repository = $this->entityManager->getRepository(Client::class); + $repository = $this->entityManager->getRepository($this->clientFqcn); $criteria = self::filterToCriteria($clientFilter); + /** @var list */ return array_values($repository->findBy($criteria)); } diff --git a/src/Manager/InMemory/ClientManager.php b/src/Manager/InMemory/ClientManager.php index 22154b40..e97e5b03 100644 --- a/src/Manager/InMemory/ClientManager.php +++ b/src/Manager/InMemory/ClientManager.php @@ -6,7 +6,7 @@ use League\Bundle\OAuth2ServerBundle\Manager\ClientFilter; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; -use League\Bundle\OAuth2ServerBundle\Model\Client; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\Bundle\OAuth2ServerBundle\Model\Grant; use League\Bundle\OAuth2ServerBundle\Model\RedirectUri; use League\Bundle\OAuth2ServerBundle\Model\Scope; @@ -14,27 +14,27 @@ final class ClientManager implements ClientManagerInterface { /** - * @var array + * @var array */ private $clients = []; - public function find(string $identifier): ?Client + public function find(string $identifier): ?AbstractClient { return $this->clients[$identifier] ?? null; } - public function save(Client $client): void + public function save(AbstractClient $client): void { $this->clients[$client->getIdentifier()] = $client; } - public function remove(Client $client): void + public function remove(AbstractClient $client): void { unset($this->clients[$client->getIdentifier()]); } /** - * @return list + * @return list */ public function list(?ClientFilter $clientFilter): array { @@ -42,7 +42,7 @@ public function list(?ClientFilter $clientFilter): array return array_values($this->clients); } - return array_values(array_filter($this->clients, static function (Client $client) use ($clientFilter): bool { + return array_values(array_filter($this->clients, static function (AbstractClient $client) use ($clientFilter): bool { if (!self::passesFilter($client->getGrants(), $clientFilter->getGrants())) { return false; } diff --git a/src/Model/AbstractClient.php b/src/Model/AbstractClient.php new file mode 100644 index 00000000..2eba67f5 --- /dev/null +++ b/src/Model/AbstractClient.php @@ -0,0 +1,186 @@ + + */ +abstract class AbstractClient +{ + /** + * @var string + */ + private $name; + + /** + * @var string + */ + private $identifier; + + /** + * @var string|null + */ + private $secret; + + /** + * @var list + */ + private $redirectUris = []; + + /** + * @var list + */ + private $grants = []; + + /** + * @var list + */ + private $scopes = []; + + /** + * @var bool + */ + private $active = true; + + /** + * @var bool + */ + private $allowPlainTextPkce = false; + + /** + * @psalm-mutation-free + */ + public function __construct(string $name, string $identifier, ?string $secret) + { + $this->name = $name; + $this->identifier = $identifier; + $this->secret = $secret; + } + + /** + * @psalm-mutation-free + */ + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + /** + * @psalm-mutation-free + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @psalm-mutation-free + */ + public function getSecret(): ?string + { + return $this->secret; + } + + /** + * @return list + * + * @psalm-mutation-free + */ + public function getRedirectUris(): array + { + return $this->redirectUris; + } + + public function setRedirectUris(RedirectUri ...$redirectUris): self + { + /** @var list $redirectUris */ + $this->redirectUris = $redirectUris; + + return $this; + } + + /** + * @return list + * + * @psalm-mutation-free + */ + public function getGrants(): array + { + return $this->grants; + } + + public function setGrants(Grant ...$grants): self + { + /** @var list $grants */ + $this->grants = $grants; + + return $this; + } + + /** + * @return list + * + * @psalm-mutation-free + */ + public function getScopes(): array + { + return $this->scopes; + } + + public function setScopes(Scope ...$scopes): self + { + /** @var list $scopes */ + $this->scopes = $scopes; + + return $this; + } + + /** + * @psalm-mutation-free + */ + public function isActive(): bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + + return $this; + } + + /** + * @psalm-mutation-free + */ + public function isConfidential(): bool + { + return !empty($this->secret); + } + + /** + * @psalm-mutation-free + */ + public function isPlainTextPkceAllowed(): bool + { + return $this->allowPlainTextPkce; + } + + public function setAllowPlainTextPkce(bool $allowPlainTextPkce): self + { + $this->allowPlainTextPkce = $allowPlainTextPkce; + + return $this; + } +} diff --git a/src/Model/AccessToken.php b/src/Model/AccessToken.php index ec230e40..d887e1e0 100644 --- a/src/Model/AccessToken.php +++ b/src/Model/AccessToken.php @@ -22,7 +22,7 @@ class AccessToken private $userIdentifier; /** - * @var Client + * @var AbstractClient */ private $client; @@ -44,7 +44,7 @@ class AccessToken public function __construct( string $identifier, \DateTimeInterface $expiry, - Client $client, + AbstractClient $client, ?string $userIdentifier, array $scopes ) { @@ -90,7 +90,7 @@ public function getUserIdentifier(): ?string /** * @psalm-mutation-free */ - public function getClient(): Client + public function getClient(): AbstractClient { return $this->client; } diff --git a/src/Model/AuthorizationCode.php b/src/Model/AuthorizationCode.php index 28a048b9..7df631d5 100644 --- a/src/Model/AuthorizationCode.php +++ b/src/Model/AuthorizationCode.php @@ -22,7 +22,7 @@ class AuthorizationCode private $userIdentifier; /** - * @var Client + * @var AbstractClient */ private $client; @@ -44,7 +44,7 @@ class AuthorizationCode public function __construct( string $identifier, \DateTimeInterface $expiry, - Client $client, + AbstractClient $client, ?string $userIdentifier, array $scopes ) { @@ -90,7 +90,7 @@ public function getUserIdentifier(): ?string /** * @psalm-mutation-free */ - public function getClient(): Client + public function getClient(): AbstractClient { return $this->client; } diff --git a/src/Model/Client.php b/src/Model/Client.php index aad290f9..d60b6aa2 100644 --- a/src/Model/Client.php +++ b/src/Model/Client.php @@ -4,179 +4,6 @@ namespace League\Bundle\OAuth2ServerBundle\Model; -class Client +class Client extends AbstractClient { - /** - * @var string - */ - private $name; - - /** - * @var string - */ - private $identifier; - - /** - * @var string|null - */ - private $secret; - - /** - * @var list - */ - private $redirectUris = []; - - /** - * @var list - */ - private $grants = []; - - /** - * @var list - */ - private $scopes = []; - - /** - * @var bool - */ - private $active = true; - - /** - * @var bool - */ - private $allowPlainTextPkce = false; - - /** - * @psalm-mutation-free - */ - public function __construct(string $name, string $identifier, ?string $secret) - { - $this->name = $name; - $this->identifier = $identifier; - $this->secret = $secret; - } - - /** - * @psalm-mutation-free - */ - public function __toString(): string - { - return $this->getIdentifier(); - } - - /** - * @psalm-mutation-free - */ - public function getName(): string - { - return $this->name; - } - - /** - * @psalm-mutation-free - */ - public function getIdentifier(): string - { - return $this->identifier; - } - - /** - * @psalm-mutation-free - */ - public function getSecret(): ?string - { - return $this->secret; - } - - /** - * @return list - * - * @psalm-mutation-free - */ - public function getRedirectUris(): array - { - return $this->redirectUris; - } - - public function setRedirectUris(RedirectUri ...$redirectUris): self - { - /** @var list $redirectUris */ - $this->redirectUris = $redirectUris; - - return $this; - } - - /** - * @return list - * - * @psalm-mutation-free - */ - public function getGrants(): array - { - return $this->grants; - } - - public function setGrants(Grant ...$grants): self - { - /** @var list $grants */ - $this->grants = $grants; - - return $this; - } - - /** - * @return list - * - * @psalm-mutation-free - */ - public function getScopes(): array - { - return $this->scopes; - } - - public function setScopes(Scope ...$scopes): self - { - /** @var list $scopes */ - $this->scopes = $scopes; - - return $this; - } - - /** - * @psalm-mutation-free - */ - public function isActive(): bool - { - return $this->active; - } - - public function setActive(bool $active): self - { - $this->active = $active; - - return $this; - } - - /** - * @psalm-mutation-free - */ - public function isConfidential(): bool - { - return !empty($this->secret); - } - - /** - * @psalm-mutation-free - */ - public function isPlainTextPkceAllowed(): bool - { - return $this->allowPlainTextPkce; - } - - public function setAllowPlainTextPkce(bool $allowPlainTextPkce): self - { - $this->allowPlainTextPkce = $allowPlainTextPkce; - - return $this; - } } diff --git a/src/Persistence/Mapping/Driver.php b/src/Persistence/Mapping/Driver.php new file mode 100644 index 00000000..3cf39f03 --- /dev/null +++ b/src/Persistence/Mapping/Driver.php @@ -0,0 +1,135 @@ + + */ +class Driver implements MappingDriver +{ + /** + * @var bool + */ + private $withCustomClientClass; + + public function __construct(bool $withCustomClientClass) + { + $this->withCustomClientClass = $withCustomClientClass; + } + + public function loadMetadataForClass($className, ClassMetadata $metadata): void + { + switch ($className) { + case AbstractClient::class: + $this->buildAbstractClientMetadata($metadata); + + break; + case AccessToken::class: + $this->buildAccessTokenMetadata($metadata); + + break; + case AuthorizationCode::class: + $this->buildAuthorizationCodeMetadata($metadata); + + break; + case Client::class: + $this->buildClientMetadata($metadata); + + break; + case RefreshToken::class: + $this->buildRefreshTokenMetadata($metadata); + + break; + default: + throw new \RuntimeException(sprintf('%s cannot load metadata for class %s', __CLASS__, $className)); + } + } + + public function getAllClassNames(): array + { + return array_merge( + [ + AbstractClient::class, + AccessToken::class, + AuthorizationCode::class, + RefreshToken::class, + ], + $this->withCustomClientClass ? [] : [Client::class] + ); + } + + public function isTransient($className): bool + { + return AbstractClient::class !== $className; + } + + private function buildAbstractClientMetadata(ClassMetadata $metadata): void + { + (new ClassMetadataBuilder($metadata)) + ->setMappedSuperClass() + ->createField('identifier', 'string')->makePrimaryKey()->length(32)->build() + ->createField('name', 'string')->length(128)->build() + ->createField('secret', 'string')->length(128)->nullable(true)->build() + ->createField('redirectUris', 'oauth2_redirect_uri')->nullable(true)->build() + ->createField('grants', 'oauth2_grant')->nullable(true)->build() + ->createField('scopes', 'oauth2_scope')->nullable(true)->build() + ->addField('active', 'boolean') + ->createField('allowPlainTextPkce', 'boolean')->option('default', 0)->build() + ; + } + + private function buildAccessTokenMetadata(ClassMetadata $metadata): void + { + (new ClassMetadataBuilder($metadata)) + ->setTable('oauth2_access_token') + ->createField('identifier', 'string')->makePrimaryKey()->length(80)->option('fixed', true)->build() + ->addField('expiry', 'datetime_immutable') + ->createField('userIdentifier', 'string')->length(128)->nullable(true)->build() + ->createField('scopes', 'oauth2_scope')->nullable(true)->build() + ->addField('revoked', 'boolean') + ->createManyToOne('client', Client::class)->addJoinColumn('client', 'identifier', false, false, 'CASCADE')->build() + ; + } + + private function buildAuthorizationCodeMetadata(ClassMetadata $metadata): void + { + (new ClassMetadataBuilder($metadata)) + ->setTable('oauth2_authorization_code') + ->createField('identifier', 'string')->makePrimaryKey()->length(80)->option('fixed', true)->build() + ->addField('expiry', 'datetime_immutable') + ->createField('userIdentifier', 'string')->length(128)->nullable(true)->build() + ->createField('scopes', 'oauth2_scope')->nullable(true)->build() + ->addField('revoked', 'boolean') + ->createManyToOne('client', Client::class)->addJoinColumn('client', 'identifier', false, false, 'CASCADE')->build() + ; + } + + private function buildClientMetadata(ClassMetadata $metadata): void + { + (new ClassMetadataBuilder($metadata))->setTable('oauth2_client'); + } + + private function buildRefreshTokenMetadata(ClassMetadata $metadata): void + { + (new ClassMetadataBuilder($metadata)) + ->setTable('oauth2_refresh_token') + ->createField('identifier', 'string')->makePrimaryKey()->length(80)->option('fixed', true)->build() + ->addField('expiry', 'datetime_immutable') + ->addField('revoked', 'boolean') + ->createManyToOne('accessToken', AccessToken::class)->addJoinColumn('access_token', 'identifier', true, false, 'SET NULL')->build() + ; + } +} diff --git a/src/Repository/AccessTokenRepository.php b/src/Repository/AccessTokenRepository.php index 205a1b08..b6a65971 100644 --- a/src/Repository/AccessTokenRepository.php +++ b/src/Repository/AccessTokenRepository.php @@ -8,8 +8,8 @@ use League\Bundle\OAuth2ServerBundle\Entity\AccessToken as AccessTokenEntity; use League\Bundle\OAuth2ServerBundle\Manager\AccessTokenManagerInterface; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\Bundle\OAuth2ServerBundle\Model\AccessToken as AccessTokenModel; -use League\Bundle\OAuth2ServerBundle\Model\Client; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; @@ -107,7 +107,7 @@ public function isAccessTokenRevoked($tokenId): bool private function buildAccessTokenModel(AccessTokenEntityInterface $accessTokenEntity): AccessTokenModel { - /** @var Client $client */ + /** @var AbstractClient $client */ $client = $this->clientManager->find($accessTokenEntity->getClient()->getIdentifier()); $userIdentifier = $accessTokenEntity->getUserIdentifier(); diff --git a/src/Repository/AuthCodeRepository.php b/src/Repository/AuthCodeRepository.php index 775f0ad4..b9ebc22a 100644 --- a/src/Repository/AuthCodeRepository.php +++ b/src/Repository/AuthCodeRepository.php @@ -8,8 +8,8 @@ use League\Bundle\OAuth2ServerBundle\Entity\AuthCode; use League\Bundle\OAuth2ServerBundle\Manager\AuthorizationCodeManagerInterface; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\Bundle\OAuth2ServerBundle\Model\AuthorizationCode; -use League\Bundle\OAuth2ServerBundle\Model\Client; use League\OAuth2\Server\Entities\AuthCodeEntityInterface; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; @@ -99,7 +99,7 @@ public function isAuthCodeRevoked($codeId): bool private function buildAuthorizationCode(AuthCodeEntityInterface $authCode): AuthorizationCode { - /** @var Client $client */ + /** @var AbstractClient $client */ $client = $this->clientManager->find($authCode->getClient()->getIdentifier()); $userIdentifier = $authCode->getUserIdentifier(); diff --git a/src/Repository/ClientRepository.php b/src/Repository/ClientRepository.php index e6c3c764..d270d940 100644 --- a/src/Repository/ClientRepository.php +++ b/src/Repository/ClientRepository.php @@ -6,7 +6,7 @@ use League\Bundle\OAuth2ServerBundle\Entity\Client as ClientEntity; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; -use League\Bundle\OAuth2ServerBundle\Model\Client as ClientModel; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; final class ClientRepository implements ClientRepositoryInterface @@ -61,7 +61,7 @@ public function validateClient($clientIdentifier, $clientSecret, $grantType): bo return false; } - private function buildClientEntity(ClientModel $client): ClientEntity + private function buildClientEntity(AbstractClient $client): ClientEntity { $clientEntity = new ClientEntity(); $clientEntity->setName($client->getName()); @@ -73,7 +73,7 @@ private function buildClientEntity(ClientModel $client): ClientEntity return $clientEntity; } - private function isGrantSupported(ClientModel $client, ?string $grant): bool + private function isGrantSupported(AbstractClient $client, ?string $grant): bool { if (null === $grant) { return true; diff --git a/src/Repository/ScopeRepository.php b/src/Repository/ScopeRepository.php index e804ca56..9ab6a2a5 100644 --- a/src/Repository/ScopeRepository.php +++ b/src/Repository/ScopeRepository.php @@ -8,10 +8,9 @@ use League\Bundle\OAuth2ServerBundle\Event\ScopeResolveEvent; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; use League\Bundle\OAuth2ServerBundle\Manager\ScopeManagerInterface; -use League\Bundle\OAuth2ServerBundle\Model\Client; -use League\Bundle\OAuth2ServerBundle\Model\Client as ClientModel; -use League\Bundle\OAuth2ServerBundle\Model\Grant as GrantModel; -use League\Bundle\OAuth2ServerBundle\Model\Scope as ScopeModel; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; +use League\Bundle\OAuth2ServerBundle\Model\Grant; +use League\Bundle\OAuth2ServerBundle\Model\Scope; use League\Bundle\OAuth2ServerBundle\OAuth2Events; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; @@ -80,7 +79,7 @@ public function finalizeScopes( ClientEntityInterface $clientEntity, $userIdentifier = null ): array { - /** @var Client $client */ + /** @var AbstractClient $client */ $client = $this->clientManager->find($clientEntity->getIdentifier()); $scopes = $this->setupScopes($client, $this->scopeConverter->toDomainArray(array_values($scopes))); @@ -89,7 +88,7 @@ public function finalizeScopes( $event = $this->eventDispatcher->dispatch( new ScopeResolveEvent( $scopes, - new GrantModel($grantType), + new Grant($grantType), $client, $userIdentifier ), @@ -100,11 +99,11 @@ public function finalizeScopes( } /** - * @param list $requestedScopes + * @param list $requestedScopes * - * @return list + * @return list */ - private function setupScopes(ClientModel $client, array $requestedScopes): array + private function setupScopes(AbstractClient $client, array $requestedScopes): array { $clientScopes = $client->getScopes(); diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index aa9c0fe5..783ca798 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -7,8 +7,8 @@ use League\Bundle\OAuth2ServerBundle\Converter\UserConverterInterface; use League\Bundle\OAuth2ServerBundle\Event\UserResolveEvent; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; -use League\Bundle\OAuth2ServerBundle\Model\Client; -use League\Bundle\OAuth2ServerBundle\Model\Grant as GrantModel; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; +use League\Bundle\OAuth2ServerBundle\Model\Grant; use League\Bundle\OAuth2ServerBundle\OAuth2Events; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; @@ -51,7 +51,7 @@ public function getUserEntityByUserCredentials( $grantType, ClientEntityInterface $clientEntity ): ?UserEntityInterface { - /** @var Client $client */ + /** @var AbstractClient $client */ $client = $this->clientManager->find($clientEntity->getIdentifier()); /** @var UserResolveEvent $event */ @@ -59,7 +59,7 @@ public function getUserEntityByUserCredentials( new UserResolveEvent( $username, $password, - new GrantModel($grantType), + new Grant($grantType), $client ), OAuth2Events::USER_RESOLVE diff --git a/src/Resources/config/doctrine/model/AccessToken.orm.xml b/src/Resources/config/doctrine/model/AccessToken.orm.xml deleted file mode 100644 index 6a6c6b9b..00000000 --- a/src/Resources/config/doctrine/model/AccessToken.orm.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/Resources/config/doctrine/model/AuthorizationCode.orm.xml b/src/Resources/config/doctrine/model/AuthorizationCode.orm.xml deleted file mode 100644 index 01e3d20d..00000000 --- a/src/Resources/config/doctrine/model/AuthorizationCode.orm.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/Resources/config/doctrine/model/Client.orm.xml b/src/Resources/config/doctrine/model/Client.orm.xml deleted file mode 100644 index ccac17ce..00000000 --- a/src/Resources/config/doctrine/model/Client.orm.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/Resources/config/doctrine/model/RefreshToken.orm.xml b/src/Resources/config/doctrine/model/RefreshToken.orm.xml deleted file mode 100644 index 868893fe..00000000 --- a/src/Resources/config/doctrine/model/RefreshToken.orm.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index ee60ef67..a9b9e831 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -227,6 +227,7 @@ ->set('league.oauth2_server.command.create_client', CreateClientCommand::class) ->args([ service(ClientManagerInterface::class), + null, ]) ->tag('console.command') ->alias(CreateClientCommand::class, 'league.oauth2_server.command.create_client') diff --git a/src/Resources/config/storage/doctrine.php b/src/Resources/config/storage/doctrine.php index 17d78fb5..360f2fa3 100644 --- a/src/Resources/config/storage/doctrine.php +++ b/src/Resources/config/storage/doctrine.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use function Symfony\Component\DependencyInjection\Loader\Configurator\service; use League\Bundle\OAuth2ServerBundle\Manager\AccessTokenManagerInterface; use League\Bundle\OAuth2ServerBundle\Manager\AuthorizationCodeManagerInterface; use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; @@ -10,6 +11,7 @@ use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\ClientManager; use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\RefreshTokenManager; use League\Bundle\OAuth2ServerBundle\Manager\RefreshTokenManagerInterface; +use League\Bundle\OAuth2ServerBundle\Persistence\Mapping\Driver; use League\Bundle\OAuth2ServerBundle\Service\CredentialsRevoker\DoctrineCredentialsRevoker; use League\Bundle\OAuth2ServerBundle\Service\CredentialsRevokerInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -17,9 +19,16 @@ return static function (ContainerConfigurator $container): void { $container->services() + ->set('league.oauth2_server.persistence.driver', Driver::class) + ->args([ + null, + ]) + ->alias(Driver::class, 'league.oauth2_server.persistence.driver') + ->set('league.oauth2_server.manager.doctrine.client', ClientManager::class) ->args([ null, + null, ]) ->alias(ClientManagerInterface::class, 'league.oauth2_server.manager.doctrine.client') ->alias(ClientManager::class, 'league.oauth2_server.manager.doctrine.client') @@ -48,6 +57,7 @@ ->set('league.oauth2_server.credentials_revoker.doctrine', DoctrineCredentialsRevoker::class) ->args([ null, + service(ClientManagerInterface::class), ]) ->alias(CredentialsRevokerInterface::class, 'league.oauth2_server.credentials_revoker.doctrine') ->alias(DoctrineCredentialsRevoker::class, 'league.oauth2_server.credentials_revoker.doctrine') diff --git a/src/Resources/config/storage/in_memory.php b/src/Resources/config/storage/in_memory.php index b314a345..b900e060 100644 --- a/src/Resources/config/storage/in_memory.php +++ b/src/Resources/config/storage/in_memory.php @@ -16,9 +16,6 @@ $container->services() ->set('league.oauth2_server.manager.in_memory.client', ClientManager::class) - ->args([ - null, - ]) ->alias(ClientManagerInterface::class, 'league.oauth2_server.manager.in_memory.client') ->alias(ClientManager::class, 'league.oauth2_server.manager.in_memory.client') diff --git a/src/Service/CredentialsRevoker/DoctrineCredentialsRevoker.php b/src/Service/CredentialsRevoker/DoctrineCredentialsRevoker.php index a8ea4896..de09e097 100644 --- a/src/Service/CredentialsRevoker/DoctrineCredentialsRevoker.php +++ b/src/Service/CredentialsRevoker/DoctrineCredentialsRevoker.php @@ -5,20 +5,30 @@ namespace League\Bundle\OAuth2ServerBundle\Service\CredentialsRevoker; use Doctrine\ORM\EntityManagerInterface; +use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use League\Bundle\OAuth2ServerBundle\Model\AccessToken; use League\Bundle\OAuth2ServerBundle\Model\AuthorizationCode; -use League\Bundle\OAuth2ServerBundle\Model\Client; use League\Bundle\OAuth2ServerBundle\Model\RefreshToken; use League\Bundle\OAuth2ServerBundle\Service\CredentialsRevokerInterface; use Symfony\Component\Security\Core\User\UserInterface; final class DoctrineCredentialsRevoker implements CredentialsRevokerInterface { + /** + * @var EntityManagerInterface + */ private $entityManager; - public function __construct(EntityManagerInterface $entityManager) + /** + * @var ClientManagerInterface + */ + private $clientManager; + + public function __construct(EntityManagerInterface $entityManager, ClientManagerInterface $clientManager) { $this->entityManager = $entityManager; + $this->clientManager = $clientManager; } public function revokeCredentialsForUser(UserInterface $user): void @@ -58,12 +68,10 @@ public function revokeCredentialsForUser(UserInterface $user): void ->execute(); } - public function revokeCredentialsForClient(Client $client): void + public function revokeCredentialsForClient(AbstractClient $client): void { - /** @var Client $doctrineClient */ - $doctrineClient = $this->entityManager - ->getRepository(Client::class) - ->findOneBy(['identifier' => $client->getIdentifier()]); + /** @var AbstractClient $doctrineClient */ + $doctrineClient = $this->clientManager->find($client->getIdentifier()); $this->entityManager->createQueryBuilder() ->update(AccessToken::class, 'at') diff --git a/src/Service/CredentialsRevokerInterface.php b/src/Service/CredentialsRevokerInterface.php index b90a9cbd..5fe350af 100644 --- a/src/Service/CredentialsRevokerInterface.php +++ b/src/Service/CredentialsRevokerInterface.php @@ -4,7 +4,7 @@ namespace League\Bundle\OAuth2ServerBundle\Service; -use League\Bundle\OAuth2ServerBundle\Model\Client; +use League\Bundle\OAuth2ServerBundle\Model\AbstractClient; use Symfony\Component\Security\Core\User\UserInterface; /** @@ -15,5 +15,5 @@ interface CredentialsRevokerInterface { public function revokeCredentialsForUser(UserInterface $user): void; - public function revokeCredentialsForClient(Client $client): void; + public function revokeCredentialsForClient(AbstractClient $client): void; } diff --git a/tests/Acceptance/CreateClientCommandTest.php b/tests/Acceptance/CreateClientCommandTest.php index ebdd22df..e3985fd1 100644 --- a/tests/Acceptance/CreateClientCommandTest.php +++ b/tests/Acceptance/CreateClientCommandTest.php @@ -20,7 +20,7 @@ public function testCreateClient(): void ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('New oAuth2 client created successfully', $output); + $this->assertStringContainsString('New OAuth2 client created successfully', $output); } public function testCreateClientWithIdentifier(): void @@ -34,7 +34,7 @@ public function testCreateClientWithIdentifier(): void ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('New oAuth2 client created successfully', $output); + $this->assertStringContainsString('New OAuth2 client created successfully', $output); $this->assertStringContainsString('foobar', $output); /** @var Client $client */ @@ -64,7 +64,7 @@ public function testCreatePublicClientWithIdentifier(): void $this->assertSame(0, $commandTester->getStatusCode()); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('New oAuth2 client created successfully', $output); + $this->assertStringContainsString('New OAuth2 client created successfully', $output); $this->assertStringContainsString($clientIdentifier, $output); /** @var Client $client */ @@ -116,7 +116,7 @@ public function testCreateClientWithSecret(): void ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('New oAuth2 client created successfully', $output); + $this->assertStringContainsString('New OAuth2 client created successfully', $output); /** @var Client $client */ $client = $this->client @@ -141,7 +141,7 @@ public function testCreateClientWhoIsAllowedToUsePlainPkceChallengeMethod(): voi ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('New oAuth2 client created successfully', $output); + $this->assertStringContainsString('New OAuth2 client created successfully', $output); /** @var Client $client */ $client = $this->client @@ -164,7 +164,7 @@ public function testCreateClientWithRedirectUris(): void ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('New oAuth2 client created successfully', $output); + $this->assertStringContainsString('New OAuth2 client created successfully', $output); $client = $this->client ->getContainer() ->get(ClientManagerInterface::class) @@ -185,7 +185,7 @@ public function testCreateClientWithGrantTypes(): void ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('New oAuth2 client created successfully', $output); + $this->assertStringContainsString('New OAuth2 client created successfully', $output); $client = $this->client ->getContainer() ->get(ClientManagerInterface::class) @@ -206,7 +206,7 @@ public function testCreateClientWithScopes(): void ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('New oAuth2 client created successfully', $output); + $this->assertStringContainsString('New OAuth2 client created successfully', $output); $client = $this->client ->getContainer() ->get(ClientManagerInterface::class) diff --git a/tests/Acceptance/DeleteClientCommandTest.php b/tests/Acceptance/DeleteClientCommandTest.php index dc8342cf..9eaf0b09 100644 --- a/tests/Acceptance/DeleteClientCommandTest.php +++ b/tests/Acceptance/DeleteClientCommandTest.php @@ -26,7 +26,7 @@ public function testDeleteClient(): void 'identifier' => $client->getIdentifier(), ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('Given oAuth2 client deleted successfully', $output); + $this->assertStringContainsString('OAuth2 client deleted successfully.', $output); $client = $this->findClient($client->getIdentifier()); $this->assertNull($client); @@ -42,7 +42,7 @@ public function testDeleteNonExistentClient(): void 'identifier' => $identifierName, ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString(sprintf('oAuth2 client identified as "%s" does not exist', $identifierName), $output); + $this->assertStringContainsString(sprintf('OAuth2 client identified as "%s" does not exist.', $identifierName), $output); } private function findClient(string $identifier): ?Client diff --git a/tests/Acceptance/DoctrineClientManagerTest.php b/tests/Acceptance/DoctrineClientManagerTest.php index 1bdb90b9..2a6c9ca0 100644 --- a/tests/Acceptance/DoctrineClientManagerTest.php +++ b/tests/Acceptance/DoctrineClientManagerTest.php @@ -20,7 +20,7 @@ public function testSimpleDelete(): void { /** @var $em EntityManagerInterface */ $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); - $doctrineClientManager = new DoctrineClientManager($em); + $doctrineClientManager = new DoctrineClientManager($em, Client::class); $client = new Client('client', 'client', 'secret'); $em->persist($client); @@ -39,7 +39,7 @@ public function testClientDeleteCascadesToAccessTokens(): void { /** @var $em EntityManagerInterface */ $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); - $doctrineClientManager = new DoctrineClientManager($em); + $doctrineClientManager = new DoctrineClientManager($em, Client::class); $client = new Client('client', 'client', 'secret'); $em->persist($client); @@ -72,7 +72,7 @@ public function testClientDeleteCascadesToAccessTokensAndRefreshTokens(): void { /** @var $em EntityManagerInterface */ $em = $this->client->getContainer()->get('doctrine.orm.entity_manager'); - $doctrineClientManager = new DoctrineClientManager($em); + $doctrineClientManager = new DoctrineClientManager($em, Client::class); $client = new Client('client', 'client', 'secret'); $em->persist($client); diff --git a/tests/Acceptance/DoctrineCredentialsRevokerTest.php b/tests/Acceptance/DoctrineCredentialsRevokerTest.php index 1703d002..904fcd21 100644 --- a/tests/Acceptance/DoctrineCredentialsRevokerTest.php +++ b/tests/Acceptance/DoctrineCredentialsRevokerTest.php @@ -5,6 +5,7 @@ namespace League\Bundle\OAuth2ServerBundle\Tests\Acceptance; use Doctrine\ORM\EntityManagerInterface; +use League\Bundle\OAuth2ServerBundle\Manager\Doctrine\ClientManager; use League\Bundle\OAuth2ServerBundle\Model\AccessToken; use League\Bundle\OAuth2ServerBundle\Model\AuthorizationCode; use League\Bundle\OAuth2ServerBundle\Model\Client; @@ -36,7 +37,7 @@ public function testRevokesAllCredentialsForUser(): void $em->persist($refreshToken); $em->flush(); - $revoker = new DoctrineCredentialsRevoker($em); + $revoker = new DoctrineCredentialsRevoker($em, new ClientManager($em, Client::class)); $revoker->revokeCredentialsForUser(FixtureFactory::createUser()); @@ -65,7 +66,7 @@ public function testRevokesAllCredentialsForClient(): void $em->persist($refreshToken); $em->flush(); - $revoker = new DoctrineCredentialsRevoker($em); + $revoker = new DoctrineCredentialsRevoker($em, new ClientManager($em, Client::class)); $revoker->revokeCredentialsForClient($client); diff --git a/tests/Acceptance/UpdateClientCommandTest.php b/tests/Acceptance/UpdateClientCommandTest.php index 6d23fe72..8b0d07a8 100644 --- a/tests/Acceptance/UpdateClientCommandTest.php +++ b/tests/Acceptance/UpdateClientCommandTest.php @@ -24,7 +24,7 @@ public function testUpdateRedirectUris(): void '--redirect-uri' => ['http://example.com', 'http://example.org'], ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('Given oAuth2 client updated successfully', $output); + $this->assertStringContainsString('OAuth2 client updated successfully.', $output); $this->assertCount(2, $client->getRedirectUris()); } @@ -42,7 +42,7 @@ public function testUpdateGrantTypes(): void '--grant-type' => ['password', 'client_credentials'], ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('Given oAuth2 client updated successfully', $output); + $this->assertStringContainsString('OAuth2 client updated successfully.', $output); $this->assertCount(2, $client->getGrants()); } @@ -60,10 +60,45 @@ public function testUpdateScopes(): void '--scope' => ['foo', 'bar'], ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('Given oAuth2 client updated successfully', $output); + $this->assertStringContainsString('OAuth2 client updated successfully.', $output); $this->assertCount(2, $client->getScopes()); } + public function testUpdateName(): void + { + $client = $this->fakeAClient('foobar'); + $this->getClientManager()->save($client); + $this->assertCount(0, $client->getRedirectUris()); + + $command = $this->application->find('league:oauth2-server:update-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => $client->getIdentifier(), + '--name' => 'newName', + ]); + $output = $commandTester->getDisplay(); + $this->assertStringContainsString('OAuth2 client updated successfully.', $output); + $this->assertSame('newName', $client->getName()); + } + + public function testNameIsNotUpdatedIfNotSet(): void + { + $client = $this->fakeAClient('foobar'); + $this->getClientManager()->save($client); + $this->assertSame('name', $client->getName()); + + $command = $this->application->find('league:oauth2-server:update-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => $client->getIdentifier(), + ]); + $output = $commandTester->getDisplay(); + $this->assertStringContainsString('OAuth2 client updated successfully.', $output); + $this->assertSame('name', $client->getName()); + } + public function testDeactivate(): void { $client = $this->fakeAClient('foobar'); @@ -78,7 +113,7 @@ public function testDeactivate(): void '--deactivated' => true, ]); $output = $commandTester->getDisplay(); - $this->assertStringContainsString('Given oAuth2 client updated successfully', $output); + $this->assertStringContainsString('OAuth2 client updated successfully.', $output); $updatedClient = $this->getClientManager()->find($client->getIdentifier()); $this->assertFalse($updatedClient->isActive()); } diff --git a/tests/Acceptance/resource/list-client-empty.txt b/tests/Acceptance/resource/list-client-empty.txt index 4c64cfa5..59a38706 100644 --- a/tests/Acceptance/resource/list-client-empty.txt +++ b/tests/Acceptance/resource/list-client-empty.txt @@ -1,4 +1,4 @@ - ------------ -------- ------- -------------- ------------ - identifier secret scope redirect uri grant type - ------------ -------- ------- -------------- ------------ + ------ ------------ -------- ------- -------------- ------------ + name identifier secret scope redirect uri grant type + ------ ------------ -------- ------- -------------- ------------ diff --git a/tests/Acceptance/resource/list-clients-with-client-having-no-secret.txt b/tests/Acceptance/resource/list-clients-with-client-having-no-secret.txt index 62ee511c..d6be84b4 100644 --- a/tests/Acceptance/resource/list-clients-with-client-having-no-secret.txt +++ b/tests/Acceptance/resource/list-clients-with-client-having-no-secret.txt @@ -1,6 +1,6 @@ - ------------ -------- ------- -------------- ------------ - identifier secret scope redirect uri grant type - ------------ -------- ------- -------------- ------------ - foobar - ------------ -------- ------- -------------- ------------ + -------- ------------ -------- ------- -------------- ------------ + name identifier secret scope redirect uri grant type + -------- ------------ -------- ------- -------------- ------------ + client foobar + -------- ------------ -------- ------- -------------- ------------ diff --git a/tests/Acceptance/resource/list-clients.txt b/tests/Acceptance/resource/list-clients.txt index 3ad9a3a3..d069e034 100644 --- a/tests/Acceptance/resource/list-clients.txt +++ b/tests/Acceptance/resource/list-clients.txt @@ -1,6 +1,6 @@ - ------------ -------- ------- -------------- ------------ - identifier secret scope redirect uri grant type - ------------ -------- ------- -------------- ------------ - foobar quzbaz - ------------ -------- ------- -------------- ------------ + -------- ------------ -------- ------- -------------- ------------ + name identifier secret scope redirect uri grant type + -------- ------------ -------- ------- -------------- ------------ + client foobar quzbaz + -------- ------------ -------- ------- -------------- ------------ diff --git a/tests/Acceptance/resource/list-filters-clients.txt b/tests/Acceptance/resource/list-filters-clients.txt index c0ba2535..ee0f3f63 100644 --- a/tests/Acceptance/resource/list-filters-clients.txt +++ b/tests/Acceptance/resource/list-filters-clients.txt @@ -1,6 +1,6 @@ - ------------ ----------------- ---------------- -------------- ------------ - identifier secret scope redirect uri grant type - ------------ ----------------- ---------------- -------------- ------------ - client-b client-b-secret client-b-scope - ------------ ----------------- ---------------- -------------- ------------ + -------- ------------ ----------------- ---------------- -------------- ------------ + name identifier secret scope redirect uri grant type + -------- ------------ ----------------- ---------------- -------------- ------------ + client client-b client-b-secret client-b-scope + -------- ------------ ----------------- ---------------- -------------- ------------