diff --git a/.github/workflows/conflicting-pr-label.yml b/.github/workflows/conflicting-pr-label.yml index da6e5f3..fb4a14e 100644 --- a/.github/workflows/conflicting-pr-label.yml +++ b/.github/workflows/conflicting-pr-label.yml @@ -11,6 +11,10 @@ on: types: [synchronize] branches: [ main ] +permissions: + pull-requests: write + contents: read + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 292d771..2ad69b1 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -7,6 +7,10 @@ on: pull_request: branches: [ main, dev ] +permissions: + contents: read + pull-requests: read + jobs: build: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c17511..902ba91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +## [1.1.0] + +### Added +- Enables initialising the `InMemoryAccessTokenCache` with tokens for re-use by the auth provider +- Exposes the access token cache used in the `PhpLeagueAccessTokenProvider` using `getAccessTokenCache()` + ## [1.0.2] ### Changed diff --git a/README.md b/README.md index c33424b..981110d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ run `composer require microsoft/kiota-authentication-phpleague` or add the follo ```Shell { "require": { - "microsoft/kiota-authentication-phpleague": "^1.0.1" + "microsoft/kiota-authentication-phpleague": "^1.1.0" } } ``` diff --git a/src/Cache/InMemoryAccessTokenCache.php b/src/Cache/InMemoryAccessTokenCache.php index 0e94268..c427ab9 100644 --- a/src/Cache/InMemoryAccessTokenCache.php +++ b/src/Cache/InMemoryAccessTokenCache.php @@ -2,7 +2,9 @@ namespace Microsoft\Kiota\Authentication\Cache; +use InvalidArgumentException; use League\OAuth2\Client\Token\AccessToken; +use Microsoft\Kiota\Authentication\Oauth\TokenRequestContext; /** * Class InMemoryAccessTokenCache @@ -21,13 +23,76 @@ class InMemoryAccessTokenCache implements AccessTokenCache */ private array $accessTokens = []; + /** + * Initializes the InMemoryAccessTokenCache with an access token and its related context. To add more access tokens + * use withToken(). + * + * @param TokenRequestContext|null $tokenRequestContext + * @param AccessToken|null $accessToken + */ + public function __construct(?TokenRequestContext $tokenRequestContext = null, ?AccessToken $accessToken = null) + { + if ($tokenRequestContext && $accessToken) { + $this->withToken($tokenRequestContext, $accessToken); + } + } + + /** + * Initializes the InMemoryAccessTokenCache with an access token and its related context. + * + * @param TokenRequestContext $tokenRequestContext + * @param AccessToken $accessToken + * @return self + * @throws InvalidArgumentException if the cache key cannot be initialized + * OR the cache already contains an access token with the same identity/TokenRequestContext + */ + public function withToken(TokenRequestContext $tokenRequestContext, AccessToken $accessToken): self + { + $tokenRequestContext->setCacheKey($accessToken); + if (!$tokenRequestContext->getCacheKey()) { + throw new InvalidArgumentException("Unable to initialize cache key for context using access token"); + } + if (array_key_exists($tokenRequestContext->getCacheKey(), $this->accessTokens)) { + throw new InvalidArgumentException("Cache already contains an access token with the same identity"); + } + $this->accessTokens[$tokenRequestContext->getCacheKey()] = $accessToken; + return $this; + } + + /** + * Returns the access token with the given identity from the cache + * + * @param string $identity + * @return AccessToken|null + */ public function getAccessToken(string $identity): ?AccessToken { return $this->accessTokens[$identity] ?? null; } + /** + * Adds an access token with the given identity to the cache + * + * @param string $identity + * @param AccessToken $accessToken + * @return void + */ public function persistAccessToken(string $identity, AccessToken $accessToken): void { $this->accessTokens[$identity] = $accessToken; } + + /** + * Returns the access token given the token request context + * + * @param TokenRequestContext $tokenRequestContext + * @return AccessToken|null + * @throws InvalidArgumentException if $tokenRequestContext has a null cache key + */ + public function getTokenWithContext(TokenRequestContext $tokenRequestContext): ?AccessToken { + if (is_null($tokenRequestContext->getCacheKey())) { + throw new InvalidArgumentException("Unable to get token using context with a null cache key"); + } + return $this->getAccessToken($tokenRequestContext->getCacheKey()); + } } diff --git a/src/Constants.php b/src/Constants.php index 5fc7480..4709496 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -4,5 +4,5 @@ class Constants { - public const VERSION = "1.0.2"; + public const VERSION = "1.1.0"; } diff --git a/src/PhpLeagueAccessTokenProvider.php b/src/PhpLeagueAccessTokenProvider.php index 4d3993a..6d8f9f9 100644 --- a/src/PhpLeagueAccessTokenProvider.php +++ b/src/PhpLeagueAccessTokenProvider.php @@ -211,6 +211,40 @@ public function getOauthProvider(): AbstractProvider return $this->oauthProvider; } + /** + * Returns the underlying cache + * + * @return AccessTokenCache + */ + public function getAccessTokenCache(): AccessTokenCache + { + return $this->accessTokenCache; + } + + /** + * @return TokenRequestContext + */ + public function getTokenRequestContext(): TokenRequestContext + { + return $this->tokenRequestContext; + } + + /** + * @return array + */ + public function getScopes(): array + { + return $this->scopes; + } + + /** + * @return array + */ + public function getAllowedHosts(): array + { + return $this->allowedHostsValidator->getAllowedHosts(); + } + /** * Attempts to cache the access token if the TokenRequestContext provides a cache key * @param AccessToken $token diff --git a/src/PhpLeagueAuthenticationProvider.php b/src/PhpLeagueAuthenticationProvider.php index 3075788..bf1640b 100644 --- a/src/PhpLeagueAuthenticationProvider.php +++ b/src/PhpLeagueAuthenticationProvider.php @@ -45,4 +45,23 @@ public function getAccessTokenProvider(): PhpLeagueAccessTokenProvider return $this->accessTokenProvider; } + /** + * Get an instance of PhpLeagueAuthenticationProvider your custom PhpLeagueAccessTokenProvider + * + * @param PhpLeagueAccessTokenProvider $phpLeagueAccessTokenProvider + * @return self + */ + public static function createWithAccessTokenProvider( + PhpLeagueAccessTokenProvider $phpLeagueAccessTokenProvider + ): self + { + $authProvider = new PhpLeagueAuthenticationProvider( + $phpLeagueAccessTokenProvider->getTokenRequestContext(), + $phpLeagueAccessTokenProvider->getScopes(), + $phpLeagueAccessTokenProvider->getAllowedHosts() + ); + $authProvider->accessTokenProvider = $phpLeagueAccessTokenProvider; + return $authProvider; + } + } diff --git a/tests/Cache/InMemoryAccessTokenCacheTest.php b/tests/Cache/InMemoryAccessTokenCacheTest.php new file mode 100644 index 0000000..3e1b127 --- /dev/null +++ b/tests/Cache/InMemoryAccessTokenCacheTest.php @@ -0,0 +1,61 @@ +testTokenRequestContext = new ClientCredentialContext("tenantId", "clientId", "clientSecret"); + } + + public function testConstructorWorksWithEmptyArguments() { + $cache = new InMemoryAccessTokenCache(); + $this->assertInstanceOf(InMemoryAccessTokenCache::class, $cache); + } + + public function testConstructorInitialisesCache() { + $cache = new InMemoryAccessTokenCache($this->testTokenRequestContext, $this->createMock(AccessToken::class)); + $this->assertInstanceOf(AccessToken::class, $cache->getTokenWithContext($this->testTokenRequestContext)); + } + + public function tesWithTokenInitialisesCache() { + $cache = new InMemoryAccessTokenCache(); + $cache->withToken($this->testTokenRequestContext, $this->createMock(AccessToken::class)); + $this->assertInstanceOf(AccessToken::class, $cache->getTokenWithContext($this->testTokenRequestContext)); + } + + public function testWithTokenThrowsExceptionIfCacheKeyCannotBeInitialised() { + $tokenRequestContext = $this->createMock(TokenRequestContext::class); + $tokenRequestContext->method('getCacheKey')->willReturn(null); + $cache = new InMemoryAccessTokenCache(); + $this->expectException(\InvalidArgumentException::class); + $cache->withToken($tokenRequestContext, $this->createMock(AccessToken::class)); + } + + public function testWithTokenThrowsExceptionIfTokenRequestContextAlreadyExists() { + $cache = new InMemoryAccessTokenCache($this->testTokenRequestContext, $this->createMock(AccessToken::class)); + $this->expectException(\InvalidArgumentException::class); + $cache->withToken($this->testTokenRequestContext, $this->createMock(AccessToken::class)); + } + + public function testWithTokenAddsMultipleTokensToCache() { + $secondContext = $this->createMock(TokenRequestContext::class); + $secondContext->method('getCacheKey')->willReturn('second-key'); + + $cache = (new InMemoryAccessTokenCache()) + ->withToken($this->testTokenRequestContext, $this->createMock(AccessToken::class)) + ->withToken($secondContext, $this->createMock(AccessToken::class)); + + $this->assertInstanceOf(AccessToken::class, $cache->getTokenWithContext($this->testTokenRequestContext)); + $this->assertInstanceOf(AccessToken::class, $cache->getTokenWithContext($secondContext)); + } +} diff --git a/tests/PhpLeagueAccessTokenProviderTest.php b/tests/PhpLeagueAccessTokenProviderTest.php index 436bfa1..7cdd8b5 100644 --- a/tests/PhpLeagueAccessTokenProviderTest.php +++ b/tests/PhpLeagueAccessTokenProviderTest.php @@ -10,6 +10,7 @@ use Http\Promise\FulfilledPromise; use InvalidArgumentException; use League\OAuth2\Client\Token\AccessToken; +use Microsoft\Kiota\Authentication\Cache\InMemoryAccessTokenCache; use Microsoft\Kiota\Authentication\Oauth\AuthorizationCodeCertificateContext; use Microsoft\Kiota\Authentication\Oauth\AuthorizationCodeContext; use Microsoft\Kiota\Authentication\Oauth\ClientCredentialCertificateContext; @@ -100,11 +101,10 @@ public function testGetAuthorizationTokenUsesCachedToken(): void $oauthContexts = $this->getOauthContexts(); /** @var TokenRequestContext $tokenRequestContext */ foreach ($oauthContexts as $tokenRequestContext) { - $tokenProvider = new PhpLeagueAccessTokenProvider($tokenRequestContext, [], [], null, $stubTokenCache = new StubAccessTokenCache()); - $tokenRequestContext->setCacheKey(new AccessToken(['access_token' => $this->testJWT])); - $stubTokenCache->accessTokens[$tokenRequestContext->getCacheKey()] = new AccessToken(['access_token' => $this->testJWT, 'expires' => time() + 5]); + $cache = new InMemoryAccessTokenCache($tokenRequestContext, new AccessToken(['access_token' => $this->testJWT, 'expires' => time() + 5])); + $tokenProvider = new PhpLeagueAccessTokenProvider($tokenRequestContext, [], [], null, $cache); $mockResponses = [ - new Response(200, [], json_encode(['access_token' => 'abc', 'expires_in' => 5])), + new Response(400), ]; $tokenProvider->getOauthProvider()->setHttpClient($this->getMockHttpClient($mockResponses)); $this->assertEquals($this->testJWT, $tokenProvider->getAuthorizationTokenAsync('https://graph.microsoft.com')->wait()); @@ -116,13 +116,14 @@ public function testNewAccessTokenIsUpdatedToTheCache(): void $oauthContexts = $this->getOauthContexts(); /** @var TokenRequestContext $tokenRequestContext */ foreach ($oauthContexts as $tokenRequestContext) { - $tokenProvider = new PhpLeagueAccessTokenProvider($tokenRequestContext, [], [], null, $stubTokenCache = new StubAccessTokenCache()); + $cache = new InMemoryAccessTokenCache(); + $tokenProvider = new PhpLeagueAccessTokenProvider($tokenRequestContext, [], [], null, $cache); $mockResponses = [ new Response(200, [], json_encode(['access_token' => $this->testJWT, 'expires_in' => 5])), ]; $tokenProvider->getOauthProvider()->setHttpClient($this->getMockHttpClient($mockResponses)); $this->assertEquals($this->testJWT, $tokenProvider->getAuthorizationTokenAsync('https://graph.microsoft.com')->wait()); - $this->assertEquals($this->testJWT, $stubTokenCache->accessTokens[$tokenRequestContext->getCacheKey()]->getToken()); + $this->assertEquals($this->testJWT, $cache->getTokenWithContext($tokenRequestContext)); } } diff --git a/tests/PhpLeagueAuthenticationProviderTest.php b/tests/PhpLeagueAuthenticationProviderTest.php index 1858b9f..ab2c132 100644 --- a/tests/PhpLeagueAuthenticationProviderTest.php +++ b/tests/PhpLeagueAuthenticationProviderTest.php @@ -3,6 +3,7 @@ namespace Microsoft\Kiota\Authentication\Test; use Microsoft\Kiota\Authentication\Oauth\ClientCredentialContext; +use Microsoft\Kiota\Authentication\PhpLeagueAccessTokenProvider; use Microsoft\Kiota\Authentication\PhpLeagueAuthenticationProvider; use PHPUnit\Framework\TestCase; @@ -23,4 +24,23 @@ public function testCorrectOauthProviderEndpointsExposed(): void $expected = "https://login.microsoftonline.com/tenantId/oauth2/v2.0/authorize"; $this->assertEquals($expected, $this->defaultAuthProvider->getAccessTokenProvider()->getOauthProvider()->getBaseAuthorizationUrl()); } + + public function testCreateWithAccessTokenProvider(): void + { + $context = new ClientCredentialContext('tenantId', 'clientId', 'secret'); + $scopes = ['https://graph.microsoft.com/.default']; + $allowedHosts = []; + $authenticationProvider = PhpLeagueAuthenticationProvider::createWithAccessTokenProvider( + new PhpLeagueAccessTokenProvider( + $context, + $scopes, + $allowedHosts + ) + ); + $this->assertInstanceOf(PhpLeagueAuthenticationProvider::class, $authenticationProvider); + $this->assertInstanceOf(PhpLeagueAccessTokenProvider::class, $authenticationProvider->getAccessTokenProvider()); + $this->assertEquals($context, $authenticationProvider->getAccessTokenProvider()->getTokenRequestContext()); + $this->assertEquals($scopes, $authenticationProvider->getAccessTokenProvider()->getScopes()); + $this->assertEquals($allowedHosts, $authenticationProvider->getAccessTokenProvider()->getAllowedHosts()); + } } diff --git a/tests/Stub/StubAccessTokenCache.php b/tests/Stub/StubAccessTokenCache.php deleted file mode 100644 index 5c046b9..0000000 --- a/tests/Stub/StubAccessTokenCache.php +++ /dev/null @@ -1,24 +0,0 @@ - - */ - public array $accessTokens = []; - - public function getAccessToken(string $identity): ?AccessToken - { - return $this->accessTokens[$identity] ?? null; - } - - public function persistAccessToken(string $identity, AccessToken $accessToken): void - { - $this->accessTokens[$identity] = $accessToken; - } -}