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';