1818
1919namespace Google \AccessToken ;
2020
21- use DateTime ;
2221use DomainException ;
2322use Exception ;
24- use ExpiredException ;
25- use Firebase \JWT \ExpiredException as ExpiredExceptionV3 ;
23+ use Firebase \ JWT \ CachedKeySet ;
24+ use Firebase \JWT \ExpiredException ;
2625use Firebase \JWT \JWT ;
27- use Firebase \JWT \Key ;
2826use Firebase \JWT \SignatureInvalidException ;
2927use Google \Auth \Cache \MemoryCacheItemPool ;
30- use Google \Exception as GoogleException ;
3128use GuzzleHttp \Client ;
32- use GuzzleHttp \ClientInterface ;
29+ use GuzzleHttp \ClientInterface as GuzzleClientInterface ;
30+ use GuzzleHttp \Psr7 \HttpFactory ;
3331use InvalidArgumentException ;
3432use LogicException ;
35- use phpseclib3 \Crypt \AES ;
36- use phpseclib3 \Crypt \PublicKeyLoader ;
37- use phpseclib3 \Math \BigInteger ;
3833use Psr \Cache \CacheItemPoolInterface ;
34+ use Psr \Http \Client \ClientInterface ;
3935
4036/**
4137 * Wrapper around Google Access Tokens which provides convenience functions
@@ -48,26 +44,21 @@ class Verify
4844 const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com ' ;
4945
5046 /**
51- * @var ClientInterface The http client
52- */
53- private $ http ;
54-
55- /**
56- * @var CacheItemPoolInterface cache class
57- */
58- private $ cache ;
47+ * @var \Firebase\JWT\JWT
48+ */
49+ public JWT $ jwt ;
5950
6051 /**
61- * @var \Firebase\JWT\JWT
52+ * @var \Firebase\JWT\CachedKeySet
6253 */
63- public $ jwt ;
54+ private CachedKeySet $ keySet ;
6455
6556 /**
6657 * Instantiates the class, but does not initiate the login flow, leaving it
6758 * to the discretion of the caller.
6859 */
6960 public function __construct (
70- ?ClientInterface $ http = null ,
61+ ?GuzzleClientInterface $ http = null ,
7162 ?CacheItemPoolInterface $ cache = null ,
7263 ?JWT $ jwt = null
7364 ) {
@@ -79,9 +70,17 @@ public function __construct(
7970 $ cache = new MemoryCacheItemPool ();
8071 }
8172
82- $ this ->http = $ http ;
83- $ this ->cache = $ cache ;
73+ if (!$ http instanceof ClientInterface) {
74+ throw new InvalidArgumentException ('http client must implement ' . ClientInterface::class);
75+ }
76+
8477 $ this ->jwt = $ jwt ?: $ this ->getJwtService ();
78+ $ this ->keySet = new CachedKeySet (
79+ self ::FEDERATED_SIGNON_CERT_URL ,
80+ $ http ,
81+ new HttpFactory (),
82+ $ cache
83+ );
8584 }
8685
8786 /**
@@ -100,123 +99,27 @@ public function verifyIdToken($idToken, $audience = null)
10099 throw new LogicException ('id_token cannot be null ' );
101100 }
102101
103- // set phpseclib constants if applicable
104- $ this ->setPhpsecConstants ();
105-
106102 // Check signature
107- $ certs = $ this ->getFederatedSignOnCerts ();
108- foreach ($ certs as $ cert ) {
109- try {
110- $ args = [$ idToken ];
111- $ publicKey = $ this ->getPublicKey ($ cert );
112- if (class_exists (Key::class)) {
113- $ args [] = new Key ($ publicKey , 'RS256 ' );
114- } else {
115- $ args [] = $ publicKey ;
116- $ args [] = ['RS256 ' ];
117- }
118- $ payload = \call_user_func_array ([$ this ->jwt , 'decode ' ], $ args );
119-
120- if (property_exists ($ payload , 'aud ' )) {
121- if ($ audience && $ payload ->aud != $ audience ) {
122- return false ;
123- }
124- }
125-
126- // support HTTP and HTTPS issuers
127- // @see https://developers.google.com/identity/sign-in/web/backend-auth
128- $ issuers = [self ::OAUTH2_ISSUER , self ::OAUTH2_ISSUER_HTTPS ];
129- if (!isset ($ payload ->iss ) || !in_array ($ payload ->iss , $ issuers )) {
130- return false ;
131- }
132-
133- return (array )$ payload ;
134- } catch (ExpiredException $ e ) { // @phpstan-ignore-line
135- return false ;
136- } catch (ExpiredExceptionV3 $ e ) {
137- return false ;
138- } catch (SignatureInvalidException $ e ) {
139- // continue
140- } catch (DomainException $ e ) {
141- // continue
142- }
143- }
144-
145- return false ;
146- }
147-
148- private function getCache ()
149- {
150- return $ this ->cache ;
151- }
152-
153- /**
154- * Retrieve and cache a certificates file.
155- *
156- * @param string $url location
157- * @return array certificates
158- * @throws \Google\Exception
159- */
160- private function retrieveCertsFromLocation ($ url )
161- {
162- // If we're retrieving a local file, just grab it.
163- if (0 !== strpos ($ url , 'http ' )) {
164- if (!$ file = file_get_contents ($ url )) {
165- throw new GoogleException (
166- "Failed to retrieve verification certificates: ' " .
167- $ url ."'. "
168- );
169- }
170-
171- return json_decode ($ file , true );
172- }
173-
174- // @phpstan-ignore-next-line
175- $ response = $ this ->http ->get ($ url );
176-
177- if ($ response ->getStatusCode () == 200 ) {
178- return json_decode ((string )$ response ->getBody (), true );
179- }
180- throw new GoogleException (
181- sprintf (
182- 'Failed to retrieve verification certificates: "%s". ' ,
183- $ response ->getBody ()->getContents ()
184- ),
185- $ response ->getStatusCode ()
186- );
187- }
188-
189- // Gets federated sign-on certificates to use for verifying identity tokens.
190- // Returns certs as array structure, where keys are key ids, and values
191- // are PEM encoded certificates.
192- private function getFederatedSignOnCerts ()
193- {
194- $ certs = null ;
195- if ($ cache = $ this ->getCache ()) {
196- $ cacheItem = $ cache ->getItem ('federated_signon_certs_v3 ' );
197- $ certs = $ cacheItem ->get ();
103+ try {
104+ $ payload = ($ this ->jwt )->decode ($ idToken , $ this ->keySet );
105+ } catch (ExpiredException | SignatureInvalidException | DomainException ) {
106+ return false ;
198107 }
199108
200-
201- if (!$ certs ) {
202- $ certs = $ this ->retrieveCertsFromLocation (
203- self ::FEDERATED_SIGNON_CERT_URL
204- );
205-
206- if ($ cache ) {
207- $ cacheItem ->expiresAt (new DateTime ('+1 hour ' ));
208- $ cacheItem ->set ($ certs );
209- $ cache ->save ($ cacheItem );
109+ if (property_exists ($ payload , 'aud ' )) {
110+ if ($ audience && $ payload ->aud != $ audience ) {
111+ return false ;
210112 }
211113 }
212114
213- if (!isset ($ certs ['keys ' ])) {
214- throw new InvalidArgumentException (
215- 'federated sign-on certs expects "keys" to be set '
216- );
115+ // support HTTP and HTTPS issuers
116+ // @see https://developers.google.com/identity/sign-in/web/backend-auth
117+ $ issuers = [self ::OAUTH2_ISSUER , self ::OAUTH2_ISSUER_HTTPS ];
118+ if (!isset ($ payload ->iss ) || !in_array ($ payload ->iss , $ issuers )) {
119+ return false ;
217120 }
218121
219- return $ certs [ ' keys ' ] ;
122+ return ( array ) $ payload ;
220123 }
221124
222125 private function getJwtService ()
@@ -230,35 +133,4 @@ private function getJwtService()
230133
231134 return $ jwt ;
232135 }
233-
234- private function getPublicKey ($ cert )
235- {
236- $ modulus = new BigInteger ($ this ->jwt ->urlsafeB64Decode ($ cert ['n ' ]), 256 );
237- $ exponent = new BigInteger ($ this ->jwt ->urlsafeB64Decode ($ cert ['e ' ]), 256 );
238- $ component = ['n ' => $ modulus , 'e ' => $ exponent ];
239-
240- $ loader = PublicKeyLoader::load ($ component );
241-
242- return $ loader ->toString ('PKCS8 ' );
243- }
244-
245- /**
246- * phpseclib calls "phpinfo" by default, which requires special
247- * whitelisting in the AppEngine VM environment. This function
248- * sets constants to bypass the need for phpseclib to check phpinfo
249- *
250- * @see phpseclib/Math/BigInteger
251- * @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85
252- */
253- private function setPhpsecConstants ()
254- {
255- if (filter_var (getenv ('GAE_VM ' ), FILTER_VALIDATE_BOOLEAN )) {
256- if (!defined ('MATH_BIGINTEGER_OPENSSL_ENABLED ' )) {
257- define ('MATH_BIGINTEGER_OPENSSL_ENABLED ' , true );
258- }
259- if (!defined ('CRYPT_RSA_MODE ' )) {
260- define ('CRYPT_RSA_MODE ' , AES ::ENGINE_OPENSSL );
261- }
262- }
263- }
264136}
0 commit comments