PluginProbe
Analytify – Google Analytics Dashboard For WordPress (GA4 analytics tracking) / 1.0.3
Analytify – Google Analytics Dashboard For WordPress (GA4 analytics tracking) v1.0.3
9.1.2 9.1.1 9.1.0 9.0.2 9.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 All 153 releases
wp-analytify / lib / Google / Auth / OAuth2.php

OAuth2.php in Analytify – Google Analytics Dashboard For WordPress (GA4 analytics tracking) 1.0.3, at lib/Google/Auth/OAuth2.php

596 lines 18.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * Copyright 2008 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 require_once ANALYTIFY_LIB_PATH . "Google/Auth/Abstract.php";
19 require_once ANALYTIFY_LIB_PATH . "Google/Auth/AssertionCredentials.php";
20 require_once ANALYTIFY_LIB_PATH . "Google/Auth/Exception.php";
21 require_once ANALYTIFY_LIB_PATH . "Google/Auth/LoginTicket.php";
22 require_once ANALYTIFY_LIB_PATH . "Google/Client.php";
23 require_once ANALYTIFY_LIB_PATH . "Google/Http/Request.php";
24 require_once ANALYTIFY_LIB_PATH . "Google/Utils.php";
25 require_once ANALYTIFY_LIB_PATH . "Google/Verifier/Pem.php";
26
27 /**
28 * Authentication class that deals with the OAuth 2 web-server authentication flow
29 *
30 * @author Chris Chabot <chabotc@google.com>
31 * @author Chirag Shah <chirags@google.com>
32 *
33 */
34 class Analytify_Google_Auth_OAuth2 extends Analytify_Google_Auth_Abstract
35 {
36 const OAUTH2_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke';
37 const OAUTH2_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token';
38 const OAUTH2_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth';
39 const CLOCK_SKEW_SECS = 300; // five minutes in seconds
40 const AUTH_TOKEN_LIFETIME_SECS = 300; // five minutes in seconds
41 const MAX_TOKEN_LIFETIME_SECS = 86400; // one day in seconds
42 const OAUTH2_ISSUER = 'accounts.google.com';
43
44 /** @var Analytify_Google_Auth_AssertionCredentials $assertionCredentials */
45 private $assertionCredentials;
46
47 /**
48 * @var string The state parameters for CSRF and other forgery protection.
49 */
50 private $state;
51
52 /**
53 * @var array The token bundle.
54 */
55 private $token = array();
56
57 /**
58 * @var Analytify_Google_Client the base client
59 */
60 private $client;
61
62 /**
63 * Instantiates the class, but does not initiate the login flow, leaving it
64 * to the discretion of the caller.
65 */
66 public function __construct(Analytify_Google_Client $client)
67 {
68 $this->client = $client;
69 }
70
71 /**
72 * Perform an authenticated / signed apiHttpRequest.
73 * This function takes the apiHttpRequest, calls apiAuth->sign on it
74 * (which can modify the request in what ever way fits the auth mechanism)
75 * and then calls apiCurlIO::makeRequest on the signed request
76 *
77 * @param Analytify_Google_Http_Request $request
78 * @return Analytify_Google_Http_Request The resulting HTTP response including the
79 * responseHttpCode, responseHeaders and responseBody.
80 */
81 public function authenticatedRequest(Analytify_Google_Http_Request $request)
82 {
83 $request = $this->sign($request);
84 return $this->client->getIo()->makeRequest($request);
85 }
86
87 /**
88 * @param string $code
89 * @throws Analytify_Google_Auth_Exception
90 * @return string
91 */
92 public function authenticate($code)
93 {
94 if (strlen($code) == 0) {
95 throw new Analytify_Google_Auth_Exception("Invalid code");
96 }
97
98 // We got here from the redirect from a successful authorization grant,
99 // fetch the access token
100 $request = new Analytify_Google_Http_Request(
101 self::OAUTH2_TOKEN_URI,
102 'POST',
103 array(),
104 array(
105 'code' => $code,
106 'grant_type' => 'authorization_code',
107 'redirect_uri' => $this->client->getClassConfig($this, 'redirect_uri'),
108 'client_id' => $this->client->getClassConfig($this, 'client_id'),
109 'client_secret' => $this->client->getClassConfig($this, 'client_secret')
110 )
111 );
112 $request->disableGzip();
113 $response = $this->client->getIo()->makeRequest($request);
114
115 if ($response->getResponseHttpCode() == 200) {
116 $this->setAccessToken($response->getResponseBody());
117 $this->token['created'] = time();
118 return $this->getAccessToken();
119 } else {
120 $decodedResponse = json_decode($response->getResponseBody(), true);
121 if ($decodedResponse != null && $decodedResponse['error']) {
122 $decodedResponse = $decodedResponse['error'];
123 }
124 throw new Analytify_Google_Auth_Exception(
125 sprintf(
126 "Error fetching OAuth2 access token, message: '%s'",
127 $decodedResponse
128 ),
129 $response->getResponseHttpCode()
130 );
131 }
132 }
133
134 /**
135 * Create a URL to obtain user authorization.
136 * The authorization endpoint allows the user to first
137 * authenticate, and then grant/deny the access request.
138 * @param string $scope The scope is expressed as a list of space-delimited strings.
139 * @return string
140 */
141 public function createAuthUrl($scope)
142 {
143 $params = array(
144 'response_type' => 'code',
145 'redirect_uri' => $this->client->getClassConfig($this, 'redirect_uri'),
146 'client_id' => $this->client->getClassConfig($this, 'client_id'),
147 'scope' => $scope,
148 'access_type' => $this->client->getClassConfig($this, 'access_type'),
149 'approval_prompt' => $this->client->getClassConfig($this, 'approval_prompt'),
150 );
151
152 $login_hint = $this->client->getClassConfig($this, 'login_hint');
153 if ($login_hint != '') {
154 $params['login_hint'] = $login_hint;
155 }
156
157 // If the list of scopes contains plus.login, add request_visible_actions
158 // to auth URL.
159 $rva = $this->client->getClassConfig($this, 'request_visible_actions');
160 if (strpos($scope, 'plus.login') && strlen($rva) > 0) {
161 $params['request_visible_actions'] = $rva;
162 }
163
164 if (isset($this->state)) {
165 $params['state'] = $this->state;
166 }
167
168 return self::OAUTH2_AUTH_URL . "?" . http_build_query($params, '', '&');
169 }
170
171 /**
172 * @param string $token
173 * @throws Analytify_Google_Auth_Exception
174 */
175 public function setAccessToken($token)
176 {
177 $token = json_decode($token, true);
178 if ($token == null) {
179 throw new Analytify_Google_Auth_Exception('Could not json decode the token');
180 }
181 if (! isset($token['access_token'])) {
182 throw new Analytify_Google_Auth_Exception("Invalid token format");
183 }
184 $this->token = $token;
185 }
186
187 public function getAccessToken()
188 {
189 return json_encode($this->token);
190 }
191
192 public function setState($state)
193 {
194 $this->state = $state;
195 }
196
197 public function setAssertionCredentials(Analytify_Google_Auth_AssertionCredentials $creds)
198 {
199 $this->assertionCredentials = $creds;
200 }
201
202 /**
203 * Include an accessToken in a given apiHttpRequest.
204 * @param Analytify_Google_Http_Request $request
205 * @return Analytify_Google_Http_Request
206 * @throws Analytify_Google_Auth_Exception
207 */
208 public function sign(Analytify_Google_Http_Request $request)
209 {
210 // add the developer key to the request before signing it
211 if ($this->client->getClassConfig($this, 'developer_key')) {
212 $request->setQueryParam('key', $this->client->getClassConfig($this, 'developer_key'));
213 }
214
215 // Cannot sign the request without an OAuth access token.
216 if (null == $this->token && null == $this->assertionCredentials) {
217 return $request;
218 }
219
220 // Check if the token is set to expire in the next 30 seconds
221 // (or has already expired).
222 if ($this->isAccessTokenExpired()) {
223 if ($this->assertionCredentials) {
224 $this->refreshTokenWithAssertion();
225 } else {
226 if (! array_key_exists('refresh_token', $this->token)) {
227 throw new Analytify_Google_Auth_Exception(
228 "The OAuth 2.0 access token has expired,"
229 ." and a refresh token is not available. Refresh tokens"
230 ." are not returned for responses that were auto-approved."
231 );
232 }
233 $this->refreshToken($this->token['refresh_token']);
234 }
235 }
236
237 // Add the OAuth2 header to the request
238 $request->setRequestHeaders(
239 array('Authorization' => 'Bearer ' . $this->token['access_token'])
240 );
241
242 return $request;
243 }
244
245 /**
246 * Fetches a fresh access token with the given refresh token.
247 * @param string $refreshToken
248 * @return void
249 */
250 public function refreshToken($refreshToken)
251 {
252 $this->refreshTokenRequest(
253 array(
254 'client_id' => $this->client->getClassConfig($this, 'client_id'),
255 'client_secret' => $this->client->getClassConfig($this, 'client_secret'),
256 'refresh_token' => $refreshToken,
257 'grant_type' => 'refresh_token'
258 )
259 );
260 }
261
262 /**
263 * Fetches a fresh access token with a given assertion token.
264 * @param Analytify_Google_Auth_AssertionCredentials $assertionCredentials optional.
265 * @return void
266 */
267 public function refreshTokenWithAssertion($assertionCredentials = null)
268 {
269 if (!$assertionCredentials) {
270 $assertionCredentials = $this->assertionCredentials;
271 }
272
273 $cacheKey = $assertionCredentials->getCacheKey();
274
275 if ($cacheKey) {
276 // We can check whether we have a token available in the
277 // cache. If it is expired, we can retrieve a new one from
278 // the assertion.
279 $token = $this->client->getCache()->get($cacheKey);
280 if ($token) {
281 $this->setAccessToken($token);
282 }
283 if (!$this->isAccessTokenExpired()) {
284 return;
285 }
286 }
287
288 $this->refreshTokenRequest(
289 array(
290 'grant_type' => 'assertion',
291 'assertion_type' => $assertionCredentials->assertionType,
292 'assertion' => $assertionCredentials->generateAssertion(),
293 )
294 );
295
296 if ($cacheKey) {
297 // Attempt to cache the token.
298 $this->client->getCache()->set(
299 $cacheKey,
300 $this->getAccessToken()
301 );
302 }
303 }
304
305 private function refreshTokenRequest($params)
306 {
307 $http = new Analytify_Google_Http_Request(
308 self::OAUTH2_TOKEN_URI,
309 'POST',
310 array(),
311 $params
312 );
313 $http->disableGzip();
314 $request = $this->client->getIo()->makeRequest($http);
315
316 $code = $request->getResponseHttpCode();
317 $body = $request->getResponseBody();
318 if (200 == $code) {
319 $token = json_decode($body, true);
320 if ($token == null) {
321 throw new Analytify_Google_Auth_Exception("Could not json decode the access token");
322 }
323
324 if (! isset($token['access_token']) || ! isset($token['expires_in'])) {
325 throw new Analytify_Google_Auth_Exception("Invalid token format");
326 }
327
328 if (isset($token['id_token'])) {
329 $this->token['id_token'] = $token['id_token'];
330 }
331 $this->token['access_token'] = $token['access_token'];
332 $this->token['expires_in'] = $token['expires_in'];
333 $this->token['created'] = time();
334 } else {
335 throw new Analytify_Google_Auth_Exception("Error refreshing the OAuth2 token, message: '$body'", $code);
336 }
337 }
338
339 /**
340 * Revoke an OAuth2 access token or refresh token. This method will revoke the current access
341 * token, if a token isn't provided.
342 * @throws Analytify_Google_Auth_Exception
343 * @param string|null $token The token (access token or a refresh token) that should be revoked.
344 * @return boolean Returns True if the revocation was successful, otherwise False.
345 */
346 public function revokeToken($token = null)
347 {
348 if (!$token) {
349 if (!$this->token) {
350 // Not initialized, no token to actually revoke
351 return false;
352 } elseif (array_key_exists('refresh_token', $this->token)) {
353 $token = $this->token['refresh_token'];
354 } else {
355 $token = $this->token['access_token'];
356 }
357 }
358 $request = new Analytify_Google_Http_Request(
359 self::OAUTH2_REVOKE_URI,
360 'POST',
361 array(),
362 "token=$token"
363 );
364 $request->disableGzip();
365 $response = $this->client->getIo()->makeRequest($request);
366 $code = $response->getResponseHttpCode();
367 if ($code == 200) {
368 $this->token = null;
369 return true;
370 }
371
372 return false;
373 }
374
375 /**
376 * Returns if the access_token is expired.
377 * @return bool Returns True if the access_token is expired.
378 */
379 public function isAccessTokenExpired()
380 {
381 if (!$this->token || !isset($this->token['created'])) {
382 return true;
383 }
384
385 // If the token is set to expire in the next 30 seconds.
386 $expired = ($this->token['created']
387 + ($this->token['expires_in'] - 30)) < time();
388
389 return $expired;
390 }
391
392 // Gets federated sign-on certificates to use for verifying identity tokens.
393 // Returns certs as array structure, where keys are key ids, and values
394 // are PEM encoded certificates.
395 private function getFederatedSignOnCerts()
396 {
397 return $this->retrieveCertsFromLocation(
398 $this->client->getClassConfig($this, 'federated_signon_certs_url')
399 );
400 }
401
402 /**
403 * Retrieve and cache a certificates file.
404 * @param $url location
405 * @return array certificates
406 */
407 public function retrieveCertsFromLocation($url)
408 {
409 // If we're retrieving a local file, just grab it.
410 if ("http" != substr($url, 0, 4)) {
411 $file = file_get_contents($url);
412 if ($file) {
413 return json_decode($file, true);
414 } else {
415 throw new Analytify_Google_Auth_Exception(
416 "Failed to retrieve verification certificates: '" .
417 $url . "'."
418 );
419 }
420 }
421
422 // This relies on makeRequest caching certificate responses.
423 $request = $this->client->getIo()->makeRequest(
424 new Analytify_Google_Http_Request(
425 $url
426 )
427 );
428 if ($request->getResponseHttpCode() == 200) {
429 $certs = json_decode($request->getResponseBody(), true);
430 if ($certs) {
431 return $certs;
432 }
433 }
434 throw new Analytify_Google_Auth_Exception(
435 "Failed to retrieve verification certificates: '" .
436 $request->getResponseBody() . "'.",
437 $request->getResponseHttpCode()
438 );
439 }
440
441 /**
442 * Verifies an id token and returns the authenticated apiLoginTicket.
443 * Throws an exception if the id token is not valid.
444 * The audience parameter can be used to control which id tokens are
445 * accepted. By default, the id token must have been issued to this OAuth2 client.
446 *
447 * @param $id_token
448 * @param $audience
449 * @return Analytify_Google_Auth_LoginTicket
450 */
451 public function verifyIdToken($id_token = null, $audience = null)
452 {
453 if (!$id_token) {
454 $id_token = $this->token['id_token'];
455 }
456 $certs = $this->getFederatedSignonCerts();
457 if (!$audience) {
458 $audience = $this->client->getClassConfig($this, 'client_id');
459 }
460
461 return $this->verifySignedJwtWithCerts($id_token, $certs, $audience, self::OAUTH2_ISSUER);
462 }
463
464 /**
465 * Verifies the id token, returns the verified token contents.
466 *
467 * @param $jwt the token
468 * @param $certs array of certificates
469 * @param $required_audience the expected consumer of the token
470 * @param [$issuer] the expected issues, defaults to Google
471 * @param [$max_expiry] the max lifetime of a token, defaults to MAX_TOKEN_LIFETIME_SECS
472 * @return token information if valid, false if not
473 */
474 public function verifySignedJwtWithCerts(
475 $jwt,
476 $certs,
477 $required_audience,
478 $issuer = null,
479 $max_expiry = null
480 ) {
481 if (!$max_expiry) {
482 // Set the maximum time we will accept a token for.
483 $max_expiry = self::MAX_TOKEN_LIFETIME_SECS;
484 }
485
486 $segments = explode(".", $jwt);
487 if (count($segments) != 3) {
488 throw new Analytify_Google_Auth_Exception("Wrong number of segments in token: $jwt");
489 }
490 $signed = $segments[0] . "." . $segments[1];
491 $signature = Analytify_Google_Utils::urlSafeB64Decode($segments[2]);
492
493 // Parse envelope.
494 $envelope = json_decode(Analytify_Google_Utils::urlSafeB64Decode($segments[0]), true);
495 if (!$envelope) {
496 throw new Analytify_Google_Auth_Exception("Can't parse token envelope: " . $segments[0]);
497 }
498
499 // Parse token
500 $json_body = Analytify_Google_Utils::urlSafeB64Decode($segments[1]);
501 $payload = json_decode($json_body, true);
502 if (!$payload) {
503 throw new Analytify_Google_Auth_Exception("Can't parse token payload: " . $segments[1]);
504 }
505
506 // Check signature
507 $verified = false;
508 foreach ($certs as $keyName => $pem) {
509 $public_key = new Analytify_Google_Verifier_Pem($pem);
510 if ($public_key->verify($signed, $signature)) {
511 $verified = true;
512 break;
513 }
514 }
515
516 if (!$verified) {
517 throw new Analytify_Google_Auth_Exception("Invalid token signature: $jwt");
518 }
519
520 // Check issued-at timestamp
521 $iat = 0;
522 if (array_key_exists("iat", $payload)) {
523 $iat = $payload["iat"];
524 }
525 if (!$iat) {
526 throw new Analytify_Google_Auth_Exception("No issue time in token: $json_body");
527 }
528 $earliest = $iat - self::CLOCK_SKEW_SECS;
529
530 // Check expiration timestamp
531 $now = time();
532 $exp = 0;
533 if (array_key_exists("exp", $payload)) {
534 $exp = $payload["exp"];
535 }
536 if (!$exp) {
537 throw new Analytify_Google_Auth_Exception("No expiration time in token: $json_body");
538 }
539 if ($exp >= $now + $max_expiry) {
540 throw new Analytify_Google_Auth_Exception(
541 sprintf("Expiration time too far in future: %s", $json_body)
542 );
543 }
544
545 $latest = $exp + self::CLOCK_SKEW_SECS;
546 if ($now < $earliest) {
547 throw new Analytify_Google_Auth_Exception(
548 sprintf(
549 "Token used too early, %s < %s: %s",
550 $now,
551 $earliest,
552 $json_body
553 )
554 );
555 }
556 if ($now > $latest) {
557 throw new Analytify_Google_Auth_Exception(
558 sprintf(
559 "Token used too late, %s > %s: %s",
560 $now,
561 $latest,
562 $json_body
563 )
564 );
565 }
566
567 $iss = $payload['iss'];
568 if ($issuer && $iss != $issuer) {
569 throw new Analytify_Google_Auth_Exception(
570 sprintf(
571 "Invalid issuer, %s != %s: %s",
572 $iss,
573 $issuer,
574 $json_body
575 )
576 );
577 }
578
579 // Check audience
580 $aud = $payload["aud"];
581 if ($aud != $required_audience) {
582 throw new Analytify_Google_Auth_Exception(
583 sprintf(
584 "Wrong recipient, %s != %s:",
585 $aud,
586 $required_audience,
587 $json_body
588 )
589 );
590 }
591
592 // All good.
593 return new Analytify_Google_Auth_LoginTicket($envelope, $payload);
594 }
595 }
596