1 | <?php |
---|
2 | /** |
---|
3 | * Note : Code is released under the GNU LGPL |
---|
4 | * |
---|
5 | * Please do not change the header of this file |
---|
6 | * |
---|
7 | * This library is free software; you can redistribute it and/or modify it under the terms of the GNU |
---|
8 | * Lesser General Public License as published by the Free Software Foundation; either version 2 of |
---|
9 | * the License, or (at your option) any later version. |
---|
10 | * |
---|
11 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; |
---|
12 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
---|
13 | * |
---|
14 | * See the GNU Lesser General Public License for more details. |
---|
15 | */ |
---|
16 | |
---|
17 | /** |
---|
18 | * Light PHP wrapper for the OAuth 2.0 protocol. |
---|
19 | * |
---|
20 | * This client is based on the OAuth2 specification draft v2.15 |
---|
21 | * http://tools.ietf.org/html/draft-ietf-oauth-v2-15 |
---|
22 | * |
---|
23 | * @author Pierrick Charron <pierrick@webstart.fr> |
---|
24 | * @author Anis Berejeb <anis.berejeb@gmail.com> |
---|
25 | * @version 1.2-dev |
---|
26 | */ |
---|
27 | |
---|
28 | class OAuth2_Client |
---|
29 | { |
---|
30 | /** |
---|
31 | * Different AUTH method |
---|
32 | */ |
---|
33 | const AUTH_TYPE_URI = 0; |
---|
34 | const AUTH_TYPE_AUTHORIZATION_BASIC = 1; |
---|
35 | const AUTH_TYPE_FORM = 2; |
---|
36 | |
---|
37 | /** |
---|
38 | * Different Access token type |
---|
39 | */ |
---|
40 | const ACCESS_TOKEN_URI = 0; |
---|
41 | const ACCESS_TOKEN_BEARER = 1; |
---|
42 | const ACCESS_TOKEN_OAUTH = 2; |
---|
43 | const ACCESS_TOKEN_MAC = 3; |
---|
44 | |
---|
45 | /** |
---|
46 | * Different Grant types |
---|
47 | */ |
---|
48 | const GRANT_TYPE_AUTH_CODE = 'authorization_code'; |
---|
49 | const GRANT_TYPE_PASSWORD = 'password'; |
---|
50 | const GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials'; |
---|
51 | const GRANT_TYPE_REFRESH_TOKEN = 'refresh_token'; |
---|
52 | |
---|
53 | /** |
---|
54 | * HTTP Methods |
---|
55 | */ |
---|
56 | const HTTP_METHOD_GET = 'GET'; |
---|
57 | const HTTP_METHOD_POST = 'POST'; |
---|
58 | const HTTP_METHOD_PUT = 'PUT'; |
---|
59 | const HTTP_METHOD_DELETE = 'DELETE'; |
---|
60 | const HTTP_METHOD_HEAD = 'HEAD'; |
---|
61 | const HTTP_METHOD_PATCH = 'PATCH'; |
---|
62 | |
---|
63 | /** |
---|
64 | * HTTP Form content types |
---|
65 | */ |
---|
66 | const HTTP_FORM_CONTENT_TYPE_APPLICATION = 0; |
---|
67 | const HTTP_FORM_CONTENT_TYPE_MULTIPART = 1; |
---|
68 | |
---|
69 | /** |
---|
70 | * Client ID |
---|
71 | * |
---|
72 | * @var string |
---|
73 | */ |
---|
74 | protected $client_id = null; |
---|
75 | |
---|
76 | /** |
---|
77 | * Client Secret |
---|
78 | * |
---|
79 | * @var string |
---|
80 | */ |
---|
81 | protected $client_secret = null; |
---|
82 | |
---|
83 | /** |
---|
84 | * Client Authentication method |
---|
85 | * |
---|
86 | * @var int |
---|
87 | */ |
---|
88 | protected $client_auth = self::AUTH_TYPE_URI; |
---|
89 | |
---|
90 | /** |
---|
91 | * Access Token |
---|
92 | * |
---|
93 | * @var string |
---|
94 | */ |
---|
95 | protected $access_token = null; |
---|
96 | |
---|
97 | /** |
---|
98 | * Access Token Type |
---|
99 | * |
---|
100 | * @var int |
---|
101 | */ |
---|
102 | protected $access_token_type = self::ACCESS_TOKEN_URI; |
---|
103 | |
---|
104 | /** |
---|
105 | * Access Token Secret |
---|
106 | * |
---|
107 | * @var string |
---|
108 | */ |
---|
109 | protected $access_token_secret = null; |
---|
110 | |
---|
111 | /** |
---|
112 | * Access Token crypt algorithm |
---|
113 | * |
---|
114 | * @var string |
---|
115 | */ |
---|
116 | protected $access_token_algorithm = null; |
---|
117 | |
---|
118 | /** |
---|
119 | * Access Token Parameter name |
---|
120 | * |
---|
121 | * @var string |
---|
122 | */ |
---|
123 | protected $access_token_param_name = 'access_token'; |
---|
124 | |
---|
125 | /** |
---|
126 | * The path to the certificate file to use for https connections |
---|
127 | * |
---|
128 | * @var string Defaults to . |
---|
129 | */ |
---|
130 | protected $certificate_file = null; |
---|
131 | |
---|
132 | /** |
---|
133 | * cURL options |
---|
134 | * |
---|
135 | * @var array |
---|
136 | */ |
---|
137 | protected $curl_options = array(); |
---|
138 | |
---|
139 | /** |
---|
140 | * Construct |
---|
141 | * |
---|
142 | * @param string $client_id Client ID |
---|
143 | * @param string $client_secret Client Secret |
---|
144 | * @param int $client_auth (AUTH_TYPE_URI, AUTH_TYPE_AUTHORIZATION_BASIC, AUTH_TYPE_FORM) |
---|
145 | * @param string $certificate_file Indicates if we want to use a certificate file to trust the server. Optional, defaults to null. |
---|
146 | * @return void |
---|
147 | */ |
---|
148 | public function __construct($client_id, $client_secret, $client_auth = self::AUTH_TYPE_URI, $certificate_file = null) |
---|
149 | { |
---|
150 | if (!extension_loaded('curl')) { |
---|
151 | throw new OAuth2_Exception('The PHP exention curl must be installed to use this library.', OAuth2_Exception::CURL_NOT_FOUND); |
---|
152 | } |
---|
153 | |
---|
154 | $this->client_id = $client_id; |
---|
155 | $this->client_secret = $client_secret; |
---|
156 | $this->client_auth = $client_auth; |
---|
157 | $this->certificate_file = $certificate_file; |
---|
158 | if (!empty($this->certificate_file) && !is_file($this->certificate_file)) { |
---|
159 | throw new OAuth2_InvalidArgumentException('The certificate file was not found', OAuth2_InvalidArgumentException::CERTIFICATE_NOT_FOUND); |
---|
160 | } |
---|
161 | } |
---|
162 | |
---|
163 | /** |
---|
164 | * Get the client Id |
---|
165 | * |
---|
166 | * @return string Client ID |
---|
167 | */ |
---|
168 | public function getClientId() |
---|
169 | { |
---|
170 | return $this->client_id; |
---|
171 | } |
---|
172 | |
---|
173 | /** |
---|
174 | * Get the client Secret |
---|
175 | * |
---|
176 | * @return string Client Secret |
---|
177 | */ |
---|
178 | public function getClientSecret() |
---|
179 | { |
---|
180 | return $this->client_secret; |
---|
181 | } |
---|
182 | |
---|
183 | /** |
---|
184 | * getAuthenticationUrl |
---|
185 | * |
---|
186 | * @param string $auth_endpoint Url of the authentication endpoint |
---|
187 | * @param string $redirect_uri Redirection URI |
---|
188 | * @param array $extra_parameters Array of extra parameters like scope or state (Ex: array('scope' => null, 'state' => '')) |
---|
189 | * @return string URL used for authentication |
---|
190 | */ |
---|
191 | public function getAuthenticationUrl($auth_endpoint, $redirect_uri, array $extra_parameters = array()) |
---|
192 | { |
---|
193 | $parameters = array_merge(array( |
---|
194 | 'response_type' => 'code', |
---|
195 | 'client_id' => $this->client_id, |
---|
196 | 'redirect_uri' => $redirect_uri |
---|
197 | ), $extra_parameters); |
---|
198 | return $auth_endpoint . '?' . http_build_query($parameters, null, '&'); |
---|
199 | } |
---|
200 | |
---|
201 | /** |
---|
202 | * getAccessToken |
---|
203 | * |
---|
204 | * @param string $token_endpoint Url of the token endpoint |
---|
205 | * @param int $grant_type Grant Type ('authorization_code', 'password', 'client_credentials', 'refresh_token', or a custom code (@see GrantType Classes) |
---|
206 | * @param array $parameters Array sent to the server (depend on which grant type you're using) |
---|
207 | * @return array Array of parameters required by the grant_type (CF SPEC) |
---|
208 | */ |
---|
209 | public function getAccessToken($token_endpoint, $grant_type, array $parameters) |
---|
210 | { |
---|
211 | if (!$grant_type) { |
---|
212 | throw new OAuth2_InvalidArgumentException('The grant_type is mandatory.', OAuth2_InvalidArgumentException::INVALID_GRANT_TYPE); |
---|
213 | } |
---|
214 | $grantTypeClassName = $this->convertToCamelCase($grant_type); |
---|
215 | $grantTypeClass = 'OAuth2_GrantType_' . $grantTypeClassName; |
---|
216 | if (!class_exists($grantTypeClass)) { |
---|
217 | throw new OAuth2_InvalidArgumentException('Unknown grant type \'' . $grant_type . '\'', OAuth2_InvalidArgumentException::INVALID_GRANT_TYPE); |
---|
218 | } |
---|
219 | $grantTypeObject = new $grantTypeClass(); |
---|
220 | $grantTypeObject->validateParameters($parameters); |
---|
221 | if (!defined($grantTypeClass . '::GRANT_TYPE')) { |
---|
222 | throw new OAuth2_Exception('Unknown constant GRANT_TYPE for class ' . $grantTypeClassName, OAuth2_Exception::GRANT_TYPE_ERROR); |
---|
223 | } |
---|
224 | $parameters['grant_type'] = $grantTypeClass::GRANT_TYPE; |
---|
225 | $http_headers = array(); |
---|
226 | switch ($this->client_auth) { |
---|
227 | case self::AUTH_TYPE_URI: |
---|
228 | case self::AUTH_TYPE_FORM: |
---|
229 | $parameters['client_id'] = $this->client_id; |
---|
230 | $parameters['client_secret'] = $this->client_secret; |
---|
231 | break; |
---|
232 | case self::AUTH_TYPE_AUTHORIZATION_BASIC: |
---|
233 | $parameters['client_id'] = $this->client_id; |
---|
234 | $http_headers['Authorization'] = 'Basic ' . base64_encode($this->client_id . ':' . $this->client_secret); |
---|
235 | break; |
---|
236 | default: |
---|
237 | throw new OAuth2_Exception('Unknown client auth type.', OAuth2_Exception::INVALID_CLIENT_AUTHENTICATION_TYPE); |
---|
238 | break; |
---|
239 | } |
---|
240 | |
---|
241 | return $this->executeRequest($token_endpoint, $parameters, self::HTTP_METHOD_POST, $http_headers, self::HTTP_FORM_CONTENT_TYPE_APPLICATION); |
---|
242 | } |
---|
243 | |
---|
244 | /** |
---|
245 | * setToken |
---|
246 | * |
---|
247 | * @param string $token Set the access token |
---|
248 | * @return void |
---|
249 | */ |
---|
250 | public function setAccessToken($token) |
---|
251 | { |
---|
252 | $this->access_token = $token; |
---|
253 | } |
---|
254 | |
---|
255 | /** |
---|
256 | * Set the client authentication type |
---|
257 | * |
---|
258 | * @param string $client_auth (AUTH_TYPE_URI, AUTH_TYPE_AUTHORIZATION_BASIC, AUTH_TYPE_FORM) |
---|
259 | * @return void |
---|
260 | */ |
---|
261 | public function setClientAuthType($client_auth) |
---|
262 | { |
---|
263 | $this->client_auth = $client_auth; |
---|
264 | } |
---|
265 | |
---|
266 | /** |
---|
267 | * Set an option for the curl transfer |
---|
268 | * |
---|
269 | * @param int $option The CURLOPT_XXX option to set |
---|
270 | * @param mixed $value The value to be set on option |
---|
271 | * @return void |
---|
272 | */ |
---|
273 | public function setCurlOption($option, $value) |
---|
274 | { |
---|
275 | $this->curl_options[$option] = $value; |
---|
276 | } |
---|
277 | |
---|
278 | /** |
---|
279 | * Set multiple options for a cURL transfer |
---|
280 | * |
---|
281 | * @param array $options An array specifying which options to set and their values |
---|
282 | * @return void |
---|
283 | */ |
---|
284 | public function setCurlOptions($options) |
---|
285 | { |
---|
286 | $this->curl_options = array_merge($this->curl_options, $options); |
---|
287 | } |
---|
288 | |
---|
289 | /** |
---|
290 | * Set the access token type |
---|
291 | * |
---|
292 | * @param int $type Access token type (ACCESS_TOKEN_BEARER, ACCESS_TOKEN_MAC, ACCESS_TOKEN_URI) |
---|
293 | * @param string $secret The secret key used to encrypt the MAC header |
---|
294 | * @param string $algorithm Algorithm used to encrypt the signature |
---|
295 | * @return void |
---|
296 | */ |
---|
297 | public function setAccessTokenType($type, $secret = null, $algorithm = null) |
---|
298 | { |
---|
299 | $this->access_token_type = $type; |
---|
300 | $this->access_token_secret = $secret; |
---|
301 | $this->access_token_algorithm = $algorithm; |
---|
302 | } |
---|
303 | |
---|
304 | /** |
---|
305 | * Fetch a protected ressource |
---|
306 | * |
---|
307 | * @param string $protected_ressource_url Protected resource URL |
---|
308 | * @param array $parameters Array of parameters |
---|
309 | * @param string $http_method HTTP Method to use (POST, PUT, GET, HEAD, DELETE) |
---|
310 | * @param array $http_headers HTTP headers |
---|
311 | * @param int $form_content_type HTTP form content type to use |
---|
312 | * @return array |
---|
313 | */ |
---|
314 | public function fetch($protected_resource_url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, array $http_headers = array(), $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART) |
---|
315 | { |
---|
316 | if ($this->access_token) { |
---|
317 | switch ($this->access_token_type) { |
---|
318 | case self::ACCESS_TOKEN_URI: |
---|
319 | if (is_array($parameters)) { |
---|
320 | $parameters[$this->access_token_param_name] = $this->access_token; |
---|
321 | } else { |
---|
322 | throw new OAuth2_InvalidArgumentException( |
---|
323 | 'You need to give parameters as array if you want to give the token within the URI.', |
---|
324 | OAuth2_InvalidArgumentException::REQUIRE_PARAMS_AS_ARRAY |
---|
325 | ); |
---|
326 | } |
---|
327 | break; |
---|
328 | case self::ACCESS_TOKEN_BEARER: |
---|
329 | $http_headers['Authorization'] = 'Bearer ' . $this->access_token; |
---|
330 | break; |
---|
331 | case self::ACCESS_TOKEN_OAUTH: |
---|
332 | $http_headers['Authorization'] = 'OAuth ' . $this->access_token; |
---|
333 | break; |
---|
334 | case self::ACCESS_TOKEN_MAC: |
---|
335 | $http_headers['Authorization'] = 'MAC ' . $this->generateMACSignature($protected_resource_url, $parameters, $http_method); |
---|
336 | break; |
---|
337 | default: |
---|
338 | throw new OAuth2_Exception('Unknown access token type.', OAuth2_Exception::INVALID_ACCESS_TOKEN_TYPE); |
---|
339 | break; |
---|
340 | } |
---|
341 | } |
---|
342 | return $this->executeRequest($protected_resource_url, $parameters, $http_method, $http_headers, $form_content_type); |
---|
343 | } |
---|
344 | |
---|
345 | /** |
---|
346 | * Generate the MAC signature |
---|
347 | * |
---|
348 | * @param string $url Called URL |
---|
349 | * @param array $parameters Parameters |
---|
350 | * @param string $http_method Http Method |
---|
351 | * @return string |
---|
352 | */ |
---|
353 | private function generateMACSignature($url, $parameters, $http_method) |
---|
354 | { |
---|
355 | $timestamp = time(); |
---|
356 | $nonce = uniqid(); |
---|
357 | $parsed_url = parse_url($url); |
---|
358 | if (!isset($parsed_url['port'])) |
---|
359 | { |
---|
360 | $parsed_url['port'] = ($parsed_url['scheme'] == 'https') ? 443 : 80; |
---|
361 | } |
---|
362 | if ($http_method == self::HTTP_METHOD_GET) { |
---|
363 | if (is_array($parameters)) { |
---|
364 | $parsed_url['path'] .= '?' . http_build_query($parameters, null, '&'); |
---|
365 | } elseif ($parameters) { |
---|
366 | $parsed_url['path'] .= '?' . $parameters; |
---|
367 | } |
---|
368 | } |
---|
369 | |
---|
370 | $signature = base64_encode(hash_hmac($this->access_token_algorithm, |
---|
371 | $timestamp . "\n" |
---|
372 | . $nonce . "\n" |
---|
373 | . $http_method . "\n" |
---|
374 | . $parsed_url['path'] . "\n" |
---|
375 | . $parsed_url['host'] . "\n" |
---|
376 | . $parsed_url['port'] . "\n\n" |
---|
377 | , $this->access_token_secret, true)); |
---|
378 | |
---|
379 | return 'id="' . $this->access_token . '", ts="' . $timestamp . '", nonce="' . $nonce . '", mac="' . $signature . '"'; |
---|
380 | } |
---|
381 | |
---|
382 | /** |
---|
383 | * Execute a request (with curl) |
---|
384 | * |
---|
385 | * @param string $url URL |
---|
386 | * @param mixed $parameters Array of parameters |
---|
387 | * @param string $http_method HTTP Method |
---|
388 | * @param array $http_headers HTTP Headers |
---|
389 | * @param int $form_content_type HTTP form content type to use |
---|
390 | * @return array |
---|
391 | */ |
---|
392 | private function executeRequest($url, $parameters = array(), $http_method = self::HTTP_METHOD_GET, array $http_headers = null, $form_content_type = self::HTTP_FORM_CONTENT_TYPE_MULTIPART) |
---|
393 | { |
---|
394 | $curl_options = array( |
---|
395 | CURLOPT_RETURNTRANSFER => true, |
---|
396 | CURLOPT_SSL_VERIFYPEER => true, |
---|
397 | CURLOPT_CUSTOMREQUEST => $http_method |
---|
398 | ); |
---|
399 | |
---|
400 | switch($http_method) { |
---|
401 | case self::HTTP_METHOD_POST: |
---|
402 | $curl_options[CURLOPT_POST] = true; |
---|
403 | /* No break */ |
---|
404 | case self::HTTP_METHOD_PUT: |
---|
405 | case self::HTTP_METHOD_PATCH: |
---|
406 | |
---|
407 | /** |
---|
408 | * Passing an array to CURLOPT_POSTFIELDS will encode the data as multipart/form-data, |
---|
409 | * while passing a URL-encoded string will encode the data as application/x-www-form-urlencoded. |
---|
410 | * http://php.net/manual/en/function.curl-setopt.php |
---|
411 | */ |
---|
412 | if(is_array($parameters) && self::HTTP_FORM_CONTENT_TYPE_APPLICATION === $form_content_type) { |
---|
413 | $parameters = http_build_query($parameters, null, '&'); |
---|
414 | } |
---|
415 | $curl_options[CURLOPT_POSTFIELDS] = $parameters; |
---|
416 | break; |
---|
417 | case self::HTTP_METHOD_HEAD: |
---|
418 | $curl_options[CURLOPT_NOBODY] = true; |
---|
419 | /* No break */ |
---|
420 | case self::HTTP_METHOD_DELETE: |
---|
421 | case self::HTTP_METHOD_GET: |
---|
422 | if (is_array($parameters)) { |
---|
423 | $url .= '?' . http_build_query($parameters, null, '&'); |
---|
424 | } elseif ($parameters) { |
---|
425 | $url .= '?' . $parameters; |
---|
426 | } |
---|
427 | break; |
---|
428 | default: |
---|
429 | break; |
---|
430 | } |
---|
431 | |
---|
432 | $curl_options[CURLOPT_URL] = $url; |
---|
433 | |
---|
434 | if (is_array($http_headers)) { |
---|
435 | $header = array(); |
---|
436 | foreach($http_headers as $key => $parsed_urlvalue) { |
---|
437 | $header[] = "$key: $parsed_urlvalue"; |
---|
438 | } |
---|
439 | $curl_options[CURLOPT_HTTPHEADER] = $header; |
---|
440 | } |
---|
441 | |
---|
442 | $ch = curl_init(); |
---|
443 | curl_setopt_array($ch, $curl_options); |
---|
444 | // https handling |
---|
445 | if (!empty($this->certificate_file)) { |
---|
446 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); |
---|
447 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); |
---|
448 | curl_setopt($ch, CURLOPT_CAINFO, $this->certificate_file); |
---|
449 | } else { |
---|
450 | // bypass ssl verification |
---|
451 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); |
---|
452 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); |
---|
453 | } |
---|
454 | if (!empty($this->curl_options)) { |
---|
455 | curl_setopt_array($ch, $this->curl_options); |
---|
456 | } |
---|
457 | $result = curl_exec($ch); |
---|
458 | $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); |
---|
459 | $content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); |
---|
460 | if ($curl_error = curl_error($ch)) { |
---|
461 | throw new OAuth2_Exception($curl_error, OAuth2_Exception::CURL_ERROR); |
---|
462 | } else { |
---|
463 | $json_decode = json_decode($result, true); |
---|
464 | } |
---|
465 | curl_close($ch); |
---|
466 | |
---|
467 | return array( |
---|
468 | 'result' => (null === $json_decode) ? $result : $json_decode, |
---|
469 | 'code' => $http_code, |
---|
470 | 'content_type' => $content_type |
---|
471 | ); |
---|
472 | } |
---|
473 | |
---|
474 | /** |
---|
475 | * Set the name of the parameter that carry the access token |
---|
476 | * |
---|
477 | * @param string $name Token parameter name |
---|
478 | * @return void |
---|
479 | */ |
---|
480 | public function setAccessTokenParamName($name) |
---|
481 | { |
---|
482 | $this->access_token_param_name = $name; |
---|
483 | } |
---|
484 | |
---|
485 | /** |
---|
486 | * Converts the class name to camel case |
---|
487 | * |
---|
488 | * @param mixed $grant_type the grant type |
---|
489 | * @return string |
---|
490 | */ |
---|
491 | private function convertToCamelCase($grant_type) |
---|
492 | { |
---|
493 | $parts = explode('_', $grant_type); |
---|
494 | array_walk($parts, function(&$item) { $item = ucfirst($item);}); |
---|
495 | return implode('', $parts); |
---|
496 | } |
---|
497 | } |
---|
498 | |
---|
499 | class OAuth2_Exception extends Exception |
---|
500 | { |
---|
501 | const CURL_NOT_FOUND = 0x01; |
---|
502 | const CURL_ERROR = 0x02; |
---|
503 | const GRANT_TYPE_ERROR = 0x03; |
---|
504 | const INVALID_CLIENT_AUTHENTICATION_TYPE = 0x04; |
---|
505 | const INVALID_ACCESS_TOKEN_TYPE = 0x05; |
---|
506 | } |
---|
507 | |
---|
508 | class OAuth2_InvalidArgumentException extends InvalidArgumentException |
---|
509 | { |
---|
510 | const INVALID_GRANT_TYPE = 0x01; |
---|
511 | const CERTIFICATE_NOT_FOUND = 0x02; |
---|
512 | const REQUIRE_PARAMS_AS_ARRAY = 0x03; |
---|
513 | const MISSING_PARAMETER = 0x04; |
---|
514 | } |
---|