Compare commits

...

2 Commits

Author SHA1 Message Date
e487a59816
Merge pull request 'restrict oauth access' (!13) from feature/oauth-restrictions into master
All checks were successful
rvr-nextgen/pipeline/head This commit looks good
Reviewed-on: #13
2023-04-12 00:15:08 +02:00
a7790319eb
restrict oauth access
All checks were successful
rvr-nextgen/pipeline/pr-master This commit looks good
2023-04-12 00:10:14 +02:00
9 changed files with 338 additions and 8 deletions

View File

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

3
rvr
View File

@ -9,5 +9,8 @@ $app->add(new RVR\Cli\MigrateDatabaseCommand());
$app->add(new RVR\Cli\AddUserCommand()); $app->add(new RVR\Cli\AddUserCommand());
$app->add(new RVR\Cli\LinkViewCommand()); $app->add(new RVR\Cli\LinkViewCommand());
$app->add(new RVR\Cli\MaintainDatabaseCommand()); $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(); $app->run();

View File

@ -0,0 +1,53 @@
<?php namespace RVR\Cli;
use DateTime;
use SokoWeb\PersistentData\PersistentDataManager;
use RVR\PersistentData\Model\OAuthClient;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AddOAuthClientCommand extends Command
{
public function configure(): void
{
$this->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('<error>Adding OAuth client failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>OAuth client was successfully added!</info>');
$output->writeln('<info>Client ID: ' . $clientId . '</info>');
$output->writeln('<info>Client secret: ' . $clientSecret . '</info>');
return 0;
}
}

View File

@ -0,0 +1,54 @@
<?php namespace RVR\Cli;
use SokoWeb\PersistentData\PersistentDataManager;
use RVR\Repository\OAuthClientRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AddOAuthRedirectUriCommand extends Command
{
public function configure(): void
{
$this->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('<error>OAuth client does not exist!</error>');
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('<error>Adding redirect URI failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$redirectUrisToPrint = [];
foreach ($redirectUris as $redirectUri) $redirectUrisToPrint[] = '* ' . $redirectUri;
$output->writeln('<info>Redirect URIS were successfully added! Current URIs:' . "\n" . implode("\n", $redirectUrisToPrint) . '</info>');
return 0;
}
}

View File

@ -0,0 +1,54 @@
<?php namespace RVR\Cli;
use SokoWeb\PersistentData\PersistentDataManager;
use RVR\Repository\OAuthClientRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RemoveOAuthRedirectUriCommand extends Command
{
public function configure(): void
{
$this->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('<error>OAuth client does not exist!</error>');
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('<error>Removing redirect URI failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$redirectUrisToPrint = [];
foreach ($redirectUris as $redirectUri) $redirectUrisToPrint[] = '* ' . $redirectUri;
$output->writeln('<info>Redirect URIS were successfully removed! Current URIs:' . "\n" . implode("\n", $redirectUrisToPrint) . '</info>');
return 0;
}
}

View File

@ -3,6 +3,7 @@
use DateTime; use DateTime;
use RVR\PersistentData\Model\OAuthToken; use RVR\PersistentData\Model\OAuthToken;
use RVR\PersistentData\Model\User; use RVR\PersistentData\Model\User;
use RVR\Repository\OAuthClientRepository;
use SokoWeb\Interfaces\Authorization\ISecured; use SokoWeb\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Request\IRequest; use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IRedirect; use SokoWeb\Interfaces\Response\IRedirect;
@ -16,10 +17,13 @@ class OAuthAuthController implements ISecured
private PersistentDataManager $pdm; private PersistentDataManager $pdm;
private OAuthClientRepository $oAuthClientRepository;
public function __construct(IRequest $request) public function __construct(IRequest $request)
{ {
$this->request = $request; $this->request = $request;
$this->pdm = new PersistentDataManager(); $this->pdm = new PersistentDataManager();
$this->oAuthClientRepository = new OAuthClientRepository();
} }
public function authorize(): bool public function authorize(): bool
@ -30,15 +34,30 @@ class OAuthAuthController implements ISecured
public function auth() public function auth()
{ {
$redirectUri = $this->request->query('redirect_uri'); $redirectUri = $this->request->query('redirect_uri');
$clientId = $this->request->query('client_id');
$scope = $this->request->query('scope') ? $this->request->query('scope'): ''; $scope = $this->request->query('scope') ? $this->request->query('scope'): '';
$state = $this->request->query('state'); $state = $this->request->query('state');
$nonce = $this->request->query('nonce') ? $this->request->query('nonce'): ''; $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.']); 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 * @var ?User $user
@ -57,13 +76,11 @@ class OAuthAuthController implements ISecured
$token->setExpiresDate(new DateTime('+5 minutes')); $token->setExpiresDate(new DateTime('+5 minutes'));
$this->pdm->saveToDb($token); $this->pdm->saveToDb($token);
$redirectUri = $redirectUri; $redirectUriQuery = array_merge($redirectUriQuery, [
$additionalUriParams = [
'state' => $state, 'state' => $state,
'code' => $code 'code' => $code
]; ]);
$and = (strpos($redirectUri, '?') !== false) ? '&' : '?'; $finalRedirectUri = $redirectUriBase . '?' . http_build_query($redirectUriQuery);
$finalRedirectUri = $redirectUri . $and . http_build_query($additionalUriParams);
return new Redirect($finalRedirectUri, IRedirect::TEMPORARY); return new Redirect($finalRedirectUri, IRedirect::TEMPORARY);
} }

View File

@ -5,6 +5,7 @@ use Firebase\JWT\JWT;
use RVR\Repository\OAuthTokenRepository; use RVR\Repository\OAuthTokenRepository;
use RVR\Repository\UserRepository; use RVR\Repository\UserRepository;
use RVR\PersistentData\Model\User; use RVR\PersistentData\Model\User;
use RVR\Repository\OAuthClientRepository;
use SokoWeb\Interfaces\Request\IRequest; use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IContent; use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\JsonContent; use SokoWeb\Response\JsonContent;
@ -13,6 +14,8 @@ class OAuthController
{ {
private IRequest $request; private IRequest $request;
private OAuthClientRepository $oAuthClientRepository;
private OAuthTokenRepository $oAuthTokenRepository; private OAuthTokenRepository $oAuthTokenRepository;
private UserRepository $userRepository; private UserRepository $userRepository;
@ -20,14 +23,31 @@ class OAuthController
public function __construct(IRequest $request) public function __construct(IRequest $request)
{ {
$this->request = $request; $this->request = $request;
$this->oAuthClientRepository = new OAuthClientRepository();
$this->oAuthTokenRepository = new OAuthTokenRepository(); $this->oAuthTokenRepository = new OAuthTokenRepository();
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
} }
public function getToken(): ?IContent 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()) { if ($token === null || $token->getExpiresDate() < new DateTime()) {
return new JsonContent([ return new JsonContent([
'error' => 'The provided code is invalid.' 'error' => 'The provided code is invalid.'

View File

@ -0,0 +1,91 @@
<?php namespace RVR\PersistentData\Model;
use DateTime;
use SokoWeb\PersistentData\Model\Model;
class OAuthClient extends Model
{
protected static string $table = 'oauth_clients';
protected static array $fields = ['client_id', 'client_secret', 'redirect_uris', 'preapproved', 'created'];
private string $clientId = '';
private string $clientSecret = '';
private array $redirectUris = [];
private bool $preapproved = false;
private DateTime $created;
public function setClientId(string $clientId): void
{
$this->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');
}
}

View File

@ -0,0 +1,28 @@
<?php namespace RVR\Repository;
use SokoWeb\Database\Query\Select;
use RVR\PersistentData\Model\OAuthClient;
use SokoWeb\PersistentData\PersistentDataManager;
class OAuthClientRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->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);
}
}