Skip to content

Commit

Permalink
feat: add support for proxy-authorization header (#347)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabihodoroaga committed Jun 25, 2021
1 parent c747738 commit aaf2363
Show file tree
Hide file tree
Showing 4 changed files with 321 additions and 6 deletions.
43 changes: 43 additions & 0 deletions README.md
Expand Up @@ -214,6 +214,49 @@ print_r((string) $response->getBody());

```

#### Call using Proxy-Authorization Header
If your application is behind a proxy such as [Google Cloud IAP][iap-proxy-header],
and your application occupies the `Authorization` request header,
you can include the ID token in a `Proxy-Authorization: Bearer`
header instead. If a valid ID token is found in a `Proxy-Authorization` header,
IAP authorizes the request with it. After authorizing the request, IAP passes
the Authorization header to your application without processing the content.
For this, use the static method `getProxyIdTokenMiddleware` on
`ApplicationDefaultCredentials`.

```php
use Google\Auth\ApplicationDefaultCredentials;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;

// specify the path to your application credentials
putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json');

// Provide the ID token audience. This can be a Client ID associated with an IAP application
// $targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com';
$targetAudience = 'YOUR_ID_TOKEN_AUDIENCE';

// create middleware
$middleware = ApplicationDefaultCredentials::getProxyIdTokenMiddleware($targetAudience);
$stack = HandlerStack::create();
$stack->push($middleware);

// create the HTTP client
$client = new Client([
'handler' => $stack,
'auth' => ['username', 'pass'], // auth option handled by your application
'proxy_auth' => 'google_auth',
]);

// make the request
$response = $client->get('/');

// show the result!
print_r((string) $response->getBody());
```

[iap-proxy-header]: https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_proxy-authorization_header

#### Verifying JWTs

If you are [using Google ID tokens to authenticate users][google-id-tokens], use
Expand Down
36 changes: 30 additions & 6 deletions src/ApplicationDefaultCredentials.php
Expand Up @@ -24,6 +24,7 @@
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\Middleware\AuthTokenMiddleware;
use Google\Auth\Middleware\ProxyAuthTokenMiddleware;
use Google\Auth\Subscriber\AuthTokenSubscriber;
use GuzzleHttp\Client;
use InvalidArgumentException;
Expand Down Expand Up @@ -126,12 +127,8 @@ public static function getMiddleware(
}

/**
* Obtains an AuthTokenMiddleware which will fetch an access token to use in
* the Authorization header. The middleware is configured with the default
* FetchAuthTokenInterface implementation to use in this environment.
*
* If supplied, $scope is used to in creating the credentials instance if
* this does not fallback to the Compute Engine defaults.
* Obtains the default FetchAuthTokenInterface implementation to use
* in this environment.
*
* @param string|array $scope the scope of the access request, expressed
* either as an Array or as a space-delimited String.
Expand Down Expand Up @@ -221,6 +218,33 @@ public static function getIdTokenMiddleware(
return new AuthTokenMiddleware($creds, $httpHandler);
}

/**
* Obtains an ProxyAuthTokenMiddleware which will fetch an ID token to use in the
* Authorization header. The middleware is configured with the default
* FetchAuthTokenInterface implementation to use in this environment.
*
* If supplied, $targetAudience is used to set the "aud" on the resulting
* ID token.
*
* @param string $targetAudience The audience for the ID token.
* @param callable $httpHandler callback which delivers psr7 request
* @param array $cacheConfig configuration for the cache when it's present
* @param CacheItemPoolInterface $cache A cache implementation, may be
* provided if you have one already available for use.
* @return ProxyAuthTokenMiddleware
* @throws DomainException if no implementation can be obtained.
*/
public static function getProxyIdTokenMiddleware(
$targetAudience,
callable $httpHandler = null,
array $cacheConfig = null,
CacheItemPoolInterface $cache = null
) {
$creds = self::getIdTokenCredentials($targetAudience, $httpHandler, $cacheConfig, $cache);

return new ProxyAuthTokenMiddleware($creds, $httpHandler);
}

/**
* Obtains the default FetchAuthTokenInterface implementation to use
* in this environment, configured with a $targetAudience for fetching an ID
Expand Down
148 changes: 148 additions & 0 deletions src/Middleware/ProxyAuthTokenMiddleware.php
@@ -0,0 +1,148 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Auth\Middleware;

use Google\Auth\FetchAuthTokenInterface;
use Google\Auth\GetQuotaProjectInterface;
use Psr\Http\Message\RequestInterface;

/**
* ProxyAuthTokenMiddleware is a Guzzle Middleware that adds an Authorization header
* provided by an object implementing FetchAuthTokenInterface.
*
* The FetchAuthTokenInterface#fetchAuthToken is used to obtain a hash; one of
* the values value in that hash is added as the authorization header.
*
* Requests will be accessed with the authorization header:
*
* 'proxy-authorization' 'Bearer <value of auth_token>'
*/
class ProxyAuthTokenMiddleware
{
/**
* @var callback
*/
private $httpHandler;

/**
* @var FetchAuthTokenInterface
*/
private $fetcher;

/**
* @var callable
*/
private $tokenCallback;

/**
* Creates a new ProxyAuthTokenMiddleware.
*
* @param FetchAuthTokenInterface $fetcher is used to fetch the auth token
* @param callable $httpHandler (optional) callback which delivers psr7 request
* @param callable $tokenCallback (optional) function to be called when a new token is fetched.
*/
public function __construct(
FetchAuthTokenInterface $fetcher,
callable $httpHandler = null,
callable $tokenCallback = null
) {
$this->fetcher = $fetcher;
$this->httpHandler = $httpHandler;
$this->tokenCallback = $tokenCallback;
}

/**
* Updates the request with an Authorization header when auth is 'google_auth'.
*
* use Google\Auth\Middleware\ProxyAuthTokenMiddleware;
* use Google\Auth\OAuth2;
* use GuzzleHttp\Client;
* use GuzzleHttp\HandlerStack;
*
* $config = [..<oauth config param>.];
* $oauth2 = new OAuth2($config)
* $middleware = new ProxyAuthTokenMiddleware($oauth2);
* $stack = HandlerStack::create();
* $stack->push($middleware);
*
* $client = new Client([
* 'handler' => $stack,
* 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
* 'proxy_auth' => 'google_auth' // authorize all requests
* ]);
*
* $res = $client->get('myproject/taskqueues/myqueue');
*
* @param callable $handler
* @return \Closure
*/
public function __invoke(callable $handler)
{
return function (RequestInterface $request, array $options) use ($handler) {
// Requests using "proxy_auth"="google_auth" will be authorized.
if (!isset($options['proxy_auth']) || $options['proxy_auth'] !== 'google_auth') {
return $handler($request, $options);
}

$request = $request->withHeader('proxy-authorization', 'Bearer ' . $this->fetchToken());

if ($quotaProject = $this->getQuotaProject()) {
$request = $request->withHeader(
GetQuotaProjectInterface::X_GOOG_USER_PROJECT_HEADER,
$quotaProject
);
}

return $handler($request, $options);
};
}

/**
* Call fetcher to fetch the token.
*
* @return string
*/
private function fetchToken()
{
$auth_tokens = $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 (array_key_exists('id_token', $auth_tokens)) {
return $auth_tokens['id_token'];
}
}

private function getQuotaProject()
{
if ($this->fetcher instanceof GetQuotaProjectInterface) {
return $this->fetcher->getQuotaProject();
}
}
}
100 changes: 100 additions & 0 deletions tests/Middleware/ProxyAuthTokenMiddlewareTest.php
@@ -0,0 +1,100 @@
<?php
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Auth\Tests\Middleware;

use Google\Auth\Middleware\ProxyAuthTokenMiddleware;
use Google\Auth\Tests\BaseTest;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Psr7\Response;
use Prophecy\Argument;

class ProxyAuthTokenMiddlewareTest extends BaseTest
{
private $mockFetcher;
private $mockRequest;

protected function setUp()
{
$this->onlyGuzzle6And7();

$this->mockFetcher = $this->prophesize('Google\Auth\FetchAuthTokenInterface');
$this->mockRequest = $this->prophesize('GuzzleHttp\Psr7\Request');
}

public function testOnlyTouchesWhenAuthConfigScoped()
{
$this->mockFetcher->fetchAuthToken(Argument::any())
->willReturn([]);
$this->mockRequest->withHeader()->shouldNotBeCalled();

$middleware = new ProxyAuthTokenMiddleware($this->mockFetcher->reveal());
$mock = new MockHandler([new Response(200)]);
$callable = $middleware($mock);
$callable($this->mockRequest->reveal(), ['proxy_auth' => 'not_google_auth']);
}

public function testAddsTheTokenAsAnAuthorizationHeader()
{
$authResult = ['id_token' => '1/abcdef1234567890'];
$this->mockFetcher->fetchAuthToken(Argument::any())
->shouldBeCalledTimes(1)
->willReturn($authResult);
$this->mockRequest->withHeader('proxy-authorization', 'Bearer ' . $authResult['id_token'])
->shouldBeCalledTimes(1)
->willReturn($this->mockRequest->reveal());

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

public function testDoesNotAddAnAuthorizationHeaderOnNoAccessToken()
{
$authResult = ['not_access_token' => '1/abcdef1234567890'];
$this->mockFetcher->fetchAuthToken(Argument::any())
->shouldBeCalledTimes(1)
->willReturn($authResult);
$this->mockRequest->withHeader('proxy-authorization', 'Bearer ')
->shouldBeCalledTimes(1)
->willReturn($this->mockRequest->reveal());

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

public function testUsesIdTokenWhenAccessTokenDoesNotExist()
{
$token = 'idtoken12345';
$authResult = ['id_token' => $token];
$this->mockFetcher->fetchAuthToken(Argument::any())
->willReturn($authResult);
$this->mockRequest->withHeader('proxy-authorization', 'Bearer ' . $token)
->shouldBeCalledTimes(1)
->willReturn($this->mockRequest->reveal());

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

0 comments on commit aaf2363

Please sign in to comment.