Google_AccessToken_Verify::verifyIdToken()   C
last analyzed

Complexity

Conditions 12
Paths 23

Size

Total Lines 53
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 12
eloc 30
c 1
b 0
f 1
nc 23
nop 2
dl 0
loc 53
rs 6.9666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * Copyright 2008 Google Inc.
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Firebase\JWT\ExpiredException as ExpiredExceptionV3;
20
use Firebase\JWT\SignatureInvalidException;
21
use GuzzleHttp\Client;
22
use GuzzleHttp\ClientInterface;
23
use Psr\Cache\CacheItemPoolInterface;
24
use Google\Auth\Cache\MemoryCacheItemPool;
25
use Stash\Driver\FileSystem;
0 ignored issues
show
Bug introduced by
The type Stash\Driver\FileSystem was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
use Stash\Pool;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Pool. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
Bug introduced by
The type Stash\Pool was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
28
/**
29
 * Wrapper around Google Access Tokens which provides convenience functions
30
 *
31
 */
32
class Google_AccessToken_Verify
33
{
34
  const FEDERATED_SIGNON_CERT_URL = 'https://www.googleapis.com/oauth2/v3/certs';
35
  const OAUTH2_ISSUER = 'accounts.google.com';
36
  const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com';
37
38
  /**
39
   * @var GuzzleHttp\ClientInterface The http client
40
   */
41
  private $http;
42
43
  /**
44
   * @var Psr\Cache\CacheItemPoolInterface cache class
45
   */
46
  private $cache;
47
48
  /**
49
   * Instantiates the class, but does not initiate the login flow, leaving it
50
   * to the discretion of the caller.
51
   */
52
  public function __construct(
53
      ClientInterface $http = null,
54
      CacheItemPoolInterface $cache = null,
55
      $jwt = null
56
  ) {
57
    if (null === $http) {
58
      $http = new Client();
59
    }
60
61
    if (null === $cache) {
62
      $cache = new MemoryCacheItemPool;
63
    }
64
65
    $this->http = $http;
66
    $this->cache = $cache;
67
    $this->jwt = $jwt ?: $this->getJwtService();
0 ignored issues
show
Bug Best Practice introduced by
The property jwt does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
68
  }
69
70
  /**
71
   * Verifies an id token and returns the authenticated apiLoginTicket.
72
   * Throws an exception if the id token is not valid.
73
   * The audience parameter can be used to control which id tokens are
74
   * accepted.  By default, the id token must have been issued to this OAuth2 client.
75
   *
76
   * @param $audience
77
   * @return array the token payload, if successful
78
   */
79
  public function verifyIdToken($idToken, $audience = null)
80
  {
81
    if (empty($idToken)) {
82
      throw new LogicException('id_token cannot be null');
83
    }
84
85
    // set phpseclib constants if applicable
86
    $this->setPhpsecConstants();
87
88
    // Check signature
89
    $certs = $this->getFederatedSignOnCerts();
90
    foreach ($certs as $cert) {
91
      $bigIntClass = $this->getBigIntClass();
92
      $rsaClass = $this->getRsaClass();
93
      $modulus = new $bigIntClass($this->jwt->urlsafeB64Decode($cert['n']), 256);
94
      $exponent = new $bigIntClass($this->jwt->urlsafeB64Decode($cert['e']), 256);
95
96
      $rsa = new $rsaClass();
97
      $rsa->loadKey(array('n' => $modulus, 'e' => $exponent));
98
99
      try {
100
        $payload = $this->jwt->decode(
101
            $idToken,
102
            $rsa->getPublicKey(),
103
            array('RS256')
104
        );
105
106
        if (property_exists($payload, 'aud')) {
107
          if ($audience && $payload->aud != $audience) {
108
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
109
          }
110
        }
111
112
        // support HTTP and HTTPS issuers
113
        // @see https://developers.google.com/identity/sign-in/web/backend-auth
114
        $issuers = array(self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS);
115
        if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) {
116
          return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
117
        }
118
119
        return (array) $payload;
120
      } catch (ExpiredException $e) {
0 ignored issues
show
Bug introduced by
The type ExpiredException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
121
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
122
      } catch (ExpiredExceptionV3 $e) {
123
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
124
      } catch (SignatureInvalidException $e) {
125
        // continue
126
      } catch (DomainException $e) {
127
        // continue
128
      }
129
    }
130
131
    return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
132
  }
133
134
  private function getCache()
135
  {
136
    return $this->cache;
137
  }
138
139
  /**
140
   * Retrieve and cache a certificates file.
141
   *
142
   * @param $url string location
143
   * @throws Google_Exception
144
   * @return array certificates
145
   */
146
  private function retrieveCertsFromLocation($url)
147
  {
148
    // If we're retrieving a local file, just grab it.
149
    if (0 !== strpos($url, 'http')) {
150
      if (!$file = file_get_contents($url)) {
151
        throw new Google_Exception(
152
            "Failed to retrieve verification certificates: '" .
153
            $url . "'."
154
        );
155
      }
156
157
      return json_decode($file, true);
158
    }
159
160
    $response = $this->http->get($url);
161
162
    if ($response->getStatusCode() == 200) {
163
      return json_decode((string) $response->getBody(), true);
164
    }
165
    throw new Google_Exception(
166
        sprintf(
167
            'Failed to retrieve verification certificates: "%s".',
168
            $response->getBody()->getContents()
169
        ),
170
        $response->getStatusCode()
171
    );
172
  }
173
174
  // Gets federated sign-on certificates to use for verifying identity tokens.
175
  // Returns certs as array structure, where keys are key ids, and values
176
  // are PEM encoded certificates.
177
  private function getFederatedSignOnCerts()
178
  {
179
    $certs = null;
180
    if ($cache = $this->getCache()) {
181
      $cacheItem = $cache->getItem('federated_signon_certs_v3');
182
      $certs = $cacheItem->get();
183
    }
184
185
186
    if (!$certs) {
187
      $certs = $this->retrieveCertsFromLocation(
188
          self::FEDERATED_SIGNON_CERT_URL
189
      );
190
191
      if ($cache) {
0 ignored issues
show
introduced by
$cache is of type Psr\Cache\CacheItemPoolInterface, thus it always evaluated to true.
Loading history...
192
        $cacheItem->expiresAt(new DateTime('+1 hour'));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cacheItem does not seem to be defined for all execution paths leading up to this point.
Loading history...
193
        $cacheItem->set($certs);
194
        $cache->save($cacheItem);
195
      }
196
    }
197
198
    if (!isset($certs['keys'])) {
199
      throw new InvalidArgumentException(
200
          'federated sign-on certs expects "keys" to be set'
201
      );
202
    }
203
204
    return $certs['keys'];
205
  }
206
207
  private function getJwtService()
208
  {
209
    $jwtClass = 'JWT';
210
    if (class_exists('\Firebase\JWT\JWT')) {
211
      $jwtClass = 'Firebase\JWT\JWT';
212
    }
213
214
    if (property_exists($jwtClass, 'leeway') && $jwtClass::$leeway < 1) {
215
      // Ensures JWT leeway is at least 1
216
      // @see https://github.com/google/google-api-php-client/issues/827
217
      $jwtClass::$leeway = 1;
218
    }
219
220
    return new $jwtClass;
221
  }
222
223
  private function getRsaClass()
224
  {
225
    if (class_exists('phpseclib\Crypt\RSA')) {
226
      return 'phpseclib\Crypt\RSA';
227
    }
228
229
    return 'Crypt_RSA';
230
  }
231
232
  private function getBigIntClass()
233
  {
234
    if (class_exists('phpseclib\Math\BigInteger')) {
235
      return 'phpseclib\Math\BigInteger';
236
    }
237
238
    return 'Math_BigInteger';
239
  }
240
241
  private function getOpenSslConstant()
242
  {
243
    if (class_exists('phpseclib\Crypt\RSA')) {
244
      return 'phpseclib\Crypt\RSA::MODE_OPENSSL';
245
    }
246
247
    if (class_exists('Crypt_RSA')) {
248
      return 'CRYPT_RSA_MODE_OPENSSL';
249
    }
250
251
    throw new \Exception('Cannot find RSA class');
252
  }
253
254
  /**
255
   * phpseclib calls "phpinfo" by default, which requires special
256
   * whitelisting in the AppEngine VM environment. This function
257
   * sets constants to bypass the need for phpseclib to check phpinfo
258
   *
259
   * @see phpseclib/Math/BigInteger
260
   * @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85
261
   */
262
  private function setPhpsecConstants()
263
  {
264
    if (filter_var(getenv('GAE_VM'), FILTER_VALIDATE_BOOLEAN)) {
265
      if (!defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) {
266
        define('MATH_BIGINTEGER_OPENSSL_ENABLED', true);
267
      }
268
      if (!defined('CRYPT_RSA_MODE')) {
269
        define('CRYPT_RSA_MODE', constant($this->getOpenSslConstant()));
270
      }
271
    }
272
  }
273
}
274