Skip to content

Commit c4e7674

Browse files
committed
Move to dedicated RefreshTokenEntity Factory
1 parent d108667 commit c4e7674

File tree

6 files changed

+142
-54
lines changed

6 files changed

+142
-54
lines changed

src/Entities/RefreshTokenEntity.php

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
2525
use SimpleSAML\Module\oidc\Entities\Traits\AssociateWithAuthCodeTrait;
2626
use SimpleSAML\Module\oidc\Entities\Traits\RevokeTokenTrait;
27-
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
28-
use SimpleSAML\Module\oidc\Utils\TimestampGenerator;
2927

3028
class RefreshTokenEntity implements RefreshTokenEntityInterface
3129
{
@@ -34,31 +32,18 @@ class RefreshTokenEntity implements RefreshTokenEntityInterface
3432
use RevokeTokenTrait;
3533
use AssociateWithAuthCodeTrait;
3634

37-
/**
38-
* @throws \Exception
39-
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
40-
*/
41-
public static function fromState(array $state): RefreshTokenEntityInterface
42-
{
43-
$refreshToken = new self();
44-
45-
if (
46-
!is_string($state['id']) ||
47-
!is_string($state['expires_at']) ||
48-
!is_a($state['access_token'], AccessTokenEntityInterface::class)
49-
) {
50-
throw OidcServerException::serverError('Invalid Refresh Token state');
51-
}
52-
53-
$refreshToken->identifier = $state['id'];
54-
$refreshToken->expiryDateTime = DateTimeImmutable::createFromMutable(
55-
TimestampGenerator::utc($state['expires_at']),
56-
);
57-
$refreshToken->accessToken = $state['access_token'];
58-
$refreshToken->isRevoked = (bool) $state['is_revoked'];
59-
$refreshToken->authCodeId = empty($state['auth_code_id']) ? null : (string)$state['auth_code_id'];
60-
61-
return $refreshToken;
35+
public function __construct(
36+
string $id,
37+
DateTimeImmutable $expiryDateTime,
38+
AccessTokenEntityInterface $accessTokenEntity,
39+
?string $authCodeId = null,
40+
bool $isRevoked = false,
41+
) {
42+
$this->setIdentifier($id);
43+
$this->setExpiryDateTime($expiryDateTime);
44+
$this->setAccessToken($accessTokenEntity);
45+
$this->setAuthCodeId($authCodeId);
46+
$this->isRevoked = $isRevoked;
6247
}
6348

6449
public function getState(): array
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\oidc\Factories\Entities;
6+
7+
use DateTimeImmutable;
8+
use SimpleSAML\Module\oidc\Entities\Interfaces\AccessTokenEntityInterface;
9+
use SimpleSAML\Module\oidc\Entities\RefreshTokenEntity;
10+
use SimpleSAML\Module\oidc\Helpers;
11+
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
12+
13+
class RefreshTokenEntityFactory
14+
{
15+
public function __construct(
16+
protected Helpers $helpers,
17+
) {
18+
}
19+
20+
public function fromData(
21+
string $id,
22+
DateTimeImmutable $expiryDateTime,
23+
AccessTokenEntityInterface $accessTokenEntity,
24+
?string $authCodeId = null,
25+
bool $isRevoked = false,
26+
): RefreshTokenEntity {
27+
return new RefreshTokenEntity(
28+
$id,
29+
$expiryDateTime,
30+
$accessTokenEntity,
31+
$authCodeId,
32+
$isRevoked,
33+
);
34+
}
35+
36+
/**
37+
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
38+
*/
39+
public function fromState(array $state): RefreshTokenEntity
40+
{
41+
if (
42+
!is_string($state['id']) ||
43+
!is_string($state['expires_at']) ||
44+
!is_a($state['access_token'], AccessTokenEntityInterface::class)
45+
) {
46+
throw OidcServerException::serverError('Invalid Refresh Token state');
47+
}
48+
49+
$id = $state['id'];
50+
$expiryDateTime = $this->helpers->dateTime()->getUtc($state['expires_at']);
51+
$accessToken = $state['access_token'];
52+
$isRevoked = (bool) $state['is_revoked'];
53+
$authCodeId = empty($state['auth_code_id']) ? null : (string)$state['auth_code_id'];
54+
55+
return $this->fromData(
56+
$id,
57+
$expiryDateTime,
58+
$accessToken,
59+
$authCodeId,
60+
$isRevoked,
61+
);
62+
}
63+
}

src/Repositories/RefreshTokenRepository.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use RuntimeException;
2222
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
2323
use SimpleSAML\Module\oidc\Entities\RefreshTokenEntity;
24+
use SimpleSAML\Module\oidc\Factories\Entities\RefreshTokenEntityFactory;
2425
use SimpleSAML\Module\oidc\ModuleConfig;
2526
use SimpleSAML\Module\oidc\Repositories\Interfaces\RefreshTokenRepositoryInterface;
2627
use SimpleSAML\Module\oidc\Repositories\Traits\RevokeTokenByAuthCodeIdTrait;
@@ -35,6 +36,7 @@ class RefreshTokenRepository extends AbstractDatabaseRepository implements Refre
3536
public function __construct(
3637
ModuleConfig $moduleConfig,
3738
protected readonly AccessTokenRepository $accessTokenRepository,
39+
protected readonly RefreshTokenEntityFactory $refreshTokenEntityFactory,
3840
) {
3941
parent::__construct($moduleConfig);
4042
}
@@ -52,7 +54,7 @@ public function getTableName(): string
5254
*/
5355
public function getNewRefreshToken(): RefreshTokenEntityInterface
5456
{
55-
return new RefreshTokenEntity();
57+
throw new RuntimeException('Not implemented. Use RefreshTokenEntityFactory instead.');
5658
}
5759

5860
/**
@@ -99,7 +101,7 @@ public function findById(string $tokenId): ?RefreshTokenEntityInterface
99101
$data = current($rows);
100102
$data['access_token'] = $this->accessTokenRepository->findById((string)$data['access_token_id']);
101103

102-
return RefreshTokenEntity::fromState($data);
104+
return $this->refreshTokenEntityFactory->fromState($data);
103105
}
104106

105107
/**

src/Services/Container.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use SimpleSAML\Module\oidc\Factories\Entities\AuthCodeEntityFactory;
4343
use SimpleSAML\Module\oidc\Factories\Entities\ClaimSetEntityFactory;
4444
use SimpleSAML\Module\oidc\Factories\Entities\ClientEntityFactory;
45+
use SimpleSAML\Module\oidc\Factories\Entities\RefreshTokenEntityFactory;
4546
use SimpleSAML\Module\oidc\Factories\FederationFactory;
4647
use SimpleSAML\Module\oidc\Factories\FormFactory;
4748
use SimpleSAML\Module\oidc\Factories\Grant\AuthCodeGrantFactory;
@@ -238,7 +239,14 @@ public function __construct()
238239
);
239240
$this->services[AccessTokenRepository::class] = $accessTokenRepository;
240241

241-
$refreshTokenRepository = new RefreshTokenRepository($moduleConfig, $accessTokenRepository);
242+
$refreshTokenEntityFactory = new RefreshTokenEntityFactory($helpers);
243+
$this->services[RefreshTokenEntityFactory::class] = $refreshTokenEntityFactory;
244+
245+
$refreshTokenRepository = new RefreshTokenRepository(
246+
$moduleConfig,
247+
$accessTokenRepository,
248+
$refreshTokenEntityFactory,
249+
);
242250
$this->services[RefreshTokenRepository::class] = $refreshTokenRepository;
243251

244252
$scopeRepository = new ScopeRepository($moduleConfig);

tests/unit/src/Entities/RefreshTokenEntityTest.php

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace SimpleSAML\Test\Module\oidc\unit\Entities;
66

7+
use DateTimeImmutable;
8+
use DateTimeZone;
79
use PDO;
810
use PHPUnit\Framework\MockObject\MockObject;
911
use PHPUnit\Framework\TestCase;
@@ -16,33 +18,37 @@
1618
*/
1719
class RefreshTokenEntityTest extends TestCase
1820
{
21+
protected string $id;
22+
protected DateTimeImmutable $expiryDateTime;
1923
protected MockObject $accessTokenEntityMock;
20-
protected array $state;
24+
protected false $isRevoked;
25+
protected string $authCodeId;
2126

2227
/**
2328
* @throws \Exception
2429
*/
2530
protected function setUp(): void
2631
{
32+
$this->id = 'id';
33+
$this->expiryDateTime = new DateTimeImmutable('1970-01-01 00:00:00', new DateTimeZone('UTC'));
2734
$this->accessTokenEntityMock = $this->createMock(AccessTokenEntity::class);
2835
$this->accessTokenEntityMock->method('getIdentifier')->willReturn('access_token_id');
29-
30-
$this->state = [
31-
'id' => 'id',
32-
'expires_at' => '1970-01-01 00:00:00',
33-
'access_token' => $this->accessTokenEntityMock,
34-
'is_revoked' => false,
35-
'auth_code_id' => '123',
36-
];
36+
$this->isRevoked = false;
37+
$this->authCodeId = 'auth_code_id';
3738
}
3839

3940
/**
4041
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
4142
*/
42-
protected function prepareMockedInstance(array $state = null): RefreshTokenEntityInterface
43+
protected function mock(): RefreshTokenEntityInterface
4344
{
44-
$state ??= $this->state;
45-
return RefreshTokenEntity::fromState($state);
45+
return new RefreshTokenEntity(
46+
$this->id,
47+
$this->expiryDateTime,
48+
$this->accessTokenEntityMock,
49+
$this->authCodeId,
50+
$this->isRevoked,
51+
);
4652
}
4753

4854
/**
@@ -52,7 +58,7 @@ public function testItIsInitializable(): void
5258
{
5359
$this->assertInstanceOf(
5460
RefreshTokenEntity::class,
55-
$this->prepareMockedInstance(),
61+
$this->mock(),
5662
);
5763
}
5864

@@ -62,13 +68,13 @@ public function testItIsInitializable(): void
6268
public function testCanGetState(): void
6369
{
6470
$this->assertSame(
65-
$this->prepareMockedInstance()->getState(),
71+
$this->mock()->getState(),
6672
[
67-
'id' => 'id',
73+
'id' => $this->id,
6874
'expires_at' => '1970-01-01 00:00:00',
69-
'access_token_id' => 'access_token_id',
70-
'is_revoked' => [$this->state['is_revoked'], PDO::PARAM_BOOL],
71-
'auth_code_id' => '123',
75+
'access_token_id' => $this->accessTokenEntityMock->getIdentifier(),
76+
'is_revoked' => [$this->isRevoked, PDO::PARAM_BOOL],
77+
'auth_code_id' => $this->authCodeId,
7278
],
7379
);
7480
}

tests/unit/src/Repositories/RefreshTokenRepositoryTest.php

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@
1616
namespace SimpleSAML\Test\Module\oidc\unit\Repositories;
1717

1818
use DateTimeImmutable;
19+
use DateTimeZone;
1920
use PHPUnit\Framework\MockObject\MockObject;
2021
use PHPUnit\Framework\TestCase;
2122
use RuntimeException;
2223
use SimpleSAML\Configuration;
2324
use SimpleSAML\Module\oidc\Entities\AccessTokenEntity;
25+
use SimpleSAML\Module\oidc\Entities\RefreshTokenEntity;
26+
use SimpleSAML\Module\oidc\Factories\Entities\RefreshTokenEntityFactory;
2427
use SimpleSAML\Module\oidc\ModuleConfig;
2528
use SimpleSAML\Module\oidc\Repositories\AccessTokenRepository;
2629
use SimpleSAML\Module\oidc\Repositories\RefreshTokenRepository;
2730
use SimpleSAML\Module\oidc\Services\DatabaseMigration;
28-
use SimpleSAML\Module\oidc\Utils\TimestampGenerator;
2931

3032
/**
3133
* @covers \SimpleSAML\Module\oidc\Repositories\RefreshTokenRepository
@@ -40,6 +42,8 @@ class RefreshTokenRepositoryTest extends TestCase
4042
protected RefreshTokenRepository $repository;
4143
protected MockObject $accessTokenMock;
4244
protected MockObject $accessTokenRepositoryMock;
45+
protected MockObject $refreshTokenEntityFactoryMock;
46+
protected MockObject $refreshTokenEntityMock;
4347

4448
/**
4549
* @throws \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException
@@ -67,10 +71,14 @@ protected function setUp(): void
6771
$this->accessTokenMock = $this->createMock(AccessTokenEntity::class);
6872
$this->accessTokenMock->method('getIdentifier')->willReturn(self::ACCESS_TOKEN_ID);
6973
$this->accessTokenRepositoryMock = $this->createMock(AccessTokenRepository::class);
74+
$this->refreshTokenEntityFactoryMock = $this->createMock(RefreshTokenEntityFactory::class);
75+
76+
$this->refreshTokenEntityMock = $this->createMock(RefreshTokenEntity::class);
7077

7178
$this->repository = new RefreshTokenRepository(
7279
new ModuleConfig(),
7380
$this->accessTokenRepositoryMock,
81+
$this->refreshTokenEntityFactoryMock,
7482
);
7583
}
7684

@@ -87,13 +95,19 @@ public function testGetTableName(): void
8795
*/
8896
public function testAddAndFound(): void
8997
{
90-
$refreshToken = $this->repository->getNewRefreshToken();
91-
$refreshToken->setIdentifier(self::REFRESH_TOKEN_ID);
92-
$refreshToken->setExpiryDateTime(DateTimeImmutable::createFromMutable(TimestampGenerator::utc('yesterday')));
93-
$refreshToken->setAccessToken($this->accessTokenMock);
94-
98+
$refreshToken = new RefreshTokenEntity(
99+
self::REFRESH_TOKEN_ID,
100+
new DateTimeImmutable('yesterday', new DateTimeZone('UTC')),
101+
$this->accessTokenMock,
102+
);
95103
$this->repository->persistNewRefreshToken($refreshToken);
96104

105+
$this->refreshTokenEntityFactoryMock->expects($this->once())
106+
->method('fromState')
107+
->with($this->callback(function (array $state): bool {
108+
return $state['id'] === self::REFRESH_TOKEN_ID;
109+
}))->willReturn($refreshToken);
110+
97111
$this->accessTokenRepositoryMock->method('findById')->willReturn($this->accessTokenMock);
98112
$foundRefreshToken = $this->repository->findById(self::REFRESH_TOKEN_ID);
99113

@@ -115,7 +129,17 @@ public function testAddAndNotFound(): void
115129
*/
116130
public function testRevokeToken(): void
117131
{
132+
$revokedRefreshTokenMock = $this->createMock(RefreshTokenEntity::class);
133+
$revokedRefreshTokenMock->method('isRevoked')->willReturn(true);
118134
$this->accessTokenRepositoryMock->method('findById')->willReturn($this->accessTokenMock);
135+
$this->refreshTokenEntityMock->expects($this->once())->method('revoke');
136+
$this->refreshTokenEntityFactoryMock->expects($this->atLeastOnce())
137+
->method('fromState')
138+
->with($this->callback(function (array $state): bool {
139+
return $state['id'] === self::REFRESH_TOKEN_ID;
140+
}))
141+
->willReturnOnConsecutiveCalls($this->refreshTokenEntityMock, $revokedRefreshTokenMock);
142+
119143
$this->repository->revokeRefreshToken(self::REFRESH_TOKEN_ID);
120144
$isRevoked = $this->repository->isRefreshTokenRevoked(self::REFRESH_TOKEN_ID);
121145

0 commit comments

Comments
 (0)