restrict oauth access #13
@ -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
3
rvr
@ -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();
|
||||||
|
53
src/Cli/AddOAuthClientCommand.php
Normal file
53
src/Cli/AddOAuthClientCommand.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
54
src/Cli/AddOAuthRedirectUriCommand.php
Normal file
54
src/Cli/AddOAuthRedirectUriCommand.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
54
src/Cli/RemoveOAuthRedirectUriCommand.php
Normal file
54
src/Cli/RemoveOAuthRedirectUriCommand.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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.'
|
||||||
|
91
src/PersistentData/Model/OAuthClient.php
Normal file
91
src/PersistentData/Model/OAuthClient.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
28
src/Repository/OAuthClientRepository.php
Normal file
28
src/Repository/OAuthClientRepository.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user