Skip to content

Commit

Permalink
[ACL-203] Add cache key suffix to differentiate between different cre…
Browse files Browse the repository at this point in the history
…dentials (#66)
  • Loading branch information
artyom-jaksov-tl authored Oct 10, 2024
1 parent 2bdbaed commit e88aaf1
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 4 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.6.0] - 2024-10-10

### Added

- Added hash of clientId, scopes and clientSecret to cacheKey of AccessToken

## [2.5.0] - 2024-08-13

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

- Support for `preselected` provider selection under the Bank Transfer payment method
- Support for `preselected` payment scheme under the preselected provider selection
- Support for `preselected` payment scheme under the preselected provider selection

## [2.3.0] - 2024-07-18

Expand Down
31 changes: 28 additions & 3 deletions src/Services/Auth/AccessToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ final class AccessToken implements AccessTokenInterface
*/
private ?int $retrievedAt = null;

/**
* @var string|null
*/
private ?string $cacheSuffix = null;

/**
* @param ApiClientInterface $api
* @param EncryptedCacheInterface|null $cache
Expand Down Expand Up @@ -85,9 +90,9 @@ public function __construct(ApiClientInterface $api,
public function getAccessToken(): ?string
{
if (!$this->accessToken) {
if ($this->cache && $this->cache->has(CacheKeys::AUTH_TOKEN)) {
if ($this->cache && $this->cache->has($this->getCacheKey())) {
/** @var array{access_token: string, expires_in: int, retrieved_at: int} $data */
$data = $this->cache->get(CacheKeys::AUTH_TOKEN);
$data = $this->cache->get($this->getCacheKey());

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

if ($this->cache) {
$this->cache->set(CacheKeys::AUTH_TOKEN, $this->toArray(), $this->getExpiresIn());
$this->cache->set($this->getCacheKey(), $this->toArray(), $this->getExpiresIn());
}
}

/**
* @return string
*/
private function getCacheKey(): string
{
return CacheKeys::AUTH_TOKEN . ':' . $this->getCacheSuffix();
}

/**
* @return string
*/
private function getCacheSuffix(): string
{
if (!$this->cacheSuffix) {
$this->cacheSuffix = \hash_hmac('sha256', \implode(',', [$this->clientId, ...$this->scopes]), $this->clientSecret);
}

return $this->cacheSuffix;
}

/**
Expand Down
144 changes: 144 additions & 0 deletions tests/integration/ApiClient/AccessTokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,150 @@ function () {
\expect($fooRequest->getHeaderLine('Authorization'))->not()->toBe('Bearer expired-token');
});

\it('uses different cache key if client id is changed', function () {
$okResponse = new Response(200);
$encrypter = new TrueLayer\Services\Util\Encryption\Encrypter(\hex2bin('31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b'), TrueLayer\Constants\Encryption::ALGORITHM);
$encryptedAccessToken = $encrypter->encrypt([
'access_token' => Mocks\AuthResponse::ACCESS_TOKEN,
'expires_in' => 3600,
'retrieved_at' => (int) Carbon::now()->timestamp,
]);

$cacheMock1 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
$cacheMock1->shouldReceive('has')->andReturnTrue();
$cacheMock1->shouldReceive('set')->andReturnTrue();
$cacheMock1->shouldReceive('get')->andReturn($encryptedAccessToken);

$client1 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
->cache($cacheMock1, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
->clientId('client_id_1')
->create()
->getApiClient();
$client1->request()->uri('/foo')->post();

$cacheKey1 = null;
$cacheDefaultValue = 'not-null';

$cacheMock1->shouldHaveReceived('get', function (...$args) use (&$cacheKey1, &$cacheDefaultValue) {
$cacheKey1 = $args[0];
$cacheDefaultValue = $args[1];
return true;
});
\expect($cacheKey1)->toBeString();
\expect($cacheKey1)->not()->toBeEmpty();
\expect($cacheDefaultValue)->toBeNull();

$cacheMock2 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
$cacheMock2->shouldReceive('has')->andReturnTrue();
$cacheMock2->shouldReceive('set')->andReturnTrue();
$cacheMock2->shouldReceive('get')->andReturn($encryptedAccessToken);

$client2 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
->cache($cacheMock2, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
->clientId('client_id_2')
->create()
->getApiClient();
$client2->request()->uri('/foo')->post();

$cacheMock2->shouldNotHaveReceived('get', [$cacheKey1, $cacheDefaultValue]);
});

\it('uses different cache key if client secret is changed', function () {
$okResponse = new Response(200);
$encrypter = new TrueLayer\Services\Util\Encryption\Encrypter(\hex2bin('31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b'), TrueLayer\Constants\Encryption::ALGORITHM);
$encryptedAccessToken = $encrypter->encrypt([
'access_token' => Mocks\AuthResponse::ACCESS_TOKEN,
'expires_in' => 3600,
'retrieved_at' => (int) Carbon::now()->timestamp,
]);

$cacheMock1 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
$cacheMock1->shouldReceive('has')->andReturnTrue();
$cacheMock1->shouldReceive('set')->andReturnTrue();
$cacheMock1->shouldReceive('get')->andReturn($encryptedAccessToken);

$client1 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
->cache($cacheMock1, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
->clientSecret('client_secret_1')
->create()
->getApiClient();
$client1->request()->uri('/foo')->post();

$cacheKey1 = null;
$cacheDefaultValue = 'not-null';

$cacheMock1->shouldHaveReceived('get', function (...$args) use (&$cacheKey1, &$cacheDefaultValue) {
$cacheKey1 = $args[0];
$cacheDefaultValue = $args[1];
return true;
});
\expect($cacheKey1)->toBeString();
\expect($cacheKey1)->not()->toBeEmpty();
\expect($cacheDefaultValue)->toBeNull();

$cacheMock2 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
$cacheMock2->shouldReceive('has')->andReturnTrue();
$cacheMock2->shouldReceive('set')->andReturnTrue();
$cacheMock2->shouldReceive('get')->andReturn($encryptedAccessToken);

$client2 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
->cache($cacheMock2, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
->clientSecret('client_secret_2')
->create()
->getApiClient();
$client2->request()->uri('/foo')->post();

$cacheMock2->shouldNotHaveReceived('get', [$cacheKey1, $cacheDefaultValue]);
});

\it('uses different cache key if scopes are changed', function () {
$okResponse = new Response(200);
$encrypter = new TrueLayer\Services\Util\Encryption\Encrypter(\hex2bin('31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b'), TrueLayer\Constants\Encryption::ALGORITHM);
$encryptedAccessToken = $encrypter->encrypt([
'access_token' => Mocks\AuthResponse::ACCESS_TOKEN,
'expires_in' => 3600,
'retrieved_at' => (int) Carbon::now()->timestamp,
]);

$cacheMock1 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
$cacheMock1->shouldReceive('has')->andReturnTrue();
$cacheMock1->shouldReceive('set')->andReturnTrue();
$cacheMock1->shouldReceive('get')->andReturn($encryptedAccessToken);

$client1 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
->cache($cacheMock1, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
->scopes('payments')
->create()
->getApiClient();
$client1->request()->uri('/foo')->post();

$cacheKey1 = null;
$cacheDefaultValue = 'not-null';

$cacheMock1->shouldHaveReceived('get', function (...$args) use (&$cacheKey1, &$cacheDefaultValue) {
$cacheKey1 = $args[0];
$cacheDefaultValue = $args[1];
return true;
});
\expect($cacheKey1)->toBeString();
\expect($cacheKey1)->not()->toBeEmpty();
\expect($cacheDefaultValue)->toBeNull();

$cacheMock2 = Mockery::mock(Psr\SimpleCache\CacheInterface::class);
$cacheMock2->shouldReceive('has')->andReturnTrue();
$cacheMock2->shouldReceive('set')->andReturnTrue();
$cacheMock2->shouldReceive('get')->andReturn($encryptedAccessToken);

$client2 = \rawClient([Mocks\AuthResponse::success(), $okResponse, $okResponse])
->cache($cacheMock2, '31c8d81a110849f83131541b9f67c3cba9c7e0bb103bc4dd19377f0fdf2d924b')
->scopes('payments', 'accounts')
->create()
->getApiClient();
$client2->request()->uri('/foo')->post();

$cacheMock2->shouldNotHaveReceived('get', [$cacheKey1, $cacheDefaultValue]);
});

\it('uses default scope if none provided', function () {
$client = \rawClient([Mocks\AuthResponse::success(), new Response(200)])->create();
$client->getApiClient()->request()->uri('/test')->post();
Expand Down

0 comments on commit e88aaf1

Please sign in to comment.