Skip to content

Commit

Permalink
Add authorization to endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
rjzondervan committed Dec 18, 2024
1 parent b624417 commit abfa0f5
Show file tree
Hide file tree
Showing 6 changed files with 316 additions and 4 deletions.
16 changes: 15 additions & 1 deletion lib/Controller/EndpointsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace OCA\OpenConnector\Controller;

use OCA\OpenConnector\Exception\AuthenticationException;
use OCA\OpenConnector\Service\AuthorizationService;
use OCA\OpenConnector\Service\ObjectService;
use OCA\OpenConnector\Service\SearchService;
use OCA\OpenConnector\Service\EndpointService;
Expand Down Expand Up @@ -33,7 +35,8 @@ public function __construct(
IRequest $request,
private IAppConfig $config,
private EndpointMapper $endpointMapper,
private EndpointService $endpointService
private EndpointService $endpointService,
private AuthorizationService $authorizationService
)
{
parent::__construct($appName, $request);
Expand Down Expand Up @@ -185,12 +188,23 @@ public function destroy(int $id): JSONResponse
*
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*
* @param string $path The request path to match
* @return JSONResponse The response from the endpoint service or 404 if no match
*/
public function handlePath(string $_path): JSONResponse
{
try {
$token = $this->request->getHeader('Authorization');
$this->authorizationService->authorize(authorization: $token);
} catch (AuthenticationException $exception) {
return new JSONResponse(
data: ['error' => $exception->getMessage(), 'details' => $exception->getDetails()],
statusCode: 401
);
}

// Find matching endpoints for the given path and method
$matchingEndpoints = $this->endpointMapper->findByPathRegex(
path: $_path,
Expand Down
6 changes: 3 additions & 3 deletions lib/Db/Consumer.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
class Consumer extends Entity implements JsonSerializable
{
protected ?string $uuid = null;
protected ?string $name = null; // The name of the consumer
protected ?string $name = null; // The name of the consumer
protected ?string $description = null; // The description of the consumer
protected ?array $domains = []; // The domains the consumer is allowed to run from
protected ?array $ips = []; // The ips the consumer is allowed to run from
protected ?string $authorizationType = null; // The authorization type of the consumer, should be one of the following: 'none', 'basic', 'bearer', 'apiKey', 'oauth2', 'jwt'. Keep in mind that the consumer needs to be able to handle the authorization type.
protected ?string $authorizationConfiguration = null; // The authorization configuration of the consumer
protected ?array $authorizationConfiguration = []; // The authorization configuration of the consumer
protected ?DateTime $created = null; // the date and time the consumer was created
protected ?DateTime $updated = null; // the date and time the consumer was updated

Expand All @@ -38,7 +38,7 @@ public function __construct() {
$this->addType('domains', 'json');
$this->addType('ips', 'json');
$this->addType('authorizationType', 'string');
$this->addType('authorizationConfiguration', 'string');
$this->addType('authorizationConfiguration', 'json');
$this->addType('created', 'datetime');
$this->addType('updated', 'datetime');
}
Expand Down
19 changes: 19 additions & 0 deletions lib/Exception/AuthenticationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace OCA\OpenConnector\Exception;

use Exception;

class AuthenticationException extends Exception
{
private array $details;
public function __construct(string $message, array $details) {
$this->details = $details;
parent::__construct($message);
}

public function getDetails(): array
{
return $this->details;
}
}
57 changes: 57 additions & 0 deletions lib/Migration/Version1Date20241218122708.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\OpenConnector\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
* FIXME Auto-generated migration step: Please modify to your needs!
*/
class Version1Date20241218122708 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/**
* @var ISchemaWrapper $schema
*/
$schema = $schemaClosure();

if($schema->hasTable(tableName: 'openconnector_consumers') === true) {
$table = $schema->getTable(tableName: 'openconnector_consumers');
$table->dropColumn('authorization_configuration');
}

return $schema;
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
}
}
58 changes: 58 additions & 0 deletions lib/Migration/Version1Date20241218122932.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\OpenConnector\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
* FIXME Auto-generated migration step: Please modify to your needs!
*/
class Version1Date20241218122932 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/**
* @var ISchemaWrapper $schema
*/
$schema = $schemaClosure();

if($schema->hasTable(tableName: 'openconnector_consumers') === true) {
$table = $schema->getTable(tableName: 'openconnector_consumers');
$table->addColumn('authorization_configuration', Types::JSON);
}

return $schema;
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
}
}
164 changes: 164 additions & 0 deletions lib/Service/AuthorizationService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

namespace OCA\OpenConnector\Service;

use DateTime;
use Jose\Component\Checker\AlgorithmChecker;
use Jose\Component\Checker\HeaderCheckerManager;
use Jose\Component\Checker\InvalidHeaderException;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWKSet;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\HS256;
use Jose\Component\Signature\Algorithm\HS384;
use Jose\Component\Signature\Algorithm\PS256;
use Jose\Component\Signature\Algorithm\PS384;
use Jose\Component\Signature\Algorithm\PS512;
use Jose\Component\Signature\Algorithm\RS256;
use Jose\Component\Signature\Algorithm\RS384;
use Jose\Component\Signature\Algorithm\RS512;
use Jose\Component\Signature\JWS;
use Jose\Component\Signature\JWSTokenSupport;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Jose\Component\Signature\Serializer\JWSSerializerManager;
use OCA\OpenConnector\Db\Consumer;
use OCA\OpenConnector\Db\ConsumerMapper;
use OCA\OpenConnector\Exception\AuthenticationException;
use OCP\IUserManager;
use OCP\IUserSession;

class AuthorizationService
{
const HMAC_ALGORITHMS = ['HS256', 'HS384', 'HS512'];
const PKCS1_ALGORITHMS = ['RS256', 'RS384', 'RS512'];
const PSS_ALGORITHMS = ['PS256', 'PS384', 'PS512'];


public function __construct(
private readonly IUserManager $userManager,
private readonly IUserSession $userSession,
private readonly ConsumerMapper $consumerMapper,
) {}

private function findIssuer(string $issuer): Consumer
{
$consumers = $this->consumerMapper->findAll(filters: ['name' => $issuer]);

if(count($consumers) === 0) {
throw new AuthenticationException(message: 'The issuer was not found', details: ['iss' => $issuer]);
}

return $consumers[0];
}

private function checkHeaders(JWS $token): void {
$headerChecker = new HeaderCheckerManager(
checkers: [
new AlgorithmChecker(array_merge(self::HMAC_ALGORITHMS, self::PKCS1_ALGORITHMS, self::PSS_ALGORITHMS))
],
tokenTypes: [new JWSTokenSupport()]);

$headerChecker->check(jwt: $token, index: 0);

}

private function getJWK(string $publicKey, string $algorithm): JWKSet
{

if (
in_array(needle: $algorithm, haystack: self::HMAC_ALGORITHMS) === true
) {
return new JWKSet([
JWKFactory::createFromSecret(
secret: $publicKey,
additional_values: ['alg' => $algorithm, 'use' => 'sig'])
]);
} else if (
in_array(
needle: $algorithm,
haystack: self::PKCS1_ALGORITHMS
) === true
|| in_array(
needle: $algorithm,
haystack: self::PSS_ALGORITHMS
) === true
) {
$stamp = microtime().getmypid();
$filename = "/var/tmp/publickey-$stamp";
file_put_contents($filename, base64_decode($publicKey));
$jwk = new JWKSet([JWKFactory::createFromKeyFile(file: $filename)]);
unlink($filename);
return $jwk;
}
throw new AuthenticationException(message: 'The token algorithm is not supported', details: ['algorithm' => $algorithm]);
}

public function validatePayload(array $payload): void
{
$now = new DateTime();

if(isset($payload['iat']) === true) {
$iat = new DateTime('@'.$payload['iat']);
} else {
throw new AuthenticationException(message: 'The token has no time of creation', details: ['iat' => null]);
}

if(isset($payload['exp']) === true) {
$exp = new DateTime('@'.$payload['exp']);
} else {
$exp = clone $iat;
$exp->modify('+1 Hour');
}

if($exp->diff($now)->format('%R') === '+') {
throw new AuthenticationException(message: 'The token has expired', details: ['iat' => $iat->getTimestamp(), 'exp' => $exp->getTimestamp(), 'time checked' => $now->getTimestamp()]);
}
}
public function authorize(string $authorization): void
{
$token = substr(string: $authorization, offset: strlen('Bearer '));

if($token === '') {
throw new AuthenticationException(message: 'No token has been provided', details: []);
}

$algorithmManager = new AlgorithmManager([
new HS256(),
new HS384(),
new HS256(),
new RS256(),
new RS384(),
new RS512(),
new PS256(),
new PS384(),
new PS512()
]);
$verifier = new JWSVerifier($algorithmManager);
$serializerManager = new JWSSerializerManager([new CompactSerializer()]);



$jws = $serializerManager->unserialize(input: $token);

try{
$this->checkHeaders($jws);
} catch (InvalidHeaderException $exception) {
throw new AuthenticationException(message: 'The token could not be validated', details: ['reason' => $exception->getMessage()]);
}

$payload = json_decode(json: $jws->getPayload(), associative: true);
$issuer = $this->findIssuer(issuer: $payload['iss']);

$publicKey = $issuer->getAuthorizationConfiguration()['publicKey'];
$algorithm = $issuer->getAuthorizationConfiguration()['algorithm'];

$jwkSet = $this->getJWK(publicKey: $publicKey, algorithm: $algorithm);

if($verifier->verifyWithKeySet(jws: $jws, jwkset: $jwkSet, signatureIndex: 0) === false) {
throw new AuthenticationException(message: 'The token could not be validated', details: ['reason' => 'The token does not match the public key']);
}
$this->validatePayload($payload);
// $this->userSession->setUser($this->userManager->get($issuer->getUserId()));
}
}

0 comments on commit abfa0f5

Please sign in to comment.