Skip to content

Commit

Permalink
feat: cached openid-configuration
Browse files Browse the repository at this point in the history
unit tests enhancements & fully local
  • Loading branch information
hschoenenberger committed Dec 6, 2024
1 parent bc2866c commit ce26688
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 109 deletions.
2 changes: 1 addition & 1 deletion src/Provider/CachedFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class CachedFile
public function __construct($filename, $ttl = null)
{
$this->filename = $filename;
$this->ttl = $ttl;
$this->ttl = (int) $ttl;

$this->initDirectory();
$this->assertReadable();
Expand Down
43 changes: 36 additions & 7 deletions src/Provider/PrestaShop.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ class PrestaShop extends AbstractProvider
*/
protected $wellKnown;

/**
* @var CachedFile
*/
protected $cachedWellKnown;

/**
* @var bool
*/
Expand Down Expand Up @@ -91,10 +96,15 @@ public function getOauth2Url()
public function getWellKnown()
{
/* @phpstan-ignore-next-line */
if (!isset($this->wellKnown)) {
if (!isset($this->wellKnown) || $this->cachedWellKnown->isExpired()) {
try {
$this->wellKnown = new WellKnown(
$this->fetchWellKnown($this->getOauth2Url(), $this->verify)
json_decode(
$this->cachedWellKnown ?

Check failure on line 103 in src/Provider/PrestaShop.php

View workflow job for this annotation

GitHub Actions / PHPStan

Ternary operator condition is always true.

Check failure on line 103 in src/Provider/PrestaShop.php

View workflow job for this annotation

GitHub Actions / PHPStan

Ternary operator condition is always true.
$this->getCachedWellKnown() :
$this->fetchWellKnown($this->getOauth2Url()),
true
)
);
} catch (\Throwable $e) {
/* @phpstan-ignore-next-line */
Expand All @@ -109,14 +119,33 @@ public function getWellKnown()
}

/**
* @param string $url
* @param bool $secure
* @param bool $forceRefresh
*
* @return array
* @return string
*
* @throws \Exception
*/
protected function fetchWellKnown($url, $secure = true)
protected function getCachedWellKnown($forceRefresh = false)
{
if (null === $this->cachedWellKnown) {
throw new \Exception('Cache file not configured');
}

if ($this->cachedWellKnown->isExpired() || $forceRefresh) {
$this->cachedWellKnown->write(
$this->fetchWellKnown($this->getOauth2Url())
);
}

return $this->cachedWellKnown->read();
}

/**
* @param string $url
*
* @return string
*/
protected function fetchWellKnown($url)
{
$wellKnownUrl = $url;
if (strpos($wellKnownUrl, '/.well-known') === false) {
Expand All @@ -125,7 +154,7 @@ protected function fetchWellKnown($url, $secure = true)

$response = $this->getResponse($this->getRequest('GET', $wellKnownUrl));

return json_decode($response->getBody(), true);
return (string) $response->getBody();
}

/**
Expand Down
174 changes: 117 additions & 57 deletions tests/src/Provider/PrestaShopTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace PrestaShop\OAuth2\Client\Test\Provider;

use GuzzleHttp\ClientInterface;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
use PrestaShop\OAuth2\Client\Provider\CachedFile;
use PrestaShop\OAuth2\Client\Provider\PrestaShop;
use PrestaShop\OAuth2\Client\Provider\PrestaShopUser;
use PrestaShop\OAuth2\Client\Provider\WellKnown;
Expand All @@ -13,34 +13,124 @@
class PrestaShopTest extends TestCase
{
/**
* @var PrestaShop
* @var CachedFile
*/
private $provider;
private $cachedOpenIdConfiguration;

/**
* @var string
*/
private $wellKnown = <<<JSON
{
"authorization_endpoint": "https://oauth.foo.bar/oauth2/auth",
"token_endpoint": "https://oauth.foo.bar/oauth2/token",
"userinfo_endpoint": "https://oauth.foo.bar/userinfo",
"jwks_uri": "https://oauth.foo.bar/.well-known/jwks.json"
}
JSON;

/**
* @return void
*/
protected function setUp(): void
{
$this->provider = $this->getMockBuilder(PrestaShop::class)
->setConstructorArgs([[
'clientId' => 'test-client',
'clientSecret' => 'secret',
'redirectUri' => 'https://test-client-redirect.net',
'uiLocales' => ['fr-CA', 'en'],
'acrValues' => ['prompt:login'],
]])
->setMethods(['getWellKnown'])
->getMock();

$oauthUrl = 'https://oauth.foo.bar';

$this->provider->method('getWellKnown')
->willReturn(new WellKnown([
'authorization_endpoint' => $oauthUrl . '/oauth2/auth',
'token_endpoint' => $oauthUrl . '/oauth2/token',
'userinfo_endpoint' => $oauthUrl . '/userinfo',
]));
// $this->cachedJwks = new CachedFile($this->getTestBaseDir() . '/var/cache/jwks.json');
$this->cachedOpenIdConfiguration = new CachedFile(
$this->getTestBaseDir() . '/var/cache/openid-configuration.json', 15 * 60
);

$this->provider = new PrestaShop([
'clientId' => 'test-client',
'clientSecret' => 'secret',
'redirectUri' => 'https://test-client-redirect.net',
'cachedWellKnown' => $this->cachedOpenIdConfiguration,
'uiLocales' => ['fr-CA', 'en'],
'acrValues' => ['prompt:login'],
]);

$this->wellKnownResponse = $this->createMockResponse($this->wellKnown);
$this->cachedOpenIdConfiguration->clear();
$this->initHttpClient();
}

/**
* @test
*/
public function itShouldNotFailIfCachedFileNotConfigured()
{
$this->provider = new PrestaShop([
'clientId' => 'test-client',
'clientSecret' => 'secret',
'redirectUri' => 'https://test-client-redirect.net',
// 'cachedWellKnown' => $this->cachedOpenIdConfiguration,
'uiLocales' => ['fr-CA', 'en'],
'acrValues' => ['prompt:login'],
]);
$this->wellKnownResponse = $this->createMockResponse($this->wellKnown);
$this->initHttpClient();

$this->assertInstanceOf(WellKnown::class, $this->provider->getWellKnown());

$this->assertFalse(file_exists($this->cachedOpenIdConfiguration->getFilename()));
}

/**
* @test
*/
public function itShouldStoreCachedOpenIdConfiguration()
{
$this->assertFalse(file_exists($this->cachedOpenIdConfiguration->getFilename()));

$this->assertInstanceOf(WellKnown::class, $this->provider->getWellKnown());

$this->assertTrue(file_exists($this->cachedOpenIdConfiguration->getFilename()));
}

/**
* @test
*/
public function itShouldRefreshCachedOpenIdConfiguration()
{
$this->cachedOpenIdConfiguration = new CachedFile(
$this->getTestBaseDir() . '/var/cache/openid-configuration.json', 1
);

$this->provider = new PrestaShop([
'clientId' => 'test-client',
'clientSecret' => 'secret',
'redirectUri' => 'https://test-client-redirect.net',
'cachedWellKnown' => $this->cachedOpenIdConfiguration,
'uiLocales' => ['fr-CA', 'en'],
'acrValues' => ['prompt:login'],
]);
$this->cachedOpenIdConfiguration->clear();
$this->wellKnownResponse = $this->createMockResponse($this->wellKnown);
$this->initHttpClient();

$openIdConfiguration = $this->provider->getWellKnown();

$this->assertFalse($this->cachedOpenIdConfiguration->isExpired());
$this->assertInstanceOf(WellKnown::class, $openIdConfiguration);
$this->assertEquals('https://oauth.foo.bar/oauth2/auth', $openIdConfiguration->authorization_endpoint);

usleep(2000000);

$this->assertTrue($this->cachedOpenIdConfiguration->isExpired());

$this->wellKnownResponse = $this->createMockResponse(<<<JSON
{
"authorization_endpoint": "https://oauth-refreshed.foo.bar/oauth2/auth",
"token_endpoint": "https://oauth-refreshed.foo.bar/oauth2/token",
"userinfo_endpoint": "https://oauth-refreshed.foo.bar/userinfo",
"jwks_uri": "https://oauth-refreshed.prestashop.com/.well-known/jwks.json"
}
JSON
);

$openIdConfiguration = $this->provider->getWellKnown();

$this->assertInstanceOf(WellKnown::class, $openIdConfiguration);
$this->assertEquals('https://oauth-refreshed.foo.bar/oauth2/auth', $openIdConfiguration->authorization_endpoint);
}

/**
Expand Down Expand Up @@ -103,7 +193,7 @@ public function itShouldGetAuthorizationUrl()
*/
public function itShouldGetAccessTokenWithAuthorizationCode()
{
$response = $this->createMockResponse(<<<JSON
$this->accessTokenResponse = $this->createMockResponse(<<<JSON
{
"access_token": "mock_access_token",
"token_type": "bearer",
Expand All @@ -115,12 +205,6 @@ public function itShouldGetAccessTokenWithAuthorizationCode()
JSON
);

$client = $this->createMock(ClientInterface::class);
$client->method('send')
->willReturn($response);

$this->provider->setHttpClient($client);

$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);

$this->assertEquals('mock_access_token', $token->getToken());
Expand All @@ -134,7 +218,7 @@ public function itShouldGetAccessTokenWithAuthorizationCode()
*/
public function itShouldGetAccessTokenWithClientCredentials()
{
$response = $this->createMockResponse(<<<JSON
$this->accessTokenResponse = $this->createMockResponse(<<<JSON
{
"access_token": "mock_access_token",
"token_type": "bearer",
Expand All @@ -144,12 +228,6 @@ public function itShouldGetAccessTokenWithClientCredentials()
}
JSON
);
$client = $this->createMock(ClientInterface::class);
$client->method('send')
->withConsecutive([])
->willReturn($response);

$this->provider->setHttpClient($client);

$token = $this->provider->getAccessToken('client_credentials');

Expand All @@ -164,7 +242,7 @@ public function itShouldGetAccessTokenWithClientCredentials()
*/
public function itShouldGetResourceOwner()
{
$response = $this->createMockResponse(<<<JSON
$this->resourceOwnerResponse = $this->createMockResponse(<<<JSON
{
"sub": "4rFN5bm2piPeHTYUFtUIwcyFKKKOp",
"email": "[email protected]",
Expand All @@ -175,12 +253,6 @@ public function itShouldGetResourceOwner()
JSON
);

$client = $this->createMock(ClientInterface::class);
$client->method('send')
->willReturn($response);

$this->provider->setHttpClient($client);

$accessToken = $this->createMock(AccessToken::class);
$accessToken->method('getToken')
->willReturn('mock_access_token');
Expand All @@ -201,20 +273,14 @@ public function itShouldGetResourceOwner()
*/
public function itShouldHandleErrors()
{
$response = $this->createMockResponse(<<<JSON
$this->accessTokenResponse = $this->createMockResponse(<<<JSON
{
"error_description": "This is the description",
"error": "error_name"
}
JSON
, 403);

$client = $this->createMock(ClientInterface::class);
$client->method('send')
->willReturn($response);

$this->provider->setHttpClient($client);

$this->expectException(IdentityProviderException::class);
$this->expectExceptionMessage('403 - error_name: This is the description');
$this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
Expand All @@ -225,13 +291,7 @@ public function itShouldHandleErrors()
*/
public function itShouldHandleEmptyErrors()
{
$response = $this->createMockResponse('{}', 403);

$client = $this->createMock(ClientInterface::class);
$client->method('send')
->willReturn($response);

$this->provider->setHttpClient($client);
$this->accessTokenResponse = $this->createMockResponse('{}', 403);

$this->expectException(IdentityProviderException::class);
$this->expectExceptionMessage('403 - : ');
Expand Down
Loading

0 comments on commit ce26688

Please sign in to comment.