Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Refactor Auth Token Middleware's token logic #492

Merged
merged 10 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 27 additions & 20 deletions src/Middleware/AuthTokenMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@

namespace Google\Auth\Middleware;

use Google\Auth\FetchAuthTokenCache;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\GetQuotaProjectInterface;
use Google\Auth\UpdateMetadataInterface;
use GuzzleHttp\Psr7\Utils;
use Psr\Http\Message\RequestInterface;

/**
Expand All @@ -40,6 +43,9 @@ class AuthTokenMiddleware
private $httpHandler;

/**
* It must be an implementation of FetchAuthTokenInterface.
* It may also implement UpdateMetadataInterface allowing direct
* retrieval of auth related headers
* @var FetchAuthTokenInterface
*/
private $fetcher;
Expand Down Expand Up @@ -99,7 +105,7 @@ public function __invoke(callable $handler)
return $handler($request, $options);
}

$request = $request->withHeader('authorization', 'Bearer ' . $this->fetchToken());
yash30201 marked this conversation as resolved.
Show resolved Hide resolved
$request = $this->addAuthHeaders($request);

if ($quotaProject = $this->getQuotaProject()) {
$request = $request->withHeader(
Expand All @@ -113,32 +119,33 @@ public function __invoke(callable $handler)
}

/**
* Call fetcher to fetch the token.
* Adds auth related headers to the request.
*
* @return string|null
* @param RequestInterface $request
* @return RequestInterface
*/
private function fetchToken()
private function addAuthHeaders(RequestInterface $request)
{
$auth_tokens = (array) $this->fetcher->fetchAuthToken($this->httpHandler);

if (array_key_exists('access_token', $auth_tokens)) {
// notify the callback if applicable
if ($this->tokenCallback) {
call_user_func(
$this->tokenCallback,
$this->fetcher->getCacheKey(),
$auth_tokens['access_token']
);
}

return $auth_tokens['access_token'];
if (!$this->fetcher instanceof UpdateMetadataInterface ||
($this->fetcher instanceof FetchAuthTokenCache &&
!$this->fetcher->getFetcher() instanceof UpdateMetadataInterface)
) {
$token = $this->fetcher->fetchAuthToken();
$request = $request->withHeader(
'authorization', 'Bearer ' . ($token['access_token'] ?? $token['id_token'])
);
} else {
$headers = $this->fetcher->updateMetadata($request->getHeaders(), null, $this->httpHandler);
$request = Utils::modifyRequest($request, ['set_headers' => $headers]);
}

if (array_key_exists('id_token', $auth_tokens)) {
return $auth_tokens['id_token'];
if ($this->tokenCallback && ($token = $this->fetcher->getLastReceivedToken())) {
if (array_key_exists('access_token', $token)) {
call_user_func($this->tokenCallback, $this->fetcher->getCacheKey(), $token['access_token']);
}
}

return null;
return $request;
}

/**
Expand Down
17 changes: 14 additions & 3 deletions tests/FetchAuthTokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Google\Auth\CredentialsLoader;
use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\OAuth2;
use Google\Auth\UpdateMetadataInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;

Expand Down Expand Up @@ -53,10 +54,20 @@ class_implements($fetcherClass)
)) {
$mockFetcher->getQuotaProject()->shouldBeCalledTimes(1);
}
$mockFetcher->fetchAuthToken(Argument::any())
->shouldBeCalledTimes(1)
->will($httpHandler);

if (is_a($fetcherClass, UpdateMetadataInterface::class, true)) {
bshaffer marked this conversation as resolved.
Show resolved Hide resolved
$mockFetcher->updateMetadata(Argument::cetera())
->shouldBeCalledTimes(1)->will(function () use (&$httpHandlerCalled) {
$httpHandlerCalled = true;
return ['authorization' => ['Bearer xyz']];
});
} else {
$mockFetcher->fetchAuthToken(Argument::any())
->shouldBeCalledTimes(1)
->will($httpHandler);
}
$mockFetcher->getCacheKey()->willReturn('');
$mockFetcher->getLastReceivedToken()->willReturn(['access_token' => 'xyz']);

$tokenCallbackCalled = false;
$tokenCallback = function ($cacheKey, $accessToken) use (&$tokenCallbackCalled) {
Expand Down
109 changes: 78 additions & 31 deletions tests/Middleware/AuthTokenMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
use Google\Auth\FetchAuthTokenCache;
use Google\Auth\Middleware\AuthTokenMiddleware;
use Google\Auth\Tests\BaseTest;
use Google\Auth\UpdateMetadataInterface;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
Expand Down Expand Up @@ -64,11 +66,7 @@ public function testAddsTheTokenAsAnAuthorizationHeader()
->shouldBeCalledTimes(1)
->willReturn($this->mockRequest->reveal());

// Run the test.
$middleware = new AuthTokenMiddleware($this->mockFetcher->reveal());
$mock = new MockHandler([new Response(200)]);
$callable = $middleware($mock);
$callable($this->mockRequest->reveal(), ['auth' => 'google_auth']);
$this->runTestCase($this->mockFetcher->reveal());
}

public function testDoesNotAddAnAuthorizationHeaderOnNoAccessToken()
Expand All @@ -80,11 +78,7 @@ public function testDoesNotAddAnAuthorizationHeaderOnNoAccessToken()
$this->mockRequest->withHeader('authorization', 'Bearer ')
->willReturn($this->mockRequest->reveal());

// Run the test.
$middleware = new AuthTokenMiddleware($this->mockFetcher->reveal());
$mock = new MockHandler([new Response(200)]);
$callable = $middleware($mock);
$callable($this->mockRequest->reveal(), ['auth' => 'google_auth']);
$this->runTestCase($this->mockFetcher->reveal());
}

public function testUsesIdTokenWhenAccessTokenDoesNotExist()
Expand All @@ -96,12 +90,10 @@ public function testUsesIdTokenWhenAccessTokenDoesNotExist()
->willReturn($authResult);
$this->mockRequest->withHeader('authorization', 'Bearer ' . $token)
->shouldBeCalledTimes(1)
->willReturn($this->mockRequest);
->willReturn($this->mockRequest->reveal());

$this->runTestCase($this->mockFetcher->reveal());

$middleware = new AuthTokenMiddleware($this->mockFetcher->reveal());
$mock = new MockHandler([new Response(200)]);
$callable = $middleware($mock);
$callable($this->mockRequest->reveal(), ['auth' => 'google_auth']);
}

public function testUsesCachedAccessToken()
Expand Down Expand Up @@ -133,10 +125,7 @@ public function testUsesCachedAccessToken()
null,
$this->mockCache->reveal()
);
$middleware = new AuthTokenMiddleware($cachedFetcher);
$mock = new MockHandler([new Response(200)]);
$callable = $middleware($mock);
$callable($this->mockRequest->reveal(), ['auth' => 'google_auth']);
$this->runTestCase($cachedFetcher);
}

public function testUsesCachedIdToken()
Expand Down Expand Up @@ -168,10 +157,7 @@ public function testUsesCachedIdToken()
null,
$this->mockCache->reveal()
);
$middleware = new AuthTokenMiddleware($cachedFetcher);
$mock = new MockHandler([new Response(200)]);
$callable = $middleware($mock);
$callable($this->mockRequest->reveal(), ['auth' => 'google_auth']);
$this->runTestCase($cachedFetcher);
}

public function testGetsCachedAuthTokenUsingCacheOptions()
Expand Down Expand Up @@ -204,10 +190,7 @@ public function testGetsCachedAuthTokenUsingCacheOptions()
['prefix' => $prefix],
$this->mockCache->reveal()
);
$middleware = new AuthTokenMiddleware($cachedFetcher);
$mock = new MockHandler([new Response(200)]);
$callable = $middleware($mock);
$callable($this->mockRequest->reveal(), ['auth' => 'google_auth']);
$this->runTestCase($cachedFetcher);
}

public function testShouldSaveValueInCacheWithSpecifiedPrefix()
Expand Down Expand Up @@ -248,10 +231,7 @@ public function testShouldSaveValueInCacheWithSpecifiedPrefix()
['prefix' => $prefix, 'lifetime' => $lifetime],
$this->mockCache->reveal()
);
$middleware = new AuthTokenMiddleware($cachedFetcher);
$mock = new MockHandler([new Response(200)]);
$callable = $middleware($mock);
$callable($this->mockRequest->reveal(), ['auth' => 'google_auth']);
$this->runTestCase($cachedFetcher);
}

/**
Expand Down Expand Up @@ -282,6 +262,8 @@ public function testShouldNotifyTokenCallback(callable $tokenCallback)
$this->mockFetcher->fetchAuthToken(Argument::any())
->shouldBeCalledTimes(1)
->willReturn($cachedValue);
$this->mockFetcher->getLastReceivedToken()
->willReturn($cachedValue);
yash30201 marked this conversation as resolved.
Show resolved Hide resolved
$this->mockRequest->withHeader(Argument::any(), Argument::any())
->willReturn($this->mockRequest->reveal());

Expand All @@ -306,6 +288,71 @@ public function testShouldNotifyTokenCallback(callable $tokenCallback)
$this->assertTrue(MiddlewareCallback::$called);
}

public function testAddAuthHeadersFromUpdateMetadata()
{
$authResult = [
'authorization' => 'Bearer 1/abcdef1234567890',
];

$this->mockFetcher->willImplement(UpdateMetadataInterface::class);
$this->mockFetcher->updateMetadata(Argument::cetera())
->shouldBeCalledTimes(1)
->willReturn($authResult);
$this->mockFetcher->getLastReceivedToken()
->willReturn(['access_token' => '1/abcdef1234567890']);

$request = new Request('GET', 'http://foo.com');

$middleware = new AuthTokenMiddleware($this->mockFetcher->reveal());
$mockHandlerCalled = false;
$mock = new MockHandler([function ($request, $options) use ($authResult, &$mockHandlerCalled) {
$this->assertEquals($authResult['authorization'], $request->getHeaderLine('authorization'));
$mockHandlerCalled = true;
return new Response(200);
}]);
$callable = $middleware($mock);
$callable($request, ['auth' => 'google_auth']);
bshaffer marked this conversation as resolved.
Show resolved Hide resolved
$this->assertTrue($mockHandlerCalled);
}

public function testOverlappingAddAuthHeadersFromUpdateMetadata()
{
$authHeaders = [
'authorization' => 'Bearer 1/abcdef1234567890',
'x-goog-api-client' => 'extra-value'
];

$request = new Request('GET', 'http://foo.com');

$this->mockFetcher->willImplement(UpdateMetadataInterface::class);
$this->mockFetcher->updateMetadata(Argument::cetera())
->shouldBeCalledTimes(1)
->willReturn($authHeaders);
$this->mockFetcher->getLastReceivedToken()
->willReturn(['access_token' => '1/abcdef1234567890']);

$middleware = new AuthTokenMiddleware($this->mockFetcher->reveal());

$mockHandlerCalled = false;
$mock = new MockHandler([function ($request, $options) use ($authHeaders, &$mockHandlerCalled) {
$this->assertEquals($authHeaders['authorization'], $request->getHeaderLine('authorization'));
$this->assertArrayHasKey('x-goog-api-client', $request->getHeaders());
$mockHandlerCalled = true;
return new Response(200);
}]);
$callable = $middleware($mock);
$callable($request, ['auth' => 'google_auth']);
bshaffer marked this conversation as resolved.
Show resolved Hide resolved
$this->assertTrue($mockHandlerCalled);
}

private function runTestCase($fetcher)
{
$middleware = new AuthTokenMiddleware($fetcher);
$mock = new MockHandler([new Response(200)]);
$callable = $middleware($mock);
$callable($this->mockRequest->reveal(), ['auth' => 'google_auth']);
}

public function provideShouldNotifyTokenCallback()
{
MiddlewareCallback::$phpunit = $this;
Expand Down