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);
+ }
+}