PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.16.5
UpdraftPlus: WP Backup & Migration Plugin v1.16.5
1.26.7 1.26.6 1.26.5 1.26.4 1.26.3 1.9.19 1.9.25 1.9.26 1.9.30 1.9.31 1.9.32 1.9.4 1.9.40 1.9.41 1.9.42 1.9.43 1.9.44 1.9.45 1.9.46 1.9.5 1.9.50 1.9.51 1.9.60 1.9.62 1.9.63 All 371 releases
updraftplus / includes / Google / Auth / OAuth2.php

OAuth2.php in UpdraftPlus: WP Backup & Migration Plugin 1.16.5, at includes/Google/Auth/OAuth2.php

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