Skip to content

Commit

Permalink
Add support for signing strings with a Credentials instance. (#221)
Browse files Browse the repository at this point in the history
This change adds `Google\Auth\SignBlobInterface`, which provides a method for signing arbitrary bytes using a credentials instance.

The signing strategy depends on the credentials type.

* AppIdentityCredentials uses the AppIdentityService.
* GCECredentials uses [IAM signing](https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/signBlob).
* ServiceAccountCredentials and its JWT variant use local signing with the private key in the service account.

I've tested locally and in various google compute environments. an overview is below:

```
Flex:
    GCECredentials
    IAM

Standard 5.5
    AppIdentityCredentials
    AppIdentityService

Standard 7.2
    GCECredentials
    IAM

Compute Engine
    GCECredentials
    IAM
```

The reason for this change is to support work on Signed URLs in Google Cloud PHP, and to fill a gap in our library which is covered by other auth clients like Python and Node.
  • Loading branch information
jdpedrie authored Apr 11, 2019
1 parent a19895b commit d48fe19
Show file tree
Hide file tree
Showing 31 changed files with 1,388 additions and 199 deletions.
58 changes: 41 additions & 17 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,48 @@
language: php

branches:
only: [master]

sudo: false

php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2

env:
-
- COMPOSER_ARGS="--prefer-lowest"
matrix:
include:
- php: "7.2"
- name: "PHP 5.4"
php: "5.4"
- name: "PHP 5.4 (Prefer Lowest Dependency Version)"
php: "5.4"
env: COMPOSER_ARGS="--prefer-lowest"

- name: "PHP 5.5"
php: "5.5"
- name: "PHP 5.5 (Prefer Lowest Dependency Version)"
php: "5.5"
env: COMPOSER_ARGS="--prefer-lowest"

- name: "PHP 5.6"
php: "5.6"
- name: "PHP 5.6 (Prefer Lowest Dependency Version)"
php: "5.6"
env: COMPOSER_ARGS="--prefer-lowest"

- name: "PHP 7.0"
php: "7.0"
- name: "PHP 7.0 (Prefer Lowest Dependency Version)"
php: "7.0"
env: COMPOSER_ARGS="--prefer-lowest"

- name: "PHP 7.1"
php: "7.1"
- name: "PHP 7.1 (Prefer Lowest Dependency Version)"
php: "7.1"
env: COMPOSER_ARGS="--prefer-lowest"

- name: "PHP 7.2"
php: "7.2"
- name: "PHP 7.2 (Prefer Lowest Dependency Version)"
php: "7.2"
env: COMPOSER_ARGS="--prefer-lowest"

- name: "PHP 7.3"
php: "7.3"

- name: "Check Style"
php: "7.3"
env: RUN_CS_FIXER=true

before_script:
Expand Down
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
"guzzlehttp/promises": "0.1.1|^1.3",
"friendsofphp/php-cs-fixer": "^1.11",
"phpunit/phpunit": "^4.8.36|^5.7",
"sebastian/comparator": ">=1.2.3"
"sebastian/comparator": ">=1.2.3",
"phpseclib/phpseclib": "^2"
},
"suggest": {
"phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings. Please require version ^2."
},
"autoload": {
"psr-4": {
Expand Down
12 changes: 12 additions & 0 deletions src/ApplicationDefaultCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
use DomainException;
use Google\Auth\Credentials\AppIdentityCredentials;
use Google\Auth\Credentials\GCECredentials;
use Google\Auth\HttpHandler\HttpClientCache;
use Google\Auth\HttpHandler\HttpHandlerFactory;
use Google\Auth\Middleware\AuthTokenMiddleware;
use Google\Auth\Subscriber\AuthTokenSubscriber;
use GuzzleHttp\Client;
use Psr\Cache\CacheItemPoolInterface;

/**
Expand Down Expand Up @@ -144,6 +147,15 @@ public static function getCredentials(
$jsonKey = CredentialsLoader::fromEnv()
?: CredentialsLoader::fromWellKnownFile();

if (!$httpHandler) {
if (!($client = HttpClientCache::getHttpClient())) {
$client = new Client();
HttpClientCache::setHttpClient($client);
}

$httpHandler = HttpHandlerFactory::build($client);
}

if (!is_null($jsonKey)) {
$creds = CredentialsLoader::makeCredentials($scope, $jsonKey);
} elseif (AppIdentityCredentials::onAppEngine() && !GCECredentials::onAppEngineFlexible()) {
Expand Down
75 changes: 60 additions & 15 deletions src/Credentials/AppIdentityCredentials.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
*/
use google\appengine\api\app_identity\AppIdentityService;
use Google\Auth\CredentialsLoader;
use Google\Auth\SignBlobInterface;

/**
* AppIdentityCredentials supports authorization on Google App Engine.
Expand All @@ -49,7 +50,7 @@
*
* $res = $client->get('volumes?q=Henry+David+Thoreau&country=US');
*/
class AppIdentityCredentials extends CredentialsLoader
class AppIdentityCredentials extends CredentialsLoader implements SignBlobInterface
{
/**
* Result of fetchAuthToken.
Expand All @@ -63,6 +64,11 @@ class AppIdentityCredentials extends CredentialsLoader
*/
private $scope;

/**
* @var string
*/
private $clientName;

public function __construct($scope = array())
{
$this->scope = $scope;
Expand Down Expand Up @@ -100,23 +106,16 @@ public static function onAppEngine()
* @param callable $httpHandler callback which delivers psr7 request
*
* @return array A set of auth related metadata, containing the following
* keys:
* - access_token (string)
* - expiration_time (string)
*
* @throws \Exception
* keys:
* - access_token (string)
* - expiration_time (string)
*/
public function fetchAuthToken(callable $httpHandler = null)
{
if (!self::onAppEngine()) {
return array();
}

if (!class_exists('google\appengine\api\app_identity\AppIdentityService')) {
throw new \Exception(
'This class must be run in App Engine, or you must include the AppIdentityService '
. 'mock class defined in tests/mocks/AppIdentityService.php'
);
try {
$this->checkAppEngineContext();
} catch (\Exception $e) {
return [];
}

// AppIdentityService expects an array when multiple scopes are supplied
Expand All @@ -128,6 +127,42 @@ public function fetchAuthToken(callable $httpHandler = null)
return $token;
}

/**
* Sign a string using AppIdentityService.
*
* @param string $stringToSign The string to sign.
* @param bool $forceOpenSsl [optional] Does not apply to this credentials
* type.
* @return string The signature, base64-encoded.
* @throws \Exception If AppEngine SDK or mock is not available.
*/
public function signBlob($stringToSign, $forceOpenSsl = false)
{
$this->checkAppEngineContext();

return base64_encode(AppIdentityService::signForApp($stringToSign)['signature']);
}

/**
* Get the client name from AppIdentityService.
*
* Subsequent calls to this method will return a cached value.
*
* @param callable $httpHandler Not used in this implementation.
* @return string
* @throws \Exception If AppEngine SDK or mock is not available.
*/
public function getClientName(callable $httpHandler = null)
{
$this->checkAppEngineContext();

if (!$this->clientName) {
$this->clientName = AppIdentityService::getServiceAccountName();
}

return $this->clientName;
}

/**
* @return array|null
*/
Expand All @@ -153,4 +188,14 @@ public function getCacheKey()
{
return '';
}

private function checkAppEngineContext()
{
if (!self::onAppEngine() || !class_exists('google\appengine\api\app_identity\AppIdentityService')) {
throw new \Exception(
'This class must be run in App Engine, or you must include the AppIdentityService '
. 'mock class defined in tests/mocks/AppIdentityService.php'
);
}
}
}
Loading

0 comments on commit d48fe19

Please sign in to comment.