Skip to content

Commit

Permalink
adds jwt abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer committed Feb 11, 2021
1 parent f1e7f2b commit ecc069d
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 414 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ composer.lock
.cache
.docs
.gitmodules
.phpunit.result.cache

# IntelliJ
.idea
Expand Down
21 changes: 21 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,27 @@ $httpClient = new Google\Http\Client\GuzzleClient($guzzle);
$auth = new GoogleAuth(['httpClient' => $httpClient]);
```

#### Improved JWT handling

* Provides an abstraction from `firebase/jwt`, `phpseclib/phpseclib`, and `kelvinmo/simplejwt`
* Using the composer "[replace](https://stackoverflow.com/questions/18882201/how-does-the-replace-property-work-with-composer)" keyword, users can ignore sub-dependencies such as Firebase JWT in favor of a separate JWT library
* **TODO**: Provide documentation on how to use a different library
* Adds `JwtClientInterface` and `FirebaseJwtClient`

**Example**

```php
$jwt = new class implements Google\Auth\Jwt\JwtClientInterface {
public function encode($payload, $signingKey, $signingAlg, $keyId) {
// encode method
}

// ... other JWT hander interface methods go here ...
};
$googleAuth = new GoogleAuth(['jwtClient' => $jwt]);
$googleAuth->verify($someJwt);
```

#### New `GoogleAuth` class

`GoogleAuth` replaces `ApplicationDefaultCredentials`, and provides a
Expand Down
132 changes: 83 additions & 49 deletions src/Auth/GoogleAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,22 @@
namespace Google\Auth;

use DomainException;
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
use Google\Auth\Credentials\ComputeCredentials;
use Google\Auth\Credentials\ServiceAccountCredentials;
use Google\Auth\Credentials\ServiceAccountJwtAccessCredentials;
use Google\Auth\Credentials\CredentialsInterface;
use Google\Auth\Credentials\UserRefreshCredentials;
use Google\Auth\Http\ClientFactory;
use Google\Auth\Jwt\FirebaseJwtClient;
use Google\Auth\Jwt\JwtClientInterface;
use Google\Cache\MemoryCacheItemPool;
use GuzzleHttp\Psr7\Request;
use InvalidArgumentException;
use Psr\Cache\CacheItemPoolInterface;
use RuntimeException;
use UnexpectedValueException;

/**
* GoogleAuth obtains the default credentials for
Expand Down Expand Up @@ -73,20 +78,19 @@ class GoogleAuth
{
private const TOKEN_REVOKE_URI = 'https://oauth2.googleapis.com/revoke';
private const OIDC_CERT_URI = 'https://www.googleapis.com/oauth2/v3/certs';
private const OIDC_ISSUERS = ['accounts.google.com', 'https://accounts.google.com'];
private const OIDC_ISSUERS = ['http://accounts.google.com', 'https://accounts.google.com'];
private const IAP_JWK_URI = 'https://www.gstatic.com/iap/verify/public_key-jwk';
private const IAP_ISSUERS = ['https://cloud.google.com/iap'];

private const ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS';
private const WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json';
private const NON_WINDOWS_WELL_KNOWN_PATH_BASE = '.config';

private const ON_COMPUTE_CACHE_KEY = 'google_auth_on_gce_cache';

private $httpClient;
private $cache;
private $cacheLifetime;
private $cachePrefix;
private $httpClient;
private $jwtClient;

/**
* Obtains an AuthTokenMiddleware which will fetch an access token to use in
Expand All @@ -98,6 +102,7 @@ class GoogleAuth
*
* @param array $options {
* @type ClientInterface $httpClient client which delivers psr7 request
* @type JwtClientInterface $jwtClient
* @type CacheItemPoolInterface $cache A cache implementation, may be
* provided if you have one already available for use.
* @type int $cacheLifetime
Expand All @@ -107,15 +112,18 @@ class GoogleAuth
public function __construct(array $options = [])
{
$options += [
'httpClient' => null,
'cache' => null,
'cacheLifetime' => 1500,
'cachePrefix' => '',
'httpClient' => null,
'jwtClient' => null,
];
$this->httpClient = $options['httpClient'] ?: ClientFactory::build();
$this->cache = $options['cache'] ?: new MemoryCacheItemPool();
$this->cacheLifetime = $options['cacheLifetime'];
$this->cachePrefix = $options['cachePrefix'];
$this->httpClient = $options['httpClient'] ?: ClientFactory::build();
$this->jwtClient = $options['jwtClient']
?: new FirebaseJwtClient(new JWT(), new JWK());
}

/**
Expand Down Expand Up @@ -233,13 +241,15 @@ public function makeCredentials(array $options = []): CredentialsInterface
* Determines if this a GCE instance, by accessing the expected metadata
* host.
*
* @param array $options [optional] Configuration options.
* @param string $options.cacheKey cache key used for caching the result
*
* @return bool
*/
public function onCompute(): bool
public function onCompute(?array $options): bool
{
$cacheItem = $this->cache->getItem(
$this->cachePrefix . self::ON_COMPUTE_CACHE_KEY
);
$cacheKey = $options['cacheKey'] ?? 'google_auth_on_gce_cache';
$cacheItem = $this->cache->getItem($this->cachePrefix . $cacheKey);

if ($cacheItem->isHit()) {
return $cacheItem->get();
Expand All @@ -257,23 +267,34 @@ public function onCompute(): bool
* @param string $token The JSON Web Token to be verified.
* @param array $options [optional] Configuration options.
* @param string $options.audience The indended recipient of the token.
* @param string $options.issuer The intended issuer of the token.
* @param string $certsLocation URI for JSON certificate array conforming to
* @param array $options.issuers The intended issuers of the token.
* @param string $options.cacheKey cache key used for caching certs
* @param string $options.certsLocation URI for JSON certificate array conforming to
* the JWK spec (see https://tools.ietf.org/html/rfc7517).
*/
public function verify(string $token, array $options = []): array
public function verify(string $token, ?array $options): bool
{
$location = isset($options['certsLocation'])
? $options['certsLocation']
: self::OIDC_CERT_URI;
$location = $options['certsLocation'] ?? self::OIDC_CERT_URI;
$cacheKey = $options['cacheKey'] ??
sprintf('google_auth_certs_cache|%s', sha1($location));
$certs = $this->getCerts($location, $cacheKey);
$alg = $this->determineAlg($certs);
$issuers = $options['issuers'] ??
['RS256' => self::OIDC_ISSUERS, 'ES256' => self::IAP_ISSUERS][$alg];
$aud = $options['audience'] ?? null;

$cacheKey = isset($options['cacheKey'])
? $options['cacheKey']
: $this->getCacheKeyFromCertLocation($location);
$keys = $this->jwtClient->parseKeySet($certs);
$payload = $this->jwtClient->decode($token, $keys, [$alg]);

$certs = $this->getCerts($location, $cacheKey);
$oauth = new OAuth2();
return $oauth->verify($token, $certs, $options);
if (empty($payload['iss']) || !in_array($payload['iss'], $issuers)) {
throw new UnexpectedValueException('Issuer does not match');
}

if ($aud && isset($payload['aud']) && $payload['aud'] != $aud) {
throw new UnexpectedValueException('Audience does not match');
}

return true;
}

/**
Expand All @@ -286,8 +307,9 @@ public function verify(string $token, array $options = []): array
* @return array
* @throws InvalidArgumentException If received certs are in an invalid format.
*/
private function getCerts(string $location, string $cacheKey): array {
$cacheItem = $this->cache->getItem($cacheKey);
private function getCerts(string $location, string $cacheKey): array
{
$cacheItem = $this->cache->getItem($this->cachePrefix . $cacheKey);
$certs = $cacheItem ? $cacheItem->get() : null;

$gotNewCerts = false;
Expand All @@ -298,11 +320,6 @@ private function getCerts(string $location, string $cacheKey): array {
}

if (!isset($certs['keys'])) {
if ($location !== self::IAP_JWK_URI) {
throw new InvalidArgumentException(
'federated sign-on certs expects "keys" to be set'
);
}
throw new InvalidArgumentException(
'certs expects "keys" to be set'
);
Expand All @@ -316,7 +333,40 @@ private function getCerts(string $location, string $cacheKey): array {
$this->cache->save($cacheItem);
}

return $certs['keys'];
return $certs;
}

/**
* Identifies the expected algorithm to verify by looking at the "alg" key
* of the provided certs.
*
* @param array $certs Certificate array according to the JWK spec (see
* https://tools.ietf.org/html/rfc7517).
* @return string The expected algorithm, such as "ES256" or "RS256".
*/
private function determineAlg(array $certs): string
{
$alg = null;
foreach ($certs['keys'] as $cert) {
if (empty($cert['alg'])) {
throw new InvalidArgumentException(
'certs expects "alg" to be set'
);
}
$alg = $alg ?: $cert['alg'];

if ($alg != $cert['alg']) {
throw new InvalidArgumentException(
'More than one alg detected in certs'
);
}
}
if (!in_array($alg, ['RS256', 'ES256'])) {
throw new InvalidArgumentException(
'unrecognized "alg" in certs, expected ES256 or RS256'
);
}
return $alg;
}

/**
Expand Down Expand Up @@ -353,22 +403,6 @@ private function retrieveCertsFromLocation(string $url): array
), $response->getStatusCode());
}

/**
* Generate a cache key based on the cert location using sha1 with the
* exception of using "federated_signon_certs_v3" to preserve BC.
*
* @param string $certsLocation
* @return string
*/
private function getCacheKeyFromCertLocation($certsLocation)
{
$key = $certsLocation === self::OIDC_CERT_URI
? 'federated_signon_certs_v3'
: sha1($certsLocation);

return 'google_auth_certs_cache|' . $key;
}

/**
* Revoke an OAuth2 access token or refresh token. This method will revoke the current access
* token, if a token isn't provided.
Expand All @@ -378,11 +412,11 @@ private function getCacheKeyFromCertLocation($certsLocation)
*/
public function revoke($token): bool
{
$oauth = new OAuth2([
$oauth2 = new OAuth2([
'tokenRevokeUri' => self::TOKEN_REVOKE_URI,
'httpClient' => $this->httpClient,
]);

return $oauth->revoke($token);
return $oauth2->revoke($token);
}

/**
Expand Down
54 changes: 54 additions & 0 deletions src/Auth/Jwt/FirebaseJwtClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
/*
* Copyright 2020 Google LLC
*
* 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.
*/

declare(strict_types=1);

namespace Google\Auth\Jwt;

use Firebase\JWT\JWT;
use Firebase\JWT\JWK;

class FirebaseJwtClient implements JwtClientInterface
{
private $jwt;
private $jwk;

public function __construct(JWT $jwt, JWK $jwk)
{
$this->jwt = $jwt;
$this->jwk = $jwk;
}

public function encode(
array $payload,
string $signingKey,
string $signingAlg,
string $keyId
): string {
return $this->jwt->encode($payload, $signingKey, $signingAlg, $keyId);
}

public function decode(string $jwt, array $keys, array $allowedAlgs): array
{
return (array) $this->jwt->decode($jwt, $keys, $allowedAlgs);
}

public function parseKeySet(array $keySet): array
{
return $this->jwk->parseKeySet($keySet);
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
<?php
/**
/*
* Copyright 2020 Google LLC
*
* 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
* 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\HttpHandler;

class Guzzle7HttpHandler extends Guzzle6HttpHandler
declare(strict_types=1);

namespace Google\Auth\Jwt;

interface JwtClientInterface
{
}
public function encode(
array $payload,
string $signingKey,
string $signingAlg,
string $keyId
): string;

public function decode(string $jwt, array $keys, array $allowedAlgs): array;

public function parseKeySet(array $keySey): array;
}
Loading

0 comments on commit ecc069d

Please sign in to comment.