diff --git a/database/migrations/structure/20230411_2004_oauth_restrictions.sql b/database/migrations/structure/20230411_2004_oauth_restrictions.sql new file mode 100644 index 0000000..410ce96 --- /dev/null +++ b/database/migrations/structure/20230411_2004_oauth_restrictions.sql @@ -0,0 +1,10 @@ +CREATE TABLE `oauth_clients` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `client_id` varchar(16) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `client_secret` varchar(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `redirect_uris` text NOT NULL, + `preapproved` tinyint(1) NOT NULL DEFAULT 0, + `created` timestamp NOT NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `client_id` (`client_id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; diff --git a/rvr b/rvr index 4901de4..bd396bd 100755 --- a/rvr +++ b/rvr @@ -9,5 +9,8 @@ $app->add(new RVR\Cli\MigrateDatabaseCommand()); $app->add(new RVR\Cli\AddUserCommand()); $app->add(new RVR\Cli\LinkViewCommand()); $app->add(new RVR\Cli\MaintainDatabaseCommand()); +$app->add(new RVR\Cli\AddOAuthClientCommand()); +$app->add(new RVR\Cli\AddOAuthRedirectUriCommand()); +$app->add(new RVR\Cli\RemoveOAuthRedirectUriCommand()); $app->run(); diff --git a/src/Cli/AddOAuthClientCommand.php b/src/Cli/AddOAuthClientCommand.php new file mode 100644 index 0000000..9e1d329 --- /dev/null +++ b/src/Cli/AddOAuthClientCommand.php @@ -0,0 +1,53 @@ +setName('oauth:add-client') + ->setDescription('Adding of OAuth client.') + ->addArgument('preapproved', InputArgument::OPTIONAL, 'Preapproved'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $clientId = bin2hex(random_bytes(8)); + $clientSecret = bin2hex(random_bytes(20)); + + $oAuthClient = new OAuthClient(); + $oAuthClient->setClientId($clientId); + $oAuthClient->setClientSecret($clientSecret); + $oAuthClient->setCreatedDate(new DateTime()); + + if ($input->hasArgument('preapproved') && $input->getArgument('preapproved')) { + $oAuthClient->setPreapproved($input->getArgument('preapproved')); + } + + try { + $pdm = new PersistentDataManager(); + $pdm->saveToDb($oAuthClient); + } catch (\Exception $e) { + $output->writeln('Adding OAuth client failed!'); + $output->writeln(''); + + $output->writeln((string) $e); + $output->writeln(''); + + return 1; + } + + $output->writeln('OAuth client was successfully added!'); + $output->writeln('Client ID: ' . $clientId . ''); + $output->writeln('Client secret: ' . $clientSecret . ''); + + return 0; + } +} diff --git a/src/Cli/AddOAuthRedirectUriCommand.php b/src/Cli/AddOAuthRedirectUriCommand.php new file mode 100644 index 0000000..276e46a --- /dev/null +++ b/src/Cli/AddOAuthRedirectUriCommand.php @@ -0,0 +1,54 @@ +setName('oauth:add-redirect-uri') + ->setDescription('Adding of redirect URI for OAuth client.') + ->addArgument('client_id', InputArgument::REQUIRED, 'The OAuth client ID') + ->addArgument('redirect_uris', InputArgument::IS_ARRAY, 'Redirect URIs to add'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $oAuthClientRepository = new OAuthClientRepository(); + $oAuthClient = $oAuthClientRepository->getByClientId($input->getArgument('client_id')); + + if ($oAuthClient === null) { + $output->writeln('OAuth client does not exist!'); + return 1; + } + + $redirectUris = array_unique(array_merge($oAuthClient->getRedirectUrisArray(), $input->getArgument('redirect_uris'))); + + $oAuthClient->setRedirectUrisArray($redirectUris); + + try { + $pdm = new PersistentDataManager(); + $pdm->saveToDb($oAuthClient); + } catch (\Exception $e) { + $output->writeln('Adding redirect URI failed!'); + $output->writeln(''); + + $output->writeln((string) $e); + $output->writeln(''); + + return 1; + } + + $redirectUrisToPrint = []; + foreach ($redirectUris as $redirectUri) $redirectUrisToPrint[] = '* ' . $redirectUri; + + $output->writeln('Redirect URIS were successfully added! Current URIs:' . "\n" . implode("\n", $redirectUrisToPrint) . ''); + + return 0; + } +} diff --git a/src/Cli/RemoveOAuthRedirectUriCommand.php b/src/Cli/RemoveOAuthRedirectUriCommand.php new file mode 100644 index 0000000..c784a8d --- /dev/null +++ b/src/Cli/RemoveOAuthRedirectUriCommand.php @@ -0,0 +1,54 @@ +setName('oauth:remove-redirect-uri') + ->setDescription('Removing of redirect URI for OAuth client.') + ->addArgument('client_id', InputArgument::REQUIRED, 'The OAuth client ID') + ->addArgument('redirect_uris', InputArgument::IS_ARRAY, 'Redirect URIs to remove'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $oAuthClientRepository = new OAuthClientRepository(); + $oAuthClient = $oAuthClientRepository->getByClientId($input->getArgument('client_id')); + + if ($oAuthClient === null) { + $output->writeln('OAuth client does not exist!'); + return 1; + } + + $redirectUris = array_diff($oAuthClient->getRedirectUrisArray(), $input->getArgument('redirect_uris')); + + $oAuthClient->setRedirectUrisArray($redirectUris); + + try { + $pdm = new PersistentDataManager(); + $pdm->saveToDb($oAuthClient); + } catch (\Exception $e) { + $output->writeln('Removing redirect URI failed!'); + $output->writeln(''); + + $output->writeln((string) $e); + $output->writeln(''); + + return 1; + } + + $redirectUrisToPrint = []; + foreach ($redirectUris as $redirectUri) $redirectUrisToPrint[] = '* ' . $redirectUri; + + $output->writeln('Redirect URIS were successfully removed! Current URIs:' . "\n" . implode("\n", $redirectUrisToPrint) . ''); + + return 0; + } +} diff --git a/src/Controller/OAuthAuthController.php b/src/Controller/OAuthAuthController.php index f68b4c0..7dd983a 100644 --- a/src/Controller/OAuthAuthController.php +++ b/src/Controller/OAuthAuthController.php @@ -3,6 +3,7 @@ use DateTime; use RVR\PersistentData\Model\OAuthToken; use RVR\PersistentData\Model\User; +use RVR\Repository\OAuthClientRepository; use SokoWeb\Interfaces\Authorization\ISecured; use SokoWeb\Interfaces\Request\IRequest; use SokoWeb\Interfaces\Response\IRedirect; @@ -16,10 +17,13 @@ class OAuthAuthController implements ISecured private PersistentDataManager $pdm; + private OAuthClientRepository $oAuthClientRepository; + public function __construct(IRequest $request) { $this->request = $request; $this->pdm = new PersistentDataManager(); + $this->oAuthClientRepository = new OAuthClientRepository(); } public function authorize(): bool @@ -30,15 +34,30 @@ class OAuthAuthController implements ISecured public function auth() { $redirectUri = $this->request->query('redirect_uri'); + $clientId = $this->request->query('client_id'); $scope = $this->request->query('scope') ? $this->request->query('scope'): ''; $state = $this->request->query('state'); $nonce = $this->request->query('nonce') ? $this->request->query('nonce'): ''; - if (!$redirectUri || !$state) { + if (!$clientId || !$redirectUri || !$state) { return new HtmlContent('oauth/oauth_error', ['error' => 'An invalid request was made. Please start authentication again.']); } - $this->request->session()->delete('oauth_payload'); + $client = $this->oAuthClientRepository->getByClientId($clientId); + if ($client === null) { + return new HtmlContent('oauth/oauth_error', ['error' => 'Client is not authorized.']); + } + + $redirectUriParsed = parse_url($redirectUri); + $redirectUriBase = $redirectUriParsed['scheme'] . '://' . $redirectUriParsed['host'] . $redirectUriParsed['path']; + $redirectUriQuery = []; + if (isset($redirectUriParsed['query'])) { + parse_str($redirectUriParsed['query'], $redirectUriQuery); + } + + if (!in_array($redirectUriBase, $client->getRedirectUrisArray())) { + return new HtmlContent('oauth/oauth_error', ['error' => 'Redirect URI \'' . $redirectUriBase .'\' is not allowed for this client.']); + } /** * @var ?User $user @@ -57,13 +76,11 @@ class OAuthAuthController implements ISecured $token->setExpiresDate(new DateTime('+5 minutes')); $this->pdm->saveToDb($token); - $redirectUri = $redirectUri; - $additionalUriParams = [ + $redirectUriQuery = array_merge($redirectUriQuery, [ 'state' => $state, 'code' => $code - ]; - $and = (strpos($redirectUri, '?') !== false) ? '&' : '?'; - $finalRedirectUri = $redirectUri . $and . http_build_query($additionalUriParams); + ]); + $finalRedirectUri = $redirectUriBase . '?' . http_build_query($redirectUriQuery); return new Redirect($finalRedirectUri, IRedirect::TEMPORARY); } diff --git a/src/Controller/OAuthController.php b/src/Controller/OAuthController.php index bd3845c..abd7a70 100644 --- a/src/Controller/OAuthController.php +++ b/src/Controller/OAuthController.php @@ -5,6 +5,7 @@ use Firebase\JWT\JWT; use RVR\Repository\OAuthTokenRepository; use RVR\Repository\UserRepository; use RVR\PersistentData\Model\User; +use RVR\Repository\OAuthClientRepository; use SokoWeb\Interfaces\Request\IRequest; use SokoWeb\Interfaces\Response\IContent; use SokoWeb\Response\JsonContent; @@ -13,6 +14,8 @@ class OAuthController { private IRequest $request; + private OAuthClientRepository $oAuthClientRepository; + private OAuthTokenRepository $oAuthTokenRepository; private UserRepository $userRepository; @@ -20,14 +23,31 @@ class OAuthController public function __construct(IRequest $request) { $this->request = $request; + $this->oAuthClientRepository = new OAuthClientRepository(); $this->oAuthTokenRepository = new OAuthTokenRepository(); $this->userRepository = new UserRepository(); } public function getToken(): ?IContent { - $token = $this->oAuthTokenRepository->getByCode($this->request->post('code')); + $clientId = $this->request->post('client_id'); + $clientSecret = $this->request->post('client_secret'); + $code = $this->request->post('code'); + if (!$clientId || !$clientSecret || !$code) { + return new JsonContent([ + 'error' => 'An invalid request was made.' + ]); + } + + $client = $this->oAuthClientRepository->getByClientId($clientId); + if ($client === null || $client->getClientSecret() !== $clientSecret) { + return new JsonContent([ + 'error' => 'Client is not authorized.' + ]); + } + + $token = $this->oAuthTokenRepository->getByCode($code); if ($token === null || $token->getExpiresDate() < new DateTime()) { return new JsonContent([ 'error' => 'The provided code is invalid.' diff --git a/src/PersistentData/Model/OAuthClient.php b/src/PersistentData/Model/OAuthClient.php new file mode 100644 index 0000000..80573a8 --- /dev/null +++ b/src/PersistentData/Model/OAuthClient.php @@ -0,0 +1,91 @@ +clientId = $clientId; + } + + public function setClientSecret(string $clientSecret): void + { + $this->clientSecret = $clientSecret; + } + + public function setRedirectUrisArray(array $redirectUris): void + { + $this->redirectUris = $redirectUris; + } + + public function setRedirectUris(string $redirectUris): void + { + $this->redirectUris = json_decode($redirectUris, true); + } + + public function setPreapproved(bool $preapproved): void + { + $this->preapproved = $preapproved; + } + + public function setCreatedDate(DateTime $created): void + { + $this->created = $created; + } + + public function setCreated(string $created): void + { + $this->created = new DateTime($created); + } + + public function getClientId(): string + { + return $this->clientId; + } + + public function getClientSecret(): string + { + return $this->clientSecret; + } + + public function getRedirectUrisArray(): array + { + return $this->redirectUris; + } + + public function getRedirectUris(): string + { + return json_encode($this->redirectUris); + } + + public function getPreapproved(): bool + { + return $this->preapproved; + } + + public function getCreatedDate(): DateTime + { + return $this->created; + } + + public function getCreated(): string + { + return $this->created->format('Y-m-d H:i:s'); + } +} diff --git a/src/Repository/OAuthClientRepository.php b/src/Repository/OAuthClientRepository.php new file mode 100644 index 0000000..ba99961 --- /dev/null +++ b/src/Repository/OAuthClientRepository.php @@ -0,0 +1,28 @@ +pdm = new PersistentDataManager(); + } + + public function getById(int $id): ?OAuthClient + { + return $this->pdm->selectFromDbById($id, OAuthClient::class); + } + + public function getByClientId(string $clientId): ?OAuthClient + { + $select = new Select(\Container::$dbConnection); + $select->where('client_id', '=', $clientId); + + return $this->pdm->selectFromDb($select, OAuthClient::class); + } +}