Skip to content

Commit

Permalink
Merge pull request #72 from microsoft/feat/token-reuse
Browse files Browse the repository at this point in the history
Feat/token reuse
  • Loading branch information
Ndiritu authored Feb 23, 2024
2 parents bfa6d02 + 36efdac commit cfd4506
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 32 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
```
Expand Down
65 changes: 65 additions & 0 deletions src/Cache/InMemoryAccessTokenCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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());
}
}
2 changes: 1 addition & 1 deletion src/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

class Constants
{
public const VERSION = "1.0.2";
public const VERSION = "1.1.0";
}
34 changes: 34 additions & 0 deletions src/PhpLeagueAccessTokenProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
*/
public function getScopes(): array
{
return $this->scopes;
}

/**
* @return array<string>
*/
public function getAllowedHosts(): array
{
return $this->allowedHostsValidator->getAllowedHosts();
}

/**
* Attempts to cache the access token if the TokenRequestContext provides a cache key
* @param AccessToken $token
Expand Down
19 changes: 19 additions & 0 deletions src/PhpLeagueAuthenticationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
61 changes: 61 additions & 0 deletions tests/Cache/InMemoryAccessTokenCacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace Microsoft\Kiota\Authentication\Test\Cache;

use League\OAuth2\Client\Token\AccessToken;
use Microsoft\Kiota\Authentication\Cache\InMemoryAccessTokenCache;
use Microsoft\Kiota\Authentication\Oauth\ClientCredentialContext;
use Microsoft\Kiota\Authentication\Oauth\TokenRequestContext;
use PHPUnit\Framework\TestCase;

class InMemoryAccessTokenCacheTest extends TestCase
{
private ClientCredentialContext $testTokenRequestContext;
private string $testTokenRequestContextCacheKey = "tenantId-clientId";

public function setUp(): void {
$this->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));
}
}
13 changes: 7 additions & 6 deletions tests/PhpLeagueAccessTokenProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand All @@ -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));
}
}

Expand Down
20 changes: 20 additions & 0 deletions tests/PhpLeagueAuthenticationProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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());
}
}
24 changes: 0 additions & 24 deletions tests/Stub/StubAccessTokenCache.php

This file was deleted.

0 comments on commit cfd4506

Please sign in to comment.