Skip to content

Commit e88aaf1

Browse files
[ACL-203] Add cache key suffix to differentiate between different credentials (#66)
1 parent 2bdbaed commit e88aaf1

File tree

3 files changed

+179
-4
lines changed

3 files changed

+179
-4
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
66
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.6.0] - 2024-10-10
9+
10+
### Added
11+
12+
- Added hash of clientId, scopes and clientSecret to cacheKey of AccessToken
13+
814
## [2.5.0] - 2024-08-13
915

1016
### Added
@@ -19,7 +25,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1925
### Added
2026

2127
- Support for `preselected` provider selection under the Bank Transfer payment method
22-
- Support for `preselected` payment scheme under the preselected provider selection
28+
- Support for `preselected` payment scheme under the preselected provider selection
2329

2430
## [2.3.0] - 2024-07-18
2531

src/Services/Auth/AccessToken.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ final class AccessToken implements AccessTokenInterface
5656
*/
5757
private ?int $retrievedAt = null;
5858

59+
/**
60+
* @var string|null
61+
*/
62+
private ?string $cacheSuffix = null;
63+
5964
/**
6065
* @param ApiClientInterface $api
6166
* @param EncryptedCacheInterface|null $cache
@@ -85,9 +90,9 @@ public function __construct(ApiClientInterface $api,
8590
public function getAccessToken(): ?string
8691
{
8792
if (!$this->accessToken) {
88-
if ($this->cache && $this->cache->has(CacheKeys::AUTH_TOKEN)) {
93+
if ($this->cache && $this->cache->has($this->getCacheKey())) {
8994
/** @var array{access_token: string, expires_in: int, retrieved_at: int} $data */
90-
$data = $this->cache->get(CacheKeys::AUTH_TOKEN);
95+
$data = $this->cache->get($this->getCacheKey());
9196

9297
$this->accessToken = $data['access_token'];
9398
$this->expiresIn = $data['expires_in'];
@@ -161,8 +166,28 @@ private function retrieve(): void
161166
$this->retrievedAt = (int) Carbon::now()->timestamp;
162167

163168
if ($this->cache) {
164-
$this->cache->set(CacheKeys::AUTH_TOKEN, $this->toArray(), $this->getExpiresIn());
169+
$this->cache->set($this->getCacheKey(), $this->toArray(), $this->getExpiresIn());
170+
}
171+
}
172+
173+
/**
174+
* @return string
175+
*/
176+
private function getCacheKey(): string
177+
{
178+
return CacheKeys::AUTH_TOKEN . ':' . $this->getCacheSuffix();
179+
}
180+
181+
/**
182+
* @return string
183+
*/
184+
private function getCacheSuffix(): string
185+
{
186+
if (!$this->cacheSuffix) {
187+
$this->cacheSuffix = \hash_hmac('sha256', \implode(',', [$this->clientId, ...$this->scopes]), $this->clientSecret);
165188
}
189+
190+
return $this->cacheSuffix;
166191
}
167192

168193
/**

tests/integration/ApiClient/AccessTokenTest.php

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,150 @@ function () {
131131
\expect($fooRequest->getHeaderLine('Authorization'))->not()->toBe('Bearer expired-token');
132132
});
133133

134+
\it('uses different cache key if client id is changed', function () {
135+
$okResponse = new Response(200);
136+
$encrypter = new TrueLayer\Services\Util\Encryption\Encrypter(\hex2bin('31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b'), TrueLayer\Constants\Encryption::ALGORITHM);
137+
$encryptedAccessToken = $encrypter->encrypt([
138+
'access_token' => Mocks\AuthResponse::ACCESS_TOKEN,
139+
'expires_in' => 3600,
140+
'retrieved_at' => (int) Carbon::now()->timestamp,
141+
]);
142+
143+
$cacheMock1 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
144+
$cacheMock1->shouldReceive('has')->andReturnTrue();
145+
$cacheMock1->shouldReceive('set')->andReturnTrue();
146+
$cacheMock1->shouldReceive('get')->andReturn($encryptedAccessToken);
147+
148+
$client1 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
149+
->cache($cacheMock1, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
150+
->clientId('client_id_1')
151+
->create()
152+
->getApiClient();
153+
$client1->request()->uri('/foo')->post();
154+
155+
$cacheKey1 = null;
156+
$cacheDefaultValue = 'not-null';
157+
158+
$cacheMock1->shouldHaveReceived('get', function (...$args) use (&$cacheKey1, &$cacheDefaultValue) {
159+
$cacheKey1 = $args[0];
160+
$cacheDefaultValue = $args[1];
161+
return true;
162+
});
163+
\expect($cacheKey1)->toBeString();
164+
\expect($cacheKey1)->not()->toBeEmpty();
165+
\expect($cacheDefaultValue)->toBeNull();
166+
167+
$cacheMock2 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
168+
$cacheMock2->shouldReceive('has')->andReturnTrue();
169+
$cacheMock2->shouldReceive('set')->andReturnTrue();
170+
$cacheMock2->shouldReceive('get')->andReturn($encryptedAccessToken);
171+
172+
$client2 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
173+
->cache($cacheMock2, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
174+
->clientId('client_id_2')
175+
->create()
176+
->getApiClient();
177+
$client2->request()->uri('/foo')->post();
178+
179+
$cacheMock2->shouldNotHaveReceived('get', [$cacheKey1, $cacheDefaultValue]);
180+
});
181+
182+
\it('uses different cache key if client secret is changed', function () {
183+
$okResponse = new Response(200);
184+
$encrypter = new TrueLayer\Services\Util\Encryption\Encrypter(\hex2bin('31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b'), TrueLayer\Constants\Encryption::ALGORITHM);
185+
$encryptedAccessToken = $encrypter->encrypt([
186+
'access_token' => Mocks\AuthResponse::ACCESS_TOKEN,
187+
'expires_in' => 3600,
188+
'retrieved_at' => (int) Carbon::now()->timestamp,
189+
]);
190+
191+
$cacheMock1 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
192+
$cacheMock1->shouldReceive('has')->andReturnTrue();
193+
$cacheMock1->shouldReceive('set')->andReturnTrue();
194+
$cacheMock1->shouldReceive('get')->andReturn($encryptedAccessToken);
195+
196+
$client1 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
197+
->cache($cacheMock1, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
198+
->clientSecret('client_secret_1')
199+
->create()
200+
->getApiClient();
201+
$client1->request()->uri('/foo')->post();
202+
203+
$cacheKey1 = null;
204+
$cacheDefaultValue = 'not-null';
205+
206+
$cacheMock1->shouldHaveReceived('get', function (...$args) use (&$cacheKey1, &$cacheDefaultValue) {
207+
$cacheKey1 = $args[0];
208+
$cacheDefaultValue = $args[1];
209+
return true;
210+
});
211+
\expect($cacheKey1)->toBeString();
212+
\expect($cacheKey1)->not()->toBeEmpty();
213+
\expect($cacheDefaultValue)->toBeNull();
214+
215+
$cacheMock2 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
216+
$cacheMock2->shouldReceive('has')->andReturnTrue();
217+
$cacheMock2->shouldReceive('set')->andReturnTrue();
218+
$cacheMock2->shouldReceive('get')->andReturn($encryptedAccessToken);
219+
220+
$client2 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
221+
->cache($cacheMock2, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
222+
->clientSecret('client_secret_2')
223+
->create()
224+
->getApiClient();
225+
$client2->request()->uri('/foo')->post();
226+
227+
$cacheMock2->shouldNotHaveReceived('get', [$cacheKey1, $cacheDefaultValue]);
228+
});
229+
230+
\it('uses different cache key if scopes are changed', function () {
231+
$okResponse = new Response(200);
232+
$encrypter = new TrueLayer\Services\Util\Encryption\Encrypter(\hex2bin('31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b'), TrueLayer\Constants\Encryption::ALGORITHM);
233+
$encryptedAccessToken = $encrypter->encrypt([
234+
'access_token' => Mocks\AuthResponse::ACCESS_TOKEN,
235+
'expires_in' => 3600,
236+
'retrieved_at' => (int) Carbon::now()->timestamp,
237+
]);
238+
239+
$cacheMock1 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
240+
$cacheMock1->shouldReceive('has')->andReturnTrue();
241+
$cacheMock1->shouldReceive('set')->andReturnTrue();
242+
$cacheMock1->shouldReceive('get')->andReturn($encryptedAccessToken);
243+
244+
$client1 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
245+
->cache($cacheMock1, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
246+
->scopes('payments')
247+
->create()
248+
->getApiClient();
249+
$client1->request()->uri('/foo')->post();
250+
251+
$cacheKey1 = null;
252+
$cacheDefaultValue = 'not-null';
253+
254+
$cacheMock1->shouldHaveReceived('get', function (...$args) use (&$cacheKey1, &$cacheDefaultValue) {
255+
$cacheKey1 = $args[0];
256+
$cacheDefaultValue = $args[1];
257+
return true;
258+
});
259+
\expect($cacheKey1)->toBeString();
260+
\expect($cacheKey1)->not()->toBeEmpty();
261+
\expect($cacheDefaultValue)->toBeNull();
262+
263+
$cacheMock2 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
264+
$cacheMock2->shouldReceive('has')->andReturnTrue();
265+
$cacheMock2->shouldReceive('set')->andReturnTrue();
266+
$cacheMock2->shouldReceive('get')->andReturn($encryptedAccessToken);
267+
268+
$client2 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
269+
->cache($cacheMock2, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
270+
->scopes('payments', 'accounts')
271+
->create()
272+
->getApiClient();
273+
$client2->request()->uri('/foo')->post();
274+
275+
$cacheMock2->shouldNotHaveReceived('get', [$cacheKey1, $cacheDefaultValue]);
276+
});
277+
134278
\it('uses default scope if none provided', function () {
135279
$client = \rawClient([Mocks\AuthResponse::success(), new Response(200)])->create();
136280
$client->getApiClient()->request()->uri('/test')->post();

0 commit comments

Comments
 (0)