PluginProbe
Authorizer / 2.6.17
Authorizer v2.6.17
3.15.3 3.15.2 3.15.1 3.15.0 3.14.3 3.14.4 3.14.2 3.14.1 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.9.0 2.9.1 2.9.10 2.9.11 2.9.12 2.9.13 2.9.2 2.9.3 2.9.6 All 126 releases
authorizer / vendor / google-api-php-client / src / Google / Auth / OAuth2.php

OAuth2.php in Authorizer 2.6.17, at vendor/google-api-php-client/src/Google/Auth/OAuth2.php

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