PluginProbe
Authorizer / 2.2
Authorizer v2.2
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 / inc / google-api-php-client / src / Google / Auth / OAuth2.php

OAuth2.php in Authorizer 2.2, at inc/google-api-php-client/src/Google/Auth/OAuth2.php

588 lines 17.3 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 "Google/Auth/Abstract.php";
19 require_once "Google/Auth/AssertionCredentials.php";
20 require_once "Google/Auth/Exception.php";
21 require_once "Google/Auth/LoginTicket.php";
22 require_once "Google/Client.php";
23 require_once "Google/Http/Request.php";
24 require_once "Google/Utils.php";
25 require_once "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 Google_Auth_OAuth2 extends 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 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 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(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 Google_Http_Request $request
78 * @return Google_Http_Request The resulting HTTP response including the
79 * responseHttpCode, responseHeaders and responseBody.
80 */
81 public function authenticatedRequest(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 Google_Auth_Exception
90 * @return string
91 */
92 public function authenticate($code)
93 {
94 if (strlen($code) == 0) {
95 throw new 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 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 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 // If the list of scopes contains plus.login, add request_visible_actions
153 // to auth URL.
154 $rva = $this->client->getClassConfig($this, 'request_visible_actions');
155 if (strpos($scope, 'plus.login') && strlen($rva) > 0) {
156 $params['request_visible_actions'] = $rva;
157 }
158
159 if (isset($this->state)) {
160 $params['state'] = $this->state;
161 }
162
163 return self::OAUTH2_AUTH_URL . "?" . http_build_query($params, '', '&');
164 }
165
166 /**
167 * @param string $token
168 * @throws Google_Auth_Exception
169 */
170 public function setAccessToken($token)
171 {
172 $token = json_decode($token, true);
173 if ($token == null) {
174 throw new Google_Auth_Exception('Could not json decode the token');
175 }
176 if (! isset($token['access_token'])) {
177 throw new Google_Auth_Exception("Invalid token format");
178 }
179 $this->token = $token;
180 }
181
182 public function getAccessToken()
183 {
184 return json_encode($this->token);
185 }
186
187 public function setState($state)
188 {
189 $this->state = $state;
190 }
191
192 public function setAssertionCredentials(Google_Auth_AssertionCredentials $creds)
193 {
194 $this->assertionCredentials = $creds;
195 }
196
197 /**
198 * Include an accessToken in a given apiHttpRequest.
199 * @param Google_Http_Request $request
200 * @return Google_Http_Request
201 * @throws Google_Auth_Exception
202 */
203 public function sign(Google_Http_Request $request)
204 {
205 // add the developer key to the request before signing it
206 if ($this->client->getClassConfig($this, 'developer_key')) {
207 $request->setQueryParam('key', $this->client->getClassConfig($this, 'developer_key'));
208 }
209
210 // Cannot sign the request without an OAuth access token.
211 if (null == $this->token && null == $this->assertionCredentials) {
212 return $request;
213 }
214
215 // Check if the token is set to expire in the next 30 seconds
216 // (or has already expired).
217 if ($this->isAccessTokenExpired()) {
218 if ($this->assertionCredentials) {
219 $this->refreshTokenWithAssertion();
220 } else {
221 if (! array_key_exists('refresh_token', $this->token)) {
222 throw new Google_Auth_Exception(
223 "The OAuth 2.0 access token has expired,"
224 ." and a refresh token is not available. Refresh tokens"
225 ." are not returned for responses that were auto-approved."
226 );
227 }
228 $this->refreshToken($this->token['refresh_token']);
229 }
230 }
231
232 // Add the OAuth2 header to the request
233 $request->setRequestHeaders(
234 array('Authorization' => 'Bearer ' . $this->token['access_token'])
235 );
236
237 return $request;
238 }
239
240 /**
241 * Fetches a fresh access token with the given refresh token.
242 * @param string $refreshToken
243 * @return void
244 */
245 public function refreshToken($refreshToken)
246 {
247 $this->refreshTokenRequest(
248 array(
249 'client_id' => $this->client->getClassConfig($this, 'client_id'),
250 'client_secret' => $this->client->getClassConfig($this, 'client_secret'),
251 'refresh_token' => $refreshToken,
252 'grant_type' => 'refresh_token'
253 )
254 );
255 }
256
257 /**
258 * Fetches a fresh access token with a given assertion token.
259 * @param Google_Auth_AssertionCredentials $assertionCredentials optional.
260 * @return void
261 */
262 public function refreshTokenWithAssertion($assertionCredentials = null)
263 {
264 if (!$assertionCredentials) {
265 $assertionCredentials = $this->assertionCredentials;
266 }
267
268 $cacheKey = $assertionCredentials->getCacheKey();
269
270 if ($cacheKey) {
271 // We can check whether we have a token available in the
272 // cache. If it is expired, we can retrieve a new one from
273 // the assertion.
274 $token = $this->client->getCache()->get($cacheKey);
275 if ($token) {
276 $this->setAccessToken($token);
277 }
278 if (!$this->isAccessTokenExpired()) {
279 return;
280 }
281 }
282
283 $this->refreshTokenRequest(
284 array(
285 'grant_type' => 'assertion',
286 'assertion_type' => $assertionCredentials->assertionType,
287 'assertion' => $assertionCredentials->generateAssertion(),
288 )
289 );
290
291 if ($cacheKey) {
292 // Attempt to cache the token.
293 $this->client->getCache()->set(
294 $cacheKey,
295 $this->getAccessToken()
296 );
297 }
298 }
299
300 private function refreshTokenRequest($params)
301 {
302 $http = new Google_Http_Request(
303 self::OAUTH2_TOKEN_URI,
304 'POST',
305 array(),
306 $params
307 );
308 $http->disableGzip();
309 $request = $this->client->getIo()->makeRequest($http);
310
311 $code = $request->getResponseHttpCode();
312 $body = $request->getResponseBody();
313 if (200 == $code) {
314 $token = json_decode($body, true);
315 if ($token == null) {
316 throw new Google_Auth_Exception("Could not json decode the access token");
317 }
318
319 if (! isset($token['access_token']) || ! isset($token['expires_in'])) {
320 throw new Google_Auth_Exception("Invalid token format");
321 }
322
323 $this->token['access_token'] = $token['access_token'];
324 $this->token['expires_in'] = $token['expires_in'];
325 $this->token['created'] = time();
326 } else {
327 throw new Google_Auth_Exception("Error refreshing the OAuth2 token, message: '$body'", $code);
328 }
329 }
330
331 /**
332 * Revoke an OAuth2 access token or refresh token. This method will revoke the current access
333 * token, if a token isn't provided.
334 * @throws Google_Auth_Exception
335 * @param string|null $token The token (access token or a refresh token) that should be revoked.
336 * @return boolean Returns True if the revocation was successful, otherwise False.
337 */
338 public function revokeToken($token = null)
339 {
340 if (!$token) {
341 if (!$this->token) {
342 // Not initialized, no token to actually revoke
343 return false;
344 } elseif (array_key_exists('refresh_token', $this->token)) {
345 $token = $this->token['refresh_token'];
346 } else {
347 $token = $this->token['access_token'];
348 }
349 }
350 $request = new Google_Http_Request(
351 self::OAUTH2_REVOKE_URI,
352 'POST',
353 array(),
354 "token=$token"
355 );
356 $request->disableGzip();
357 $response = $this->client->getIo()->makeRequest($request);
358 $code = $response->getResponseHttpCode();
359 if ($code == 200) {
360 $this->token = null;
361 return true;
362 }
363
364 return false;
365 }
366
367 /**
368 * Returns if the access_token is expired.
369 * @return bool Returns True if the access_token is expired.
370 */
371 public function isAccessTokenExpired()
372 {
373 if (!$this->token || !isset($this->token['created'])) {
374 return true;
375 }
376
377 // If the token is set to expire in the next 30 seconds.
378 $expired = ($this->token['created']
379 + ($this->token['expires_in'] - 30)) < time();
380
381 return $expired;
382 }
383
384 // Gets federated sign-on certificates to use for verifying identity tokens.
385 // Returns certs as array structure, where keys are key ids, and values
386 // are PEM encoded certificates.
387 private function getFederatedSignOnCerts()
388 {
389 return $this->retrieveCertsFromLocation(
390 $this->client->getClassConfig($this, 'federated_signon_certs_url')
391 );
392 }
393
394 /**
395 * Retrieve and cache a certificates file.
396 * @param $url location
397 * @return array certificates
398 */
399 public function retrieveCertsFromLocation($url)
400 {
401 // If we're retrieving a local file, just grab it.
402 if ("http" != substr($url, 0, 4)) {
403 $file = file_get_contents($url);
404 if ($file) {
405 return json_decode($file, true);
406 } else {
407 throw new Google_Auth_Exception(
408 "Failed to retrieve verification certificates: '" .
409 $url . "'."
410 );
411 }
412 }
413
414 // This relies on makeRequest caching certificate responses.
415 $request = $this->client->getIo()->makeRequest(
416 new Google_Http_Request(
417 $url
418 )
419 );
420 if ($request->getResponseHttpCode() == 200) {
421 $certs = json_decode($request->getResponseBody(), true);
422 if ($certs) {
423 return $certs;
424 }
425 }
426 throw new Google_Auth_Exception(
427 "Failed to retrieve verification certificates: '" .
428 $request->getResponseBody() . "'.",
429 $request->getResponseHttpCode()
430 );
431 }
432
433 /**
434 * Verifies an id token and returns the authenticated apiLoginTicket.
435 * Throws an exception if the id token is not valid.
436 * The audience parameter can be used to control which id tokens are
437 * accepted. By default, the id token must have been issued to this OAuth2 client.
438 *
439 * @param $id_token
440 * @param $audience
441 * @return Google_Auth_LoginTicket
442 */
443 public function verifyIdToken($id_token = null, $audience = null)
444 {
445 if (!$id_token) {
446 $id_token = $this->token['id_token'];
447 }
448 $certs = $this->getFederatedSignonCerts();
449 if (!$audience) {
450 $audience = $this->client->getClassConfig($this, 'client_id');
451 }
452
453 return $this->verifySignedJwtWithCerts($id_token, $certs, $audience, self::OAUTH2_ISSUER);
454 }
455
456 /**
457 * Verifies the id token, returns the verified token contents.
458 *
459 * @param $jwt the token
460 * @param $certs array of certificates
461 * @param $required_audience the expected consumer of the token
462 * @param [$issuer] the expected issues, defaults to Google
463 * @param [$max_expiry] the max lifetime of a token, defaults to MAX_TOKEN_LIFETIME_SECS
464 * @return token information if valid, false if not
465 */
466 public function verifySignedJwtWithCerts(
467 $jwt,
468 $certs,
469 $required_audience,
470 $issuer = null,
471 $max_expiry = null
472 ) {
473 if (!$max_expiry) {
474 // Set the maximum time we will accept a token for.
475 $max_expiry = self::MAX_TOKEN_LIFETIME_SECS;
476 }
477
478 $segments = explode(".", $jwt);
479 if (count($segments) != 3) {
480 throw new Google_Auth_Exception("Wrong number of segments in token: $jwt");
481 }
482 $signed = $segments[0] . "." . $segments[1];
483 $signature = Google_Utils::urlSafeB64Decode($segments[2]);
484
485 // Parse envelope.
486 $envelope = json_decode(Google_Utils::urlSafeB64Decode($segments[0]), true);
487 if (!$envelope) {
488 throw new Google_Auth_Exception("Can't parse token envelope: " . $segments[0]);
489 }
490
491 // Parse token
492 $json_body = Google_Utils::urlSafeB64Decode($segments[1]);
493 $payload = json_decode($json_body, true);
494 if (!$payload) {
495 throw new Google_Auth_Exception("Can't parse token payload: " . $segments[1]);
496 }
497
498 // Check signature
499 $verified = false;
500 foreach ($certs as $keyName => $pem) {
501 $public_key = new Google_Verifier_Pem($pem);
502 if ($public_key->verify($signed, $signature)) {
503 $verified = true;
504 break;
505 }
506 }
507
508 if (!$verified) {
509 throw new Google_Auth_Exception("Invalid token signature: $jwt");
510 }
511
512 // Check issued-at timestamp
513 $iat = 0;
514 if (array_key_exists("iat", $payload)) {
515 $iat = $payload["iat"];
516 }
517 if (!$iat) {
518 throw new Google_Auth_Exception("No issue time in token: $json_body");
519 }
520 $earliest = $iat - self::CLOCK_SKEW_SECS;
521
522 // Check expiration timestamp
523 $now = time();
524 $exp = 0;
525 if (array_key_exists("exp", $payload)) {
526 $exp = $payload["exp"];
527 }
528 if (!$exp) {
529 throw new Google_Auth_Exception("No expiration time in token: $json_body");
530 }
531 if ($exp >= $now + $max_expiry) {
532 throw new Google_Auth_Exception(
533 sprintf("Expiration time too far in future: %s", $json_body)
534 );
535 }
536
537 $latest = $exp + self::CLOCK_SKEW_SECS;
538 if ($now < $earliest) {
539 throw new Google_Auth_Exception(
540 sprintf(
541 "Token used too early, %s < %s: %s",
542 $now,
543 $earliest,
544 $json_body
545 )
546 );
547 }
548 if ($now > $latest) {
549 throw new Google_Auth_Exception(
550 sprintf(
551 "Token used too late, %s > %s: %s",
552 $now,
553 $latest,
554 $json_body
555 )
556 );
557 }
558
559 $iss = $payload['iss'];
560 if ($issuer && $iss != $issuer) {
561 throw new Google_Auth_Exception(
562 sprintf(
563 "Invalid issuer, %s != %s: %s",
564 $iss,
565 $issuer,
566 $json_body
567 )
568 );
569 }
570
571 // Check audience
572 $aud = $payload["aud"];
573 if ($aud != $required_audience) {
574 throw new Google_Auth_Exception(
575 sprintf(
576 "Wrong recipient, %s != %s:",
577 $aud,
578 $required_audience,
579 $json_body
580 )
581 );
582 }
583
584 // All good.
585 return new Google_Auth_LoginTicket($envelope, $payload);
586 }
587 }
588