From dedcfa064fce563c1a5933120e4b1c861e1f5366 Mon Sep 17 00:00:00 2001 From: Konrad Oboza Date: Tue, 25 Jun 2024 16:18:51 +0200 Subject: [PATCH] IBX-8356: Reworked `Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface` usages to comply with Symfony-based authentication (#101) * IBX-8290: Reworked REST authentication to comply with the new Symfony authenticator mechanism under separate firewall * improved UnauthorizedException throwing, introduced dedicated exception * IBX-8290: Re-implemented REST authorization to comply with the new authenticators mechanism * IBX-8290: Reworked REST authentication to comply with the new Symfony authenticator mechanism under separate firewall * removed CsrfTokenManagerTest as it brings not much value, regenerated PHPStan * fixed test case * adjusted test cases, fixed outstanding PHPStan issues * reverted session refresh endpoint removal * part1 of unit tests reworking * part2 of unit tests reworking * fixed expected authorization error, fixed deprecation in BaseContentTest * narrowed down authentication for cases were both Accept and Content-Type headers are provided * fixed several functional test cases * made `$userId` more strict type-wise * added InteractiveLoginEvent support * changed RestAuthenticator to be triggered by route instead of headers * IBX-8290: Re-implemented REST authorization to comply with the new authenticators mechanism * IBX-8290: Reworked REST authentication to comply with the new Symfony authenticator mechanism under separate firewall * removed CsrfTokenManagerTest as it brings not much value, regenerated PHPStan * adjusted test cases, fixed outstanding PHPStan issues * reverted session refresh endpoint removal * part2 of unit tests reworking * fixed expected authorization error, fixed deprecation in BaseContentTest * narrowed down authentication for cases were both Accept and Content-Type headers are provided * IBX-8356: Reworked `Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface` usages to comply with Symfony-based authentication * IBX-8356: Reworked `Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface` usages to comply with Symfony-based authentication * IBX-8356: Reworked `Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface` usages to comply with Symfony-based authentication * added old Content-Type header replacing subscriber * provided BC for REST response for JWT, documented parts that need to be dropped on the new REST API release * added listeners test coverage * restored original controller * cr remarks * documented AuthorizationHeaderRESTRequestMatcher usage * cr remark vol.2 * cr remark vol.3 * added failsafe for non-rest requests * fixes after rebase --- phpstan-baseline.neon | 15 -- src/bundle/Resources/config/input_parsers.yml | 5 - src/bundle/Resources/config/routing.yml | 1 - src/bundle/Resources/config/security.yml | 8 +- src/bundle/Resources/config/services.yml | 5 +- .../config/value_object_visitors.yml | 5 - .../AuthorizationHeaderRESTRequestMatcher.php | 10 +- .../JWT/AuthenticationSuccessSubscriber.php | 78 +++++++++ .../JsonLoginHeaderReplacingSubscriber.php | 46 ++++++ src/lib/Server/Controller/JWT.php | 62 +------ .../Exceptions/BadResponseException.php | 15 ++ src/lib/Server/Input/Parser/JWTInput.php | 30 ---- .../Server/Output/ValueObjectVisitor/JWT.php | 27 --- .../EventListener/SecurityListener.php | 54 ------ src/lib/Server/Values/JWT.php | 22 --- src/lib/Server/Values/JWTInput.php | 26 --- tests/bundle/Functional/BinaryContentTest.php | 2 +- ...horizationHeaderRESTRequestMatcherTest.php | 2 +- .../AuthenticationSuccessSubscriberTest.php | 156 ++++++++++++++++++ ...JsonLoginHeaderReplacingSubscriberTest.php | 94 +++++++++++ .../lib/Server/Input/Parser/JWTInputTest.php | 81 --------- .../Output/ValueObjectVisitor/JWTTest.php | 83 ---------- .../Server/Security/SecurityListenerTest.php | 75 --------- 23 files changed, 411 insertions(+), 491 deletions(-) rename src/{contracts => lib}/Security/AuthorizationHeaderRESTRequestMatcher.php (82%) create mode 100644 src/lib/Security/EventListener/JWT/AuthenticationSuccessSubscriber.php create mode 100644 src/lib/Security/EventListener/JWT/JsonLoginHeaderReplacingSubscriber.php create mode 100644 src/lib/Server/Exceptions/BadResponseException.php delete mode 100644 src/lib/Server/Input/Parser/JWTInput.php delete mode 100644 src/lib/Server/Output/ValueObjectVisitor/JWT.php delete mode 100644 src/lib/Server/Security/EventListener/SecurityListener.php delete mode 100644 src/lib/Server/Values/JWT.php delete mode 100644 src/lib/Server/Values/JWTInput.php create mode 100644 tests/lib/Security/EventListener/JWT/AuthenticationSuccessSubscriberTest.php create mode 100644 tests/lib/Security/EventListener/JWT/JsonLoginHeaderReplacingSubscriberTest.php delete mode 100644 tests/lib/Server/Input/Parser/JWTInputTest.php delete mode 100644 tests/lib/Server/Output/ValueObjectVisitor/JWTTest.php delete mode 100644 tests/lib/Server/Security/SecurityListenerTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6f86bb0e..7e653757 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -460,11 +460,6 @@ parameters: count: 1 path: src/contracts/Output/Visitor.php - - - message: "#^Method Ibexa\\\\Contracts\\\\Rest\\\\Security\\\\AuthorizationHeaderRESTRequestMatcher\\:\\:__construct\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#" - count: 1 - path: src/contracts/Security/AuthorizationHeaderRESTRequestMatcher.php - - message: "#^Method Ibexa\\\\Rest\\\\FieldTypeProcessor\\\\BaseRelationProcessor\\:\\:setLocationService\\(\\) has no return type specified\\.$#" count: 1 @@ -1325,11 +1320,6 @@ parameters: count: 1 path: src/lib/Server/Controller/ContentType.php - - - message: "#^Parameter \\#1 \\$wrappedUser of class Ibexa\\\\Rest\\\\Server\\\\Security\\\\JWTUser constructor expects Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#" - count: 1 - path: src/lib/Server/Controller/JWT.php - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Controller\\\\Location\\:\\:loadLocation\\(\\) should return Ibexa\\\\Rest\\\\Server\\\\Values\\\\RestLocation but returns Ibexa\\\\Rest\\\\Server\\\\Values\\\\CachedValue\\.$#" count: 1 @@ -8260,11 +8250,6 @@ parameters: count: 1 path: tests/lib/Server/Output/ValueObjectVisitor/ImageVariationTest.php - - - message: "#^Static method Ibexa\\\\Tests\\\\Rest\\\\Output\\\\ValueObjectVisitorBaseTest\\:\\:assertXMLTag\\(\\) invoked with 4 parameters, 2\\-3 required\\.$#" - count: 2 - path: tests/lib/Server/Output/ValueObjectVisitor/JWTTest.php - - message: "#^Method Ibexa\\\\Tests\\\\Rest\\\\Output\\\\ValueObjectVisitorBaseTest\\:\\:assertXMLTag\\(\\) invoked with 4 parameters, 2\\-3 required\\.$#" count: 2 diff --git a/src/bundle/Resources/config/input_parsers.yml b/src/bundle/Resources/config/input_parsers.yml index 225153fd..867e9222 100644 --- a/src/bundle/Resources/config/input_parsers.yml +++ b/src/bundle/Resources/config/input_parsers.yml @@ -82,11 +82,6 @@ services: tags: - { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.FieldDefinitionUpdate } - Ibexa\Rest\Server\Input\Parser\JWTInput: - parent: Ibexa\Rest\Server\Common\Parser - tags: - - { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.JWTInput } - Ibexa\Rest\Server\Input\Parser\LocationCreate: parent: Ibexa\Rest\Server\Common\Parser class: Ibexa\Rest\Server\Input\Parser\LocationCreate diff --git a/src/bundle/Resources/config/routing.yml b/src/bundle/Resources/config/routing.yml index 629b8cd0..cd4e81b4 100644 --- a/src/bundle/Resources/config/routing.yml +++ b/src/bundle/Resources/config/routing.yml @@ -1244,7 +1244,6 @@ ibexa.rest.load_bookmarks: methods: [GET] # JWT - ibexa.rest.create_token: path: /user/token/jwt controller: Ibexa\Rest\Server\Controller\JWT::createToken diff --git a/src/bundle/Resources/config/security.yml b/src/bundle/Resources/config/security.yml index 61c75b76..f92ce3cd 100644 --- a/src/bundle/Resources/config/security.yml +++ b/src/bundle/Resources/config/security.yml @@ -7,7 +7,7 @@ services: autoconfigure: false public: false - Ibexa\Contracts\Rest\Security\AuthorizationHeaderRESTRequestMatcher: + Ibexa\Rest\Security\AuthorizationHeaderRESTRequestMatcher: arguments: $headerName: '%ibexa.rest.authorization_header_name%' @@ -17,7 +17,11 @@ services: - '@?security.csrf.token_storage' - '@?request_stack' - Ibexa\Rest\Server\Security\EventListener\SecurityListener: + Ibexa\Rest\Security\EventListener\JWT\AuthenticationSuccessSubscriber: + tags: + - { name: kernel.event_subscriber } + + Ibexa\Rest\Security\EventListener\JWT\JsonLoginHeaderReplacingSubscriber: tags: - { name: kernel.event_subscriber } diff --git a/src/bundle/Resources/config/services.yml b/src/bundle/Resources/config/services.yml index 7bfedb25..e3f80340 100644 --- a/src/bundle/Resources/config/services.yml +++ b/src/bundle/Resources/config/services.yml @@ -196,10 +196,9 @@ services: tags: [controller.service_arguments] Ibexa\Rest\Server\Controller\JWT: + autowire: true + autoconfigure: true parent: Ibexa\Rest\Server\Controller - arguments: - - '@Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface' - - '@?ibexa.rest.session_authenticator' tags: [controller.service_arguments] Ibexa\Bundle\Rest\EventListener\RequestListener: diff --git a/src/bundle/Resources/config/value_object_visitors.yml b/src/bundle/Resources/config/value_object_visitors.yml index 0ce2c6a6..a954a3af 100644 --- a/src/bundle/Resources/config/value_object_visitors.yml +++ b/src/bundle/Resources/config/value_object_visitors.yml @@ -334,11 +334,6 @@ services: tags: - { name: ibexa.rest.output.value_object.visitor, type: Ibexa\Rest\Server\Values\DeletedUserSession } - Ibexa\Rest\Server\Output\ValueObjectVisitor\JWT: - parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor - tags: - - { name: ibexa.rest.output.value_object.visitor, type: Ibexa\Rest\Server\Values\JWT } - # ContentType Ibexa\Rest\Server\Output\ValueObjectVisitor\ContentType: parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor diff --git a/src/contracts/Security/AuthorizationHeaderRESTRequestMatcher.php b/src/lib/Security/AuthorizationHeaderRESTRequestMatcher.php similarity index 82% rename from src/contracts/Security/AuthorizationHeaderRESTRequestMatcher.php rename to src/lib/Security/AuthorizationHeaderRESTRequestMatcher.php index df6af354..13284042 100644 --- a/src/contracts/Security/AuthorizationHeaderRESTRequestMatcher.php +++ b/src/lib/Security/AuthorizationHeaderRESTRequestMatcher.php @@ -6,15 +6,23 @@ */ declare(strict_types=1); -namespace Ibexa\Contracts\Rest\Security; +namespace Ibexa\Rest\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestMatcher; +/** + * @internal + * + * This class is mandatory for JWT REST calls recognition. It's used within security.firewalls.ibexa_jwt_rest.request_matcher configuration key. + */ final class AuthorizationHeaderRESTRequestMatcher extends RequestMatcher { private ?string $headerName; + /** + * @param array $attributes + */ public function __construct( ?string $headerName = null, string $path = null, diff --git a/src/lib/Security/EventListener/JWT/AuthenticationSuccessSubscriber.php b/src/lib/Security/EventListener/JWT/AuthenticationSuccessSubscriber.php new file mode 100644 index 00000000..36f58cd8 --- /dev/null +++ b/src/lib/Security/EventListener/JWT/AuthenticationSuccessSubscriber.php @@ -0,0 +1,78 @@ + ['onAuthenticationSuccess', 10], + ]; + } + + public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void + { + $request = $this->requestStack->getCurrentRequest(); + if ($request === null) { + return; + } + + if (!$request->attributes->get('is_rest_request')) { + return; + } + + $user = $event->getUser(); + if ($user instanceof IbexaUser) { + $this->permissionResolver->setCurrentUserReference($user->getAPIUser()); + } + + $this->normalizeResponseToRest($event); + } + + /* + * This method provides BC compatibility for the JWT Token REST response + * since the new Lexik/JWT json_login authenticator changes its form. + * + * @deprecated 5.0.0. Will be removed in the next REST API version. + */ + /** + * @throws \Ibexa\Rest\Server\Exceptions\BadResponseException + */ + private function normalizeResponseToRest(AuthenticationSuccessEvent $event): void + { + $eventData = $event->getData(); + if (!isset($eventData['token'])) { + throw new BadResponseException('JWT Token has not been generated.'); + } + + $token = $eventData['token']; + $event->setData([ + 'JWT' => [ + '_media-type' => 'application/vnd.ibexa.api.JWT+json', + '_token' => $token, + 'token' => $token, + ], + ]); + } +} diff --git a/src/lib/Security/EventListener/JWT/JsonLoginHeaderReplacingSubscriber.php b/src/lib/Security/EventListener/JWT/JsonLoginHeaderReplacingSubscriber.php new file mode 100644 index 00000000..791b095f --- /dev/null +++ b/src/lib/Security/EventListener/JWT/JsonLoginHeaderReplacingSubscriber.php @@ -0,0 +1,46 @@ + ['replaceJsonLoginHeader', 10], + ]; + } + + public function replaceJsonLoginHeader(RequestEvent $event): void + { + $request = $event->getRequest(); + if (!$request->headers->has(self::CONTENT_TYPE_HEADER)) { + return; + } + + if ($request->headers->get(self::CONTENT_TYPE_HEADER) !== 'application/vnd.ibexa.api.JWTInput+json') { + return; + } + + $request->headers->set(self::CONTENT_TYPE_HEADER, 'application/json'); + } +} diff --git a/src/lib/Server/Controller/JWT.php b/src/lib/Server/Controller/JWT.php index e0b08723..f1f705b3 100644 --- a/src/lib/Server/Controller/JWT.php +++ b/src/lib/Server/Controller/JWT.php @@ -8,70 +8,14 @@ namespace Ibexa\Rest\Server\Controller; -use Ibexa\Contracts\Rest\Exceptions\UnauthorizedException; -use Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface; -use Ibexa\Rest\Message; use Ibexa\Rest\Server\Controller as RestController; -use Ibexa\Rest\Server\Security\JWTUser; -use Ibexa\Rest\Server\Values; -use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Security\Core\Exception\AuthenticationException; final class JWT extends RestController { - /** @var \Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface */ - private $tokenManager; - - /** @var \Ibexa\Core\MVC\Symfony\Security\Authentication\AuthenticatorInterface|null */ - private $authenticator; - - public function __construct( - JWTTokenManagerInterface $tokenManager, - ?AuthenticatorInterface $authenticator = null - ) { - $this->tokenManager = $tokenManager; - $this->authenticator = $authenticator; - } - - public function createToken(Request $request): Values\JWT - { - /** @var \Ibexa\Rest\Server\Values\JWTInput $jwtTokenInput */ - $jwtTokenInput = $this->inputDispatcher->parse( - new Message( - ['Content-Type' => $request->headers->get('Content-Type')], - $request->getContent() - ) - ); - - try { - $request->attributes->set('username', $jwtTokenInput->username); - $request->attributes->set('password', (string) $jwtTokenInput->password); - - $user = $this->getAuthenticator()->authenticate($request)->getUser(); - - $jwtToken = $this->tokenManager->create( - new JWTUser($user, $jwtTokenInput->username) - ); - - return new Values\JWT($jwtToken); - } catch (AuthenticationException $e) { - $this->getAuthenticator()->logout($request); - throw new UnauthorizedException('Invalid login or password'); - } - } - - private function getAuthenticator(): AuthenticatorInterface + public function createToken(Request $request): void { - if (null === $this->authenticator) { - throw new \RuntimeException( - sprintf( - "No %s instance injected. Ensure 'ibexa.rest.session' is configured under your firewall", - AuthenticatorInterface::class - ) - ); - } - - return $this->authenticator; + //empty method for Symfony json_login authenticator which is used by Lexik/JWT under the hood + // for more detail refer to: https://symfony.com/bundles/LexikJWTAuthenticationBundle/current/index.html#symfony-5-3-and-higher } } diff --git a/src/lib/Server/Exceptions/BadResponseException.php b/src/lib/Server/Exceptions/BadResponseException.php new file mode 100644 index 00000000..ad8f3bc4 --- /dev/null +++ b/src/lib/Server/Exceptions/BadResponseException.php @@ -0,0 +1,15 @@ +setStatus(200); - $visitor->setHeader('Content-Type', $generator->getMediaType('JWT')); - - $generator->startObjectElement('JWT'); - $generator->attribute('token', $data->token); - $generator->valueElement('token', $data->token); - $generator->endObjectElement('JWT'); - } -} diff --git a/src/lib/Server/Security/EventListener/SecurityListener.php b/src/lib/Server/Security/EventListener/SecurityListener.php deleted file mode 100644 index 5e204274..00000000 --- a/src/lib/Server/Security/EventListener/SecurityListener.php +++ /dev/null @@ -1,54 +0,0 @@ -permissionResolver = $permissionResolver; - } - - public static function getSubscribedEvents(): array - { - return [ - SecurityEvents::INTERACTIVE_LOGIN => [ - ['onInteractiveLogin', 10], - ], - ]; - } - - public function onInteractiveLogin(BaseInteractiveLoginEvent $event): void - { - $token = $event->getAuthenticationToken(); - - if (!$token instanceof JWTUserToken) { - return; - } - - $user = $event->getAuthenticationToken()->getUser(); - if ($user instanceof IbexaUser) { - $this->permissionResolver->setCurrentUserReference($user->getAPIUser()); - } - } -} diff --git a/src/lib/Server/Values/JWT.php b/src/lib/Server/Values/JWT.php deleted file mode 100644 index 5bfbdc83..00000000 --- a/src/lib/Server/Values/JWT.php +++ /dev/null @@ -1,22 +0,0 @@ -token = $token; - } -} diff --git a/src/lib/Server/Values/JWTInput.php b/src/lib/Server/Values/JWTInput.php deleted file mode 100644 index 94517aee..00000000 --- a/src/lib/Server/Values/JWTInput.php +++ /dev/null @@ -1,26 +0,0 @@ -username = $username; - $this->password = $password; - } -} diff --git a/tests/bundle/Functional/BinaryContentTest.php b/tests/bundle/Functional/BinaryContentTest.php index 43006b5f..f0f54616 100644 --- a/tests/bundle/Functional/BinaryContentTest.php +++ b/tests/bundle/Functional/BinaryContentTest.php @@ -67,7 +67,7 @@ public function testCreateContentWithImageData(): string $response = $this->sendHttpRequest($request); $this->assertHttpResponseCodeEquals($response, Response::HTTP_CREATED); - self::assertHttpResponseHasHeader($response, 'Location'); + $this->assertHttpResponseHasHeader($response, 'Location'); $href = $response->getHeader('Location')[0]; $this->addCreatedElement($href); diff --git a/tests/contracts/Security/AuthorizationHeaderRESTRequestMatcherTest.php b/tests/contracts/Security/AuthorizationHeaderRESTRequestMatcherTest.php index ad248c34..72ffc98f 100644 --- a/tests/contracts/Security/AuthorizationHeaderRESTRequestMatcherTest.php +++ b/tests/contracts/Security/AuthorizationHeaderRESTRequestMatcherTest.php @@ -8,7 +8,7 @@ namespace Ibexa\Tests\Contracts\Rest\Security; -use Ibexa\Contracts\Rest\Security\AuthorizationHeaderRESTRequestMatcher; +use Ibexa\Rest\Security\AuthorizationHeaderRESTRequestMatcher; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; diff --git a/tests/lib/Security/EventListener/JWT/AuthenticationSuccessSubscriberTest.php b/tests/lib/Security/EventListener/JWT/AuthenticationSuccessSubscriberTest.php new file mode 100644 index 00000000..16f13c0d --- /dev/null +++ b/tests/lib/Security/EventListener/JWT/AuthenticationSuccessSubscriberTest.php @@ -0,0 +1,156 @@ +createMock(PermissionResolver::class), + $this->getRequestStackMock() + ); + + self::assertEquals( + [ + Events::AUTHENTICATION_SUCCESS => ['onAuthenticationSuccess', 10], + ], + $subscriber->getSubscribedEvents() + ); + } + + /** + * @dataProvider dataProviderForTestOnAuthenticationSuccess + */ + public function testOnAuthenticationSuccess( + UserInterface $user, + bool $isPermissionResolverInvoked + ): void { + $permissionResolver = $this->createMock(PermissionResolver::class); + $permissionResolver + ->expects($isPermissionResolverInvoked === true ? self::once() : self::never()) + ->method('setCurrentUserReference'); + + $event = new AuthenticationSuccessEvent(['token' => 'foo_token'], $user, new Response()); + + $subscriber = new AuthenticationSuccessSubscriber( + $permissionResolver, + $this->getRequestStackMock() + ); + + $subscriber->onAuthenticationSuccess($event); + + self::assertSame( + [ + 'JWT' => [ + '_media-type' => 'application/vnd.ibexa.api.JWT+json', + '_token' => 'foo_token', + 'token' => 'foo_token', + ], + ], + $event->getData() + ); + } + + /** + * @return iterable + */ + public function dataProviderForTestOnAuthenticationSuccess(): iterable + { + yield 'authorizing Ibexa user' => [ + new User($this->createMock(ApiUser::class)), + true, + ]; + + yield 'authorizing non-Ibexa user' => [ + new InMemoryUser('foo', 'bar'), + false, + ]; + } + + public function testResponseIsMissingJwtToken(): void + { + $subscriber = new AuthenticationSuccessSubscriber( + $this->createMock(PermissionResolver::class), + $this->getRequestStackMock() + ); + + $event = new AuthenticationSuccessEvent( + [ + 'some' => 'data', + 'some_other' => 'data', + 'but_no_token' => 'anywhere', + ], + new User($this->createMock(ApiUser::class)), + new Response() + ); + + $this->expectException(BadResponseException::class); + + $subscriber->onAuthenticationSuccess($event); + } + + public function testSkippingResponseNormalizingForNonRestRequest(): void + { + $subscriber = new AuthenticationSuccessSubscriber( + $this->createMock(PermissionResolver::class), + $this->getRequestStackMock(false) + ); + + $event = new AuthenticationSuccessEvent( + [ + 'token' => 'foo', + ], + new User($this->createMock(ApiUser::class)), + new Response() + ); + + $subscriber->onAuthenticationSuccess($event); + + self::assertSame( + [ + 'token' => 'foo', + ], + $event->getData() + ); + } + + /** + * @return \Symfony\Component\HttpFoundation\RequestStack&\PHPUnit\Framework\MockObject\MockObject + */ + private function getRequestStackMock(bool $isRestRequest = true): RequestStack + { + $request = new Request( + [], + [], + $isRestRequest === true ? ['is_rest_request' => true] : [] + ); + + $requestStackMock = $this->createMock(RequestStack::class); + $requestStackMock + ->method('getCurrentRequest') + ->willReturn($request); + + return $requestStackMock; + } +} diff --git a/tests/lib/Security/EventListener/JWT/JsonLoginHeaderReplacingSubscriberTest.php b/tests/lib/Security/EventListener/JWT/JsonLoginHeaderReplacingSubscriberTest.php new file mode 100644 index 00000000..6cd24c3e --- /dev/null +++ b/tests/lib/Security/EventListener/JWT/JsonLoginHeaderReplacingSubscriberTest.php @@ -0,0 +1,94 @@ +subscriber = new JsonLoginHeaderReplacingSubscriber(); + } + + public function testGetSubscribedEvents(): void + { + self::assertEquals( + [ + KernelEvents::REQUEST => ['replaceJsonLoginHeader', 10], + ], + $this->subscriber->getSubscribedEvents() + ); + } + + /** + * @dataProvider dataProviderForTestReplacingJsonHeader + */ + public function testReplacingJsonHeader( + string $headerToReplace, + string $expectedHeader, + ): void { + $requestEvent = $this->getRequestEventMock([ + 'Content-Type' => $headerToReplace, + ]); + + $this->subscriber->replaceJsonLoginHeader($requestEvent); + + self::assertSame( + $expectedHeader, + $requestEvent->getRequest()->headers->get('Content-Type') + ); + } + + /** + * @return iterable + */ + public function dataProviderForTestReplacingJsonHeader(): iterable + { + yield 'replacing REST header to the required one' => [ + 'application/vnd.ibexa.api.JWTInput+json', + 'application/json', + ]; + + yield 'replacing not JTW REST header does not occur' => [ + 'application/vnd.ibexa.api.Content+json', + 'application/vnd.ibexa.api.Content+json', + ]; + + yield 'replacing other header does not occur' => [ + 'foo_header', + 'foo_header', + ]; + } + + /** + * @param array $headers + * + * @return \Symfony\Component\HttpKernel\Event\RequestEvent&\PHPUnit\Framework\MockObject\MockObject + */ + private function getRequestEventMock(array $headers): RequestEvent + { + $request = new Request(); + $request->headers = new HeaderBag($headers); + + $requestEvent = $this->createMock(RequestEvent::class); + $requestEvent + ->method('getRequest') + ->willReturn($request); + + return $requestEvent; + } +} diff --git a/tests/lib/Server/Input/Parser/JWTInputTest.php b/tests/lib/Server/Input/Parser/JWTInputTest.php deleted file mode 100644 index 2d25ee6d..00000000 --- a/tests/lib/Server/Input/Parser/JWTInputTest.php +++ /dev/null @@ -1,81 +0,0 @@ - $username, - 'password' => $password, - ]; - - $jwtInput = $this->internalGetParser(); - $result = $jwtInput->parse($inputArray, $this->getParsingDispatcherMock()); - - self::assertInstanceOf( - JWTInputValue::class, - $result, - 'JWTInput not created correctly.' - ); - - self::assertEquals( - $username, - $result->username - ); - - self::assertEquals( - $password, - $result->password - ); - } - - public function testParseExceptionOnEmptyPassword(): void - { - $username = 'johndoe'; - - $inputArray = [ - 'username' => $username, - ]; - - $this->expectException(Parser::class); - $this->expectExceptionMessage("Missing 'password' attribute for JWTInput."); - - $jwtInput = $this->internalGetParser(); - $jwtInput->parse($inputArray, $this->getParsingDispatcherMock()); - } - - public function testParseExceptionOnEmptyUsername(): void - { - $password = 'ibexa'; - - $inputArray = [ - 'password' => $password, - ]; - - $this->expectException(Parser::class); - $this->expectExceptionMessage("Missing 'username' attribute for JWTInput."); - - $jwtInput = $this->internalGetParser(); - $jwtInput->parse($inputArray, $this->getParsingDispatcherMock()); - } - - protected function internalGetParser(): JWTInput - { - return new JWTInput(); - } -} diff --git a/tests/lib/Server/Output/ValueObjectVisitor/JWTTest.php b/tests/lib/Server/Output/ValueObjectVisitor/JWTTest.php deleted file mode 100644 index 2cbbd0ea..00000000 --- a/tests/lib/Server/Output/ValueObjectVisitor/JWTTest.php +++ /dev/null @@ -1,83 +0,0 @@ -getVisitor(); - $generator = $this->getGenerator(); - - $generator->startDocument(null); - - $token = new JWTValue('abc'); - - $visitor->visit( - $this->getVisitorMock(), - $generator, - $token - ); - - $result = $generator->endDocument(null); - - self::assertNotNull($result); - - return $result; - } - - /** - * @param string $result - * - * @depends testVisit - */ - public function testResultContainsTokenTagWithTokenAttribute(string $result): void - { - self::assertXMLTag( - [ - 'tag' => 'JWT', - 'attributes' => [ - 'token' => 'abc', - ], - ], - $result, - 'Missing token attributes.', - false - ); - } - - /** - * @param string $result - * - * @depends testVisit - */ - public function testResultContainsTokenTagWithMediaTypeAttribute(string $result): void - { - self::assertXMLTag( - [ - 'tag' => 'JWT', - 'attributes' => [ - 'media-type' => 'application/vnd.ibexa.api.JWT+xml', - ], - ], - $result, - 'Missing media-type attribute.', - false - ); - } - - protected function internalGetVisitor(): JWT - { - return new JWT(); - } -} diff --git a/tests/lib/Server/Security/SecurityListenerTest.php b/tests/lib/Server/Security/SecurityListenerTest.php deleted file mode 100644 index 884a9812..00000000 --- a/tests/lib/Server/Security/SecurityListenerTest.php +++ /dev/null @@ -1,75 +0,0 @@ -permissionResolver = $this->createMock(PermissionResolver::class); - } - - public function testOnInteractiveLoginWithJWTUserTokenButNotEzPlatformUser(): void - { - $requestMock = $this->createMock(Request::class); - $tokenMock = $this->createMock(JWTUserToken::class); - - $interactiveLoginEvent = new InteractiveLoginEvent($requestMock, $tokenMock); - - $tokenMock - ->expects(self::once()) - ->method('getUser') - ->willReturn(null); - - $securityListener = new SecurityListener($this->permissionResolver); - $securityListener->onInteractiveLogin($interactiveLoginEvent); - } - - public function testOnInteractiveLoginWithJWTUserTokenAndEzPlatformUser(): void - { - $requestMock = $this->createMock(Request::class); - $tokenMock = $this->createMock(JWTUserToken::class); - - $interactiveLoginEvent = new InteractiveLoginEvent($requestMock, $tokenMock); - - $userMock = $this->createMock(UserInterface::class); - $apiUserMock = $this->createMock(User::class); - - $tokenMock - ->expects(self::once()) - ->method('getUser') - ->willReturn($userMock); - - $userMock - ->expects(self::once()) - ->method('getApiUser') - ->willReturn($apiUserMock); - - $this->permissionResolver - ->expects(self::once()) - ->method('setCurrentUserReference') - ->with($apiUserMock); - - $securityListener = new SecurityListener($this->permissionResolver); - $securityListener->onInteractiveLogin($interactiveLoginEvent); - } -}