Skip to content

Commit 6a4b8f0

Browse files
committed
Merge branch 'b-7.2.x-password_change_token_invalidation-OXDEV-8407' into b-7.2.x
2 parents 2cb6258 + d7ff9b3 commit 6a4b8f0

19 files changed

Lines changed: 539 additions & 46 deletions

CHANGELOG-v10.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
- New configuration options:
2727
- `sRefreshTokenLifetime` - options for refresh token lifetime, from 24 hours to 90 days
2828
- `sFingerprintCookieMode` - option for the authentication fingerprint cookie mode, same or cross origin
29+
- Access and refresh tokens are now invalidated when the user's password is changed
30+
- New methods:
31+
- `OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepositoryInterface::invalidateUserTokens`
32+
- `OxidEsales\GraphQL\Base\Infrastructure\Token::invalidateUserTokens`
33+
- New event subscriber:
34+
- `OxidEsales\GraphQL\Base\Event\Subscriber\PasswordChangeSubscriber`
35+
36+
## Changed
37+
- Renamed OxidEsales\GraphQL\Base\Infrastructure\Token::cleanUpTokens() to deleteOrphanedTokens()
2938

3039
[10.0.0]: https://github.com/OXID-eSales/graphql-base-module/compare/v9.0.0...b-7.2.x

services.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ services:
8080
OxidEsales\GraphQL\Base\Service\FingerprintServiceInterface:
8181
class: OxidEsales\GraphQL\Base\Service\FingerprintService
8282

83+
OxidEsales\GraphQL\Base\Service\UserModelService:
84+
class: OxidEsales\GraphQL\Base\Service\UserModelService
85+
8386
OxidEsales\GraphQL\Base\Controller\:
8487
resource: 'src/Controller/'
8588
public: true
@@ -107,6 +110,10 @@ services:
107110
class: OxidEsales\GraphQL\Base\Event\Subscriber\UserDeleteSubscriber
108111
tags: [ 'kernel.event_subscriber' ]
109112

113+
OxidEsales\GraphQL\Base\Event\Subscriber\PasswordChangeSubscriber:
114+
class: OxidEsales\GraphQL\Base\Event\Subscriber\PasswordChangeSubscriber
115+
tags: [ 'kernel.event_subscriber' ]
116+
110117
OxidEsales\GraphQL\Base\Event\Subscriber\BeforeTokenCreationSubscriber:
111118
tags: [ 'kernel.event_subscriber' ]
112119

src/Controller/Login.php

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99

1010
namespace OxidEsales\GraphQL\Base\Controller;
1111

12-
use OxidEsales\GraphQL\Base\DataType\Login as LoginDatatype;
12+
use OxidEsales\GraphQL\Base\DataType\LoginInterface;
1313
use OxidEsales\GraphQL\Base\Service\LoginServiceInterface;
14-
use OxidEsales\GraphQL\Base\Service\RefreshTokenServiceInterface;
1514
use OxidEsales\GraphQL\Base\Service\Token;
1615
use TheCodingMachine\GraphQLite\Annotations\Query;
1716

@@ -20,7 +19,6 @@ class Login
2019
public function __construct(
2120
protected Token $tokenService,
2221
protected LoginServiceInterface $loginService,
23-
protected RefreshTokenServiceInterface $refreshTokenService,
2422
) {
2523
}
2624

@@ -44,13 +42,8 @@ public function token(?string $username = null, ?string $password = null): strin
4442
*
4543
* @Query
4644
*/
47-
public function login(?string $username = null, ?string $password = null): LoginDatatype
45+
public function login(?string $username = null, ?string $password = null): LoginInterface
4846
{
49-
$user = $this->loginService->login($username, $password);
50-
51-
return new LoginDatatype(
52-
refreshToken: $this->refreshTokenService->createRefreshTokenForUser($user),
53-
accessToken: $this->tokenService->createTokenForUser($user),
54-
);
47+
return $this->loginService->login($username, $password);
5548
}
5649
}

src/DataType/LoginInterface.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@
99

1010
namespace OxidEsales\GraphQL\Base\DataType;
1111

12+
use TheCodingMachine\GraphQLite\Annotations\Field;
13+
use TheCodingMachine\GraphQLite\Annotations\Type;
14+
15+
#[Type]
1216
interface LoginInterface
1317
{
18+
#[Field]
1419
public function refreshToken(): string;
1520

21+
#[Field]
1622
public function accessToken(): string;
1723
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
/**
4+
* Copyright © OXID eSales AG. All rights reserved.
5+
* See LICENSE file for license details.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OxidEsales\GraphQL\Base\Event\Subscriber;
11+
12+
use OxidEsales\Eshop\Application\Model\User;
13+
use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\AfterModelUpdateEvent;
14+
use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\BeforeModelUpdateEvent;
15+
use OxidEsales\GraphQL\Base\Infrastructure\RefreshTokenRepositoryInterface;
16+
use OxidEsales\GraphQL\Base\Infrastructure\Token;
17+
use OxidEsales\GraphQL\Base\Service\UserModelService;
18+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
19+
use Symfony\Contracts\EventDispatcher\Event;
20+
21+
class PasswordChangeSubscriber implements EventSubscriberInterface
22+
{
23+
/**
24+
* Whether the password had been changed.
25+
*
26+
* @var array
27+
*/
28+
protected array $userIdWithChangedPwd = [];
29+
30+
public function __construct(
31+
private readonly UserModelService $userModelService,
32+
private readonly RefreshTokenRepositoryInterface $refreshTokenRepository,
33+
private readonly Token $tokenInfrastructure
34+
) {
35+
}
36+
37+
/**
38+
* Handle ApplicationModelChangedEvent.
39+
*
40+
* @param Event $event Event to be handled
41+
*/
42+
public function handleBeforeUpdate(Event $event): void
43+
{
44+
/** @phpstan-ignore-next-line method.notFound */
45+
$model = $event->getModel();
46+
47+
if (!$model instanceof User) {
48+
return;
49+
}
50+
51+
$newPassword = $model->getFieldData('oxpassword');
52+
if (!$this->userModelService->isPasswordChanged($model->getId(), $newPassword)) {
53+
return;
54+
}
55+
56+
$this->userIdWithChangedPwd[$model->getId()] = true;
57+
}
58+
59+
/**
60+
* Handle ApplicationModelChangedEvent.
61+
*
62+
* @param Event $event Event to be handled
63+
*/
64+
public function handleAfterUpdate(Event $event): void
65+
{
66+
/** @phpstan-ignore-next-line method.notFound */
67+
$model = $event->getModel();
68+
69+
if (!$model instanceof User || !isset($this->userIdWithChangedPwd[$model->getId()])) {
70+
return;
71+
}
72+
73+
$this->refreshTokenRepository->invalidateUserTokens($model->getId());
74+
$this->tokenInfrastructure->invalidateUserTokens($model->getId());
75+
unset($this->userIdWithChangedPwd[$model->getId()]);
76+
}
77+
78+
/**
79+
* Returns an array of event names this subscriber wants to listen to.
80+
*
81+
* The array keys are event names and the value can be:
82+
*
83+
* * The method name to call (priority defaults to 0)
84+
* * An array composed of the method name to call and the priority
85+
* * An array of arrays composed of the method names to call and respective
86+
* priorities, or 0 if unset
87+
*
88+
* For instance:
89+
*
90+
* * array('eventName' => 'methodName')
91+
* * array('eventName' => array('methodName', $priority))
92+
* * array('eventName' => array(array('methodName1', $priority), array('methodName2')))
93+
*
94+
* @return array<class-string,string>
95+
*/
96+
public static function getSubscribedEvents(): array
97+
{
98+
return [
99+
BeforeModelUpdateEvent::class => 'handleBeforeUpdate',
100+
AfterModelUpdateEvent::class => 'handleAfterUpdate'
101+
];
102+
}
103+
}

src/Event/Subscriber/UserDeleteSubscriber.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function handle(Event $event): Event
3535
return $event;
3636
}
3737

38-
$this->tokenInfrastructure->cleanUpTokens();
38+
$this->tokenInfrastructure->deleteOrphanedTokens();
3939

4040
return $event;
4141
}

src/Infrastructure/RefreshTokenRepository.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,17 @@ public function getTokenUser(string $refreshToken): UserInterface
8787

8888
return new UserDataType($userModel, $isAnonymous);
8989
}
90+
91+
public function invalidateUserTokens(string $userId): void
92+
{
93+
$queryBuilder = $this->queryBuilderFactory->create()
94+
->update('oegraphqlrefreshtoken')
95+
->where('OXUSERID = :userId')
96+
->set('EXPIRES_AT', 'NOW()')
97+
->setParameters([
98+
'userId' => $userId,
99+
]);
100+
101+
$queryBuilder->execute();
102+
}
90103
}

src/Infrastructure/RefreshTokenRepositoryInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ public function removeExpiredTokens(): void;
2424
* @throws InvalidRefreshToken
2525
*/
2626
public function getTokenUser(string $refreshToken): UserInterface;
27+
28+
public function invalidateUserTokens(string $user): void;
2729
}

src/Infrastructure/Token.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public function removeExpiredTokens(UserInterface $user): void
6262
$queryBuilder->execute();
6363
}
6464

65-
public function cleanUpTokens(): void
65+
public function deleteOrphanedTokens(): void
6666
{
6767
/** @var \Doctrine\DBAL\Driver\Statement $execute */
6868
$execute = $this->queryBuilderFactory->create()
@@ -155,4 +155,17 @@ public function userHasToken(UserInterface $user, string $tokenId): bool
155155

156156
return false;
157157
}
158+
159+
public function invalidateUserTokens(string $userId): void
160+
{
161+
$queryBuilder = $this->queryBuilderFactory->create()
162+
->update('oegraphqltoken')
163+
->where('OXUSERID = :userId')
164+
->set('EXPIRES_AT', 'NOW()')
165+
->setParameters([
166+
'userId' => $userId,
167+
]);
168+
169+
$queryBuilder->execute();
170+
}
158171
}

src/Service/LoginService.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
namespace OxidEsales\GraphQL\Base\Service;
1111

12-
use OxidEsales\GraphQL\Base\DataType\UserInterface;
12+
use OxidEsales\GraphQL\Base\DataType\Login as LoginDatatype;
13+
use OxidEsales\GraphQL\Base\DataType\LoginInterface;
1314
use OxidEsales\GraphQL\Base\Infrastructure\Legacy;
1415

1516
/**
@@ -19,11 +20,18 @@ class LoginService implements LoginServiceInterface
1920
{
2021
public function __construct(
2122
private readonly Legacy $legacyInfrastructure,
23+
protected Token $tokenService,
24+
protected RefreshTokenServiceInterface $refreshTokenService,
2225
) {
2326
}
2427

25-
public function login(?string $userName, ?string $password): UserInterface
28+
public function login(?string $userName, ?string $password): LoginInterface
2629
{
27-
return $this->legacyInfrastructure->login($userName, $password);
30+
$user = $this->legacyInfrastructure->login($userName, $password);
31+
32+
return new LoginDatatype(
33+
refreshToken: $this->refreshTokenService->createRefreshTokenForUser($user),
34+
accessToken: $this->tokenService->createTokenForUser($user),
35+
);
2836
}
2937
}

0 commit comments

Comments
 (0)