From a574d35b66bf3e98190b247fae17bbf3034c6ee8 Mon Sep 17 00:00:00 2001 From: Tatyana Koleva Date: Wed, 11 Sep 2024 18:50:31 +0300 Subject: [PATCH 01/19] OXDEV-8682 Add subscriber to detect password change --- services.yaml | 7 ++ .../Subscriber/PasswordChangeSubscriber.php | 104 ++++++++++++++++++ src/Service/UserModelService.php | 34 ++++++ .../Service/UserModelServiceTest.php | 71 ++++++++++++ .../PasswordChangeSubscriberTest.php | 53 +++++++++ 5 files changed, 269 insertions(+) create mode 100644 src/Event/Subscriber/PasswordChangeSubscriber.php create mode 100644 src/Service/UserModelService.php create mode 100644 tests/Integration/Service/UserModelServiceTest.php create mode 100644 tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php diff --git a/services.yaml b/services.yaml index 2c453716..1944a3ea 100644 --- a/services.yaml +++ b/services.yaml @@ -80,6 +80,9 @@ services: OxidEsales\GraphQL\Base\Service\FingerprintServiceInterface: class: OxidEsales\GraphQL\Base\Service\FingerprintService + OxidEsales\GraphQL\Base\Service\UserModelService: + class: OxidEsales\GraphQL\Base\Service\UserModelService + OxidEsales\GraphQL\Base\Controller\: resource: 'src/Controller/' public: true @@ -107,6 +110,10 @@ services: class: OxidEsales\GraphQL\Base\Event\Subscriber\UserDeleteSubscriber tags: [ 'kernel.event_subscriber' ] + OxidEsales\GraphQL\Base\Event\Subscriber\PasswordChangeSubscriber: + class: OxidEsales\GraphQL\Base\Event\Subscriber\PasswordChangeSubscriber + tags: [ 'kernel.event_subscriber' ] + OxidEsales\GraphQL\Base\Event\Subscriber\BeforeTokenCreationSubscriber: tags: [ 'kernel.event_subscriber' ] diff --git a/src/Event/Subscriber/PasswordChangeSubscriber.php b/src/Event/Subscriber/PasswordChangeSubscriber.php new file mode 100644 index 00000000..914d748b --- /dev/null +++ b/src/Event/Subscriber/PasswordChangeSubscriber.php @@ -0,0 +1,104 @@ +getModel(); + + if (!$model instanceof User || !$model->getId()) { + return $event; + } + + $newPassword = $model->getFieldData('oxpassword'); + if (!$this->userModelService->isPasswordChanged($model->getId(), $newPassword)) { + return $event; + } + + $this->userIdWithChangedPwd = $model->getId(); + + return $event; + } + + /** + * Handle ApplicationModelChangedEvent. + * + * @param Event $event Event to be handled + */ + public function handleAfterUpdate(Event $event): Event + { + /** @phpstan-ignore-next-line method.notFound */ + $model = $event->getModel(); + + if (!$model instanceof User) { + return $event; + } + + if ($model->getId() !== $this->userIdWithChangedPwd || !$this->userIdWithChangedPwd) { + return $event; + } + + //todo: delete tokens here + + return $event; + } + + /** + * Returns an array of event names this subscriber wants to listen to. + * + * The array keys are event names and the value can be: + * + * * The method name to call (priority defaults to 0) + * * An array composed of the method name to call and the priority + * * An array of arrays composed of the method names to call and respective + * priorities, or 0 if unset + * + * For instance: + * + * * array('eventName' => 'methodName') + * * array('eventName' => array('methodName', $priority)) + * * array('eventName' => array(array('methodName1', $priority), array('methodName2'))) + * + * @return array + */ + public static function getSubscribedEvents(): array + { + return [ + BeforeModelUpdateEvent::class => 'handleBeforeUpdate', + AfterModelUpdateEvent::class => 'handleAfterUpdate' + ]; + } +} diff --git a/src/Service/UserModelService.php b/src/Service/UserModelService.php new file mode 100644 index 00000000..bc92faad --- /dev/null +++ b/src/Service/UserModelService.php @@ -0,0 +1,34 @@ +legacyInfrastructure->getUserModel($userId); + $currentPassword = $userModel->getFieldData('oxpassword'); + if (!$passwordNew || !$currentPassword) { + return false; + } + + return $currentPassword !== $passwordNew; + } +} diff --git a/tests/Integration/Service/UserModelServiceTest.php b/tests/Integration/Service/UserModelServiceTest.php new file mode 100644 index 00000000..e5e9c215 --- /dev/null +++ b/tests/Integration/Service/UserModelServiceTest.php @@ -0,0 +1,71 @@ +getUserModelStub($userId, $password); + + $sut = new UserModelService( + legacyInfrastructure: $this->getLegacyMock($user), + ); + + $this->assertTrue($sut->isPasswordChanged($userId, 'mynewpwd')); + } + + public function testIsPasswordChangedOnSamePassword(): void + { + $userId = uniqid(); + $password = uniqid(); + + $user = $this->getUserModelStub($userId, $password); + + $sut = new UserModelService( + legacyInfrastructure: $this->getLegacyMock($user), + ); + + $this->assertFalse($sut->isPasswordChanged($userId, $password)); + } + + protected function getUserModelStub(string $id, string $password): UserModel + { + $userModel = oxNew(UserModel::class); + $userModel->assign([ + 'oxid' => $id, + 'oxpassword' => $password, + ]); + + return $userModel; + } + + private function getLegacyMock(UserModel $user): Legacy + { + $legacyInfrastructureMock = $this->getMockBuilder(Legacy::class) + ->disableOriginalConstructor() + ->getMock(); + + $legacyInfrastructureMock->method('getUserModel') + ->willReturn($user); + + return $legacyInfrastructureMock; + } +} diff --git a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php new file mode 100644 index 00000000..20a5384a --- /dev/null +++ b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php @@ -0,0 +1,53 @@ +getSut(); + $configuration = $sut->getSubscribedEvents(); + + $this->assertTrue(array_key_exists(BeforeModelUpdateEvent::class, $configuration)); + $this->assertTrue(array_key_exists(AfterModelUpdateEvent::class, $configuration)); + $this->assertTrue($configuration[BeforeModelUpdateEvent::class] === 'handleBeforeUpdate'); + $this->assertTrue($configuration[AfterModelUpdateEvent::class] === 'handleAfterUpdate'); + } + + public function testHandleBeforeUpdateReturnsOriginalEvent(): void + { + $sut = $this->getSut(); + + $eventStub = $this->createStub(BeforeModelUpdateEvent::class); + $this->assertSame($eventStub, $sut->handleBeforeUpdate($eventStub)); + } + + public function testHandleAfterUpdateReturnsOriginalEvent(): void + { + $sut = $this->getSut(); + + $eventStub = $this->createStub(AfterModelUpdateEvent::class); + $this->assertSame($eventStub, $sut->handleAfterUpdate($eventStub)); + } + + public function getSut(UserModelService $userModelService = null): PasswordChangeSubscriber + { + return new PasswordChangeSubscriber( + userModelService: $userModelService ?? $this->createStub(UserModelService::class) + ); + } +} From 6f8fcc416a254941bb06470abf82e4c13fb8a041 Mon Sep 17 00:00:00 2001 From: Angel Dimitrov Date: Tue, 10 Sep 2024 16:23:05 +0300 Subject: [PATCH 02/19] OXDEV-8688 Rename token cleanup method --- src/Event/Subscriber/UserDeleteSubscriber.php | 2 +- src/Infrastructure/Token.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Event/Subscriber/UserDeleteSubscriber.php b/src/Event/Subscriber/UserDeleteSubscriber.php index bd35f916..5ce49507 100644 --- a/src/Event/Subscriber/UserDeleteSubscriber.php +++ b/src/Event/Subscriber/UserDeleteSubscriber.php @@ -35,7 +35,7 @@ public function handle(Event $event): Event return $event; } - $this->tokenInfrastructure->cleanUpTokens(); + $this->tokenInfrastructure->deleteOrphanedTokens(); return $event; } diff --git a/src/Infrastructure/Token.php b/src/Infrastructure/Token.php index 1a317cce..10032b19 100644 --- a/src/Infrastructure/Token.php +++ b/src/Infrastructure/Token.php @@ -62,7 +62,7 @@ public function removeExpiredTokens(UserInterface $user): void $queryBuilder->execute(); } - public function cleanUpTokens(): void + public function deleteOrphanedTokens(): void { /** @var \Doctrine\DBAL\Driver\Statement $execute */ $execute = $this->queryBuilderFactory->create() From 1c3a2236698e0cd1a5eee3743e6517eb5600724f Mon Sep 17 00:00:00 2001 From: Angel Dimitrov Date: Wed, 11 Sep 2024 23:22:25 +0300 Subject: [PATCH 03/19] OXDEV-8688 Add token invalidation --- src/Event/Subscriber/PasswordChangeSubscriber.php | 9 ++++++--- src/Infrastructure/RefreshTokenRepository.php | 15 +++++++++++++++ .../RefreshTokenRepositoryInterface.php | 2 ++ src/Infrastructure/Token.php | 15 +++++++++++++++ src/Service/Token.php | 1 - 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/Event/Subscriber/PasswordChangeSubscriber.php b/src/Event/Subscriber/PasswordChangeSubscriber.php index 914d748b..697fb87e 100644 --- a/src/Event/Subscriber/PasswordChangeSubscriber.php +++ b/src/Event/Subscriber/PasswordChangeSubscriber.php @@ -12,6 +12,7 @@ use OxidEsales\Eshop\Application\Model\User; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\AfterModelUpdateEvent; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\BeforeModelUpdateEvent; +use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository; use OxidEsales\GraphQL\Base\Service\UserModelService; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Contracts\EventDispatcher\Event; @@ -25,8 +26,10 @@ class PasswordChangeSubscriber implements EventSubscriberInterface */ protected ?string $userIdWithChangedPwd = null; - public function __construct(private readonly UserModelService $userModelService) - { + public function __construct( + private readonly UserModelService $userModelService, + private readonly RefreshTokenRepository $refreshTokenRepository + ) { } /** @@ -71,7 +74,7 @@ public function handleAfterUpdate(Event $event): Event return $event; } - //todo: delete tokens here + $this->refreshTokenRepository->invalidateUserTokens($model->getId()); return $event; } diff --git a/src/Infrastructure/RefreshTokenRepository.php b/src/Infrastructure/RefreshTokenRepository.php index e7051a39..3107a535 100644 --- a/src/Infrastructure/RefreshTokenRepository.php +++ b/src/Infrastructure/RefreshTokenRepository.php @@ -87,4 +87,19 @@ public function getTokenUser(string $refreshToken): UserInterface return new UserDataType($userModel, $isAnonymous); } + + public function invalidateUserTokens(string $userId): int + { + $queryBuilder = $this->queryBuilderFactory->create() + ->update('oegraphqlrefreshtoken') + ->where('OXUSERID = :userId') + ->set('EXPIRES_AT', 'NOW()') + ->setParameters([ + 'userId' => (string)$user->id(), + ]); + + $result = $queryBuilder->execute(); + + return is_object($result) ? (int)$result->rowCount() : (int)$result; + } } diff --git a/src/Infrastructure/RefreshTokenRepositoryInterface.php b/src/Infrastructure/RefreshTokenRepositoryInterface.php index d9aa83fb..b95505c3 100644 --- a/src/Infrastructure/RefreshTokenRepositoryInterface.php +++ b/src/Infrastructure/RefreshTokenRepositoryInterface.php @@ -24,4 +24,6 @@ public function removeExpiredTokens(): void; * @throws InvalidRefreshToken */ public function getTokenUser(string $refreshToken): UserInterface; + + public function invalidateUserTokens(string $user): int; } diff --git a/src/Infrastructure/Token.php b/src/Infrastructure/Token.php index 10032b19..426ba472 100644 --- a/src/Infrastructure/Token.php +++ b/src/Infrastructure/Token.php @@ -155,4 +155,19 @@ public function userHasToken(UserInterface $user, string $tokenId): bool return false; } + + public function invalidateUserTokens(UserInterface $user): int + { + $queryBuilder = $this->queryBuilderFactory->create() + ->update('oegraphqltoken') + ->where('OXUSERID = :userId') + ->set('EXPIRES_AT', 'NOW()') + ->setParameters([ + 'userId' => (string)$user->id(), + ]); + + $result = $queryBuilder->execute(); + + return is_object($result) ? $result->columnCount() : (int)$result; + } } diff --git a/src/Service/Token.php b/src/Service/Token.php index 82f5ff76..87781e81 100644 --- a/src/Service/Token.php +++ b/src/Service/Token.php @@ -11,7 +11,6 @@ use DateTimeImmutable; use Lcobucci\JWT\UnencryptedToken; -use OxidEsales\GraphQL\Base\DataType\User as UserDataType; use OxidEsales\GraphQL\Base\DataType\UserInterface; use OxidEsales\GraphQL\Base\Event\BeforeTokenCreation; use OxidEsales\GraphQL\Base\Exception\InvalidLogin; From 9ec3ae0a6112bb675c915b357bd5fc8ed5401be8 Mon Sep 17 00:00:00 2001 From: Tatyana Koleva Date: Tue, 17 Sep 2024 13:10:40 +0300 Subject: [PATCH 04/19] OXDEV-8689 Return Login data type in service --- src/Controller/Login.php | 9 +------ src/Service/LoginService.php | 13 +++++++--- src/Service/LoginServiceInterface.php | 4 +-- tests/Unit/Controller/LoginTest.php | 33 +++++++++---------------- tests/Unit/Service/LoginServiceTest.php | 22 +++++++++++++++-- 5 files changed, 44 insertions(+), 37 deletions(-) diff --git a/src/Controller/Login.php b/src/Controller/Login.php index 9c32a296..b6b716e0 100644 --- a/src/Controller/Login.php +++ b/src/Controller/Login.php @@ -11,7 +11,6 @@ use OxidEsales\GraphQL\Base\DataType\Login as LoginDatatype; use OxidEsales\GraphQL\Base\Service\LoginServiceInterface; -use OxidEsales\GraphQL\Base\Service\RefreshTokenServiceInterface; use OxidEsales\GraphQL\Base\Service\Token; use TheCodingMachine\GraphQLite\Annotations\Query; @@ -20,7 +19,6 @@ class Login public function __construct( protected Token $tokenService, protected LoginServiceInterface $loginService, - protected RefreshTokenServiceInterface $refreshTokenService, ) { } @@ -46,11 +44,6 @@ public function token(?string $username = null, ?string $password = null): strin */ public function login(?string $username = null, ?string $password = null): LoginDatatype { - $user = $this->loginService->login($username, $password); - - return new LoginDatatype( - refreshToken: $this->refreshTokenService->createRefreshTokenForUser($user), - accessToken: $this->tokenService->createTokenForUser($user), - ); + return $this->loginService->login($username, $password); } } diff --git a/src/Service/LoginService.php b/src/Service/LoginService.php index 27790479..e4c61780 100644 --- a/src/Service/LoginService.php +++ b/src/Service/LoginService.php @@ -9,7 +9,7 @@ namespace OxidEsales\GraphQL\Base\Service; -use OxidEsales\GraphQL\Base\DataType\UserInterface; +use OxidEsales\GraphQL\Base\DataType\Login as LoginDatatype; use OxidEsales\GraphQL\Base\Infrastructure\Legacy; /** @@ -19,11 +19,18 @@ class LoginService implements LoginServiceInterface { public function __construct( private readonly Legacy $legacyInfrastructure, + protected Token $tokenService, + protected RefreshTokenServiceInterface $refreshTokenService, ) { } - public function login(?string $userName, ?string $password): UserInterface + public function login(?string $userName, ?string $password): LoginDatatype { - return $this->legacyInfrastructure->login($userName, $password); + $user = $this->legacyInfrastructure->login($userName, $password); + + return new LoginDatatype( + refreshToken: $this->refreshTokenService->createRefreshTokenForUser($user), + accessToken: $this->tokenService->createTokenForUser($user), + ); } } diff --git a/src/Service/LoginServiceInterface.php b/src/Service/LoginServiceInterface.php index eeba3672..42bf83d2 100644 --- a/src/Service/LoginServiceInterface.php +++ b/src/Service/LoginServiceInterface.php @@ -9,12 +9,12 @@ namespace OxidEsales\GraphQL\Base\Service; -use OxidEsales\GraphQL\Base\DataType\UserInterface; +use OxidEsales\GraphQL\Base\DataType\Login as LoginDatatype; /** * User login service */ interface LoginServiceInterface { - public function login(?string $userName, ?string $password): UserInterface; + public function login(?string $userName, ?string $password): LoginDatatype; } diff --git a/tests/Unit/Controller/LoginTest.php b/tests/Unit/Controller/LoginTest.php index a6aceb82..4181f972 100644 --- a/tests/Unit/Controller/LoginTest.php +++ b/tests/Unit/Controller/LoginTest.php @@ -12,13 +12,12 @@ use Lcobucci\JWT\UnencryptedToken; use OxidEsales\Eshop\Application\Model\User as UserModel; use OxidEsales\GraphQL\Base\Controller\Login; +use OxidEsales\GraphQL\Base\DataType\Login as LoginDatatype; use OxidEsales\GraphQL\Base\DataType\User; -use OxidEsales\GraphQL\Base\DataType\UserInterface; use OxidEsales\GraphQL\Base\Infrastructure\Legacy; use OxidEsales\GraphQL\Base\Infrastructure\Token as TokenInfrastructure; use OxidEsales\GraphQL\Base\Service\JwtConfigurationBuilder; use OxidEsales\GraphQL\Base\Service\LoginServiceInterface; -use OxidEsales\GraphQL\Base\Service\RefreshTokenServiceInterface; use OxidEsales\GraphQL\Base\Service\Token as TokenService; use OxidEsales\GraphQL\Base\Tests\Unit\BaseTestCase; use PHPUnit\Framework\MockObject\MockObject; @@ -60,7 +59,6 @@ public function setUp(): void new EventDispatcher(), $this->getModuleConfigurationMock(), $this->tokenInfrastructure, - $this->getRefreshRepositoryMock() ); } @@ -79,7 +77,6 @@ public function testCreateTokenWithValidCredentials(): void $loginController = new Login( $this->tokenService, $this->getLoginService($this->legacy), - $this->createStub(RefreshTokenServiceInterface::class), ); $jwt = $loginController->token($username, $password); @@ -103,7 +100,6 @@ public function testCreateTokenWithMissingPassword(): void $loginController = new Login( $this->tokenService, $this->getLoginService($this->legacy), - $this->createStub(RefreshTokenServiceInterface::class), ); $jwt = $loginController->token('none'); @@ -132,7 +128,6 @@ public function testCreateTokenWithMissingUsername(): void $loginController = new Login( $this->tokenService, $this->getLoginService($this->legacy), - $this->createStub(RefreshTokenServiceInterface::class), ); $jwt = $loginController->token(null, 'none'); @@ -161,7 +156,6 @@ public function testCreateAnonymousToken(): void $loginController = new Login( $this->tokenService, $this->getLoginService($this->legacy), - $this->createStub(RefreshTokenServiceInterface::class), ); $jwt = $loginController->token(); @@ -174,31 +168,26 @@ public function testCreateAnonymousToken(): void $this->assertNotEmpty($token->claims()->get(TokenService::CLAIM_USERID)); } - public function testLoginCreatesLoginInputTypeResult(): void + public function testLoginReturnsLoginDataType(): void { $loginController = new Login( - tokenService: $tokenServiceMock = $this->createMock(TokenService::class), + tokenService: $this->createMock(TokenService::class), loginService: $loginServiceMock = $this->createMock(LoginServiceInterface::class), - refreshTokenService: $refreshTokenMock = $this->createMock(RefreshTokenServiceInterface::class), ); $userName = uniqid(); $password = uniqid(); - $userStub = $this->createStub(UserInterface::class); - - $loginServiceMock->method('login')->with($userName, $password)->willReturn($userStub); - $refreshToken = uniqid(); - $refreshTokenMock->method('createRefreshTokenForUser')->with($userStub)->willReturn($refreshToken); - - $accessTokenStub = $this->createConfiguredStub(UnencryptedToken::class, [ - 'toString' => $accessToken = uniqid() + $accessToken = $this->createConfiguredStub(UnencryptedToken::class, [ + 'toString' => uniqid() ]); - $tokenServiceMock->method('createTokenForUser')->with($userStub)->willReturn($accessTokenStub); - $result = $loginController->login($userName, $password); + $loginDataType = new LoginDatatype( + refreshToken: $refreshToken, + accessToken: $accessToken + ); - $this->assertSame($refreshToken, $result->refreshToken()); - $this->assertSame($accessToken, $result->accessToken()); + $loginServiceMock->method('login')->with($userName, $password)->willReturn($loginDataType); + $this->assertSame($loginDataType, $loginController->login($userName, $password)); } } diff --git a/tests/Unit/Service/LoginServiceTest.php b/tests/Unit/Service/LoginServiceTest.php index 9cc34e68..72af9f9b 100644 --- a/tests/Unit/Service/LoginServiceTest.php +++ b/tests/Unit/Service/LoginServiceTest.php @@ -9,19 +9,26 @@ namespace OxidEsales\GraphQL\Base\Tests\Unit\Service; +use Lcobucci\JWT\UnencryptedToken; +use OxidEsales\GraphQL\Base\DataType\Login; +use OxidEsales\GraphQL\Base\DataType\LoginInterface; use OxidEsales\GraphQL\Base\DataType\UserInterface; use OxidEsales\GraphQL\Base\Infrastructure\Legacy; use OxidEsales\GraphQL\Base\Service\LoginService; +use OxidEsales\GraphQL\Base\Service\RefreshTokenServiceInterface; +use OxidEsales\GraphQL\Base\Service\Token as TokenService; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; #[CoversClass(LoginService::class)] class LoginServiceTest extends TestCase { - public function testLoginReturnsUserDataType(): void + public function testLoginCreatesLoginInputTypeResult(): void { $sut = new LoginService( legacyInfrastructure: $legacyInfrastructureMock = $this->createMock(Legacy::class), + tokenService: $tokenServiceMock = $this->createMock(TokenService::class), + refreshTokenService: $refreshTokenMock = $this->createMock(RefreshTokenServiceInterface::class), ); $userName = uniqid(); @@ -31,6 +38,17 @@ public function testLoginReturnsUserDataType(): void ->with($userName, $password) ->willReturn($userType = $this->createStub(UserInterface::class)); - $this->assertSame($userType, $sut->login($userName, $password)); + $refreshToken = uniqid(); + $refreshTokenMock->method('createRefreshTokenForUser')->with($userType)->willReturn($refreshToken); + + $accessTokenStub = $this->createConfiguredStub(UnencryptedToken::class, [ + 'toString' => $accessToken = uniqid() + ]); + $tokenServiceMock->method('createTokenForUser')->with($userType)->willReturn($accessTokenStub); + + $result = $sut->login($userName, $password); + + $this->assertSame($refreshToken, $result->refreshToken()); + $this->assertSame($accessToken, $result->accessToken()); } } From 63db77d780e655daf5aa2a23329599d277dc71b9 Mon Sep 17 00:00:00 2001 From: Angel Dimitrov Date: Tue, 17 Sep 2024 10:35:49 +0300 Subject: [PATCH 05/19] OXDEV-8688 Fix unit tests --- src/Infrastructure/RefreshTokenRepository.php | 2 +- .../Event/Subscriber/PasswordChangeSubscriberTest.php | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Infrastructure/RefreshTokenRepository.php b/src/Infrastructure/RefreshTokenRepository.php index 3107a535..1495ec0f 100644 --- a/src/Infrastructure/RefreshTokenRepository.php +++ b/src/Infrastructure/RefreshTokenRepository.php @@ -95,7 +95,7 @@ public function invalidateUserTokens(string $userId): int ->where('OXUSERID = :userId') ->set('EXPIRES_AT', 'NOW()') ->setParameters([ - 'userId' => (string)$user->id(), + 'userId' => $userId, ]); $result = $queryBuilder->execute(); diff --git a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php index 20a5384a..4a371d1d 100644 --- a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php +++ b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php @@ -12,6 +12,7 @@ use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\AfterModelUpdateEvent; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\BeforeModelUpdateEvent; use OxidEsales\GraphQL\Base\Event\Subscriber\PasswordChangeSubscriber; +use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository; use OxidEsales\GraphQL\Base\Service\UserModelService; use OxidEsales\GraphQL\Base\Tests\Unit\BaseTestCase; @@ -44,10 +45,13 @@ public function testHandleAfterUpdateReturnsOriginalEvent(): void $this->assertSame($eventStub, $sut->handleAfterUpdate($eventStub)); } - public function getSut(UserModelService $userModelService = null): PasswordChangeSubscriber - { + public function getSut( + UserModelService $userModelService = null, + RefreshTokenRepository $refreshTokenRepository = null + ): PasswordChangeSubscriber { return new PasswordChangeSubscriber( - userModelService: $userModelService ?? $this->createStub(UserModelService::class) + userModelService: $userModelService ?? $this->createStub(UserModelService::class), + refreshTokenRepository: $refreshTokenRepository ?? $this->createStub(RefreshTokenRepository::class) ); } } From e5ed79504464b9bc10eb770a58105f8256b14a2f Mon Sep 17 00:00:00 2001 From: Angel Dimitrov Date: Wed, 18 Sep 2024 10:01:28 +0300 Subject: [PATCH 06/19] OXDEV-8688 Fix services configuration --- services.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services.yaml b/services.yaml index 1944a3ea..1354ed9a 100644 --- a/services.yaml +++ b/services.yaml @@ -63,6 +63,9 @@ services: OxidEsales\GraphQL\Base\Infrastructure\Token: class: OxidEsales\GraphQL\Base\Infrastructure\Token + OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository: + class: OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository + OxidEsales\GraphQL\Base\Infrastructure\Repository: class: OxidEsales\GraphQL\Base\Infrastructure\Repository From 3f7a440468f4908a32c166daf18671417c7aa719 Mon Sep 17 00:00:00 2001 From: Tatyana Koleva Date: Thu, 19 Sep 2024 17:53:09 +0300 Subject: [PATCH 07/19] OXDEV-8688 Add test for change event result --- .../Subscriber/PasswordChangeSubscriber.php | 5 ++- src/Infrastructure/Token.php | 4 +- .../Integration/Infrastructure/TokenTest.php | 44 +++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/Event/Subscriber/PasswordChangeSubscriber.php b/src/Event/Subscriber/PasswordChangeSubscriber.php index 697fb87e..444c49ba 100644 --- a/src/Event/Subscriber/PasswordChangeSubscriber.php +++ b/src/Event/Subscriber/PasswordChangeSubscriber.php @@ -13,6 +13,7 @@ use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\AfterModelUpdateEvent; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\BeforeModelUpdateEvent; use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository; +use OxidEsales\GraphQL\Base\Infrastructure\Token; use OxidEsales\GraphQL\Base\Service\UserModelService; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Contracts\EventDispatcher\Event; @@ -28,7 +29,8 @@ class PasswordChangeSubscriber implements EventSubscriberInterface public function __construct( private readonly UserModelService $userModelService, - private readonly RefreshTokenRepository $refreshTokenRepository + private readonly RefreshTokenRepository $refreshTokenRepository, + private readonly Token $tokenInfrastructure ) { } @@ -75,6 +77,7 @@ public function handleAfterUpdate(Event $event): Event } $this->refreshTokenRepository->invalidateUserTokens($model->getId()); + $this->tokenInfrastructure->invalidateUserTokens($model->getId()); return $event; } diff --git a/src/Infrastructure/Token.php b/src/Infrastructure/Token.php index 426ba472..89afe097 100644 --- a/src/Infrastructure/Token.php +++ b/src/Infrastructure/Token.php @@ -156,14 +156,14 @@ public function userHasToken(UserInterface $user, string $tokenId): bool return false; } - public function invalidateUserTokens(UserInterface $user): int + public function invalidateUserTokens(string $userId): int { $queryBuilder = $this->queryBuilderFactory->create() ->update('oegraphqltoken') ->where('OXUSERID = :userId') ->set('EXPIRES_AT', 'NOW()') ->setParameters([ - 'userId' => (string)$user->id(), + 'userId' => $userId, ]); $result = $queryBuilder->execute(); diff --git a/tests/Integration/Infrastructure/TokenTest.php b/tests/Integration/Infrastructure/TokenTest.php index 23f4b2ba..bd0c6016 100644 --- a/tests/Integration/Infrastructure/TokenTest.php +++ b/tests/Integration/Infrastructure/TokenTest.php @@ -13,6 +13,7 @@ use Lcobucci\JWT\Token\DataSet; use Lcobucci\JWT\UnencryptedToken; use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\EshopCommunity\Internal\Framework\Database\ConnectionProviderInterface; use OxidEsales\EshopCommunity\Tests\Integration\IntegrationTestCase; use OxidEsales\EshopCommunity\Tests\TestContainerFactory; use OxidEsales\GraphQL\Base\DataType\Token as TokenDataType; @@ -30,6 +31,9 @@ class TokenTest extends IntegrationTestCase /** @var TokenInfrastructure */ private $tokenInfrastructure; + /** @var ConnectionProviderInterface */ + private $connection; + public function setUp(): void { parent::setUp(); @@ -37,6 +41,7 @@ public function setUp(): void $container = $containerFactory->create(); $container->compile(); $this->tokenInfrastructure = $container->get(TokenInfrastructure::class); + $this->connection = $container->get(ConnectionProviderInterface::class)->get(); } public function testRegisterToken(): void @@ -306,6 +311,45 @@ public function testInvalidateTokenAfterDeleteUser(): void $this->assertFalse($this->tokenInfrastructure->isTokenRegistered('_deletedUser')); } + public function testExpireTokenAfterUserPasswordChange(): void + { + $userModel = oxNew(User::class); + $userModel->load('e7af1c3b786fd02906ccd75698f4e6b9'); + + $issued = new DateTimeImmutable('now'); + $expires = new DateTimeImmutable('+8 hours'); + $tokenModel = oxNew(TokenModel::class); + $tokenModel->setId('_changePwdUserToken'); + $tokenModel->assign( + [ + 'OXID' => '_changePwdUserToken', + 'OXSHOPID' => '1', + 'OXUSERID' => 'e7af1c3b786fd02906ccd75698f4e6b9', + 'ISSUED_AT' => $issued->format('Y-m-d H:i:s'), + 'EXPIRES_AT' => $expires->format('Y-m-d H:i:s'), + 'USERAGENT' => '', + 'TOKEN' => 'very_large_string', + ] + ); + $tokenModel->save(); + $tokenModel->load('_changePwdUserToken'); + + $user = new UserDataType($userModel); + $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_changePwdUserToken')); + $this->assertFalse(new DateTimeImmutable($tokenModel->getRawFieldData('expires_at')) <= new DateTimeImmutable('now')); + + $userModel->setPassword('_newPassword'); + $userModel->save(); + + $result = $this->connection->executeQuery( + "select expires_at from `oegraphqltoken` where oxid=:tokenId", + ['tokenId' => '_changePwdUserToken'] + ); + $tokenDateAfterChange = $result->fetchOne(); + + $this->assertTrue(new DateTimeImmutable($tokenDateAfterChange) <= new DateTimeImmutable('now')); + } + private function getTokenMock( string $tokenId = self::TEST_TOKEN_ID, string $userId = self::TEST_USER_ID From ed31525601931069e6f162747a6dea248ffa090c Mon Sep 17 00:00:00 2001 From: Angel Dimitrov Date: Fri, 20 Sep 2024 15:06:42 +0300 Subject: [PATCH 08/19] OXDEV-8688 Separate event tests --- .../Integration/Event/PasswordChangeTest.php | 79 ++++++++++++++++++ tests/Integration/Event/UserDeleteTest.php | 81 +++++++++++++++++++ .../Integration/Infrastructure/TokenTest.php | 60 -------------- .../PasswordChangeSubscriberTest.php | 4 +- 4 files changed, 163 insertions(+), 61 deletions(-) create mode 100644 tests/Integration/Event/PasswordChangeTest.php create mode 100644 tests/Integration/Event/UserDeleteTest.php diff --git a/tests/Integration/Event/PasswordChangeTest.php b/tests/Integration/Event/PasswordChangeTest.php new file mode 100644 index 00000000..dc7b40c0 --- /dev/null +++ b/tests/Integration/Event/PasswordChangeTest.php @@ -0,0 +1,79 @@ +create(); + $container->compile(); + $this->tokenInfrastructure = $container->get(TokenInfrastructure::class); + $this->connection = $container->get(ConnectionProviderInterface::class)->get(); + } + + public function testExpireTokenAfterUserPasswordChange(): void + { + $userModel = oxNew(User::class); + $userModel->load('e7af1c3b786fd02906ccd75698f4e6b9'); + + $issued = new DateTimeImmutable('now'); + $expires = new DateTimeImmutable('+8 hours'); + $tokenModel = oxNew(TokenModel::class); + $tokenModel->setId('_changePwdUserToken'); + $tokenModel->assign( + [ + 'OXID' => '_changePwdUserToken', + 'OXSHOPID' => '1', + 'OXUSERID' => 'e7af1c3b786fd02906ccd75698f4e6b9', + 'ISSUED_AT' => $issued->format('Y-m-d H:i:s'), + 'EXPIRES_AT' => $expires->format('Y-m-d H:i:s'), + 'USERAGENT' => '', + 'TOKEN' => 'very_large_string', + ] + ); + $tokenModel->save(); + $tokenModel->load('_changePwdUserToken'); + + $expiresAtAfterChange = new DateTimeImmutable($tokenModel->getRawFieldData('expires_at')); + $user = new UserDataType($userModel); + + $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_changePwdUserToken')); + $this->assertFalse($expiresAtAfterChange <= new DateTimeImmutable('now')); + + $userModel->setPassword('_newPassword'); + $userModel->save(); + + $result = $this->connection->executeQuery( + "select expires_at from `oegraphqltoken` where oxid=:tokenId", + ['tokenId' => '_changePwdUserToken'] + ); + $tokenDateAfterChange = $result->fetchOne(); + + $this->assertTrue(new DateTimeImmutable($tokenDateAfterChange) <= new DateTimeImmutable('now')); + } +} diff --git a/tests/Integration/Event/UserDeleteTest.php b/tests/Integration/Event/UserDeleteTest.php new file mode 100644 index 00000000..db522dbc --- /dev/null +++ b/tests/Integration/Event/UserDeleteTest.php @@ -0,0 +1,81 @@ +create(); + $container->compile(); + $this->tokenInfrastructure = $container->get(TokenInfrastructure::class); + } + + public function testInvalidateTokenAfterDeleteUser(): void + { + $userModel = oxNew(User::class); + $userModel->setId('_testUser'); + $userModel->setPassword('_testPassword'); + $userModel->assign(['oxusername' => '_testUsername']); + $userModel->save(); + + $this->tokenInfrastructure->registerToken( + $this->getTokenMock('_deletedUser'), + new DateTimeImmutable('now'), + new DateTimeImmutable('+8 hours') + ); + + $user = new UserDataType($userModel); + $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_deletedUser')); + + $userModel->delete(self::TEST_USER_ID); + $this->assertFalse($this->tokenInfrastructure->isTokenRegistered('_deletedUser')); + } + + private function getTokenMock( + string $tokenId = self::TEST_TOKEN_ID, + string $userId = self::TEST_USER_ID + ): UnencryptedToken { + $claims = new DataSet( + [ + TokenService::CLAIM_TOKENID => $tokenId, + TokenService::CLAIM_SHOPID => 1, + TokenService::CLAIM_USERID => $userId, + ], + '' + ); + + $token = $this->getMockBuilder(UnencryptedToken::class) + ->getMock(); + $token->method('claims')->willReturn($claims); + $token->method('toString')->willReturn('here_is_the_string_token'); + + return $token; + } +} diff --git a/tests/Integration/Infrastructure/TokenTest.php b/tests/Integration/Infrastructure/TokenTest.php index bd0c6016..c34a0404 100644 --- a/tests/Integration/Infrastructure/TokenTest.php +++ b/tests/Integration/Infrastructure/TokenTest.php @@ -290,66 +290,6 @@ public function testUserHasToken(): void $this->assertFalse($this->tokenInfrastructure->userHasToken($otherUser, '_second')); } - public function testInvalidateTokenAfterDeleteUser(): void - { - $userModel = oxNew(User::class); - $userModel->setId('_testUser'); - $userModel->setPassword('_testPassword'); - $userModel->assign(['oxusername' => '_testUsername']); - $userModel->save(); - - $this->tokenInfrastructure->registerToken( - $this->getTokenMock('_deletedUser'), - new DateTimeImmutable('now'), - new DateTimeImmutable('+8 hours') - ); - - $user = new UserDataType($userModel); - $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_deletedUser')); - - $userModel->delete(self::TEST_USER_ID); - $this->assertFalse($this->tokenInfrastructure->isTokenRegistered('_deletedUser')); - } - - public function testExpireTokenAfterUserPasswordChange(): void - { - $userModel = oxNew(User::class); - $userModel->load('e7af1c3b786fd02906ccd75698f4e6b9'); - - $issued = new DateTimeImmutable('now'); - $expires = new DateTimeImmutable('+8 hours'); - $tokenModel = oxNew(TokenModel::class); - $tokenModel->setId('_changePwdUserToken'); - $tokenModel->assign( - [ - 'OXID' => '_changePwdUserToken', - 'OXSHOPID' => '1', - 'OXUSERID' => 'e7af1c3b786fd02906ccd75698f4e6b9', - 'ISSUED_AT' => $issued->format('Y-m-d H:i:s'), - 'EXPIRES_AT' => $expires->format('Y-m-d H:i:s'), - 'USERAGENT' => '', - 'TOKEN' => 'very_large_string', - ] - ); - $tokenModel->save(); - $tokenModel->load('_changePwdUserToken'); - - $user = new UserDataType($userModel); - $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_changePwdUserToken')); - $this->assertFalse(new DateTimeImmutable($tokenModel->getRawFieldData('expires_at')) <= new DateTimeImmutable('now')); - - $userModel->setPassword('_newPassword'); - $userModel->save(); - - $result = $this->connection->executeQuery( - "select expires_at from `oegraphqltoken` where oxid=:tokenId", - ['tokenId' => '_changePwdUserToken'] - ); - $tokenDateAfterChange = $result->fetchOne(); - - $this->assertTrue(new DateTimeImmutable($tokenDateAfterChange) <= new DateTimeImmutable('now')); - } - private function getTokenMock( string $tokenId = self::TEST_TOKEN_ID, string $userId = self::TEST_USER_ID diff --git a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php index 4a371d1d..fe934247 100644 --- a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php +++ b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php @@ -13,6 +13,7 @@ use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\BeforeModelUpdateEvent; use OxidEsales\GraphQL\Base\Event\Subscriber\PasswordChangeSubscriber; use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository; +use OxidEsales\GraphQL\Base\Infrastructure\Token; use OxidEsales\GraphQL\Base\Service\UserModelService; use OxidEsales\GraphQL\Base\Tests\Unit\BaseTestCase; @@ -51,7 +52,8 @@ public function getSut( ): PasswordChangeSubscriber { return new PasswordChangeSubscriber( userModelService: $userModelService ?? $this->createStub(UserModelService::class), - refreshTokenRepository: $refreshTokenRepository ?? $this->createStub(RefreshTokenRepository::class) + refreshTokenRepository: $refreshTokenRepository ?? $this->createStub(RefreshTokenRepository::class), + tokenInfrastructure: $this->createStub(Token::class) ); } } From 86d94defd4364fe39f2981231be2ebef7aebc958 Mon Sep 17 00:00:00 2001 From: Tatyana Koleva Date: Wed, 25 Sep 2024 20:37:25 +0300 Subject: [PATCH 09/19] OXDEV-8688 Fix password change tests --- tests/Integration/Event/PasswordChangeTest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/Integration/Event/PasswordChangeTest.php b/tests/Integration/Event/PasswordChangeTest.php index dc7b40c0..e56517ec 100644 --- a/tests/Integration/Event/PasswordChangeTest.php +++ b/tests/Integration/Event/PasswordChangeTest.php @@ -10,6 +10,8 @@ namespace OxidEsales\GraphQL\Base\Tests\Integration\Event; use DateTimeImmutable; +use Lcobucci\JWT\Token\DataSet; +use Lcobucci\JWT\UnencryptedToken; use OxidEsales\Eshop\Application\Model\User; use OxidEsales\EshopCommunity\Internal\Framework\Database\ConnectionProviderInterface; use OxidEsales\EshopCommunity\Tests\Integration\IntegrationTestCase; @@ -17,6 +19,7 @@ use OxidEsales\GraphQL\Base\DataType\User as UserDataType; use OxidEsales\GraphQL\Base\Infrastructure\Model\Token as TokenModel; use OxidEsales\GraphQL\Base\Infrastructure\Token as TokenInfrastructure; +use OxidEsales\GraphQL\Base\Service\Token as TokenService; class PasswordChangeTest extends IntegrationTestCase { @@ -39,7 +42,10 @@ public function setUp(): void public function testExpireTokenAfterUserPasswordChange(): void { $userModel = oxNew(User::class); - $userModel->load('e7af1c3b786fd02906ccd75698f4e6b9'); + $userModel->setId('_testUser'); + $userModel->setPassword('_testPassword'); + $userModel->assign(['oxusername' => '_testUsername']); + $userModel->save(); $issued = new DateTimeImmutable('now'); $expires = new DateTimeImmutable('+8 hours'); @@ -49,7 +55,7 @@ public function testExpireTokenAfterUserPasswordChange(): void [ 'OXID' => '_changePwdUserToken', 'OXSHOPID' => '1', - 'OXUSERID' => 'e7af1c3b786fd02906ccd75698f4e6b9', + 'OXUSERID' => '_testUser', 'ISSUED_AT' => $issued->format('Y-m-d H:i:s'), 'EXPIRES_AT' => $expires->format('Y-m-d H:i:s'), 'USERAGENT' => '', From 7ebeaeb16c275e9906d20c9b325e5efbc623b7ca Mon Sep 17 00:00:00 2001 From: Tatyana Koleva Date: Thu, 26 Sep 2024 15:31:54 +0300 Subject: [PATCH 10/19] OXDEV-8688 Add test for change user data --- .../Integration/Event/PasswordChangeTest.php | 71 ++++++++++++++----- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/tests/Integration/Event/PasswordChangeTest.php b/tests/Integration/Event/PasswordChangeTest.php index e56517ec..a4703399 100644 --- a/tests/Integration/Event/PasswordChangeTest.php +++ b/tests/Integration/Event/PasswordChangeTest.php @@ -20,6 +20,7 @@ use OxidEsales\GraphQL\Base\Infrastructure\Model\Token as TokenModel; use OxidEsales\GraphQL\Base\Infrastructure\Token as TokenInfrastructure; use OxidEsales\GraphQL\Base\Service\Token as TokenService; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; class PasswordChangeTest extends IntegrationTestCase { @@ -39,7 +40,55 @@ public function setUp(): void $this->connection = $container->get(ConnectionProviderInterface::class)->get(); } + #[RunInSeparateProcess] public function testExpireTokenAfterUserPasswordChange(): void + { + $userModel = $this->getUserModel(); + $tokenModel = $this->getTokenModel(); + + $expiresAtBeforeChange = new DateTimeImmutable($tokenModel->getRawFieldData('expires_at')); + $user = new UserDataType($userModel); + + $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_changePwdUserToken')); + $this->assertFalse($expiresAtBeforeChange <= new DateTimeImmutable('now')); + + $userModel->setPassword('_newPassword'); + $userModel->save(); + + $result = $this->connection->executeQuery( + "select expires_at from `oegraphqltoken` where oxid=:tokenId", + ['tokenId' => '_changePwdUserToken'] + ); + $expiresAtAfterChange = $result->fetchOne(); + + $this->assertTrue(new DateTimeImmutable($expiresAtAfterChange) <= new DateTimeImmutable('now')); + } + + #[RunInSeparateProcess] + public function testKeepTokenAfterUserChangeEventAndNoPwdChange(): void + { + $userModel = $this->getUserModel(); + $tokenModel = $this->getTokenModel(); + + $expiresAtBeforeChange = new DateTimeImmutable($tokenModel->getRawFieldData('expires_at')); + $user = new UserDataType($userModel); + + $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_changePwdUserToken')); + $this->assertFalse($expiresAtBeforeChange <= new DateTimeImmutable('now')); + + $userModel->assign(['oxfname' => 'Test']); + $userModel->save(); + + $result = $this->connection->executeQuery( + "select expires_at from `oegraphqltoken` where oxid=:tokenId", + ['tokenId' => '_changePwdUserToken'] + ); + $expiresAtAfterChange = $result->fetchOne(); + + $this->assertFalse(new DateTimeImmutable($expiresAtAfterChange) <= new DateTimeImmutable('now')); + } + + private function getUserModel(): User { $userModel = oxNew(User::class); $userModel->setId('_testUser'); @@ -47,6 +96,11 @@ public function testExpireTokenAfterUserPasswordChange(): void $userModel->assign(['oxusername' => '_testUsername']); $userModel->save(); + return $userModel; + } + + private function getTokenModel(): TokenModel + { $issued = new DateTimeImmutable('now'); $expires = new DateTimeImmutable('+8 hours'); $tokenModel = oxNew(TokenModel::class); @@ -65,21 +119,6 @@ public function testExpireTokenAfterUserPasswordChange(): void $tokenModel->save(); $tokenModel->load('_changePwdUserToken'); - $expiresAtAfterChange = new DateTimeImmutable($tokenModel->getRawFieldData('expires_at')); - $user = new UserDataType($userModel); - - $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_changePwdUserToken')); - $this->assertFalse($expiresAtAfterChange <= new DateTimeImmutable('now')); - - $userModel->setPassword('_newPassword'); - $userModel->save(); - - $result = $this->connection->executeQuery( - "select expires_at from `oegraphqltoken` where oxid=:tokenId", - ['tokenId' => '_changePwdUserToken'] - ); - $tokenDateAfterChange = $result->fetchOne(); - - $this->assertTrue(new DateTimeImmutable($tokenDateAfterChange) <= new DateTimeImmutable('now')); + return $tokenModel; } } From 2cd124e73634072bf2d4107aac293a9102e46b3a Mon Sep 17 00:00:00 2001 From: Angel Dimitrov Date: Wed, 2 Oct 2024 10:20:54 +0300 Subject: [PATCH 11/19] OXDEV-8710 Update changelog --- CHANGELOG-v10.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-v10.md b/CHANGELOG-v10.md index e792e4cd..18f86cf3 100644 --- a/CHANGELOG-v10.md +++ b/CHANGELOG-v10.md @@ -28,5 +28,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New configuration options: - `sRefreshTokenLifetime` - options for refresh token lifetime, from 24 hours to 90 days - `sFingerprintCookieMode` - option for the authentication fingerprint cookie mode, same or cross origin +- Access and refresh tokens are now invalidated when the user's password is changed [10.0.0]: https://github.com/OXID-eSales/graphql-base-module/compare/v9.0.0...b-7.2.x From 26f8d43bb9b0ddacd22603204c7930151d507fb9 Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Fri, 4 Oct 2024 16:03:25 +0300 Subject: [PATCH 12/19] OXDEV-8407 Use LoginInterface instead of direct Login data type for returns Signed-off-by: Anton Fedurtsya --- src/Controller/Login.php | 4 ++-- src/DataType/LoginInterface.php | 6 ++++++ src/Service/LoginService.php | 3 ++- src/Service/LoginServiceInterface.php | 4 ++-- tests/Unit/Controller/LoginTest.php | 22 +++++++--------------- tests/Unit/Service/LoginServiceTest.php | 10 ++++------ 6 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/Controller/Login.php b/src/Controller/Login.php index b6b716e0..f2fb3d3c 100644 --- a/src/Controller/Login.php +++ b/src/Controller/Login.php @@ -9,7 +9,7 @@ namespace OxidEsales\GraphQL\Base\Controller; -use OxidEsales\GraphQL\Base\DataType\Login as LoginDatatype; +use OxidEsales\GraphQL\Base\DataType\LoginInterface; use OxidEsales\GraphQL\Base\Service\LoginServiceInterface; use OxidEsales\GraphQL\Base\Service\Token; use TheCodingMachine\GraphQLite\Annotations\Query; @@ -42,7 +42,7 @@ public function token(?string $username = null, ?string $password = null): strin * * @Query */ - public function login(?string $username = null, ?string $password = null): LoginDatatype + public function login(?string $username = null, ?string $password = null): LoginInterface { return $this->loginService->login($username, $password); } diff --git a/src/DataType/LoginInterface.php b/src/DataType/LoginInterface.php index 9f7d6149..67b8470e 100644 --- a/src/DataType/LoginInterface.php +++ b/src/DataType/LoginInterface.php @@ -9,9 +9,15 @@ namespace OxidEsales\GraphQL\Base\DataType; +use TheCodingMachine\GraphQLite\Annotations\Field; +use TheCodingMachine\GraphQLite\Annotations\Type; + +#[Type] interface LoginInterface { + #[Field] public function refreshToken(): string; + #[Field] public function accessToken(): string; } diff --git a/src/Service/LoginService.php b/src/Service/LoginService.php index e4c61780..87f6eab4 100644 --- a/src/Service/LoginService.php +++ b/src/Service/LoginService.php @@ -10,6 +10,7 @@ namespace OxidEsales\GraphQL\Base\Service; use OxidEsales\GraphQL\Base\DataType\Login as LoginDatatype; +use OxidEsales\GraphQL\Base\DataType\LoginInterface; use OxidEsales\GraphQL\Base\Infrastructure\Legacy; /** @@ -24,7 +25,7 @@ public function __construct( ) { } - public function login(?string $userName, ?string $password): LoginDatatype + public function login(?string $userName, ?string $password): LoginInterface { $user = $this->legacyInfrastructure->login($userName, $password); diff --git a/src/Service/LoginServiceInterface.php b/src/Service/LoginServiceInterface.php index 42bf83d2..39df62cc 100644 --- a/src/Service/LoginServiceInterface.php +++ b/src/Service/LoginServiceInterface.php @@ -9,12 +9,12 @@ namespace OxidEsales\GraphQL\Base\Service; -use OxidEsales\GraphQL\Base\DataType\Login as LoginDatatype; +use OxidEsales\GraphQL\Base\DataType\LoginInterface; /** * User login service */ interface LoginServiceInterface { - public function login(?string $userName, ?string $password): LoginDatatype; + public function login(?string $userName, ?string $password): LoginInterface; } diff --git a/tests/Unit/Controller/LoginTest.php b/tests/Unit/Controller/LoginTest.php index 4181f972..69e01750 100644 --- a/tests/Unit/Controller/LoginTest.php +++ b/tests/Unit/Controller/LoginTest.php @@ -9,10 +9,9 @@ namespace OxidEsales\GraphQL\Base\Tests\Unit\Controller; -use Lcobucci\JWT\UnencryptedToken; use OxidEsales\Eshop\Application\Model\User as UserModel; use OxidEsales\GraphQL\Base\Controller\Login; -use OxidEsales\GraphQL\Base\DataType\Login as LoginDatatype; +use OxidEsales\GraphQL\Base\DataType\LoginInterface; use OxidEsales\GraphQL\Base\DataType\User; use OxidEsales\GraphQL\Base\Infrastructure\Legacy; use OxidEsales\GraphQL\Base\Infrastructure\Token as TokenInfrastructure; @@ -168,26 +167,19 @@ public function testCreateAnonymousToken(): void $this->assertNotEmpty($token->claims()->get(TokenService::CLAIM_USERID)); } - public function testLoginReturnsLoginDataType(): void + public function testLoginReturnsLoginServiceResult(): void { $loginController = new Login( - tokenService: $this->createMock(TokenService::class), + tokenService: $this->createStub(TokenService::class), loginService: $loginServiceMock = $this->createMock(LoginServiceInterface::class), ); $userName = uniqid(); $password = uniqid(); - $refreshToken = uniqid(); - $accessToken = $this->createConfiguredStub(UnencryptedToken::class, [ - 'toString' => uniqid() - ]); - - $loginDataType = new LoginDatatype( - refreshToken: $refreshToken, - accessToken: $accessToken - ); - $loginServiceMock->method('login')->with($userName, $password)->willReturn($loginDataType); - $this->assertSame($loginDataType, $loginController->login($userName, $password)); + $loginDataTypeStub = $this->createStub(LoginInterface::class); + $loginServiceMock->method('login')->with($userName, $password)->willReturn($loginDataTypeStub); + + $this->assertSame($loginDataTypeStub, $loginController->login($userName, $password)); } } diff --git a/tests/Unit/Service/LoginServiceTest.php b/tests/Unit/Service/LoginServiceTest.php index 72af9f9b..e015419e 100644 --- a/tests/Unit/Service/LoginServiceTest.php +++ b/tests/Unit/Service/LoginServiceTest.php @@ -10,8 +10,6 @@ namespace OxidEsales\GraphQL\Base\Tests\Unit\Service; use Lcobucci\JWT\UnencryptedToken; -use OxidEsales\GraphQL\Base\DataType\Login; -use OxidEsales\GraphQL\Base\DataType\LoginInterface; use OxidEsales\GraphQL\Base\DataType\UserInterface; use OxidEsales\GraphQL\Base\Infrastructure\Legacy; use OxidEsales\GraphQL\Base\Service\LoginService; @@ -41,10 +39,10 @@ public function testLoginCreatesLoginInputTypeResult(): void $refreshToken = uniqid(); $refreshTokenMock->method('createRefreshTokenForUser')->with($userType)->willReturn($refreshToken); - $accessTokenStub = $this->createConfiguredStub(UnencryptedToken::class, [ - 'toString' => $accessToken = uniqid() - ]); - $tokenServiceMock->method('createTokenForUser')->with($userType)->willReturn($accessTokenStub); + $accessToken = uniqid(); + $tokenServiceMock->method('createTokenForUser')->with($userType)->willReturn( + $this->createConfiguredStub(UnencryptedToken::class, ['toString' => $accessToken]) + ); $result = $sut->login($userName, $password); From c103c647d718710016be36c9852c7d8a038b222c Mon Sep 17 00:00:00 2001 From: Anton Fedurtsya Date: Fri, 4 Oct 2024 16:23:55 +0300 Subject: [PATCH 13/19] OXDEV-8407 Simplify UserModelServiceTest and move to Units Signed-off-by: Anton Fedurtsya --- .../Service/UserModelServiceTest.php | 71 ------------------- tests/Unit/Service/UserModelServiceTest.php | 65 +++++++++++++++++ 2 files changed, 65 insertions(+), 71 deletions(-) delete mode 100644 tests/Integration/Service/UserModelServiceTest.php create mode 100644 tests/Unit/Service/UserModelServiceTest.php diff --git a/tests/Integration/Service/UserModelServiceTest.php b/tests/Integration/Service/UserModelServiceTest.php deleted file mode 100644 index e5e9c215..00000000 --- a/tests/Integration/Service/UserModelServiceTest.php +++ /dev/null @@ -1,71 +0,0 @@ -getUserModelStub($userId, $password); - - $sut = new UserModelService( - legacyInfrastructure: $this->getLegacyMock($user), - ); - - $this->assertTrue($sut->isPasswordChanged($userId, 'mynewpwd')); - } - - public function testIsPasswordChangedOnSamePassword(): void - { - $userId = uniqid(); - $password = uniqid(); - - $user = $this->getUserModelStub($userId, $password); - - $sut = new UserModelService( - legacyInfrastructure: $this->getLegacyMock($user), - ); - - $this->assertFalse($sut->isPasswordChanged($userId, $password)); - } - - protected function getUserModelStub(string $id, string $password): UserModel - { - $userModel = oxNew(UserModel::class); - $userModel->assign([ - 'oxid' => $id, - 'oxpassword' => $password, - ]); - - return $userModel; - } - - private function getLegacyMock(UserModel $user): Legacy - { - $legacyInfrastructureMock = $this->getMockBuilder(Legacy::class) - ->disableOriginalConstructor() - ->getMock(); - - $legacyInfrastructureMock->method('getUserModel') - ->willReturn($user); - - return $legacyInfrastructureMock; - } -} diff --git a/tests/Unit/Service/UserModelServiceTest.php b/tests/Unit/Service/UserModelServiceTest.php new file mode 100644 index 00000000..38ef8db4 --- /dev/null +++ b/tests/Unit/Service/UserModelServiceTest.php @@ -0,0 +1,65 @@ +createMock(Legacy::class), + ); + + $legacyInfrastructureMock->method('getUserModel')->with($userId)->willReturn( + $this->getUserModelMock($password) + ); + + $this->assertTrue($sut->isPasswordChanged($userId, $newPassword)); + + } + + public function testIsPasswordChangedOnSamePassword(): void + { + $userId = uniqid(); + $password = uniqid(); + $newPassword = $password; + + $sut = new UserModelService( + legacyInfrastructure: $legacyInfrastructureMock = $this->createMock(Legacy::class), + ); + + $legacyInfrastructureMock->method('getUserModel')->with($userId)->willReturn( + $this->getUserModelMock($password) + ); + + $this->assertFalse($sut->isPasswordChanged($userId, $newPassword)); + } + + protected function getUserModelMock(string $password): UserModel + { + $userModelMock = $this->createMock(UserModel::class); + $userModelMock->method('getFieldData')->willReturnMap([ + ['oxpassword', $password], + ]); + + return $userModelMock; + } +} From de11a312c975e0e6465eab3a2329cbcd5b0dd681 Mon Sep 17 00:00:00 2001 From: Tatyana Koleva Date: Tue, 8 Oct 2024 20:20:48 +0300 Subject: [PATCH 14/19] OXDEV-8407 Refactor tests and subscriber --- services.yaml | 3 - .../Subscriber/PasswordChangeSubscriber.php | 19 ++- .../Integration/Event/PasswordChangeTest.php | 124 ------------------ tests/Integration/Event/UserDeleteTest.php | 81 ------------ .../RefreshTokenRepositoryTest.php | 38 +++++- .../Integration/Infrastructure/TokenTest.php | 67 ++++++++++ .../PasswordChangeSubscriberTest.php | 105 ++++++++++++++- tests/Unit/Service/UserModelServiceTest.php | 1 - 8 files changed, 212 insertions(+), 226 deletions(-) delete mode 100644 tests/Integration/Event/PasswordChangeTest.php delete mode 100644 tests/Integration/Event/UserDeleteTest.php diff --git a/services.yaml b/services.yaml index 1354ed9a..1944a3ea 100644 --- a/services.yaml +++ b/services.yaml @@ -63,9 +63,6 @@ services: OxidEsales\GraphQL\Base\Infrastructure\Token: class: OxidEsales\GraphQL\Base\Infrastructure\Token - OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository: - class: OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository - OxidEsales\GraphQL\Base\Infrastructure\Repository: class: OxidEsales\GraphQL\Base\Infrastructure\Repository diff --git a/src/Event/Subscriber/PasswordChangeSubscriber.php b/src/Event/Subscriber/PasswordChangeSubscriber.php index 444c49ba..3425c722 100644 --- a/src/Event/Subscriber/PasswordChangeSubscriber.php +++ b/src/Event/Subscriber/PasswordChangeSubscriber.php @@ -12,7 +12,7 @@ use OxidEsales\Eshop\Application\Model\User; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\AfterModelUpdateEvent; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\BeforeModelUpdateEvent; -use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository; +use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepositoryInterface; use OxidEsales\GraphQL\Base\Infrastructure\Token; use OxidEsales\GraphQL\Base\Service\UserModelService; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -23,13 +23,13 @@ class PasswordChangeSubscriber implements EventSubscriberInterface /** * Whether the password had been changed. * - * @var string|null + * @var array */ - protected ?string $userIdWithChangedPwd = null; + protected array $userIdWithChangedPwd = []; public function __construct( private readonly UserModelService $userModelService, - private readonly RefreshTokenRepository $refreshTokenRepository, + private readonly RefreshTokenRepositoryInterface $refreshTokenRepository, private readonly Token $tokenInfrastructure ) { } @@ -44,7 +44,7 @@ public function handleBeforeUpdate(Event $event): Event /** @phpstan-ignore-next-line method.notFound */ $model = $event->getModel(); - if (!$model instanceof User || !$model->getId()) { + if (!$model instanceof User) { return $event; } @@ -53,7 +53,7 @@ public function handleBeforeUpdate(Event $event): Event return $event; } - $this->userIdWithChangedPwd = $model->getId(); + $this->userIdWithChangedPwd[$model->getId()] = true; return $event; } @@ -68,16 +68,13 @@ public function handleAfterUpdate(Event $event): Event /** @phpstan-ignore-next-line method.notFound */ $model = $event->getModel(); - if (!$model instanceof User) { - return $event; - } - - if ($model->getId() !== $this->userIdWithChangedPwd || !$this->userIdWithChangedPwd) { + if (!$model instanceof User || !isset($this->userIdWithChangedPwd[$model->getId()])) { return $event; } $this->refreshTokenRepository->invalidateUserTokens($model->getId()); $this->tokenInfrastructure->invalidateUserTokens($model->getId()); + unset($this->userIdWithChangedPwd[$model->getId()]); return $event; } diff --git a/tests/Integration/Event/PasswordChangeTest.php b/tests/Integration/Event/PasswordChangeTest.php deleted file mode 100644 index a4703399..00000000 --- a/tests/Integration/Event/PasswordChangeTest.php +++ /dev/null @@ -1,124 +0,0 @@ -create(); - $container->compile(); - $this->tokenInfrastructure = $container->get(TokenInfrastructure::class); - $this->connection = $container->get(ConnectionProviderInterface::class)->get(); - } - - #[RunInSeparateProcess] - public function testExpireTokenAfterUserPasswordChange(): void - { - $userModel = $this->getUserModel(); - $tokenModel = $this->getTokenModel(); - - $expiresAtBeforeChange = new DateTimeImmutable($tokenModel->getRawFieldData('expires_at')); - $user = new UserDataType($userModel); - - $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_changePwdUserToken')); - $this->assertFalse($expiresAtBeforeChange <= new DateTimeImmutable('now')); - - $userModel->setPassword('_newPassword'); - $userModel->save(); - - $result = $this->connection->executeQuery( - "select expires_at from `oegraphqltoken` where oxid=:tokenId", - ['tokenId' => '_changePwdUserToken'] - ); - $expiresAtAfterChange = $result->fetchOne(); - - $this->assertTrue(new DateTimeImmutable($expiresAtAfterChange) <= new DateTimeImmutable('now')); - } - - #[RunInSeparateProcess] - public function testKeepTokenAfterUserChangeEventAndNoPwdChange(): void - { - $userModel = $this->getUserModel(); - $tokenModel = $this->getTokenModel(); - - $expiresAtBeforeChange = new DateTimeImmutable($tokenModel->getRawFieldData('expires_at')); - $user = new UserDataType($userModel); - - $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_changePwdUserToken')); - $this->assertFalse($expiresAtBeforeChange <= new DateTimeImmutable('now')); - - $userModel->assign(['oxfname' => 'Test']); - $userModel->save(); - - $result = $this->connection->executeQuery( - "select expires_at from `oegraphqltoken` where oxid=:tokenId", - ['tokenId' => '_changePwdUserToken'] - ); - $expiresAtAfterChange = $result->fetchOne(); - - $this->assertFalse(new DateTimeImmutable($expiresAtAfterChange) <= new DateTimeImmutable('now')); - } - - private function getUserModel(): User - { - $userModel = oxNew(User::class); - $userModel->setId('_testUser'); - $userModel->setPassword('_testPassword'); - $userModel->assign(['oxusername' => '_testUsername']); - $userModel->save(); - - return $userModel; - } - - private function getTokenModel(): TokenModel - { - $issued = new DateTimeImmutable('now'); - $expires = new DateTimeImmutable('+8 hours'); - $tokenModel = oxNew(TokenModel::class); - $tokenModel->setId('_changePwdUserToken'); - $tokenModel->assign( - [ - 'OXID' => '_changePwdUserToken', - 'OXSHOPID' => '1', - 'OXUSERID' => '_testUser', - 'ISSUED_AT' => $issued->format('Y-m-d H:i:s'), - 'EXPIRES_AT' => $expires->format('Y-m-d H:i:s'), - 'USERAGENT' => '', - 'TOKEN' => 'very_large_string', - ] - ); - $tokenModel->save(); - $tokenModel->load('_changePwdUserToken'); - - return $tokenModel; - } -} diff --git a/tests/Integration/Event/UserDeleteTest.php b/tests/Integration/Event/UserDeleteTest.php deleted file mode 100644 index db522dbc..00000000 --- a/tests/Integration/Event/UserDeleteTest.php +++ /dev/null @@ -1,81 +0,0 @@ -create(); - $container->compile(); - $this->tokenInfrastructure = $container->get(TokenInfrastructure::class); - } - - public function testInvalidateTokenAfterDeleteUser(): void - { - $userModel = oxNew(User::class); - $userModel->setId('_testUser'); - $userModel->setPassword('_testPassword'); - $userModel->assign(['oxusername' => '_testUsername']); - $userModel->save(); - - $this->tokenInfrastructure->registerToken( - $this->getTokenMock('_deletedUser'), - new DateTimeImmutable('now'), - new DateTimeImmutable('+8 hours') - ); - - $user = new UserDataType($userModel); - $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_deletedUser')); - - $userModel->delete(self::TEST_USER_ID); - $this->assertFalse($this->tokenInfrastructure->isTokenRegistered('_deletedUser')); - } - - private function getTokenMock( - string $tokenId = self::TEST_TOKEN_ID, - string $userId = self::TEST_USER_ID - ): UnencryptedToken { - $claims = new DataSet( - [ - TokenService::CLAIM_TOKENID => $tokenId, - TokenService::CLAIM_SHOPID => 1, - TokenService::CLAIM_USERID => $userId, - ], - '' - ); - - $token = $this->getMockBuilder(UnencryptedToken::class) - ->getMock(); - $token->method('claims')->willReturn($claims); - $token->method('toString')->willReturn('here_is_the_string_token'); - - return $token; - } -} diff --git a/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php b/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php index c0792990..064d3342 100644 --- a/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php +++ b/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php @@ -10,10 +10,11 @@ namespace OxidEsales\GraphQL\Base\Tests\Integration\Infrastructure; use DateTime; +use DateTimeImmutable; use Doctrine\DBAL\Connection; use OxidEsales\EshopCommunity\Internal\Framework\Database\ConnectionProviderInterface; +use OxidEsales\GraphQL\Base\DataType\UserInterface; use OxidEsales\GraphQL\Base\Exception\InvalidRefreshToken; -use OxidEsales\GraphQL\Base\Exception\InvalidToken; use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository; use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepositoryInterface; use OxidEsales\GraphQL\Base\Tests\Integration\TestCase; @@ -138,6 +139,41 @@ public function testGetTokenUserExplodesOnWrongToken(): void $sut->getTokenUser(uniqid()); } + public function testInvalidateRefreshTokens(): void + { + $expires = new DateTimeImmutable('+8 hours'); + $this->addToken( + oxid: 'pwd_change_token', + expires: $expires->format('Y-m-d H:i:s'), + userId: $userId = '_testUser', + token: $token = uniqid(), + ); + + $sut = $this->getSut(); + $result = $sut->invalidateUserTokens($userId); + $this->assertTrue($result !== 0); + + $this->expectException(InvalidRefreshToken::class); + $sut->getTokenUser($token); + } + + public function testInvalidateRefreshTokensWrongUserId(): void + { + $expires = new DateTimeImmutable('+8 hours'); + $this->addToken( + oxid: 'pwd_change_token', + expires: $expires->format('Y-m-d H:i:s'), + userId: '_testUser', + token: $token = uniqid(), + ); + + $sut = $this->getSut(); + $result = $sut->invalidateUserTokens('some_user_id'); + $this->assertTrue($result == 0); + + $this->assertTrue($sut->getTokenUser($token) instanceof UserInterface); + } + private function getDbConnection(): Connection { return $this->get(ConnectionProviderInterface::class)->get(); diff --git a/tests/Integration/Infrastructure/TokenTest.php b/tests/Integration/Infrastructure/TokenTest.php index c34a0404..07f0da66 100644 --- a/tests/Integration/Infrastructure/TokenTest.php +++ b/tests/Integration/Infrastructure/TokenTest.php @@ -290,6 +290,73 @@ public function testUserHasToken(): void $this->assertFalse($this->tokenInfrastructure->userHasToken($otherUser, '_second')); } + public function testInvalidateTokenAfterDeleteUser(): void + { + $userModel = oxNew(User::class); + $userModel->setId('_testUser'); + $userModel->setPassword('_testPassword'); + $userModel->assign(['oxusername' => '_testUsername']); + $userModel->save(); + + $this->tokenInfrastructure->registerToken( + $this->getTokenMock('_deletedUser'), + new DateTimeImmutable('now'), + new DateTimeImmutable('+8 hours') + ); + + $user = new UserDataType($userModel); + $this->assertTrue($this->tokenInfrastructure->userHasToken($user, '_deletedUser')); + + $userModel->delete(self::TEST_USER_ID); + $this->assertFalse($this->tokenInfrastructure->isTokenRegistered('_deletedUser')); + } + + public function testInvalidateAccessTokens(): void + { + $this->tokenInfrastructure->registerToken( + $this->getTokenMock( + $token = 'pwd_change_token', + $userId = '_testUser' + ), + new DateTimeImmutable('now'), + new DateTimeImmutable('+8 hours') + ); + + $result = $this->tokenInfrastructure->invalidateUserTokens($userId); + $this->assertTrue($result !== 0); + + $result = $this->connection->executeQuery( + "select expires_at from `oegraphqltoken` where oxid=:tokenId", + ['tokenId' => $token] + ); + $expiresAtAfterChange = $result->fetchOne(); + + $this->assertTrue(new DateTimeImmutable($expiresAtAfterChange) <= new DateTimeImmutable('now')); + } + + public function testInvalidateAccessTokensWrongUserId(): void + { + $this->tokenInfrastructure->registerToken( + $this->getTokenMock( + tokenId: $token = 'pwd_change_token', + userId: '_testUser' + ), + new DateTimeImmutable('now'), + new DateTimeImmutable('+8 hours') + ); + + $result = $this->tokenInfrastructure->invalidateUserTokens('wrong_user_id'); + $this->assertTrue($result == 0); + + $result = $this->connection->executeQuery( + "select expires_at from `oegraphqltoken` where oxid=:tokenId", + ['tokenId' => $token] + ); + $expiresAtAfterChange = $result->fetchOne(); + + $this->assertFalse(new DateTimeImmutable($expiresAtAfterChange) <= new DateTimeImmutable('now')); + } + private function getTokenMock( string $tokenId = self::TEST_TOKEN_ID, string $userId = self::TEST_USER_ID diff --git a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php index fe934247..30ab6c04 100644 --- a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php +++ b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php @@ -9,10 +9,13 @@ namespace OxidEsales\GraphQL\Base\Tests\Unit\Event\Subscriber; +use OxidEsales\Eshop\Application\Model\Article; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Model\BaseModel; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\AfterModelUpdateEvent; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\BeforeModelUpdateEvent; use OxidEsales\GraphQL\Base\Event\Subscriber\PasswordChangeSubscriber; -use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepository; +use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepositoryInterface; use OxidEsales\GraphQL\Base\Infrastructure\Token; use OxidEsales\GraphQL\Base\Service\UserModelService; use OxidEsales\GraphQL\Base\Tests\Unit\BaseTestCase; @@ -46,14 +49,106 @@ public function testHandleAfterUpdateReturnsOriginalEvent(): void $this->assertSame($eventStub, $sut->handleAfterUpdate($eventStub)); } - public function getSut( + public function testSubscriberWithUserModelPwdChange(): void + { + $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); + $userModelService->method('isPasswordChanged')->with($userId = uniqid())->willReturn(true); + + $userModelStub = $this->createStub(User::class); + $userModelStub->method('getId') + ->willReturn($userId); + + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->once()) + ->method('invalidateUserTokens'); + + $tokenInfrastructure = $this->createMock(Token::class); + $tokenInfrastructure->expects($this->once()) + ->method('invalidateUserTokens'); + + $beforeUpdateStub = $this->getBeforeUpdateEvent($userModelStub); + $afterUpdateStub = $this->getAfterUpdateEvent($userModelStub); + + $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); + $sut->handleBeforeUpdate($beforeUpdateStub); + $sut->handleAfterUpdate($afterUpdateStub); + } + + public function testSubscriberWithNoUserModel(): void + { + $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); + $userModelService->expects($this->never()) + ->method('isPasswordChanged'); + + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->never()) + ->method('invalidateUserTokens'); + + $tokenInfrastructure = $this->createMock(Token::class); + $tokenInfrastructure->expects($this->never()) + ->method('invalidateUserTokens'); + + $beforeUpdateStub = $this->getBeforeUpdateEvent(new Article()); + $afterUpdateStub = $this->getAfterUpdateEvent(new Article()); + + $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); + $sut->handleBeforeUpdate($beforeUpdateStub); + $sut->handleAfterUpdate($afterUpdateStub); + } + + public function testSubscriberWithUserModelNoPwdChanged(): void + { + $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); + $userModelService->method('isPasswordChanged')->with($userId = uniqid())->willReturn(false); + + $userModelStub = $this->createStub(User::class); + $userModelStub->method('getId') + ->willReturn($userId); + + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->never()) + ->method('invalidateUserTokens'); + + $tokenInfrastructure = $this->createMock(Token::class); + $tokenInfrastructure->expects($this->never()) + ->method('invalidateUserTokens'); + + $beforeUpdateStub = $this->getBeforeUpdateEvent($userModelStub); + $afterUpdateStub = $this->getAfterUpdateEvent($userModelStub); + + $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); + $sut->handleBeforeUpdate($beforeUpdateStub); + $sut->handleAfterUpdate($afterUpdateStub); + } + + protected function getBeforeUpdateEvent(BaseModel $model) + { + $beforeUpdateStub = $this->createStub(BeforeModelUpdateEvent::class); + $beforeUpdateStub->method('getModel') + ->willReturn($model); + + return $beforeUpdateStub; + } + + protected function getAfterUpdateEvent(BaseModel $model) + { + $afterUpdateStub = $this->createStub(AfterModelUpdateEvent::class); + $afterUpdateStub->method('getModel') + ->willReturn($model); + + return $afterUpdateStub; + } + + protected function getSut( UserModelService $userModelService = null, - RefreshTokenRepository $refreshTokenRepository = null + RefreshTokenRepositoryInterface $refreshTokenRepository = null, + Token $tokenInfrastructure = null, ): PasswordChangeSubscriber { return new PasswordChangeSubscriber( userModelService: $userModelService ?? $this->createStub(UserModelService::class), - refreshTokenRepository: $refreshTokenRepository ?? $this->createStub(RefreshTokenRepository::class), - tokenInfrastructure: $this->createStub(Token::class) + refreshTokenRepository: + $refreshTokenRepository ?? $this->createStub(RefreshTokenRepositoryInterface::class), + tokenInfrastructure: $tokenInfrastructure ?? $this->createStub(Token::class) ); } } diff --git a/tests/Unit/Service/UserModelServiceTest.php b/tests/Unit/Service/UserModelServiceTest.php index 38ef8db4..c84a3d6d 100644 --- a/tests/Unit/Service/UserModelServiceTest.php +++ b/tests/Unit/Service/UserModelServiceTest.php @@ -33,7 +33,6 @@ public function testIsPasswordChangedOnNewPassword(): void ); $this->assertTrue($sut->isPasswordChanged($userId, $newPassword)); - } public function testIsPasswordChangedOnSamePassword(): void From 4f762bc7566153738d2ab67e77207ee6dc8fdc46 Mon Sep 17 00:00:00 2001 From: Tatyana Koleva Date: Wed, 9 Oct 2024 16:14:49 +0300 Subject: [PATCH 15/19] OXDEV-8407 Add test for multiple users password change --- .../PasswordChangeSubscriberTest.php | 51 +++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php index 30ab6c04..9ea7114b 100644 --- a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php +++ b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php @@ -54,10 +54,6 @@ public function testSubscriberWithUserModelPwdChange(): void $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); $userModelService->method('isPasswordChanged')->with($userId = uniqid())->willReturn(true); - $userModelStub = $this->createStub(User::class); - $userModelStub->method('getId') - ->willReturn($userId); - $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); $refreshTokenRepository->expects($this->once()) ->method('invalidateUserTokens'); @@ -66,6 +62,7 @@ public function testSubscriberWithUserModelPwdChange(): void $tokenInfrastructure->expects($this->once()) ->method('invalidateUserTokens'); + $userModelStub = $this->getUserModel($userId); $beforeUpdateStub = $this->getBeforeUpdateEvent($userModelStub); $afterUpdateStub = $this->getAfterUpdateEvent($userModelStub); @@ -74,6 +71,34 @@ public function testSubscriberWithUserModelPwdChange(): void $sut->handleAfterUpdate($afterUpdateStub); } + public function testSubscriberWithMultipleUserModelsPwdChange(): void + { + $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); + $userModelService->method('isPasswordChanged')->willReturn(true); + + $userModel1 = $this->getUserModel(uniqid()); + $userModel2 = $this->getUserModel(uniqid()); + + $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); + $refreshTokenRepository->expects($this->exactly(2)) + ->method('invalidateUserTokens'); + + $tokenInfrastructure = $this->createMock(Token::class); + $tokenInfrastructure->expects($this->exactly(2)) + ->method('invalidateUserTokens'); + + $beforeUpdateStub1 = $this->getBeforeUpdateEvent($userModel1); + $beforeUpdateStub2 = $this->getBeforeUpdateEvent($userModel2); + $afterUpdateStub1 = $this->getAfterUpdateEvent($userModel1); + $afterUpdateStub2 = $this->getAfterUpdateEvent($userModel2); + + $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); + $sut->handleBeforeUpdate($beforeUpdateStub1); + $sut->handleBeforeUpdate($beforeUpdateStub2); + $sut->handleAfterUpdate($afterUpdateStub1); + $sut->handleAfterUpdate($afterUpdateStub2); + } + public function testSubscriberWithNoUserModel(): void { $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); @@ -101,10 +126,6 @@ public function testSubscriberWithUserModelNoPwdChanged(): void $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); $userModelService->method('isPasswordChanged')->with($userId = uniqid())->willReturn(false); - $userModelStub = $this->createStub(User::class); - $userModelStub->method('getId') - ->willReturn($userId); - $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); $refreshTokenRepository->expects($this->never()) ->method('invalidateUserTokens'); @@ -113,6 +134,7 @@ public function testSubscriberWithUserModelNoPwdChanged(): void $tokenInfrastructure->expects($this->never()) ->method('invalidateUserTokens'); + $userModelStub = $this->getUserModel($userId); $beforeUpdateStub = $this->getBeforeUpdateEvent($userModelStub); $afterUpdateStub = $this->getAfterUpdateEvent($userModelStub); @@ -121,7 +143,7 @@ public function testSubscriberWithUserModelNoPwdChanged(): void $sut->handleAfterUpdate($afterUpdateStub); } - protected function getBeforeUpdateEvent(BaseModel $model) + protected function getBeforeUpdateEvent(BaseModel $model): BeforeModelUpdateEvent { $beforeUpdateStub = $this->createStub(BeforeModelUpdateEvent::class); $beforeUpdateStub->method('getModel') @@ -130,7 +152,7 @@ protected function getBeforeUpdateEvent(BaseModel $model) return $beforeUpdateStub; } - protected function getAfterUpdateEvent(BaseModel $model) + protected function getAfterUpdateEvent(BaseModel $model): AfterModelUpdateEvent { $afterUpdateStub = $this->createStub(AfterModelUpdateEvent::class); $afterUpdateStub->method('getModel') @@ -139,6 +161,15 @@ protected function getAfterUpdateEvent(BaseModel $model) return $afterUpdateStub; } + protected function getUserModel(string $userId): User + { + $userModelStub = $this->createStub(User::class); + $userModelStub->method('getId') + ->willReturn($userId); + + return $userModelStub; + } + protected function getSut( UserModelService $userModelService = null, RefreshTokenRepositoryInterface $refreshTokenRepository = null, From bf405c462012970468082e0dc5552ec483106a13 Mon Sep 17 00:00:00 2001 From: Tatyana Koleva Date: Wed, 9 Oct 2024 16:15:07 +0300 Subject: [PATCH 16/19] OXDEV-8407 Update changelog --- CHANGELOG-v10.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG-v10.md b/CHANGELOG-v10.md index 18f86cf3..d89c95d1 100644 --- a/CHANGELOG-v10.md +++ b/CHANGELOG-v10.md @@ -29,5 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `sRefreshTokenLifetime` - options for refresh token lifetime, from 24 hours to 90 days - `sFingerprintCookieMode` - option for the authentication fingerprint cookie mode, same or cross origin - Access and refresh tokens are now invalidated when the user's password is changed + - New event subscriber: + - `OxidEsales\GraphQL\Base\Event\Subscriber\PasswordChangeSubscriber` + +## Changed +- Renamed OxidEsales\GraphQL\Base\Infrastructure\Token::cleanUpTokens() to deleteOrphanedTokens() [10.0.0]: https://github.com/OXID-eSales/graphql-base-module/compare/v9.0.0...b-7.2.x From 10fc0f0e0606c61b349f42342242184c4f8ad29c Mon Sep 17 00:00:00 2001 From: Tatyana Koleva Date: Thu, 10 Oct 2024 16:27:18 +0300 Subject: [PATCH 17/19] OXDEV-8407 Refactor tests and invalidate methods --- .../Subscriber/PasswordChangeSubscriber.php | 14 ++- src/Infrastructure/RefreshTokenRepository.php | 6 +- .../RefreshTokenRepositoryInterface.php | 2 +- src/Infrastructure/Token.php | 6 +- .../RefreshTokenRepositoryTest.php | 6 +- .../Integration/Infrastructure/TokenTest.php | 8 +- .../PasswordChangeSubscriberTest.php | 85 ++++++------------- 7 files changed, 40 insertions(+), 87 deletions(-) diff --git a/src/Event/Subscriber/PasswordChangeSubscriber.php b/src/Event/Subscriber/PasswordChangeSubscriber.php index 3425c722..1f8d69f0 100644 --- a/src/Event/Subscriber/PasswordChangeSubscriber.php +++ b/src/Event/Subscriber/PasswordChangeSubscriber.php @@ -39,23 +39,21 @@ public function __construct( * * @param Event $event Event to be handled */ - public function handleBeforeUpdate(Event $event): Event + public function handleBeforeUpdate(Event $event): void { /** @phpstan-ignore-next-line method.notFound */ $model = $event->getModel(); if (!$model instanceof User) { - return $event; + return; } $newPassword = $model->getFieldData('oxpassword'); if (!$this->userModelService->isPasswordChanged($model->getId(), $newPassword)) { - return $event; + return; } $this->userIdWithChangedPwd[$model->getId()] = true; - - return $event; } /** @@ -63,20 +61,18 @@ public function handleBeforeUpdate(Event $event): Event * * @param Event $event Event to be handled */ - public function handleAfterUpdate(Event $event): Event + public function handleAfterUpdate(Event $event): void { /** @phpstan-ignore-next-line method.notFound */ $model = $event->getModel(); if (!$model instanceof User || !isset($this->userIdWithChangedPwd[$model->getId()])) { - return $event; + return; } $this->refreshTokenRepository->invalidateUserTokens($model->getId()); $this->tokenInfrastructure->invalidateUserTokens($model->getId()); unset($this->userIdWithChangedPwd[$model->getId()]); - - return $event; } /** diff --git a/src/Infrastructure/RefreshTokenRepository.php b/src/Infrastructure/RefreshTokenRepository.php index 1495ec0f..be5bf61e 100644 --- a/src/Infrastructure/RefreshTokenRepository.php +++ b/src/Infrastructure/RefreshTokenRepository.php @@ -88,7 +88,7 @@ public function getTokenUser(string $refreshToken): UserInterface return new UserDataType($userModel, $isAnonymous); } - public function invalidateUserTokens(string $userId): int + public function invalidateUserTokens(string $userId): void { $queryBuilder = $this->queryBuilderFactory->create() ->update('oegraphqlrefreshtoken') @@ -98,8 +98,6 @@ public function invalidateUserTokens(string $userId): int 'userId' => $userId, ]); - $result = $queryBuilder->execute(); - - return is_object($result) ? (int)$result->rowCount() : (int)$result; + $queryBuilder->execute(); } } diff --git a/src/Infrastructure/RefreshTokenRepositoryInterface.php b/src/Infrastructure/RefreshTokenRepositoryInterface.php index b95505c3..e74a7aff 100644 --- a/src/Infrastructure/RefreshTokenRepositoryInterface.php +++ b/src/Infrastructure/RefreshTokenRepositoryInterface.php @@ -25,5 +25,5 @@ public function removeExpiredTokens(): void; */ public function getTokenUser(string $refreshToken): UserInterface; - public function invalidateUserTokens(string $user): int; + public function invalidateUserTokens(string $user): void; } diff --git a/src/Infrastructure/Token.php b/src/Infrastructure/Token.php index 89afe097..16065e91 100644 --- a/src/Infrastructure/Token.php +++ b/src/Infrastructure/Token.php @@ -156,7 +156,7 @@ public function userHasToken(UserInterface $user, string $tokenId): bool return false; } - public function invalidateUserTokens(string $userId): int + public function invalidateUserTokens(string $userId): void { $queryBuilder = $this->queryBuilderFactory->create() ->update('oegraphqltoken') @@ -166,8 +166,6 @@ public function invalidateUserTokens(string $userId): int 'userId' => $userId, ]); - $result = $queryBuilder->execute(); - - return is_object($result) ? $result->columnCount() : (int)$result; + $queryBuilder->execute(); } } diff --git a/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php b/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php index 064d3342..7f205890 100644 --- a/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php +++ b/tests/Integration/Infrastructure/RefreshTokenRepositoryTest.php @@ -150,8 +150,7 @@ public function testInvalidateRefreshTokens(): void ); $sut = $this->getSut(); - $result = $sut->invalidateUserTokens($userId); - $this->assertTrue($result !== 0); + $sut->invalidateUserTokens($userId); $this->expectException(InvalidRefreshToken::class); $sut->getTokenUser($token); @@ -168,8 +167,7 @@ public function testInvalidateRefreshTokensWrongUserId(): void ); $sut = $this->getSut(); - $result = $sut->invalidateUserTokens('some_user_id'); - $this->assertTrue($result == 0); + $sut->invalidateUserTokens('some_user_id'); $this->assertTrue($sut->getTokenUser($token) instanceof UserInterface); } diff --git a/tests/Integration/Infrastructure/TokenTest.php b/tests/Integration/Infrastructure/TokenTest.php index 07f0da66..6d89c7dd 100644 --- a/tests/Integration/Infrastructure/TokenTest.php +++ b/tests/Integration/Infrastructure/TokenTest.php @@ -322,9 +322,7 @@ public function testInvalidateAccessTokens(): void new DateTimeImmutable('+8 hours') ); - $result = $this->tokenInfrastructure->invalidateUserTokens($userId); - $this->assertTrue($result !== 0); - + $this->tokenInfrastructure->invalidateUserTokens($userId); $result = $this->connection->executeQuery( "select expires_at from `oegraphqltoken` where oxid=:tokenId", ['tokenId' => $token] @@ -345,9 +343,7 @@ public function testInvalidateAccessTokensWrongUserId(): void new DateTimeImmutable('+8 hours') ); - $result = $this->tokenInfrastructure->invalidateUserTokens('wrong_user_id'); - $this->assertTrue($result == 0); - + $this->tokenInfrastructure->invalidateUserTokens('wrong_user_id'); $result = $this->connection->executeQuery( "select expires_at from `oegraphqltoken` where oxid=:tokenId", ['tokenId' => $token] diff --git a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php index 9ea7114b..8393ec28 100644 --- a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php +++ b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php @@ -33,22 +33,6 @@ public function testSubscribedEventsConfiguration(): void $this->assertTrue($configuration[AfterModelUpdateEvent::class] === 'handleAfterUpdate'); } - public function testHandleBeforeUpdateReturnsOriginalEvent(): void - { - $sut = $this->getSut(); - - $eventStub = $this->createStub(BeforeModelUpdateEvent::class); - $this->assertSame($eventStub, $sut->handleBeforeUpdate($eventStub)); - } - - public function testHandleAfterUpdateReturnsOriginalEvent(): void - { - $sut = $this->getSut(); - - $eventStub = $this->createStub(AfterModelUpdateEvent::class); - $this->assertSame($eventStub, $sut->handleAfterUpdate($eventStub)); - } - public function testSubscriberWithUserModelPwdChange(): void { $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); @@ -60,11 +44,12 @@ public function testSubscriberWithUserModelPwdChange(): void $tokenInfrastructure = $this->createMock(Token::class); $tokenInfrastructure->expects($this->once()) - ->method('invalidateUserTokens'); + ->method('invalidateUserTokens') + ->with($this->equalTo($userId)); - $userModelStub = $this->getUserModel($userId); - $beforeUpdateStub = $this->getBeforeUpdateEvent($userModelStub); - $afterUpdateStub = $this->getAfterUpdateEvent($userModelStub); + $userModelStub = $this->createConfiguredStub(User::class, ['getId' => $userId]); + $beforeUpdateStub = $this->createConfiguredStub(BeforeModelUpdateEvent::class, ['getModel' => $userModelStub]); + $afterUpdateStub = $this->createConfiguredStub(AfterModelUpdateEvent::class, ['getModel' => $userModelStub]); $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); $sut->handleBeforeUpdate($beforeUpdateStub); @@ -76,27 +61,36 @@ public function testSubscriberWithMultipleUserModelsPwdChange(): void $userModelService = $this->createPartialMock(UserModelService::class, ['isPasswordChanged']); $userModelService->method('isPasswordChanged')->willReturn(true); - $userModel1 = $this->getUserModel(uniqid()); - $userModel2 = $this->getUserModel(uniqid()); + $userModelStub1 = $this->createConfiguredStub(User::class, ['getId' => $userId1 = uniqid()]); + $userModelStub2 = $this->createConfiguredStub(User::class, ['getId' => $userId2 = uniqid()]); + $methodArgs = []; $refreshTokenRepository = $this->createMock(RefreshTokenRepositoryInterface::class); $refreshTokenRepository->expects($this->exactly(2)) - ->method('invalidateUserTokens'); + ->method('invalidateUserTokens') + ->willReturnCallback(function ($userId) use (&$methodArgs) { + $methodArgs[] = $userId; + }); $tokenInfrastructure = $this->createMock(Token::class); $tokenInfrastructure->expects($this->exactly(2)) ->method('invalidateUserTokens'); - $beforeUpdateStub1 = $this->getBeforeUpdateEvent($userModel1); - $beforeUpdateStub2 = $this->getBeforeUpdateEvent($userModel2); - $afterUpdateStub1 = $this->getAfterUpdateEvent($userModel1); - $afterUpdateStub2 = $this->getAfterUpdateEvent($userModel2); + $beforeUpdateStub1 = $this->createConfiguredStub(BeforeModelUpdateEvent::class, ['getModel' => $userModelStub1]); + $beforeUpdateStub2 = $this->createConfiguredStub(BeforeModelUpdateEvent::class, ['getModel' => $userModelStub2]); + $afterUpdateStub1 = $this->createConfiguredStub(AfterModelUpdateEvent::class, ['getModel' => $userModelStub1]); + $afterUpdateStub2 = $this->createConfiguredStub(AfterModelUpdateEvent::class, ['getModel' => $userModelStub2]); $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); $sut->handleBeforeUpdate($beforeUpdateStub1); $sut->handleBeforeUpdate($beforeUpdateStub2); $sut->handleAfterUpdate($afterUpdateStub1); $sut->handleAfterUpdate($afterUpdateStub2); + + // Ensure that invalidateUserTokens was called with the correct parameters + $this->assertCount(2, $methodArgs); + $this->assertSame($userId1, $methodArgs[0]); + $this->assertSame($userId2, $methodArgs[1]); } public function testSubscriberWithNoUserModel(): void @@ -113,8 +107,8 @@ public function testSubscriberWithNoUserModel(): void $tokenInfrastructure->expects($this->never()) ->method('invalidateUserTokens'); - $beforeUpdateStub = $this->getBeforeUpdateEvent(new Article()); - $afterUpdateStub = $this->getAfterUpdateEvent(new Article()); + $beforeUpdateStub = $this->createConfiguredStub(BeforeModelUpdateEvent::class, ['getModel' => new Article()]); + $afterUpdateStub = $this->createConfiguredStub(AfterModelUpdateEvent::class, ['getModel' => new Article()]); $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); $sut->handleBeforeUpdate($beforeUpdateStub); @@ -134,42 +128,15 @@ public function testSubscriberWithUserModelNoPwdChanged(): void $tokenInfrastructure->expects($this->never()) ->method('invalidateUserTokens'); - $userModelStub = $this->getUserModel($userId); - $beforeUpdateStub = $this->getBeforeUpdateEvent($userModelStub); - $afterUpdateStub = $this->getAfterUpdateEvent($userModelStub); + $userModelStub = $this->createConfiguredStub(User::class, ['getId' => $userId]); + $beforeUpdateStub = $this->createConfiguredStub(BeforeModelUpdateEvent::class, ['getModel' => $userModelStub]); + $afterUpdateStub = $this->createConfiguredStub(AfterModelUpdateEvent::class, ['getModel' => $userModelStub]); $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); $sut->handleBeforeUpdate($beforeUpdateStub); $sut->handleAfterUpdate($afterUpdateStub); } - protected function getBeforeUpdateEvent(BaseModel $model): BeforeModelUpdateEvent - { - $beforeUpdateStub = $this->createStub(BeforeModelUpdateEvent::class); - $beforeUpdateStub->method('getModel') - ->willReturn($model); - - return $beforeUpdateStub; - } - - protected function getAfterUpdateEvent(BaseModel $model): AfterModelUpdateEvent - { - $afterUpdateStub = $this->createStub(AfterModelUpdateEvent::class); - $afterUpdateStub->method('getModel') - ->willReturn($model); - - return $afterUpdateStub; - } - - protected function getUserModel(string $userId): User - { - $userModelStub = $this->createStub(User::class); - $userModelStub->method('getId') - ->willReturn($userId); - - return $userModelStub; - } - protected function getSut( UserModelService $userModelService = null, RefreshTokenRepositoryInterface $refreshTokenRepository = null, From 32bf1c05bd043c15c02d1b50ef91cc9d27241672 Mon Sep 17 00:00:00 2001 From: Tatyana Koleva Date: Thu, 10 Oct 2024 16:35:07 +0300 Subject: [PATCH 18/19] OXDEV-8407 Fix phpcs warnings --- .../PasswordChangeSubscriberTest.php | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php index 8393ec28..d9d7c1bb 100644 --- a/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php +++ b/tests/Unit/Event/Subscriber/PasswordChangeSubscriberTest.php @@ -76,10 +76,22 @@ public function testSubscriberWithMultipleUserModelsPwdChange(): void $tokenInfrastructure->expects($this->exactly(2)) ->method('invalidateUserTokens'); - $beforeUpdateStub1 = $this->createConfiguredStub(BeforeModelUpdateEvent::class, ['getModel' => $userModelStub1]); - $beforeUpdateStub2 = $this->createConfiguredStub(BeforeModelUpdateEvent::class, ['getModel' => $userModelStub2]); - $afterUpdateStub1 = $this->createConfiguredStub(AfterModelUpdateEvent::class, ['getModel' => $userModelStub1]); - $afterUpdateStub2 = $this->createConfiguredStub(AfterModelUpdateEvent::class, ['getModel' => $userModelStub2]); + $beforeUpdateStub1 = $this->createConfiguredStub( + BeforeModelUpdateEvent::class, + ['getModel' => $userModelStub1] + ); + $beforeUpdateStub2 = $this->createConfiguredStub( + BeforeModelUpdateEvent::class, + ['getModel' => $userModelStub2] + ); + $afterUpdateStub1 = $this->createConfiguredStub( + AfterModelUpdateEvent::class, + ['getModel' => $userModelStub1] + ); + $afterUpdateStub2 = $this->createConfiguredStub( + AfterModelUpdateEvent::class, + ['getModel' => $userModelStub2] + ); $sut = $this->getSut($userModelService, $refreshTokenRepository, $tokenInfrastructure); $sut->handleBeforeUpdate($beforeUpdateStub1); From d7ff9b3f78d679bb9e44227e4eb4a9296d223d95 Mon Sep 17 00:00:00 2001 From: Tatyana Koleva Date: Thu, 17 Oct 2024 18:57:12 +0300 Subject: [PATCH 19/19] OXDEV-8407 Update changelog --- CHANGELOG-v10.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG-v10.md b/CHANGELOG-v10.md index d89c95d1..7f0f8796 100644 --- a/CHANGELOG-v10.md +++ b/CHANGELOG-v10.md @@ -29,6 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `sRefreshTokenLifetime` - options for refresh token lifetime, from 24 hours to 90 days - `sFingerprintCookieMode` - option for the authentication fingerprint cookie mode, same or cross origin - Access and refresh tokens are now invalidated when the user's password is changed + - New methods: + - `OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepositoryInterface::invalidateUserTokens` + - `OxidEsales\GraphQL\Base\Infrastructure\Token::invalidateUserTokens` - New event subscriber: - `OxidEsales\GraphQL\Base\Event\Subscriber\PasswordChangeSubscriber`