2020namespace Google \Auth ;
2121
2222use DomainException ;
23+ use Firebase \JWT \JWT ;
24+ use Firebase \JWT \JWK ;
2325use Google \Auth \Credentials \ComputeCredentials ;
2426use Google \Auth \Credentials \ServiceAccountCredentials ;
2527use Google \Auth \Credentials \ServiceAccountJwtAccessCredentials ;
2628use Google \Auth \Credentials \CredentialsInterface ;
2729use Google \Auth \Credentials \UserRefreshCredentials ;
2830use Google \Auth \Http \ClientFactory ;
31+ use Google \Auth \Jwt \FirebaseJwtClient ;
32+ use Google \Auth \Jwt \JwtClientInterface ;
2933use Google \Cache \MemoryCacheItemPool ;
3034use GuzzleHttp \Psr7 \Request ;
3135use InvalidArgumentException ;
3236use Psr \Cache \CacheItemPoolInterface ;
3337use RuntimeException ;
38+ use UnexpectedValueException ;
3439
3540/**
3641 * GoogleAuth obtains the default credentials for
@@ -73,20 +78,19 @@ class GoogleAuth
7378{
7479 private const TOKEN_REVOKE_URI = 'https://oauth2.googleapis.com/revoke ' ;
7580 private const OIDC_CERT_URI = 'https://www.googleapis.com/oauth2/v3/certs ' ;
76- private const OIDC_ISSUERS = ['accounts.google.com ' , 'https://accounts.google.com ' ];
81+ private const OIDC_ISSUERS = ['http:// accounts.google.com ' , 'https://accounts.google.com ' ];
7782 private const IAP_JWK_URI = 'https://www.gstatic.com/iap/verify/public_key-jwk ' ;
7883 private const IAP_ISSUERS = ['https://cloud.google.com/iap ' ];
7984
8085 private const ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS ' ;
8186 private const WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json ' ;
8287 private const NON_WINDOWS_WELL_KNOWN_PATH_BASE = '.config ' ;
8388
84- private const ON_COMPUTE_CACHE_KEY = 'google_auth_on_gce_cache ' ;
85-
86- private $ httpClient ;
8789 private $ cache ;
8890 private $ cacheLifetime ;
8991 private $ cachePrefix ;
92+ private $ httpClient ;
93+ private $ jwtClient ;
9094
9195 /**
9296 * Obtains an AuthTokenMiddleware which will fetch an access token to use in
@@ -98,6 +102,7 @@ class GoogleAuth
98102 *
99103 * @param array $options {
100104 * @type ClientInterface $httpClient client which delivers psr7 request
105+ * @type JwtClientInterface $jwtClient
101106 * @type CacheItemPoolInterface $cache A cache implementation, may be
102107 * provided if you have one already available for use.
103108 * @type int $cacheLifetime
@@ -107,15 +112,18 @@ class GoogleAuth
107112 public function __construct (array $ options = [])
108113 {
109114 $ options += [
110- 'httpClient ' => null ,
111115 'cache ' => null ,
112116 'cacheLifetime ' => 1500 ,
113117 'cachePrefix ' => '' ,
118+ 'httpClient ' => null ,
119+ 'jwtClient ' => null ,
114120 ];
115- $ this ->httpClient = $ options ['httpClient ' ] ?: ClientFactory::build ();
116121 $ this ->cache = $ options ['cache ' ] ?: new MemoryCacheItemPool ();
117122 $ this ->cacheLifetime = $ options ['cacheLifetime ' ];
118123 $ this ->cachePrefix = $ options ['cachePrefix ' ];
124+ $ this ->httpClient = $ options ['httpClient ' ] ?: ClientFactory::build ();
125+ $ this ->jwtClient = $ options ['jwtClient ' ]
126+ ?: new FirebaseJwtClient (new JWT (), new JWK ());
119127 }
120128
121129 /**
@@ -233,13 +241,15 @@ public function makeCredentials(array $options = []): CredentialsInterface
233241 * Determines if this a GCE instance, by accessing the expected metadata
234242 * host.
235243 *
244+ * @param array $options [optional] Configuration options.
245+ * @param string $options.cacheKey cache key used for caching the result
246+ *
236247 * @return bool
237248 */
238- public function onCompute (): bool
249+ public function onCompute (? array $ options ): bool
239250 {
240- $ cacheItem = $ this ->cache ->getItem (
241- $ this ->cachePrefix . self ::ON_COMPUTE_CACHE_KEY
242- );
251+ $ cacheKey = $ options ['cacheKey ' ] ?? 'google_auth_on_gce_cache ' ;
252+ $ cacheItem = $ this ->cache ->getItem ($ this ->cachePrefix . $ cacheKey );
243253
244254 if ($ cacheItem ->isHit ()) {
245255 return $ cacheItem ->get ();
@@ -257,23 +267,34 @@ public function onCompute(): bool
257267 * @param string $token The JSON Web Token to be verified.
258268 * @param array $options [optional] Configuration options.
259269 * @param string $options.audience The indended recipient of the token.
260- * @param string $options.issuer The intended issuer of the token.
261- * @param string $certsLocation URI for JSON certificate array conforming to
270+ * @param array $options.issuers The intended issuers of the token.
271+ * @param string $options.cacheKey cache key used for caching certs
272+ * @param string $options.certsLocation URI for JSON certificate array conforming to
262273 * the JWK spec (see https://tools.ietf.org/html/rfc7517).
263274 */
264- public function verify (string $ token , array $ options = [] ): array
275+ public function verify (string $ token , ? array $ options ): bool
265276 {
266- $ location = isset ($ options ['certsLocation ' ])
267- ? $ options ['certsLocation ' ]
268- : self ::OIDC_CERT_URI ;
277+ $ location = $ options ['certsLocation ' ] ?? self ::OIDC_CERT_URI ;
278+ $ cacheKey = $ options ['cacheKey ' ] ??
279+ sprintf ('google_auth_certs_cache|%s ' , sha1 ($ location ));
280+ $ certs = $ this ->getCerts ($ location , $ cacheKey );
281+ $ alg = $ this ->determineAlg ($ certs );
282+ $ issuers = $ options ['issuers ' ] ??
283+ ['RS256 ' => self ::OIDC_ISSUERS , 'ES256 ' => self ::IAP_ISSUERS ][$ alg ];
284+ $ aud = $ options ['audience ' ] ?? null ;
269285
270- $ cacheKey = isset ($ options ['cacheKey ' ])
271- ? $ options ['cacheKey ' ]
272- : $ this ->getCacheKeyFromCertLocation ($ location );
286+ $ keys = $ this ->jwtClient ->parseKeySet ($ certs );
287+ $ payload = $ this ->jwtClient ->decode ($ token , $ keys , [$ alg ]);
273288
274- $ certs = $ this ->getCerts ($ location , $ cacheKey );
275- $ oauth = new OAuth2 ();
276- return $ oauth ->verify ($ token , $ certs , $ options );
289+ if (empty ($ payload ['iss ' ]) || !in_array ($ payload ['iss ' ], $ issuers )) {
290+ throw new UnexpectedValueException ('Issuer does not match ' );
291+ }
292+
293+ if ($ aud && isset ($ payload ['aud ' ]) && $ payload ['aud ' ] != $ aud ) {
294+ throw new UnexpectedValueException ('Audience does not match ' );
295+ }
296+
297+ return true ;
277298 }
278299
279300 /**
@@ -286,8 +307,9 @@ public function verify(string $token, array $options = []): array
286307 * @return array
287308 * @throws InvalidArgumentException If received certs are in an invalid format.
288309 */
289- private function getCerts (string $ location , string $ cacheKey ): array {
290- $ cacheItem = $ this ->cache ->getItem ($ cacheKey );
310+ private function getCerts (string $ location , string $ cacheKey ): array
311+ {
312+ $ cacheItem = $ this ->cache ->getItem ($ this ->cachePrefix . $ cacheKey );
291313 $ certs = $ cacheItem ? $ cacheItem ->get () : null ;
292314
293315 $ gotNewCerts = false ;
@@ -298,11 +320,6 @@ private function getCerts(string $location, string $cacheKey): array {
298320 }
299321
300322 if (!isset ($ certs ['keys ' ])) {
301- if ($ location !== self ::IAP_JWK_URI ) {
302- throw new InvalidArgumentException (
303- 'federated sign-on certs expects "keys" to be set '
304- );
305- }
306323 throw new InvalidArgumentException (
307324 'certs expects "keys" to be set '
308325 );
@@ -316,7 +333,40 @@ private function getCerts(string $location, string $cacheKey): array {
316333 $ this ->cache ->save ($ cacheItem );
317334 }
318335
319- return $ certs ['keys ' ];
336+ return $ certs ;
337+ }
338+
339+ /**
340+ * Identifies the expected algorithm to verify by looking at the "alg" key
341+ * of the provided certs.
342+ *
343+ * @param array $certs Certificate array according to the JWK spec (see
344+ * https://tools.ietf.org/html/rfc7517).
345+ * @return string The expected algorithm, such as "ES256" or "RS256".
346+ */
347+ private function determineAlg (array $ certs ): string
348+ {
349+ $ alg = null ;
350+ foreach ($ certs ['keys ' ] as $ cert ) {
351+ if (empty ($ cert ['alg ' ])) {
352+ throw new InvalidArgumentException (
353+ 'certs expects "alg" to be set '
354+ );
355+ }
356+ $ alg = $ alg ?: $ cert ['alg ' ];
357+
358+ if ($ alg != $ cert ['alg ' ]) {
359+ throw new InvalidArgumentException (
360+ 'More than one alg detected in certs '
361+ );
362+ }
363+ }
364+ if (!in_array ($ alg , ['RS256 ' , 'ES256 ' ])) {
365+ throw new InvalidArgumentException (
366+ 'unrecognized "alg" in certs, expected ES256 or RS256 '
367+ );
368+ }
369+ return $ alg ;
320370 }
321371
322372 /**
@@ -353,22 +403,6 @@ private function retrieveCertsFromLocation(string $url): array
353403 ), $ response ->getStatusCode ());
354404 }
355405
356- /**
357- * Generate a cache key based on the cert location using sha1 with the
358- * exception of using "federated_signon_certs_v3" to preserve BC.
359- *
360- * @param string $certsLocation
361- * @return string
362- */
363- private function getCacheKeyFromCertLocation ($ certsLocation )
364- {
365- $ key = $ certsLocation === self ::OIDC_CERT_URI
366- ? 'federated_signon_certs_v3 '
367- : sha1 ($ certsLocation );
368-
369- return 'google_auth_certs_cache| ' . $ key ;
370- }
371-
372406 /**
373407 * Revoke an OAuth2 access token or refresh token. This method will revoke the current access
374408 * token, if a token isn't provided.
@@ -378,11 +412,11 @@ private function getCacheKeyFromCertLocation($certsLocation)
378412 */
379413 public function revoke ($ token ): bool
380414 {
381- $ oauth = new OAuth2 ([
415+ $ oauth2 = new OAuth2 ([
382416 'tokenRevokeUri ' => self ::TOKEN_REVOKE_URI ,
417+ 'httpClient ' => $ this ->httpClient ,
383418 ]);
384-
385- return $ oauth ->revoke ($ token );
419+ return $ oauth2 ->revoke ($ token );
386420 }
387421
388422 /**
0 commit comments