Skip to content

Commit ef95780

Browse files
authored
Move to dedicated RefreshTokenEntity Factory (#259)
* Move to dedicated RefreshTokenEntity Factory * Update issueRefreshToken method for AuthCodeGrant * Update AuthCodeGrant * Introduce RefreshTokenIssuer --------- Co-authored-by: Marko Ivančić <[email protected]>
1 parent d108667 commit ef95780

File tree

17 files changed

+334
-85
lines changed

17 files changed

+334
-85
lines changed

routing/services/services.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ services:
3535
SimpleSAML\Module\oidc\Bridges\:
3636
resource: '../../src/Bridges/*'
3737

38+
SimpleSAML\Module\oidc\Server\TokenIssuers\:
39+
resource: '../../src/Server/TokenIssuers/*'
40+
3841
SimpleSAML\Module\oidc\ModuleConfig: ~
3942
SimpleSAML\Module\oidc\Helpers: ~
4043
SimpleSAML\Module\oidc\Forms\Controls\CsrfProtection: ~

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/Factories/Grant/AuthCodeGrantFactory.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use SimpleSAML\Module\oidc\Repositories\RefreshTokenRepository;
2525
use SimpleSAML\Module\oidc\Server\Grants\AuthCodeGrant;
2626
use SimpleSAML\Module\oidc\Server\RequestRules\RequestRulesManager;
27+
use SimpleSAML\Module\oidc\Server\TokenIssuers\RefreshTokenIssuer;
2728
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
2829

2930
class AuthCodeGrantFactory
@@ -37,6 +38,7 @@ public function __construct(
3738
private readonly RequestParamsResolver $requestParamsResolver,
3839
private readonly AccessTokenEntityFactory $accessTokenEntityFactory,
3940
private readonly AuthCodeEntityFactory $authCodeEntityFactory,
41+
private readonly RefreshTokenIssuer $refreshTokenIssuer,
4042
) {
4143
}
4244

@@ -54,6 +56,7 @@ public function build(): AuthCodeGrant
5456
$this->requestParamsResolver,
5557
$this->accessTokenEntityFactory,
5658
$this->authCodeEntityFactory,
59+
$this->refreshTokenIssuer,
5760
);
5861
$authCodeGrant->setRefreshTokenTTL($this->moduleConfig->getRefreshTokenDuration());
5962

src/Factories/Grant/RefreshTokenGrantFactory.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,26 @@
1919
use SimpleSAML\Module\oidc\ModuleConfig;
2020
use SimpleSAML\Module\oidc\Repositories\RefreshTokenRepository;
2121
use SimpleSAML\Module\oidc\Server\Grants\RefreshTokenGrant;
22+
use SimpleSAML\Module\oidc\Server\TokenIssuers\RefreshTokenIssuer;
2223

2324
class RefreshTokenGrantFactory
2425
{
2526
public function __construct(
2627
private readonly ModuleConfig $moduleConfig,
2728
private readonly RefreshTokenRepository $refreshTokenRepository,
2829
private readonly AccessTokenEntityFactory $accessTokenEntityFactory,
30+
private readonly RefreshTokenIssuer $refreshTokenIssuer,
2931
) {
3032
}
3133

3234
public function build(): RefreshTokenGrant
3335
{
34-
$refreshTokenGrant = new RefreshTokenGrant($this->refreshTokenRepository, $this->accessTokenEntityFactory);
36+
$refreshTokenGrant = new RefreshTokenGrant(
37+
$this->refreshTokenRepository,
38+
$this->accessTokenEntityFactory,
39+
$this->refreshTokenIssuer,
40+
);
41+
3542
$refreshTokenGrant->setRefreshTokenTTL($this->moduleConfig->getRefreshTokenDuration());
3643

3744
return $refreshTokenGrant;

src/Helpers.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use SimpleSAML\Module\oidc\Helpers\Client;
99
use SimpleSAML\Module\oidc\Helpers\DateTime;
1010
use SimpleSAML\Module\oidc\Helpers\Http;
11+
use SimpleSAML\Module\oidc\Helpers\Random;
1112
use SimpleSAML\Module\oidc\Helpers\Str;
1213

1314
class Helpers
@@ -17,6 +18,7 @@ class Helpers
1718
protected static ?DateTime $dateTIme = null;
1819
protected static ?Str $str = null;
1920
protected static ?Arr $arr = null;
21+
protected static ?Random $random = null;
2022

2123
public function http(): Http
2224
{
@@ -44,4 +46,9 @@ public function arr(): Arr
4446
{
4547
return static::$arr ??= new Arr();
4648
}
49+
50+
public function random(): Random
51+
{
52+
return static::$random ??= new Random();
53+
}
4754
}

src/Helpers/Random.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\oidc\Helpers;
6+
7+
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
8+
use Throwable;
9+
10+
class Random
11+
{
12+
/**
13+
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
14+
*/
15+
public function getIdentifier(int $length = 40): string
16+
{
17+
if ($length < 1) {
18+
throw OidcServerException::serverError('Random string length can not be less than 1');
19+
}
20+
21+
try {
22+
return bin2hex(random_bytes($length));
23+
} catch (Throwable $e) {
24+
throw OidcServerException::serverError('Could not generate a random string', $e);
25+
}
26+
}
27+
}

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/Server/Grants/AuthCodeGrant.php

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
2222
use LogicException;
2323
use Psr\Http\Message\ServerRequestInterface;
24+
use SimpleSAML\Module\oidc\Entities\Interfaces\AccessTokenEntityInterface;
2425
use SimpleSAML\Module\oidc\Entities\Interfaces\AuthCodeEntityInterface;
2526
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
2627
use SimpleSAML\Module\oidc\Entities\UserEntity;
@@ -56,6 +57,7 @@
5657
use SimpleSAML\Module\oidc\Server\ResponseTypes\Interfaces\AuthTimeResponseTypeInterface;
5758
use SimpleSAML\Module\oidc\Server\ResponseTypes\Interfaces\NonceResponseTypeInterface;
5859
use SimpleSAML\Module\oidc\Server\ResponseTypes\Interfaces\SessionIdResponseTypeInterface;
60+
use SimpleSAML\Module\oidc\Server\TokenIssuers\RefreshTokenIssuer;
5961
use SimpleSAML\Module\oidc\Utils\Arr;
6062
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
6163
use SimpleSAML\Module\oidc\Utils\ScopeHelper;
@@ -162,6 +164,7 @@ public function __construct(
162164
protected RequestParamsResolver $requestParamsResolver,
163165
AccessTokenEntityFactory $accessTokenEntityFactory,
164166
protected AuthCodeEntityFactory $authCodeEntityFactory,
167+
protected RefreshTokenIssuer $refreshTokenIssuer,
165168
) {
166169
parent::__construct($authCodeRepository, $refreshTokenRepository, $authCodeTTL);
167170

@@ -747,34 +750,15 @@ protected function issueRefreshToken(
747750
OAuth2AccessTokenEntityInterface $accessToken,
748751
string $authCodeId = null,
749752
): ?RefreshTokenEntityInterface {
750-
if (! is_a($this->refreshTokenRepository, RefreshTokenRepositoryInterface::class)) {
751-
throw OidcServerException::serverError('Unexpected refresh token repository entity type.');
752-
}
753-
754-
$refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
755-
756-
if ($refreshToken === null) {
757-
return null;
758-
}
759-
760-
$refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL));
761-
$refreshToken->setAccessToken($accessToken);
762-
$refreshToken->setAuthCodeId($authCodeId);
763-
764-
$maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
765-
766-
while ($maxGenerationAttempts-- > 0) {
767-
$refreshToken->setIdentifier($this->generateUniqueIdentifier());
768-
try {
769-
$this->refreshTokenRepository->persistNewRefreshToken($refreshToken);
770-
break;
771-
} catch (UniqueTokenIdentifierConstraintViolationException $e) {
772-
if ($maxGenerationAttempts === 0) {
773-
throw $e;
774-
}
775-
}
753+
if (! is_a($accessToken, AccessTokenEntityInterface::class)) {
754+
throw OidcServerException::serverError('Unexpected access token entity type.');
776755
}
777756

778-
return $refreshToken;
757+
return $this->refreshTokenIssuer->issue(
758+
$accessToken,
759+
$this->refreshTokenTTL,
760+
$authCodeId,
761+
self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS,
762+
);
779763
}
780764
}

src/Server/Grants/RefreshTokenGrant.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
namespace SimpleSAML\Module\oidc\Server\Grants;
66

77
use Exception;
8+
use League\OAuth2\Server\Entities\AccessTokenEntityInterface as OAuth2AccessTokenEntityInterface;
89
use League\OAuth2\Server\Grant\RefreshTokenGrant as OAuth2RefreshTokenGrant;
910
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
1011
use League\OAuth2\Server\RequestEvent;
1112
use Psr\Http\Message\ServerRequestInterface;
13+
use SimpleSAML\Module\oidc\Entities\Interfaces\AccessTokenEntityInterface;
14+
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
1215
use SimpleSAML\Module\oidc\Factories\Entities\AccessTokenEntityFactory;
1316
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
1417
use SimpleSAML\Module\oidc\Server\Grants\Traits\IssueAccessTokenTrait;
18+
use SimpleSAML\Module\oidc\Server\TokenIssuers\RefreshTokenIssuer;
1519

1620
use function is_null;
1721
use function json_decode;
@@ -48,6 +52,7 @@ class RefreshTokenGrant extends OAuth2RefreshTokenGrant
4852
public function __construct(
4953
RefreshTokenRepositoryInterface $refreshTokenRepository,
5054
AccessTokenEntityFactory $accessTokenEntityFactory,
55+
protected readonly RefreshTokenIssuer $refreshTokenIssuer,
5156
) {
5257
parent::__construct($refreshTokenRepository);
5358
$this->accessTokenEntityFactory = $accessTokenEntityFactory;
@@ -95,4 +100,20 @@ protected function validateOldRefreshToken(ServerRequestInterface $request, $cli
95100

96101
return $refreshTokenData;
97102
}
103+
104+
protected function issueRefreshToken(
105+
OAuth2AccessTokenEntityInterface $accessToken,
106+
string $authCodeId = null,
107+
): ?RefreshTokenEntityInterface {
108+
if (! is_a($accessToken, AccessTokenEntityInterface::class)) {
109+
throw OidcServerException::serverError('Unexpected access token entity type.');
110+
}
111+
112+
return $this->refreshTokenIssuer->issue(
113+
$accessToken,
114+
$this->refreshTokenTTL,
115+
$authCodeId,
116+
self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS,
117+
);
118+
}
98119
}

0 commit comments

Comments
 (0)