From 7a3892ac88dd16d045db55e7bdebd24c24318de2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 22 Apr 2022 15:37:53 +0000 Subject: [PATCH] ci: commit oat-sa/environment-management# --- composer.json | 3 +- docs/tenant-id.md | 4 +- src/Exception/GrpcCallFailedException.php | 5 + .../LtiMessageExtractFailedException.php | 31 ++++++ .../RegistrationIdNotFoundException.php | 31 ++++++ src/Exception/TenantIdNotFoundException.php | 4 +- src/Exception/TokenUnauthorizedException.php | 8 ++ src/Grpc/ConfigurationRepository.php | 41 ++++++-- src/Grpc/Factory/GrpcClientFactory.php | 22 ++++- src/Grpc/FeatureFlagRepository.php | 41 ++++++-- src/Grpc/GrpcCallTrait.php | 12 +++ src/Grpc/LtiRegistrationRepository.php | 42 ++++++-- src/Grpc/OAuth2ClientRepository.php | 49 +++++++++- src/Http/AuthorizationDetailsHeaderMarker.php | 5 +- .../AuthorizationDetailsMarkerInterface.php | 2 +- src/Http/BearerJWTTokenExtractor.php | 70 +++++++++++++ src/Http/JWTTokenExtractorInterface.php | 35 +++++++ src/Http/LtiMessageExtractor.php | 54 +++++++++++ src/Http/LtiMessageExtractorInterface.php | 37 +++++++ src/Http/RegistrationIdExtractor.php | 51 ++++++++++ src/Http/RegistrationIdExtractorInterface.php | 36 +++++++ ...derExtractor.php => TenantIdExtractor.php} | 28 +++--- src/Http/TenantIdExtractorInterface.php | 6 +- src/Model/LtiRegistration.php | 41 +++++--- src/Model/LtiRegistrationCollection.php | 3 + src/Model/OAuth2Client.php | 9 ++ src/Model/OAuth2User.php | 73 ++++++++++++++ .../OAuth2ClientRepositoryInterface.php | 3 + ...EnvironmentManagementTokenTestingTrait.php | 97 +++++++++++++++++++ .../Unit/Grpc/ConfigurationRepositoryTest.php | 8 ++ tests/Unit/Grpc/FeatureFlagRepositoryTest.php | 8 ++ .../Grpc/LtiRegistrationRepositoryTest.php | 8 ++ .../Unit/Grpc/OAuth2ClientRepositoryTest.php | 4 + .../AuthorizationDetailsHeaderMarkerTest.php | 17 +++- .../Unit/Http/BearerJWTTokenExtractorTest.php | 71 ++++++++++++++ tests/Unit/Http/LtiMessageExtractorTest.php | 66 +++++++++++++ .../Unit/Http/RegistrationIdExtractorTest.php | 90 +++++++++++++++++ tests/Unit/Http/TenantIdExtractorTest.php | 90 +++++++++++++++++ .../Unit/Http/TenantIdHeaderExtractorTest.php | 56 ----------- .../Model/LtiRegistrationCollectionTest.php | 1 + 40 files changed, 1135 insertions(+), 127 deletions(-) create mode 100644 src/Exception/LtiMessageExtractFailedException.php create mode 100644 src/Exception/RegistrationIdNotFoundException.php create mode 100644 src/Exception/TokenUnauthorizedException.php create mode 100644 src/Http/BearerJWTTokenExtractor.php create mode 100644 src/Http/JWTTokenExtractorInterface.php create mode 100644 src/Http/LtiMessageExtractor.php create mode 100644 src/Http/LtiMessageExtractorInterface.php create mode 100644 src/Http/RegistrationIdExtractor.php create mode 100644 src/Http/RegistrationIdExtractorInterface.php rename src/Http/{TenantIdHeaderExtractor.php => TenantIdExtractor.php} (56%) create mode 100644 src/Model/OAuth2User.php create mode 100644 src/Trait/EnvironmentManagementTokenTestingTrait.php create mode 100644 tests/Unit/Http/BearerJWTTokenExtractorTest.php create mode 100644 tests/Unit/Http/LtiMessageExtractorTest.php create mode 100644 tests/Unit/Http/RegistrationIdExtractorTest.php create mode 100644 tests/Unit/Http/TenantIdExtractorTest.php delete mode 100644 tests/Unit/Http/TenantIdHeaderExtractorTest.php diff --git a/composer.json b/composer.json index 6afce95..46b9160 100644 --- a/composer.json +++ b/composer.json @@ -12,9 +12,10 @@ ], "require": { "php": ">=7.4.0", - "ext-grpc": "*", + "lcobucci/jwt": "^4.1.5", "oat-sa/lib-em-php-proto": "*", "psr/http-message": "^1.0", + "oat-sa/lib-lti1p3-core": "*", "psr/log": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { diff --git a/docs/tenant-id.md b/docs/tenant-id.md index 0a2dbb3..79a21c2 100644 --- a/docs/tenant-id.md +++ b/docs/tenant-id.md @@ -8,7 +8,7 @@ myMethod(); ``` diff --git a/src/Exception/GrpcCallFailedException.php b/src/Exception/GrpcCallFailedException.php index 4e18d5f..220eab1 100644 --- a/src/Exception/GrpcCallFailedException.php +++ b/src/Exception/GrpcCallFailedException.php @@ -27,6 +27,11 @@ final class GrpcCallFailedException extends EnvironmentManagementClientException { + public static function serverNotReady(): self + { + return new self('gRPC server is not in ready state'); + } + public static function duringCall(string $requestName, Throwable $previous): self { return new self( diff --git a/src/Exception/LtiMessageExtractFailedException.php b/src/Exception/LtiMessageExtractFailedException.php new file mode 100644 index 0000000..17224c7 --- /dev/null +++ b/src/Exception/LtiMessageExtractFailedException.php @@ -0,0 +1,31 @@ +grpcClient = $grpcClient; + $this->logger = $logger ?? new NullLogger(); } public function find(string $tenantId, string $configId): Configuration @@ -46,10 +50,20 @@ public function find(string $tenantId, string $configId): Configuration $grpcRequest->setTenantId($tenantId); $grpcRequest->setConfigurationId($configId); - return Configuration::fromProtobuf($this->doUnaryCall( - $this->grpcClient->GetConfig($grpcRequest), - GetConfigRequest::class - )); + $this->checkClientAvailability($this->grpcClient); + + $this->logger->debug('Fetching Configuration', [ + 'tenantId' => $tenantId, + 'configId' => $configId, + 'grpc_endpoint' => $this->grpcClient->getTarget(), + ]); + + return Configuration::fromProtobuf( + $this->doUnaryCall( + $this->grpcClient->GetConfig($grpcRequest, [], ['timeout' => 10 * 1000000]), + GetConfigRequest::class + ) + ); } public function findAll(string $tenantId): ConfigurationCollection @@ -57,9 +71,18 @@ public function findAll(string $tenantId): ConfigurationCollection $grpcRequest = new ListConfigsRequest(); $grpcRequest->setTenantId($tenantId); - return ConfigurationCollection::fromProtobuf($this->doUnaryCall( - $this->grpcClient->ListConfigs($grpcRequest), - ListConfigsRequest::class - )); + $this->checkClientAvailability($this->grpcClient); + + $this->logger->debug('Fetching all Configurations', [ + 'tenantId' => $tenantId, + 'grpc_endpoint' => $this->grpcClient->getTarget(), + ]); + + return ConfigurationCollection::fromProtobuf( + $this->doUnaryCall( + $this->grpcClient->ListConfigs($grpcRequest, [], ['timeout' => 10 * 1000000]), + ListConfigsRequest::class + ) + ); } } diff --git a/src/Grpc/Factory/GrpcClientFactory.php b/src/Grpc/Factory/GrpcClientFactory.php index ca2acbe..8345de3 100644 --- a/src/Grpc/Factory/GrpcClientFactory.php +++ b/src/Grpc/Factory/GrpcClientFactory.php @@ -27,14 +27,19 @@ use Oat\Envmgmt\Sidecar\FeatureFlagServiceClient; use Oat\Envmgmt\Sidecar\LtiServiceClient; use Oat\Envmgmt\Sidecar\Oauth2ClientServiceClient; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; final class GrpcClientFactory { private string $hostname; private array $opts; + private ?LoggerInterface $logger; - public function __construct() + public function __construct(?LoggerInterface $logger = null) { + $this->logger = $logger ?? new NullLogger(); + $this->hostname = sprintf( '%s:%s', getenv('EM_SIDECAR_HOST') ?: 'localhost', @@ -48,21 +53,36 @@ public function __construct() public function createConfigServiceClient(): ConfigServiceClient { + $this->log(ConfigServiceClient::class); + return new ConfigServiceClient($this->hostname, $this->opts); } public function createFeatureFlagServiceClient(): FeatureFlagServiceClient { + $this->log(FeatureFlagServiceClient::class); + return new FeatureFlagServiceClient($this->hostname, $this->opts); } public function createLtiServiceClient(): LtiServiceClient { + $this->log(LtiServiceClient::class); + return new LtiServiceClient($this->hostname, $this->opts); } public function createOauth2ClientServiceClient(): Oauth2ClientServiceClient { + $this->log(Oauth2ClientServiceClient::class); + return new Oauth2ClientServiceClient($this->hostname, $this->opts); } + + private function log(string $clientName): void + { + $this->logger->debug(sprintf('Creating "%s"', $clientName), [ + 'hostname' => $this->hostname, + ]); + } } diff --git a/src/Grpc/FeatureFlagRepository.php b/src/Grpc/FeatureFlagRepository.php index 4a3e849..47239cc 100644 --- a/src/Grpc/FeatureFlagRepository.php +++ b/src/Grpc/FeatureFlagRepository.php @@ -28,16 +28,20 @@ use OAT\Library\EnvironmentManagementClient\Model\FeatureFlag; use OAT\Library\EnvironmentManagementClient\Model\FeatureFlagCollection; use OAT\Library\EnvironmentManagementClient\Repository\FeatureFlagRepositoryInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; final class FeatureFlagRepository implements FeatureFlagRepositoryInterface { use GrpcCallTrait; private FeatureFlagServiceClient $grpcClient; + private ?LoggerInterface $logger; - public function __construct(FeatureFlagServiceClient $grpcClient) + public function __construct(FeatureFlagServiceClient $grpcClient, ?LoggerInterface $logger = null) { $this->grpcClient = $grpcClient; + $this->logger = $logger ?? new NullLogger(); } public function find(string $tenantId, string $featureFlagId): FeatureFlag @@ -46,10 +50,20 @@ public function find(string $tenantId, string $featureFlagId): FeatureFlag $grpcRequest->setTenantId($tenantId); $grpcRequest->setFeatureFlagId($featureFlagId); - return FeatureFlag::fromProtobuf($this->doUnaryCall( - $this->grpcClient->GetFeatureFlag($grpcRequest), - GetFeatureFlagRequest::class - )); + $this->checkClientAvailability($this->grpcClient); + + $this->logger->debug('Fetching Feature Flag', [ + 'tenantId' => $tenantId, + 'featureFlagId' => $featureFlagId, + 'grpc_endpoint' => $this->grpcClient->getTarget(), + ]); + + return FeatureFlag::fromProtobuf( + $this->doUnaryCall( + $this->grpcClient->GetFeatureFlag($grpcRequest, [], ['timeout' => 10 * 1000000]), + GetFeatureFlagRequest::class + ) + ); } public function findAll(string $tenantId): FeatureFlagCollection @@ -57,9 +71,18 @@ public function findAll(string $tenantId): FeatureFlagCollection $grpcRequest = new ListFeatureFlagsRequest(); $grpcRequest->setTenantId($tenantId); - return FeatureFlagCollection::fromProtobuf($this->doUnaryCall( - $this->grpcClient->ListFeatureFlags($grpcRequest), - ListFeatureFlagsRequest::class - )); + $this->checkClientAvailability($this->grpcClient); + + $this->logger->debug('Fetching all Feature Flags', [ + 'tenantId' => $tenantId, + 'grpc_endpoint' => $this->grpcClient->getTarget(), + ]); + + return FeatureFlagCollection::fromProtobuf( + $this->doUnaryCall( + $this->grpcClient->ListFeatureFlags($grpcRequest, [], ['timeout' => 10 * 1000000]), + ListFeatureFlagsRequest::class + ) + ); } } diff --git a/src/Grpc/GrpcCallTrait.php b/src/Grpc/GrpcCallTrait.php index 3678932..745c056 100644 --- a/src/Grpc/GrpcCallTrait.php +++ b/src/Grpc/GrpcCallTrait.php @@ -22,6 +22,8 @@ namespace OAT\Library\EnvironmentManagementClient\Grpc; +use Exception; +use Grpc\BaseStub; use Grpc\UnaryCall; use OAT\Library\EnvironmentManagementClient\Exception\GrpcCallFailedException; use Throwable; @@ -44,4 +46,14 @@ private function doUnaryCall(UnaryCall $call, string $requestName) return $grpcResponse; } + + /** + * @throws Exception + */ + private function checkClientAvailability(BaseStub $client): void + { + if (!$client->waitForReady(10 * 1000000)) { // 10 seconds + throw GrpcCallFailedException::serverNotReady(); + } + } } diff --git a/src/Grpc/LtiRegistrationRepository.php b/src/Grpc/LtiRegistrationRepository.php index 61c943e..73e9157 100644 --- a/src/Grpc/LtiRegistrationRepository.php +++ b/src/Grpc/LtiRegistrationRepository.php @@ -28,16 +28,20 @@ use OAT\Library\EnvironmentManagementClient\Model\LtiRegistration; use OAT\Library\EnvironmentManagementClient\Model\LtiRegistrationCollection; use OAT\Library\EnvironmentManagementClient\Repository\LtiRegistrationRepositoryInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; final class LtiRegistrationRepository implements LtiRegistrationRepositoryInterface { use GrpcCallTrait; private LtiServiceClient $grpcClient; + private ?LoggerInterface $logger; - public function __construct(LtiServiceClient $grpcClient) + public function __construct(LtiServiceClient $grpcClient, ?LoggerInterface $logger = null) { $this->grpcClient = $grpcClient; + $this->logger = $logger ?? new NullLogger(); } public function find(string $registrationId): LtiRegistration @@ -45,10 +49,19 @@ public function find(string $registrationId): LtiRegistration $grpcRequest = new GetRegistrationRequest(); $grpcRequest->setRegistrationId($registrationId); - return LtiRegistration::fromProtobuf($this->doUnaryCall( - $this->grpcClient->GetRegistration($grpcRequest), - GetRegistrationRequest::class - )); + $this->checkClientAvailability($this->grpcClient); + + $this->logger->debug('Fetching Lti Registration', [ + 'registrationId' => $registrationId, + 'grpc_endpoint' => $this->grpcClient->getTarget(), + ]); + + return LtiRegistration::fromProtobuf( + $this->doUnaryCall( + $this->grpcClient->GetRegistration($grpcRequest), + GetRegistrationRequest::class + ) + ); } public function findAll( @@ -70,9 +83,20 @@ public function findAll( $grpcRequest->setToolIssuer($toolIssuer); } - return LtiRegistrationCollection::fromProtobuf($this->doUnaryCall( - $this->grpcClient->ListRegistrations($grpcRequest), - ListRegistrationsRequest::class - )); + $this->checkClientAvailability($this->grpcClient); + + $this->logger->debug('Fetching all Lti Registrations', [ + 'clientId' => $clientId, + 'platformIssuer' => $platformIssuer, + 'toolIssuer' => $toolIssuer, + 'grpc_endpoint' => $this->grpcClient->getTarget(), + ]); + + return LtiRegistrationCollection::fromProtobuf( + $this->doUnaryCall( + $this->grpcClient->ListRegistrations($grpcRequest, [], ['timeout' => 10 * 1000000]), + ListRegistrationsRequest::class + ) + ); } } diff --git a/src/Grpc/OAuth2ClientRepository.php b/src/Grpc/OAuth2ClientRepository.php index 1f59b98..cffca8f 100644 --- a/src/Grpc/OAuth2ClientRepository.php +++ b/src/Grpc/OAuth2ClientRepository.php @@ -23,19 +23,25 @@ namespace OAT\Library\EnvironmentManagementClient\Grpc; use Oat\Envmgmt\Sidecar\GetClientRequest; +use Oat\Envmgmt\Sidecar\GetClientUserRequest; use Oat\Envmgmt\Sidecar\Oauth2ClientServiceClient; use OAT\Library\EnvironmentManagementClient\Model\OAuth2Client; +use OAT\Library\EnvironmentManagementClient\Model\OAuth2User; use OAT\Library\EnvironmentManagementClient\Repository\OAuth2ClientRepositoryInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; final class OAuth2ClientRepository implements OAuth2ClientRepositoryInterface { use GrpcCallTrait; private Oauth2ClientServiceClient $grpcClient; + private ?LoggerInterface $logger; - public function __construct(Oauth2ClientServiceClient $grpcClient) + public function __construct(Oauth2ClientServiceClient $grpcClient, ?LoggerInterface $logger = null) { $this->grpcClient = $grpcClient; + $this->logger = $logger ?? new NullLogger(); } public function find(string $clientId): OAuth2Client @@ -43,9 +49,42 @@ public function find(string $clientId): OAuth2Client $grpcRequest = new GetClientRequest(); $grpcRequest->setId($clientId); - return OAuth2Client::fromProtobuf($this->doUnaryCall( - $this->grpcClient->GetClient($grpcRequest), - GetClientRequest::class - )); + $this->checkClientAvailability($this->grpcClient); + + $this->logger->debug('Fetching OAuth2 Client', [ + 'clientId' => $clientId, + 'grpc_endpoint' => $this->grpcClient->getTarget(), + ]); + + return OAuth2Client::fromProtobuf( + $this->doUnaryCall( + $this->grpcClient->GetClient($grpcRequest, [], ['timeout' => 10 * 1000000]), + GetClientRequest::class + ) + ); + } + + public function findUser(string $clientId, string $username): Oauth2User + { + $grpcRequest = new GetClientUserRequest(); + + $grpcRequest + ->setId($clientId) + ->setUsername($username); + + $this->checkClientAvailability($this->grpcClient); + + $this->logger->debug('Fetching OAuth2 Username of Client', [ + 'clientId' => $clientId, + 'username' => $username, + 'grpc_endpoint' => $this->grpcClient->getTarget(), + ]); + + return Oauth2User::fromProtobuf( + $this->doUnaryCall( + $this->grpcClient->GetClientUser($grpcRequest, [], ['timeout' => 10 * 1000000]), + GetClientUserRequest::class, + ) + ); } } diff --git a/src/Http/AuthorizationDetailsHeaderMarker.php b/src/Http/AuthorizationDetailsHeaderMarker.php index f14f263..7ed9f00 100644 --- a/src/Http/AuthorizationDetailsHeaderMarker.php +++ b/src/Http/AuthorizationDetailsHeaderMarker.php @@ -28,8 +28,9 @@ final class AuthorizationDetailsHeaderMarker implements AuthorizationDetailsMark { private const DEFAULT_HEADER_NAME = 'X-OAT-WITH-AUTH-DETAILS'; - public function withAuthDetails(ResponseInterface $response): ResponseInterface + public function withAuthDetails(ResponseInterface $response, string $clientId, string $refreshTokenId): ResponseInterface { - return $response->withHeader(self::DEFAULT_HEADER_NAME, 1); + $withAuthDetails = array('clientId' => $clientId, 'refreshTokenId' => $refreshTokenId); + return $response->withHeader(self::DEFAULT_HEADER_NAME, json_encode($withAuthDetails)); } } diff --git a/src/Http/AuthorizationDetailsMarkerInterface.php b/src/Http/AuthorizationDetailsMarkerInterface.php index c303e4f..d0010c6 100644 --- a/src/Http/AuthorizationDetailsMarkerInterface.php +++ b/src/Http/AuthorizationDetailsMarkerInterface.php @@ -26,5 +26,5 @@ interface AuthorizationDetailsMarkerInterface { - public function withAuthDetails(ResponseInterface $response): ResponseInterface; + public function withAuthDetails(ResponseInterface $response, string $clientId, string $refreshTokenId): ResponseInterface; } diff --git a/src/Http/BearerJWTTokenExtractor.php b/src/Http/BearerJWTTokenExtractor.php new file mode 100644 index 0000000..6db34c0 --- /dev/null +++ b/src/Http/BearerJWTTokenExtractor.php @@ -0,0 +1,70 @@ +hasHeader('authorization')) { + throw new TokenUnauthorizedException('Missing Authorization header'); + } + + $header = $request->getHeader('authorization'); + $jwt = trim((string)preg_replace('/^\s*Bearer\s/', '', $header[0])); + + try { + return $this->createConfiguration()->parser()->parse($jwt); + } catch (Throwable $exception) { + throw new EnvironmentManagementClientException( + sprintf('Cannot parse token: %s', $exception->getMessage()), + $exception->getCode(), + $exception + ); + } + } + + /** + * No need to validate JWT in any way, since it is already done by the Envoy filter. + */ + private function createConfiguration(): Configuration + { + return Configuration::forSymmetricSigner( + new Sha256(), + InMemory::empty() + ); + } +} diff --git a/src/Http/JWTTokenExtractorInterface.php b/src/Http/JWTTokenExtractorInterface.php new file mode 100644 index 0000000..8468659 --- /dev/null +++ b/src/Http/JWTTokenExtractorInterface.php @@ -0,0 +1,35 @@ +tokenExtractor = $tokenExtractor ?? new BearerJWTTokenExtractor(); + } + + /** + * @throws TokenUnauthorizedException + */ + public function extract(ServerRequestInterface $request): LtiMessagePayloadInterface + { + if (!empty($request->hasHeader("Authorization"))) { + $token = $this->tokenExtractor->extract($request); + + return new LtiMessagePayload(new Token($token)); + } + + throw LtiMessageExtractFailedException::unableToExtractLtiMessage(); + } +} diff --git a/src/Http/LtiMessageExtractorInterface.php b/src/Http/LtiMessageExtractorInterface.php new file mode 100644 index 0000000..c983eb2 --- /dev/null +++ b/src/Http/LtiMessageExtractorInterface.php @@ -0,0 +1,37 @@ +tokenExtractor = $tokenExtractor ?? new BearerJWTTokenExtractor(); + } + + /** + * @throws TokenUnauthorizedException + */ + public function extract(ServerRequestInterface $request): string + { + $token = $this->tokenExtractor->extract($request); + + if ($token->claims()->has('registration_id')) { + return (string)$token->claims()->get('registration_id'); + } + + throw RegistrationIdNotFoundException::notInToken(); + } +} diff --git a/src/Http/RegistrationIdExtractorInterface.php b/src/Http/RegistrationIdExtractorInterface.php new file mode 100644 index 0000000..303d743 --- /dev/null +++ b/src/Http/RegistrationIdExtractorInterface.php @@ -0,0 +1,36 @@ +headerPrefix = $headerPrefix; + $this->tokenExtractor = $tokenExtractor ?? new BearerJWTTokenExtractor(); } - public function extract(MessageInterface $message): string + /** + * @throws TokenUnauthorizedException + */ + public function extract(ServerRequestInterface $request): string { - $headers = $message->getHeaders(); + $token = $this->tokenExtractor->extract($request); - foreach ($headers as $headerName => $headerValues) { - if (strtolower($headerName) === strtolower($this->headerPrefix)) { - return array_pop($headerValues); - } + if ($token->claims()->has('tenant_id')) { + return (string)$token->claims()->get('tenant_id'); } - throw TenantIdNotFoundException::notInHeader(); + throw TenantIdNotFoundException::notInToken(); } } diff --git a/src/Http/TenantIdExtractorInterface.php b/src/Http/TenantIdExtractorInterface.php index 24ff4d2..9aa7f17 100644 --- a/src/Http/TenantIdExtractorInterface.php +++ b/src/Http/TenantIdExtractorInterface.php @@ -22,13 +22,15 @@ namespace OAT\Library\EnvironmentManagementClient\Http; +use OAT\Library\EnvironmentManagementClient\Exception\TokenUnauthorizedException; use OAT\Library\EnvironmentManagementClient\Exception\TenantIdNotFoundException; -use Psr\Http\Message\MessageInterface; +use Psr\Http\Message\ServerRequestInterface; interface TenantIdExtractorInterface { /** * @throws TenantIdNotFoundException + * @throws TokenUnauthorizedException */ - public function extract(MessageInterface $message): string; + public function extract(ServerRequestInterface $request): string; } diff --git a/src/Model/LtiRegistration.php b/src/Model/LtiRegistration.php index 7bcb036..a4175c9 100644 --- a/src/Model/LtiRegistration.php +++ b/src/Model/LtiRegistration.php @@ -37,6 +37,7 @@ final class LtiRegistration private ?LtiKeyChain $toolKeyChain; private ?LtiPlatform $ltiPlatform; private ?LtiTool $ltiTool; + private ?string $tenantId; public function __construct( string $id, @@ -49,7 +50,8 @@ public function __construct( ?LtiKeyChain $platformKeyChain, ?LtiKeyChain $toolKeyChain, ?LtiPlatform $ltiPlatform, - ?LtiTool $ltiTool + ?LtiTool $ltiTool, + ?string $tenantId ) { $this->id = $id; $this->clientId = $clientId; @@ -62,22 +64,32 @@ public function __construct( $this->toolKeyChain = $toolKeyChain; $this->ltiPlatform = $ltiPlatform; $this->ltiTool = $ltiTool; + $this->tenantId = $tenantId; } public static function fromProtobuf(ProtoLtiRegistration $protoRegistration): self { return new self( - $protoRegistration->getId(), - $protoRegistration->getClientId(), - $protoRegistration->getPlatformId(), - $protoRegistration->getToolId(), - iterator_to_array($protoRegistration->getDeploymentIds()), - $protoRegistration->getPlatformJwksUrl(), - $protoRegistration->getToolJwksUrl(), - LtiKeyChain::fromProtobuf($protoRegistration->getPlatformKeyChain()), - LtiKeyChain::fromProtobuf($protoRegistration->getToolKeyChain()), - $protoRegistration->hasPlatform() ? LtiPlatform::fromProtobuf($protoRegistration->getPlatform()) : null, - $protoRegistration->hasTool() ? LtiTool::fromProtobuf($protoRegistration->getTool()) : null, + $protoRegistration->getId(), + $protoRegistration->getClientId(), + $protoRegistration->getPlatformId(), + $protoRegistration->getToolId(), + iterator_to_array($protoRegistration->getDeploymentIds()), + $protoRegistration->getPlatformJwksUrl(), + $protoRegistration->getToolJwksUrl(), + $protoRegistration->hasPlatformKeyChain() + ? LtiKeyChain::fromProtobuf($protoRegistration->getPlatformKeyChain()) + : null, + $protoRegistration->hasToolKeyChain() + ? LtiKeyChain::fromProtobuf($protoRegistration->getToolKeyChain()) + : null, + $protoRegistration->hasPlatform() + ? LtiPlatform::fromProtobuf($protoRegistration->getPlatform()) + : null, + $protoRegistration->hasTool() + ? LtiTool::fromProtobuf($protoRegistration->getTool()) + : null, + $protoRegistration->getTenantId() ); } @@ -135,4 +147,9 @@ public function getLtiTool(): ?LtiTool { return $this->ltiTool; } + + public function getTenantId(): ?string + { + return $this->tenantId; + } } diff --git a/src/Model/LtiRegistrationCollection.php b/src/Model/LtiRegistrationCollection.php index 214fd12..5cf585c 100644 --- a/src/Model/LtiRegistrationCollection.php +++ b/src/Model/LtiRegistrationCollection.php @@ -68,6 +68,9 @@ public function get(string $registrationId): LtiRegistration return $this->registrations[$registrationId]; } + /** + * @return ArrayIterator + */ public function getIterator(): Traversable { return new ArrayIterator($this->registrations); diff --git a/src/Model/OAuth2Client.php b/src/Model/OAuth2Client.php index 052e8aa..a90c270 100644 --- a/src/Model/OAuth2Client.php +++ b/src/Model/OAuth2Client.php @@ -29,6 +29,7 @@ final class OAuth2Client private string $name; private string $clientId; private string $clientSecret; + private bool $isConfidential; private array $scopes; private ?string $tenantId; private ?string $instanceUrl; @@ -37,6 +38,7 @@ public function __construct( string $name, string $clientId, string $clientSecret, + bool $isConfidential, array $scopes, ?string $tenantId, ?string $instanceUrl @@ -44,6 +46,7 @@ public function __construct( $this->name = $name; $this->clientId = $clientId; $this->clientSecret = $clientSecret; + $this->isConfidential = $isConfidential; $this->scopes = $scopes; $this->tenantId = $tenantId; $this->instanceUrl = $instanceUrl; @@ -55,6 +58,7 @@ public static function fromProtobuf(ProtoOauth2Client $protoOauth2Client): self $protoOauth2Client->getName(), $protoOauth2Client->getClientId(), $protoOauth2Client->getClientSecret(), + $protoOauth2Client->getIsConfidential(), iterator_to_array($protoOauth2Client->getScopes()), $protoOauth2Client->hasTenantId() ? $protoOauth2Client->getTenantId() : null, $protoOauth2Client->hasInstanceUrl() ? $protoOauth2Client->getInstanceUrl() : null @@ -76,6 +80,11 @@ public function getClientSecret(): string return $this->clientSecret; } + public function getIsConfidential(): bool + { + return $this->isConfidential; + } + public function getScopes(): array { return $this->scopes; diff --git a/src/Model/OAuth2User.php b/src/Model/OAuth2User.php new file mode 100644 index 0000000..b2f25e7 --- /dev/null +++ b/src/Model/OAuth2User.php @@ -0,0 +1,73 @@ +username = $username; + $this->password = $password; + $this->roles = $roles; + } + + public static function fromProtobuf(ProtoOauth2User $protoOauth2User): self + { + return new self( + $protoOauth2User->getUsername(), + $protoOauth2User->getPassword(), + iterator_to_array($protoOauth2User->getRoles()), + ); + } + + public function getUsername(): string + { + return $this->username; + } + + public function getPassword(): string + { + return $this->password; + } + + public function getRoles(): array + { + return $this->roles; + } +} diff --git a/src/Repository/OAuth2ClientRepositoryInterface.php b/src/Repository/OAuth2ClientRepositoryInterface.php index 438311a..e974f63 100644 --- a/src/Repository/OAuth2ClientRepositoryInterface.php +++ b/src/Repository/OAuth2ClientRepositoryInterface.php @@ -23,8 +23,11 @@ namespace OAT\Library\EnvironmentManagementClient\Repository; use OAT\Library\EnvironmentManagementClient\Model\OAuth2Client; +use OAT\Library\EnvironmentManagementClient\Model\OAuth2User; interface OAuth2ClientRepositoryInterface { public function find(string $clientId): OAuth2Client; + + public function findUser(string $clientId, string $username): Oauth2User; } diff --git a/src/Trait/EnvironmentManagementTokenTestingTrait.php b/src/Trait/EnvironmentManagementTokenTestingTrait.php new file mode 100644 index 0000000..2835bcb --- /dev/null +++ b/src/Trait/EnvironmentManagementTokenTestingTrait.php @@ -0,0 +1,97 @@ +toImmutable(); + } + + if ($expiryDateTime === null) { + $expiryDateTime = Carbon::createFromImmutable($nowDateTime)->addHour()->toImmutable(); + } + + return $configuration->builder() + ->identifiedBy($identifier) + ->withHeader('kid', $keyChainIdentifier) + ->relatedTo($userIdentifier) + ->issuedBy($issuer) + ->expiresAt($expiryDateTime) + ->issuedAt($nowDateTime) + ->canOnlyBeUsedAfter($nowDateTime) + ->withClaim('scopes', $scopes) + ->withClaim('tenant_id', $tenantId) + ->getToken($configuration->signer(), $configuration->signingKey()); + } + + /** + * Returns a refresh token identical to the refresh token issued by the Environment Management's Auth Server + * @see https://github.com/oat-sa/environment-management/blob/develop/auth-server/src/Oauth2/Entity/RefreshToken.php + * + * @param string $identifier + * @param string $keyChainIdentifier + * @param string $userIdentifier + * @param string $issuer + * @param array $scopes + * @param string $tenantId + * @param DateTimeImmutable|null $expiryDateTime + * @param DateTimeImmutable|null $nowDateTime + * @return Token + */ + public function buildAuthServerRefreshToken( + string $identifier = 'client_id', + string $keyChainIdentifier = 'key_chain_id', + string $userIdentifier = 'user_id', + string $issuer = 'issuer', + array $scopes = [], + string $tenantId = 'tenant_id', + DateTimeImmutable $expiryDateTime = null, + DateTimeImmutable $nowDateTime = null, + ): Token { + // At the moment we are generating the refresh tokens on the same way as the access tokens + return $this->buildAuthServerAccessToken( + $identifier, + $keyChainIdentifier, + $userIdentifier, + $issuer, + $scopes, + $tenantId, + $expiryDateTime, + $nowDateTime, + ); + } +} diff --git a/tests/Unit/Grpc/ConfigurationRepositoryTest.php b/tests/Unit/Grpc/ConfigurationRepositoryTest.php index 7ec7ded..692e604 100644 --- a/tests/Unit/Grpc/ConfigurationRepositoryTest.php +++ b/tests/Unit/Grpc/ConfigurationRepositoryTest.php @@ -64,6 +64,10 @@ public function testFindSuccessfullyRuns(): void })) ->willReturn($this->createMockCall($protoConfiguration)); + $this->mockGrpcClient->expects($this->once()) + ->method('waitForReady') + ->willReturn(true); + $configuration = $this->repository->find('t1', 'conf-1'); $this->assertInstanceOf(Configuration::class, $configuration); @@ -89,6 +93,10 @@ public function testFindAllSuccessfullyRuns(): void })) ->willReturn($this->createMockCall($protoCollection)); + $this->mockGrpcClient->expects($this->once()) + ->method('waitForReady') + ->willReturn(true); + $collection = $this->repository->findAll('t1'); $this->assertInstanceOf(ConfigurationCollection::class, $collection); diff --git a/tests/Unit/Grpc/FeatureFlagRepositoryTest.php b/tests/Unit/Grpc/FeatureFlagRepositoryTest.php index 3753ca6..3c5db95 100644 --- a/tests/Unit/Grpc/FeatureFlagRepositoryTest.php +++ b/tests/Unit/Grpc/FeatureFlagRepositoryTest.php @@ -64,6 +64,10 @@ public function testFindSuccessfullyRuns(): void })) ->willReturn($this->createMockCall($protoFlag)); + $this->mockGrpcClient->expects($this->once()) + ->method('waitForReady') + ->willReturn(true); + $flag = $this->repository->find('t1', 'flag-1'); $this->assertInstanceOf(FeatureFlag::class, $flag); @@ -89,6 +93,10 @@ public function testFindAllSuccessfullyRuns(): void })) ->willReturn($this->createMockCall($protoCollection)); + $this->mockGrpcClient->expects($this->once()) + ->method('waitForReady') + ->willReturn(true); + $collection = $this->repository->findAll('t1'); $this->assertInstanceOf(FeatureFlagCollection::class, $collection); diff --git a/tests/Unit/Grpc/LtiRegistrationRepositoryTest.php b/tests/Unit/Grpc/LtiRegistrationRepositoryTest.php index a58b2c5..e714cb6 100644 --- a/tests/Unit/Grpc/LtiRegistrationRepositoryTest.php +++ b/tests/Unit/Grpc/LtiRegistrationRepositoryTest.php @@ -78,6 +78,10 @@ public function testFindSuccessfullyRuns(): void })) ->willReturn($this->createMockCall($protoReg)); + $this->mockGrpcClient->expects($this->once()) + ->method('waitForReady') + ->willReturn(true); + $registration = $this->repository->find('reg-1'); $this->assertInstanceOf(LtiRegistration::class, $registration); @@ -124,6 +128,10 @@ public function testFindAllSuccessfullyRuns(): void })) ->willReturn($this->createMockCall($protoCollection)); + $this->mockGrpcClient->expects($this->once()) + ->method('waitForReady') + ->willReturn(true); + $collection = $this->repository->findAll('client-id', 'platform-iss', 'tool-iss'); $this->assertInstanceOf(LtiRegistrationCollection::class, $collection); diff --git a/tests/Unit/Grpc/OAuth2ClientRepositoryTest.php b/tests/Unit/Grpc/OAuth2ClientRepositoryTest.php index 5ef2694..6452f26 100644 --- a/tests/Unit/Grpc/OAuth2ClientRepositoryTest.php +++ b/tests/Unit/Grpc/OAuth2ClientRepositoryTest.php @@ -64,6 +64,10 @@ public function testFindSuccessfullyRuns(): void })) ->willReturn($this->createMockCall($protoClient)); + $this->mockGrpcClient->expects($this->once()) + ->method('waitForReady') + ->willReturn(true); + $client = $this->repository->find('client-1'); $this->assertInstanceOf(OAuth2Client::class, $client); diff --git a/tests/Unit/Http/AuthorizationDetailsHeaderMarkerTest.php b/tests/Unit/Http/AuthorizationDetailsHeaderMarkerTest.php index 50eb289..cc79d54 100644 --- a/tests/Unit/Http/AuthorizationDetailsHeaderMarkerTest.php +++ b/tests/Unit/Http/AuthorizationDetailsHeaderMarkerTest.php @@ -33,9 +33,22 @@ public function testAuthDetailsHeaderAdded(): void $message = (new Psr17Factory())->createResponse(); $marker = new AuthorizationDetailsHeaderMarker(); - $message = $marker->withAuthDetails($message); + $message = $marker->withAuthDetails($message, "client1", "refreshToken1"); $this->assertTrue($message->hasHeader('X-OAT-WITH-AUTH-DETAILS')); - $this->assertSame('1', $message->getHeader('X-OAT-WITH-AUTH-DETAILS')[0]); + + $withAuthDetails = $message->getHeader('X-OAT-WITH-AUTH-DETAILS')[0]; + + $this->assertNotNull( + $withAuthDetails, + "withAuthDetails is null" + ); + + $res_array = (array)json_decode($withAuthDetails); + + $this->assertArrayHasKey('clientId', $res_array); + $this->assertEquals('client1', $res_array['clientId']); + $this->assertArrayHasKey('refreshTokenId', $res_array); + $this->assertEquals('refreshToken1', $res_array['refreshTokenId']); } } diff --git a/tests/Unit/Http/BearerJWTTokenExtractorTest.php b/tests/Unit/Http/BearerJWTTokenExtractorTest.php new file mode 100644 index 0000000..1f4c94b --- /dev/null +++ b/tests/Unit/Http/BearerJWTTokenExtractorTest.php @@ -0,0 +1,71 @@ +subject = new BearerJWTTokenExtractor(); + } + + public function testItThrowsExceptionWhenAuthorizationHeaderMissing(): void + { + $this->expectException(TokenUnauthorizedException::class); + $this->expectExceptionMessage('Missing Authorization header'); + + $request = (new Psr17Factory())->createServerRequest('GET', 'http://example.test'); + $this->subject->extract($request); + } + + public function testItThrowsExceptionWhenInvalidJWTProvided(): void + { + $this->expectException(EnvironmentManagementClientException::class); + $this->expectExceptionMessage('Cannot parse token: The JWT string must have two dots'); + + $request = (new Psr17Factory())->createServerRequest('GET', 'http://example.test'); + $request = $request->withHeader('Authorization', 'Bearer NOT-VALID-JWT'); + + $this->subject->extract($request); + } + + public function testJWTTokenParsedSuccessfully(): void + { + $token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InByaW1hcnlLZXlQYWlyIn0.eyJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS9yZXNvdXJjZV9saW5rIjp7ImlkIjoiMTVhOGZkMjUtN2NlZi00ODBkLThhNTQtZjZjZGE1ZjM1MWE0In0sImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL3ZlcnNpb24iOiIxLjMuMCIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL21lc3NhZ2VfdHlwZSI6Ikx0aVJlc291cmNlTGlua1JlcXVlc3QiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS9kZXBsb3ltZW50X2lkIjoiZGVwbG95bWVudElkMSIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL3RhcmdldF9saW5rX3VyaSI6Imh0dHA6Ly9kZXZraXQtbHRpMXAzLmxvY2FsaG9zdC90b29sL2xhdW5jaCIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL3JvbGVzIjpbXSwicmVnaXN0cmF0aW9uX2lkIjoiYXV0aFNlcnZlciIsImlzcyI6ImxvY2FsaG9zdDo4MDA1IiwiYXVkIjoiY2xpZW50LWF1dGgiLCJzdWIiOiJjM3BvIiwibmFtZSI6IkMzUE8iLCJlbWFpbCI6ImMzcG9AcmViZWxzLmNvbSIsImdpdmVuX25hbWUiOiJDM1BPIiwibG9jYWxlIjoiZW4iLCJwaWN0dXJlIjoiaHR0cHM6Ly9jZG40Lmljb25maW5kZXIuY29tL2RhdGEvaWNvbnMvZmFtb3VzLWNoYXJhY3RlcnMtYWRkLW9uLXZvbC0xLWZsYXQvNDgvRmFtb3VzX0NoYXJhY3Rlcl8tX0FkZF9Pbl8xLTM0LTUxMi5wbmciLCJ0ZW5hbnRfaWQiOiJhY2MtMS5pbnMtMSIsImp0aSI6ImUzYTA0YWNkLTMwOWYtNDBjNy05MTE0LTczOGEzYjliYTA0NyIsImlhdCI6MTYzNDgyMjQyMi44NTM3NTUsIm5iZiI6MTYzNDgyMjQyMi44NTM3NTUsImV4cCI6MTYzNDgyMzAyMi44NTM3NTV9.oPq5TssKXV0IWcQh-nZwcTgRcK1D7tMT3mX-kA5CIYNz03ra1UV8I8hQ4fPTDzv_9hUEKw3n5saVsErg9bbqsRNOiOw9-GzfZOKANoCNkeY37Cs6gY5npe6_vCoUFQWqKjYxXaPOTppQwU_Ujwd2rfPranX5LZKQg7DmN1Fv3mZmIbeyqOhMsO8VQL4hNN81m49inWisc-jIqa2gxNO70xVUJ-BuJ9NpakSz7Y83n1vJUQX-1IVfrVqcbV5jFKlHCo90ofUoQ0TqLwd1xBCCzAwqrzF1NcYYhPpvrI8vyIWgwGDHPLRXi_NyrrlTX9b2m77nf1LSplsfcYqAiqj4RqmXpenONFq-9Ewj3ZGCeib_YVzynEFcuMq8hIYJxr68eR-XkisKQ_fABvt7_yPIKnoD3TcCjxtZCbw5_5yceygJk9UEUKlYvZw16Ex9ymjQPGrcRKHFxqQva4emxpgwY2hwV4eYHQGNaNslqFdHhpa58GL9fH9A4pw1xiVPY2-q_Sje_63H3zPC2Q2O28VxYJcaNEPXYc4nmH5sAL0jqmhRT-niV_PJ1IKdpVZoo-p_0vABR6seGBjtpT_bQVRfVfw1xXr2Su1RPTqPSd9qyfKQauR07XnXtzrZwwXkHlAbvpz6XtsFWFfVxXn9UnwhO8rznTDKD8SOKTt_b_npyx0'; + $request = (new Psr17Factory())->createServerRequest('GET', 'http://example.test'); + $request = $request->withHeader('Authorization', sprintf('Bearer %s', $token)); + + $token = $this->subject->extract($request); + + $this->assertNotEmpty($token->headers()->all()); + $this->assertNotEmpty($token->claims()->all()); + } +} diff --git a/tests/Unit/Http/LtiMessageExtractorTest.php b/tests/Unit/Http/LtiMessageExtractorTest.php new file mode 100644 index 0000000..14d1935 --- /dev/null +++ b/tests/Unit/Http/LtiMessageExtractorTest.php @@ -0,0 +1,66 @@ +subject = new LtiMessageExtractor; + } + + public function testExtractLtiMessage(): void + { + $request = new ServerRequest('GET', 'http://example.test', + [ + 'Authorization' => 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InBsYXRmb3JtS2V5In0.eyJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS9yZXNvdXJjZV9saW5rIjp7ImlkIjoiMDc0MWNkMWYtM2I0MS00N2ZiLWIyN2ItN2VmY2ZkYTRkMzAzIn0sImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL3ZlcnNpb24iOiIxLjMuMCIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL21lc3NhZ2VfdHlwZSI6Ikx0aVJlc291cmNlTGlua1JlcXVlc3QiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL2x0aS9jbGFpbS9kZXBsb3ltZW50X2lkIjoiZGVwbG95bWVudElkMSIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvbHRpL2NsYWltL3RhcmdldF9saW5rX3VyaSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MTAwMDAvYXBwIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9sdGkvY2xhaW0vcm9sZXMiOltdLCJyZWdpc3RyYXRpb25faWQiOiJhdXRoX3NlcnZlcl9yZWdpc3RyYXRpb24iLCJub25jZSI6ImM3YTIzNjUxLWQ3MDItNGJmZC1hMDI1LTllNDA5NTg3MGE2YiIsImp0aSI6IjcxZmFkYWVmLTZkMjQtNDIwZi1hYTJjLWUzYzc2NGY3MmE2NyIsImlhdCI6MTY0Mzk2MDUyMS41MDQ3MjMsIm5iZiI6MTY0Mzk2MDUyMS41MDQ3MjMsImV4cCI6MTY0Mzk2MTEyMS41MDQ3MjMsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODAwNy9wbGF0Zm9ybSIsImF1ZCI6ImNsaWVudC1hdXRoIn0.TZ09i9iDlV--uc2NjukO9UK03CHogl_4MluGWzhidfX2U_1vUVq0lnuU3nZK20aT-qNI0oq0hHa4-pvWub3mneB-J14aHpatUuPfNmnZ2MQcg92goB8PdgyiKVWH0xPArm6QjH7pwQZgBzNe2xEC1Afll1uN3wG6nqIeRxnZ9vff_geHmkFZw_bDL4W8gpee93VFW0CHEFdQ2SffqW7hkxRHJfwvHbwUSzhj_d0rJdJPeOil74ppegxmOoWtstFUeJZWUbBzfq3u0a1CKD2TEJg57JMokSrsrww1grL8YYnkHTr39DYJcb-F1JyiSuL716n1z5JPbayFRnwZuJgyEw' + ]); + + $ltiMessage = $this->subject->extract($request); + + $this->assertEquals('http://localhost:10000/app', $ltiMessage->getTargetLinkUri()); + $this->assertEquals("1.3.0", $ltiMessage->getVersion()); + $this->assertEquals("LtiResourceLinkRequest", $ltiMessage->getMessageType()); + $this->assertEquals([], $ltiMessage->getRoles()); + $this->assertEquals("http://localhost:8007/platform", $ltiMessage->getClaim(MessagePayloadInterface::CLAIM_ISS)); + $this->assertEquals(["client-auth"], $ltiMessage->getClaim(MessagePayloadInterface::CLAIM_AUD)); + $this->assertNotEmpty($ltiMessage->getToken()->toString()); + } + + public function testExtractLtiMessageWillThrowErrorWhenRequestIsEmpty(): void + { + $this->expectException(LtiMessageExtractFailedException::class); + $this->expectExceptionMessage('Not able to parse Lti message from JWT token'); + + $this->subject->extract(new ServerRequest('GET', 'http://example.test')); + } + +} diff --git a/tests/Unit/Http/RegistrationIdExtractorTest.php b/tests/Unit/Http/RegistrationIdExtractorTest.php new file mode 100644 index 0000000..5eebc3a --- /dev/null +++ b/tests/Unit/Http/RegistrationIdExtractorTest.php @@ -0,0 +1,90 @@ +jwtExtractorMock = $this->createMock(JWTTokenExtractorInterface::class); + $this->subject = new RegistrationIdExtractor($this->jwtExtractorMock); + } + + public function testExtractRegistrationIdSuccessfully(): void + { + $request = (new Psr17Factory())->createServerRequest('GET', 'http://example.test'); + + $tokenMock = $this->createMock(UnencryptedToken::class); + $claims = new DataSet(['registration_id' => 'reg-2'], ''); + + $tokenMock->expects($this->exactly(2)) + ->method('claims') + ->willReturn($claims); + + $this->jwtExtractorMock->expects($this->once()) + ->method('extract') + ->with($request) + ->willReturn($tokenMock); + + $tenantId = $this->subject->extract($request); + + $this->assertEquals('reg-2', $tenantId); + } + + public function testExtractRegistrationIdThrowsExceptionIfItNotPresentInToken(): void + { + $this->expectException(RegistrationIdNotFoundException::class); + $this->expectExceptionMessage('LTI Registration Id not found in JWT token.'); + + $request = (new Psr17Factory())->createServerRequest('GET', 'http://example.test'); + + $tokenMock = $this->createMock(UnencryptedToken::class); + $claims = new DataSet([], ''); + + $tokenMock->expects($this->once()) + ->method('claims') + ->willReturn($claims); + + $this->jwtExtractorMock->expects($this->once()) + ->method('extract') + ->with($request) + ->willReturn($tokenMock); + + $this->subject->extract($request); + } +} diff --git a/tests/Unit/Http/TenantIdExtractorTest.php b/tests/Unit/Http/TenantIdExtractorTest.php new file mode 100644 index 0000000..f2e01ad --- /dev/null +++ b/tests/Unit/Http/TenantIdExtractorTest.php @@ -0,0 +1,90 @@ +jwtExtractorMock = $this->createMock(JWTTokenExtractorInterface::class); + $this->subject = new TenantIdExtractor($this->jwtExtractorMock); + } + + public function testExtractTenantIdSuccessfully(): void + { + $request = (new Psr17Factory())->createServerRequest('GET', 'http://example.test'); + + $tokenMock = $this->createMock(UnencryptedToken::class); + $claims = new DataSet(['tenant_id' => 'tenant-2'], ''); + + $tokenMock->expects($this->exactly(2)) + ->method('claims') + ->willReturn($claims); + + $this->jwtExtractorMock->expects($this->once()) + ->method('extract') + ->with($request) + ->willReturn($tokenMock); + + $tenantId = $this->subject->extract($request); + + $this->assertEquals('tenant-2', $tenantId); + } + + public function testExtractTenantIdThrowsExceptionIfItNotPresentInToken(): void + { + $this->expectException(TenantIdNotFoundException::class); + $this->expectExceptionMessage('Tenant Id not found in JWT token.'); + + $request = (new Psr17Factory())->createServerRequest('GET', 'http://example.test'); + + $tokenMock = $this->createMock(UnencryptedToken::class); + $claims = new DataSet([], ''); + + $tokenMock->expects($this->once()) + ->method('claims') + ->willReturn($claims); + + $this->jwtExtractorMock->expects($this->once()) + ->method('extract') + ->with($request) + ->willReturn($tokenMock); + + $this->subject->extract($request); + } +} diff --git a/tests/Unit/Http/TenantIdHeaderExtractorTest.php b/tests/Unit/Http/TenantIdHeaderExtractorTest.php deleted file mode 100644 index 77def20..0000000 --- a/tests/Unit/Http/TenantIdHeaderExtractorTest.php +++ /dev/null @@ -1,56 +0,0 @@ -createResponse(); - $message = $message->withHeader('X-OAT-Tenant-Id', 'tenant-zzz') - ->withAddedHeader('X-Oat-Tenant-Id', 'tenant-zzz-2') - ->withHeader('Some-Other-Custom-Flag-2', 'some'); - - $extractor = new TenantIdHeaderExtractor(); - $tenantId = $extractor->extract($message); - - $this->assertEquals('tenant-zzz-2', $tenantId); - } - - public function testExtractTenantIdThrowsExceptionIfNoTenantHeaderPresent(): void - { - $message = (new Psr17Factory())->createResponse(); - $message = $message->withHeader('Some-Other-Custom-Flag-2', 'some'); - - $this->expectException(TenantIdNotFoundException::class); - $this->expectExceptionMessage('Tenant Id not found in request header.'); - - $extractor = new TenantIdHeaderExtractor(); - $extractor->extract($message); - } -} diff --git a/tests/Unit/Model/LtiRegistrationCollectionTest.php b/tests/Unit/Model/LtiRegistrationCollectionTest.php index 249821f..1326cc4 100644 --- a/tests/Unit/Model/LtiRegistrationCollectionTest.php +++ b/tests/Unit/Model/LtiRegistrationCollectionTest.php @@ -63,6 +63,7 @@ public function testRegistrationCollectionLifeCycle(): void ), null, null, + null ); $collection->add($reg1);