diff --git a/src/Cli/Commands/OAuth/ClientCapabilities.php b/src/Cli/Commands/OAuth/ClientCapabilities.php index 9273842..9f9de06 100644 --- a/src/Cli/Commands/OAuth/ClientCapabilities.php +++ b/src/Cli/Commands/OAuth/ClientCapabilities.php @@ -6,6 +6,8 @@ namespace Siteworxpro\App\Cli\Commands\OAuth; use Siteworxpro\App\Cli\ClimateOutput; use Siteworxpro\App\Cli\Commands\Command; +use Siteworxpro\App\OAuth\Entities\Client; +use Siteworxpro\App\OAuth\Entities\Scope; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; @@ -30,6 +32,8 @@ class ClientCapabilities extends Command $output->info('[1] Username Password: ' . ($capabilities['userPass'] ? 'Enabled' : 'Disabled')); $output->info('[2] Magic Link: ' . ($capabilities['magicLink'] ? 'Enabled' : 'Disabled')); $output->info('[3] Social Logins: ' . ($capabilities['socials'] ? 'Enabled' : 'Disabled')); + $output->info('[4] External Client (require pkce): ' . (!$client->confidential ? 'Enabled' : 'Disabled')); + $output->info('[5] Manage Scopes'); $question = new Question('What do you want to edit: ', ''); $selection = $this->helper->ask($input, $output, $question); @@ -48,6 +52,12 @@ class ClientCapabilities extends Command case '3': $output->info('Social Logins cannot be modified via CLI at this time.'); break; + case '4': + $client->confidential = !$client->confidential; + break; + case '5': + $this->manageClientScopes($input, $output, $client); + break; default: $output->error('Invalid selection. Please try again.'); continue 2; @@ -60,4 +70,48 @@ class ClientCapabilities extends Command return \Symfony\Component\Console\Command\Command::SUCCESS; } + + private function manageClientScopes($input, ClimateOutput|OutputInterface $output, Client $client): void + { + $allScopes = Scope::all(); + + $output->info('These are scope that are available for this client.'); + $output->info('Type "exit" to finish managing scopes.'); + + while (true) { + $clientScopes = $client->scopes()->get(); + + $output->info('Available Scopes:'); + + /** @var Scope $scope */ + foreach ($allScopes as $scope) { + $status = $clientScopes->contains($scope) ? 'Enabled' : 'Disabled'; + $output->info("$scope->id - $scope->name $status"); + } + + $question = new Question('Enter scope ID to toggle (or type "exit" to finish): ', ''); + $question->setAutocompleterValues($allScopes->pluck('id')->toArray()); + + $scopeId = $this->helper->ask($input, $output, $question); + + if (strtolower($scopeId) === 'exit') { + break; + } + + $scope = $allScopes->where('id', $scopeId)->first(); + + if ($scope === null) { + $output->error('Scope not found. Please try again.'); + continue; + } + + if ($clientScopes->contains($scope)) { + $client->disableScope($scope); + $output->info("Scope '$scope->name' disabled for client."); + } else { + $client->enableScope($scope); + $output->info("Scope '$scope->name' enabled for client."); + } + } + } } diff --git a/src/Cli/Commands/OAuth/CreateClient.php b/src/Cli/Commands/OAuth/CreateClient.php index aec4616..7303ce7 100644 --- a/src/Cli/Commands/OAuth/CreateClient.php +++ b/src/Cli/Commands/OAuth/CreateClient.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Siteworxpro\App\Cli\Commands\OAuth; -use Siteworxpro\App\Cli\ClimateOutput; use Siteworxpro\App\CommandBus\Commands\CreateClient as CreateClientCommand; use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException; use Siteworxpro\App\Models\Enums\ClientGrant as ClientGrantAlias; @@ -12,9 +11,6 @@ use Siteworxpro\App\OAuth\Entities\Client; use Siteworxpro\App\Services\Facades\CommandBus; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; @@ -31,8 +27,8 @@ class CreateClient extends \Siteworxpro\App\Cli\Commands\Command $question = new ChoiceQuestion('Enter client grants', [ 'authorization_code', - 'client_credentials', 'refresh_token', + 'client_credentials', 'password', ], 0); $question->setMultiselect(true); @@ -44,6 +40,18 @@ class CreateClient extends \Siteworxpro\App\Cli\Commands\Command $grantsEnum[] = ClientGrantAlias::from($grant); } + $question = $this->helper->ask( + $input, + $output, + new \Symfony\Component\Console\Question\ConfirmationQuestion( + 'External Client (Require PKCE)? (y/N): ', + false, + '/^(y|yes)/i' + ) + ); + + + $command = new CreateClientCommand($clientName, $grantsEnum, $clientDescription); try { /** @var Client $client */ diff --git a/src/Cli/Commands/OAuth/ListClients.php b/src/Cli/Commands/OAuth/ListClients.php index b32d5aa..0a1fc6b 100644 --- a/src/Cli/Commands/OAuth/ListClients.php +++ b/src/Cli/Commands/OAuth/ListClients.php @@ -46,7 +46,7 @@ class ListClients extends Command 'Access Token Url' => Config::get('app.url') . '/client/access_token', 'OAuth Config Url' => Config::get('app.url') . '/client/' . $client->id . '/.well-known/openid-configuration', - 'Scopes' => $client->scopes->toArray(), + 'Scopes' => $client->scopes->pluck('name')->toArray(), 'Capabilities' => $client->capabilities->toArray(), ]); diff --git a/src/CommandBus/Commands/CreateClient.php b/src/CommandBus/Commands/CreateClient.php index 7c62803..8bba77e 100644 --- a/src/CommandBus/Commands/CreateClient.php +++ b/src/CommandBus/Commands/CreateClient.php @@ -14,7 +14,8 @@ readonly class CreateClient extends Command public function __construct( private string $clientName, private array $clientGrants = [], - private string $clientDescription = '' + private string $clientDescription = '', + private bool $isExternal = false ) { foreach ($this->clientGrants as $grant) { if ($grant instanceof ClientGrant === false) { @@ -32,6 +33,14 @@ readonly class CreateClient extends Command } } + /** + * @return bool + */ + public function isExternal(): bool + { + return $this->isExternal; + } + /** * @return string */ diff --git a/src/CommandBus/Handlers/OAuth/CreateClient.php b/src/CommandBus/Handlers/OAuth/CreateClient.php index 9051d6c..0a8fd94 100644 --- a/src/CommandBus/Handlers/OAuth/CreateClient.php +++ b/src/CommandBus/Handlers/OAuth/CreateClient.php @@ -26,6 +26,7 @@ class CreateClient extends CommandHandler $client->description = $command->getClientDescription(); $client->grant_types = new Collection($command->getClientGrants()); // @phpstan-ignore-line assign.propertyType $client->capabilities = new ClientCapabilities(); + $client->confidential = !$command->isExternal(); $client->save(); diff --git a/src/Models/ClientScope.php b/src/Models/ClientScope.php index 590cf38..07e5b95 100644 --- a/src/Models/ClientScope.php +++ b/src/Models/ClientScope.php @@ -14,4 +14,5 @@ namespace Siteworxpro\App\Models; */ class ClientScope extends Model { + public $timestamps = false; } diff --git a/src/OAuth/Entities/Client.php b/src/OAuth/Entities/Client.php index b4df14b..c5f8092 100644 --- a/src/OAuth/Entities/Client.php +++ b/src/OAuth/Entities/Client.php @@ -241,4 +241,23 @@ class Client extends Model implements ClientEntityInterface return $user->verifyPassword($password) ? $user : null; } + + public function disableScope(Scope $scope): void + { + /** @var ClientScope | null $clientScope */ + $clientScope = ClientScope::where('client_id', $this->id) + ->where('scope_id', $scope->id) + ->first(); + + $clientScope?->delete(); + } + + public function enableScope(Scope $scope): void + { + $clientScope = new ClientScope(); + $clientScope->client_id = $this->id; + $clientScope->scope_id = $scope->id; + + $clientScope->save(); + } } diff --git a/src/OAuth/Entities/Scope.php b/src/OAuth/Entities/Scope.php index a5ec439..bf2fc49 100644 --- a/src/OAuth/Entities/Scope.php +++ b/src/OAuth/Entities/Scope.php @@ -20,6 +20,10 @@ class Scope extends Model implements ScopeEntityInterface { use ScopeTrait; + protected $casts = [ + 'id' => 'string', + ]; + public function getIdentifier(): string { return $this->name;