diff --git a/Command/CreateClientCommand.php b/Command/CreateClientCommand.php new file mode 100644 index 00000000..5d4b8208 --- /dev/null +++ b/Command/CreateClientCommand.php @@ -0,0 +1,112 @@ +clientManager = $clientManager; + } + + protected function configure() + { + $this + ->setDescription('Creates a new oAuth2 client') + ->addOption( + 'redirect-uri', + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs.', + null + ) + ->addOption( + 'grant-type', + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Sets allowed grant type for client. Use this option multiple times to set multiple grant types.', + null + ) + ->addOption( + 'scope', + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Sets allowed scope for client. Use this option multiple times to set multiple scopes.', + null + ) + ->addArgument( + 'identifier', + InputArgument::OPTIONAL, + 'The client identifier' + ) + ->addArgument( + 'secret', + InputArgument::OPTIONAL, + 'The client secret' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $client = $this->buildClientFromInput($input); + $this->clientManager->save($client); + $io->success('New oAuth2 client created successfully.'); + + $headers = ['Identifier', 'Secret']; + $rows = [ + [$client->getIdentifier(), $client->getSecret()], + ]; + $io->table($headers, $rows); + + return 0; + } + + private function buildClientFromInput(InputInterface $input) + { + $identifier = $input->getArgument('identifier') ?? hash('md5', random_bytes(16)); + $secret = $input->getArgument('secret') ?? hash('sha512', random_bytes(32)); + + $client = new Client($identifier, $secret); + $client->setActive(true); + + $redirectUris = array_map( + function (string $redirectUri) { return new RedirectUri($redirectUri); }, + $input->getOption('redirect-uri') + ); + $client->setRedirectUris(...$redirectUris); + + $grants = array_map( + function (string $grant) { return new Grant($grant); }, + $input->getOption('grant-type') + ); + $client->setGrants(...$grants); + + $scopes = array_map( + function (string $scope) { return new Scope($scope); }, + $input->getOption('scope') + ); + $client->setScopes(...$scopes); + + return $client; + } +} diff --git a/Command/UpdateClientCommand.php b/Command/UpdateClientCommand.php new file mode 100644 index 00000000..fa9908ca --- /dev/null +++ b/Command/UpdateClientCommand.php @@ -0,0 +1,110 @@ +clientManager = $clientManager; + } + + protected function configure() + { + $this + ->setDescription('Updates an oAuth2 client') + ->addOption( + 'redirect-uri', + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs.', + null + ) + ->addOption( + 'grant-type', + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Sets allowed grant type for client. Use this option multiple times to set multiple grant types.', + null + ) + ->addOption( + 'scope', + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Sets allowed scope for client. Use this option multiple times to set multiple scopes.', + null + ) + ->addOption( + 'deactivated', + null, + InputOption::VALUE_NONE, + 'If provided, it will deactivate the given client.' + ) + ->addArgument( + 'identifier', + InputArgument::REQUIRED, + 'The client ID' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $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'))); + + return 1; + } + + $client = $this->updateClientFromInput($client, $input); + $this->clientManager->save($client); + $io->success('Given oAuth2 client updated successfully.'); + + return 0; + } + + private function updateClientFromInput(Client $client, InputInterface $input): Client + { + $client->setActive(!$input->getOption('deactivated')); + + $redirectUris = array_map( + function (string $redirectUri) { return new RedirectUri($redirectUri); }, + $input->getOption('redirect-uri') + ); + $client->setRedirectUris(...$redirectUris); + + $grants = array_map( + function (string $grant) { return new Grant($grant); }, + $input->getOption('grant-type') + ); + $client->setGrants(...$grants); + + $scopes = array_map( + function (string $scope) { return new Scope($scope); }, + $input->getOption('scope') + ); + $client->setScopes(...$scopes); + + return $client; + } +} diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 8e6bb027..edfc68a3 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -73,6 +73,16 @@ + + + + + + + + + + diff --git a/Tests/Acceptance/CreateClientCommandTest.php b/Tests/Acceptance/CreateClientCommandTest.php new file mode 100644 index 00000000..fa863d6b --- /dev/null +++ b/Tests/Acceptance/CreateClientCommandTest.php @@ -0,0 +1,122 @@ +application->find('trikoder:oauth2:create-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + ]); + + $output = $commandTester->getDisplay(); + $this->assertContains('New oAuth2 client created successfully', $output); + } + + public function testCreateClientWithIdentifier() + { + $command = $this->application->find('trikoder:oauth2:create-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => 'foobar', + ]); + + $output = $commandTester->getDisplay(); + $this->assertContains('New oAuth2 client created successfully', $output); + $this->assertContains('foobar', $output); + + $client = $this->client + ->getContainer() + ->get(ClientManagerInterface::class) + ->find('foobar'); + $this->assertInstanceOf(Client::class, $client); + } + + public function testCreateClientWithSecret() + { + $command = $this->application->find('trikoder:oauth2:create-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => 'foobar', + 'secret' => 'quzbaz', + ]); + + $output = $commandTester->getDisplay(); + $this->assertContains('New oAuth2 client created successfully', $output); + $client = $this->client + ->getContainer() + ->get(ClientManagerInterface::class) + ->find('foobar'); + $this->assertInstanceOf(Client::class, $client); + $this->assertSame('quzbaz', $client->getSecret()); + } + + public function testCreateClientWithRedirectUris() + { + $command = $this->application->find('trikoder:oauth2:create-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => 'foobar', + '--redirect-uri' => ['http://example.org', 'http://example.org'], + ]); + + $output = $commandTester->getDisplay(); + $this->assertContains('New oAuth2 client created successfully', $output); + $client = $this->client + ->getContainer() + ->get(ClientManagerInterface::class) + ->find('foobar'); + $this->assertInstanceOf(Client::class, $client); + $this->assertCount(2, $client->getRedirectUris()); + } + + public function testCreateClientWithGrantTypes() + { + $command = $this->application->find('trikoder:oauth2:create-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => 'foobar', + '--grant-type' => ['password', 'client_credentials'], + ]); + + $output = $commandTester->getDisplay(); + $this->assertContains('New oAuth2 client created successfully', $output); + $client = $this->client + ->getContainer() + ->get(ClientManagerInterface::class) + ->find('foobar'); + $this->assertInstanceOf(Client::class, $client); + $this->assertCount(2, $client->getGrants()); + } + + public function testCreateClientWithScopes() + { + $command = $this->application->find('trikoder:oauth2:create-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => 'foobar', + '--scope' => ['foo', 'bar'], + ]); + + $output = $commandTester->getDisplay(); + $this->assertContains('New oAuth2 client created successfully', $output); + $client = $this->client + ->getContainer() + ->get(ClientManagerInterface::class) + ->find('foobar'); + $this->assertInstanceOf(Client::class, $client); + $this->assertCount(2, $client->getScopes()); + } +} diff --git a/Tests/Acceptance/UpdateClientCommandTest.php b/Tests/Acceptance/UpdateClientCommandTest.php new file mode 100644 index 00000000..0b769d24 --- /dev/null +++ b/Tests/Acceptance/UpdateClientCommandTest.php @@ -0,0 +1,95 @@ +fakeAClient('foobar'); + $this->getClientManager()->save($client); + $this->assertCount(0, $client->getRedirectUris()); + + $command = $this->application->find('trikoder:oauth2:update-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => $client->getIdentifier(), + '--redirect-uri' => ['http://example.com', 'http://example.org'], + ]); + $output = $commandTester->getDisplay(); + $this->assertContains('Given oAuth2 client updated successfully', $output); + $this->assertCount(2, $client->getRedirectUris()); + } + + public function testUpdateGrantTypes() + { + $client = $this->fakeAClient('foobar'); + $this->getClientManager()->save($client); + $this->assertCount(0, $client->getGrants()); + + $command = $this->application->find('trikoder:oauth2:update-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => $client->getIdentifier(), + '--grant-type' => ['password', 'client_credentials'], + ]); + $output = $commandTester->getDisplay(); + $this->assertContains('Given oAuth2 client updated successfully', $output); + $this->assertCount(2, $client->getGrants()); + } + + public function testUpdateScopes() + { + $client = $this->fakeAClient('foobar'); + $this->getClientManager()->save($client); + $this->assertCount(0, $client->getScopes()); + + $command = $this->application->find('trikoder:oauth2:update-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => $client->getIdentifier(), + '--scope' => ['foo', 'bar'], + ]); + $output = $commandTester->getDisplay(); + $this->assertContains('Given oAuth2 client updated successfully', $output); + $this->assertCount(2, $client->getScopes()); + } + + public function testDeactivate() + { + $client = $this->fakeAClient('foobar'); + $this->getClientManager()->save($client); + $this->assertTrue($client->isActive()); + + $command = $this->application->find('trikoder:oauth2:update-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => $client->getIdentifier(), + '--deactivated' => true, + ]); + $output = $commandTester->getDisplay(); + $this->assertContains('Given oAuth2 client updated successfully', $output); + $updatedClient = $this->getClientManager()->find($client->getIdentifier()); + $this->assertFalse($updatedClient->isActive()); + } + + private function fakeAClient($identifier): Client + { + return new Client($identifier, 'quzbaz'); + } + + private function getClientManager(): ClientManagerInterface + { + return $this->client + ->getContainer() + ->get(ClientManagerInterface::class); + } +} diff --git a/docs/basic-setup.md b/docs/basic-setup.md index 9e1d29ff..2efc16a3 100644 --- a/docs/basic-setup.md +++ b/docs/basic-setup.md @@ -2,38 +2,68 @@ ## Managing clients -For now, clients have to be managed manually using SQL queries. Here are the fields that you can set on the client: - -| Field | Type | Required | Description | Notes | -| --- | --- | --- | --- | --- | -| identifier | string(32) | Yes | Client ID used for obtaining an access token. | *N/A* | -| secret | string(128) | Yes | Client secret used for obtaining an access token. | *N/A* | -| redirect_uris | string | No | List of URIs the user can get redirected to after completing the `authorization_code` flow. | Multiple values need to be separated with a space. | -| grants | string | No | List of grants the client is able to utilize. | Multiple values need to be separated with a space. | -| scopes | string | No | List of scopes the client will receive. | Multiple values need to be separated with a space. | -| active | boolean | Yes | Whether the client can obtain new access tokens or not. | *N/A* | +There are several commands available to manage clients. ### Add a client -```sql -INSERT INTO `oauth2_client` (`identifier`, `secret`, `active`) VALUES ('foo', 'bar', 1); +To add a client you should use the `trikoder:oauth2:create-client` command. + +```sh +Description: + Creates a new oAuth2 client + +Usage: + trikoder:oauth2:create-client [options] [--] [ []] + +Arguments: + identifier The client identifier + secret The client secret + +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) +``` + + +### Update a client + +To update a client you should use the `trikoder:oauth2:update-client` command. + +```sh +Description: + Updates an oAuth2 client + +Usage: + trikoder:oauth2:update-client [options] [--] + +Arguments: + identifier The client ID + +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) + --deactivated If provided, it will deactivate the given client. ``` #### Restrict which grant types a client can access -```sql -UPDATE `oauth2_client` SET `grants` = 'client_credentials password' WHERE `identifier` = 'foo'; +```sh +$ bin/console trikoder:oauth2:update-client --grant-type client_credentials --grant-type password foo ``` #### Assign which scopes the client will receive -```sql -UPDATE `oauth2_client` SET `scopes` = 'create read' WHERE `identifier` = 'foo'; + +```sh +$ bin/console trikoder:oauth2:update-client --scope create --scope read foo ``` > **NOTE:** You will have to setup an [event listener](controlling-token-scopes.md#listener) which will assign the client scopes to the issued access token. ### Delete a client +For now, clients deletion have to be managed manually using SQL queries. ```sql DELETE FROM `oauth2_client` WHERE `identifier` = 'foo';