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