Skip to content

Commit d59b3d0

Browse files
authored
Enable caching capabilities on UserRepository (#264)
* Introduce ProtocolCache to repositories * Add caching capabilities to UserRepository * Update test cases * Cache on first find * Fix phpcbf * Start using protocol cache for docker runs --------- Co-authored-by: Marko Ivančić <[email protected]>
1 parent 86d9b49 commit d59b3d0

20 files changed

+372
-55
lines changed

config-templates/module_oidc.php

+7
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,13 @@
288288
// 60 * 60 * 6, // Default lifetime in seconds (used when particular cache item doesn't define its own lifetime)
289289
//],
290290

291+
// Cache duration for user entities (authenticated users data). If not set, cache duration will be the same as
292+
// session duration. This is used to avoid fetching user data from database on every authentication event.
293+
// This is only relevant if protocol cache adapter is set up. For duration format info, check
294+
// https://www.php.net/manual/en/dateinterval.construct.php.
295+
// ModuleConfig::OPTION_PROTOCOL_USER_ENTITY_CACHE_DURATION => 'PT1H', // 1 hour
296+
ModuleConfig::OPTION_PROTOCOL_USER_ENTITY_CACHE_DURATION => null, // fallback to session duration
297+
291298
/**
292299
* Cron related options.
293300
*/

docker/ssp/module_oidc.php

+5
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,9 @@
115115
ModuleConfig::OPTION_AUTH_FORCED_ACR_VALUE_FOR_COOKIE_AUTHENTICATION => null,
116116

117117
ModuleConfig::OPTION_CRON_TAG => 'hourly',
118+
119+
ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER => \Symfony\Component\Cache\Adapter\FilesystemAdapter::class,
120+
ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS => [
121+
// Use defaults
122+
],
118123
];

routing/services/services.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,8 @@ services:
115115
SimpleSAML\OpenID\Federation:
116116
factory: [ '@SimpleSAML\Module\oidc\Factories\FederationFactory', 'build' ]
117117
SimpleSAML\OpenID\Jwks:
118-
factory: [ '@SimpleSAML\Module\oidc\Factories\JwksFactory', 'build' ]
118+
factory: [ '@SimpleSAML\Module\oidc\Factories\JwksFactory', 'build' ]
119+
120+
# SSP
121+
SimpleSAML\Database:
122+
factory: [ 'SimpleSAML\Database', 'getInstance' ]

src/Controller/Federation/Test.php

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
namespace SimpleSAML\Module\oidc\Controller\Federation;
88

9+
use SimpleSAML\Database;
910
use SimpleSAML\Module\oidc\Codebooks\RegistrationTypeEnum;
1011
use SimpleSAML\Module\oidc\Factories\CoreFactory;
1112
use SimpleSAML\Module\oidc\Factories\Entities\ClientEntityFactory;
@@ -33,6 +34,7 @@ public function __construct(
3334
protected ?FederationCache $federationCache,
3435
protected LoggerService $loggerService,
3536
protected Jwks $jwks,
37+
protected Database $database,
3638
protected ClientEntityFactory $clientEntityFactory,
3739
protected CoreFactory $coreFactory,
3840
protected \DateInterval $maxCacheDuration = new \DateInterval('PT30S'),

src/ModuleConfig.php

+17
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class ModuleConfig
8080
final public const OPTION_FEDERATION_ENTITY_STATEMENT_CACHE_DURATION = 'federation_entity_statement_cache_duration';
8181
final public const OPTION_PROTOCOL_CACHE_ADAPTER = 'protocol_cache_adapter';
8282
final public const OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS = 'protocol_cache_adapter_arguments';
83+
final public const OPTION_PROTOCOL_USER_ENTITY_CACHE_DURATION = 'protocol_user_entity_cache_duration';
8384

8485
protected static array $standardScopes = [
8586
ScopesEnum::OpenId->value => [
@@ -630,4 +631,20 @@ public function getProtocolCacheAdapterArguments(): array
630631
{
631632
return $this->config()->getOptionalArray(self::OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS, []);
632633
}
634+
635+
/**
636+
* Get cache duration for user entities (user data). If not set in configuration, it will fall back to SSP session
637+
* duration.
638+
*
639+
* @throws \Exception
640+
*/
641+
public function getProtocolUserEntityCacheDuration(): DateInterval
642+
{
643+
return new DateInterval(
644+
$this->config()->getOptionalString(
645+
self::OPTION_PROTOCOL_USER_ENTITY_CACHE_DURATION,
646+
null,
647+
) ?? "PT{$this->sspConfig()->getInteger('session.duration')}S",
648+
);
649+
}
633650
}

src/Repositories/AbstractDatabaseRepository.php

+6-9
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,21 @@
1515
*/
1616
namespace SimpleSAML\Module\oidc\Repositories;
1717

18-
use SimpleSAML\Configuration;
1918
use SimpleSAML\Database;
2019
use SimpleSAML\Module\oidc\ModuleConfig;
20+
use SimpleSAML\Module\oidc\Utils\ProtocolCache;
2121

2222
abstract class AbstractDatabaseRepository
2323
{
24-
protected Configuration $config;
25-
26-
protected Database $database;
27-
2824
/**
2925
* ClientRepository constructor.
3026
* @throws \Exception
3127
*/
32-
public function __construct(protected ModuleConfig $moduleConfig)
33-
{
34-
$this->config = $this->moduleConfig->config();
35-
$this->database = Database::getInstance();
28+
public function __construct(
29+
protected readonly ModuleConfig $moduleConfig,
30+
protected readonly Database $database,
31+
protected readonly ?ProtocolCache $protocolCache,
32+
) {
3633
}
3734

3835
abstract public function getTableName(): ?string;

src/Repositories/AccessTokenRepository.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use League\OAuth2\Server\Entities\AccessTokenEntityInterface as OAuth2AccessTokenEntityInterface;
2121
use League\OAuth2\Server\Entities\ClientEntityInterface as OAuth2ClientEntityInterface;
2222
use RuntimeException;
23+
use SimpleSAML\Database;
2324
use SimpleSAML\Error\Error;
2425
use SimpleSAML\Module\oidc\Codebooks\DateFormatsEnum;
2526
use SimpleSAML\Module\oidc\Entities\AccessTokenEntity;
@@ -30,6 +31,7 @@
3031
use SimpleSAML\Module\oidc\Repositories\Interfaces\AccessTokenRepositoryInterface;
3132
use SimpleSAML\Module\oidc\Repositories\Traits\RevokeTokenByAuthCodeIdTrait;
3233
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
34+
use SimpleSAML\Module\oidc\Utils\ProtocolCache;
3335

3436
class AccessTokenRepository extends AbstractDatabaseRepository implements AccessTokenRepositoryInterface
3537
{
@@ -39,11 +41,13 @@ class AccessTokenRepository extends AbstractDatabaseRepository implements Access
3941

4042
public function __construct(
4143
ModuleConfig $moduleConfig,
44+
Database $database,
45+
?ProtocolCache $protocolCache,
4246
protected readonly ClientRepository $clientRepository,
4347
protected readonly AccessTokenEntityFactory $accessTokenEntityFactory,
4448
protected readonly Helpers $helpers,
4549
) {
46-
parent::__construct($moduleConfig);
50+
parent::__construct($moduleConfig, $database, $protocolCache);
4751
}
4852

4953
public function getTableName(): string

src/Repositories/AuthCodeRepository.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
use League\OAuth2\Server\Entities\AuthCodeEntityInterface as OAuth2AuthCodeEntityInterface;
2020
use RuntimeException;
21+
use SimpleSAML\Database;
2122
use SimpleSAML\Error\Error;
2223
use SimpleSAML\Module\oidc\Codebooks\DateFormatsEnum;
2324
use SimpleSAML\Module\oidc\Entities\AuthCodeEntity;
@@ -26,16 +27,19 @@
2627
use SimpleSAML\Module\oidc\Helpers;
2728
use SimpleSAML\Module\oidc\ModuleConfig;
2829
use SimpleSAML\Module\oidc\Repositories\Interfaces\AuthCodeRepositoryInterface;
30+
use SimpleSAML\Module\oidc\Utils\ProtocolCache;
2931

3032
class AuthCodeRepository extends AbstractDatabaseRepository implements AuthCodeRepositoryInterface
3133
{
3234
public function __construct(
3335
ModuleConfig $moduleConfig,
36+
Database $database,
37+
?ProtocolCache $protocolCache,
3438
protected readonly ClientRepository $clientRepository,
3539
protected readonly AuthCodeEntityFactory $authCodeEntityFactory,
3640
protected readonly Helpers $helpers,
3741
) {
38-
parent::__construct($moduleConfig);
42+
parent::__construct($moduleConfig, $database, $protocolCache);
3943
}
4044

4145
final public const TABLE_NAME = 'oidc_auth_code';

src/Repositories/ClientRepository.php

+6-2
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@
1717

1818
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
1919
use PDO;
20+
use SimpleSAML\Database;
2021
use SimpleSAML\Module\oidc\Entities\ClientEntity;
2122
use SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface;
2223
use SimpleSAML\Module\oidc\Factories\Entities\ClientEntityFactory;
2324
use SimpleSAML\Module\oidc\ModuleConfig;
25+
use SimpleSAML\Module\oidc\Utils\ProtocolCache;
2426

2527
class ClientRepository extends AbstractDatabaseRepository implements ClientRepositoryInterface
2628
{
2729
public function __construct(
2830
ModuleConfig $moduleConfig,
31+
Database $database,
32+
?ProtocolCache $protocolCache,
2933
protected readonly ClientEntityFactory $clientEntityFactory,
3034
) {
31-
parent::__construct($moduleConfig);
35+
parent::__construct($moduleConfig, $database, $protocolCache);
3236
}
3337

3438
final public const TABLE_NAME = 'oidc_client';
@@ -389,7 +393,7 @@ private function count(string $query, ?string $owner): int
389393
*/
390394
private function getItemsPerPage(): int
391395
{
392-
return $this->config
396+
return $this->moduleConfig->config()
393397
->getOptionalIntegerRange(ModuleConfig::OPTION_ADMIN_UI_PAGINATION_ITEMS_PER_PAGE, 1, 100, 20);
394398
}
395399

src/Repositories/RefreshTokenRepository.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface as OAuth2RefreshTokenEntityInterface;
2020
use League\OAuth2\Server\Exception\OAuthServerException;
2121
use RuntimeException;
22+
use SimpleSAML\Database;
2223
use SimpleSAML\Module\oidc\Codebooks\DateFormatsEnum;
2324
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
2425
use SimpleSAML\Module\oidc\Entities\RefreshTokenEntity;
@@ -27,6 +28,7 @@
2728
use SimpleSAML\Module\oidc\ModuleConfig;
2829
use SimpleSAML\Module\oidc\Repositories\Interfaces\RefreshTokenRepositoryInterface;
2930
use SimpleSAML\Module\oidc\Repositories\Traits\RevokeTokenByAuthCodeIdTrait;
31+
use SimpleSAML\Module\oidc\Utils\ProtocolCache;
3032

3133
class RefreshTokenRepository extends AbstractDatabaseRepository implements RefreshTokenRepositoryInterface
3234
{
@@ -36,11 +38,13 @@ class RefreshTokenRepository extends AbstractDatabaseRepository implements Refre
3638

3739
public function __construct(
3840
ModuleConfig $moduleConfig,
41+
Database $database,
42+
?ProtocolCache $protocolCache,
3943
protected readonly AccessTokenRepository $accessTokenRepository,
4044
protected readonly RefreshTokenEntityFactory $refreshTokenEntityFactory,
4145
protected readonly Helpers $helpers,
4246
) {
43-
parent::__construct($moduleConfig);
47+
parent::__construct($moduleConfig, $database, $protocolCache);
4448
}
4549

4650
/**

src/Repositories/ScopeRepository.php

+2-8
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,12 @@
2626
use function array_key_exists;
2727
use function in_array;
2828

29-
class ScopeRepository extends AbstractDatabaseRepository implements ScopeRepositoryInterface
29+
class ScopeRepository implements ScopeRepositoryInterface
3030
{
3131
public function __construct(
32-
ModuleConfig $moduleConfig,
32+
protected readonly ModuleConfig $moduleConfig,
3333
protected readonly ScopeEntityFactory $scopeEntityFactory,
3434
) {
35-
parent::__construct($moduleConfig);
36-
}
37-
38-
public function getTableName(): ?string
39-
{
40-
return null;
4135
}
4236

4337
/**

src/Repositories/UserRepository.php

+45-7
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,38 @@
2121
use League\OAuth2\Server\Entities\ClientEntityInterface as OAuth2ClientEntityInterface;
2222
use League\OAuth2\Server\Entities\UserEntityInterface;
2323
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
24+
use SimpleSAML\Database;
2425
use SimpleSAML\Module\oidc\Entities\UserEntity;
2526
use SimpleSAML\Module\oidc\Factories\Entities\UserEntityFactory;
2627
use SimpleSAML\Module\oidc\Helpers;
2728
use SimpleSAML\Module\oidc\ModuleConfig;
2829
use SimpleSAML\Module\oidc\Repositories\Interfaces\IdentityProviderInterface;
30+
use SimpleSAML\Module\oidc\Utils\ProtocolCache;
2931

3032
class UserRepository extends AbstractDatabaseRepository implements UserRepositoryInterface, IdentityProviderInterface
3133
{
3234
final public const TABLE_NAME = 'oidc_user';
3335

3436
public function __construct(
3537
ModuleConfig $moduleConfig,
38+
Database $database,
39+
?ProtocolCache $protocolCache,
3640
protected readonly Helpers $helpers,
3741
protected readonly UserEntityFactory $userEntityFactory,
3842
) {
39-
parent::__construct($moduleConfig);
43+
parent::__construct($moduleConfig, $database, $protocolCache);
4044
}
4145

4246
public function getTableName(): string
4347
{
4448
return $this->database->applyPrefix(self::TABLE_NAME);
4549
}
4650

51+
public function getCacheKey(string $identifier): string
52+
{
53+
return $this->getTableName() . '_' . $identifier;
54+
}
55+
4756
/**
4857
* @param string $identifier
4958
*
@@ -52,6 +61,13 @@ public function getTableName(): string
5261
*/
5362
public function getUserEntityByIdentifier(string $identifier): ?UserEntity
5463
{
64+
/** @var ?array $cachedState */
65+
$cachedState = $this->protocolCache?->get(null, $this->getCacheKey($identifier));
66+
67+
if (is_array($cachedState)) {
68+
return $this->userEntityFactory->fromState($cachedState);
69+
}
70+
5571
$stmt = $this->database->read(
5672
"SELECT * FROM {$this->getTableName()} WHERE id = :id",
5773
[
@@ -69,7 +85,15 @@ public function getUserEntityByIdentifier(string $identifier): ?UserEntity
6985
return null;
7086
}
7187

72-
return $this->userEntityFactory->fromState($row);
88+
$userEntity = $this->userEntityFactory->fromState($row);
89+
90+
$this->protocolCache?->set(
91+
$userEntity->getState(),
92+
$this->moduleConfig->getProtocolUserEntityCacheDuration(),
93+
$this->getCacheKey($userEntity->getIdentifier()),
94+
);
95+
96+
return $userEntity;
7397
}
7498

7599
/**
@@ -95,21 +119,29 @@ public function add(UserEntity $userEntity): void
95119
$stmt,
96120
$userEntity->getState(),
97121
);
122+
123+
$this->protocolCache?->set(
124+
$userEntity->getState(),
125+
$this->moduleConfig->getProtocolUserEntityCacheDuration(),
126+
$this->getCacheKey($userEntity->getIdentifier()),
127+
);
98128
}
99129

100-
public function delete(UserEntity $user): void
130+
public function delete(UserEntity $userEntity): void
101131
{
102132
$this->database->write(
103133
"DELETE FROM {$this->getTableName()} WHERE id = :id",
104134
[
105-
'id' => $user->getIdentifier(),
135+
'id' => $userEntity->getIdentifier(),
106136
],
107137
);
138+
139+
$this->protocolCache?->delete($this->getCacheKey($userEntity->getIdentifier()));
108140
}
109141

110-
public function update(UserEntity $user, ?DateTimeImmutable $updatedAt = null): void
142+
public function update(UserEntity $userEntity, ?DateTimeImmutable $updatedAt = null): void
111143
{
112-
$user->setUpdatedAt($updatedAt ?? $this->helpers->dateTime()->getUtc());
144+
$userEntity->setUpdatedAt($updatedAt ?? $this->helpers->dateTime()->getUtc());
113145

114146
$stmt = sprintf(
115147
"UPDATE %s SET claims = :claims, updated_at = :updated_at, created_at = :created_at WHERE id = :id",
@@ -118,7 +150,13 @@ public function update(UserEntity $user, ?DateTimeImmutable $updatedAt = null):
118150

119151
$this->database->write(
120152
$stmt,
121-
$user->getState(),
153+
$userEntity->getState(),
154+
);
155+
156+
$this->protocolCache?->set(
157+
$userEntity->getState(),
158+
$this->moduleConfig->getProtocolUserEntityCacheDuration(),
159+
$this->getCacheKey($userEntity->getIdentifier()),
122160
);
123161
}
124162
}

0 commit comments

Comments
 (0)