source: trunk/prototype/library/oauth2/lib/OAuth2Client.php @ 6528

Revision 6528, 21.0 KB checked in by gustavo, 12 years ago (diff)

Ticket #2766 - Merge do branch das novas funcionalidaes para o trunk

  • Property svn:executable set to *
Line 
1<?php
2
3/**
4 * OAuth2.0 draft v10 client-side implementation.
5 *
6 * @author Originally written by Naitik Shah <naitik@facebook.com>.
7 * @author Update to draft v10 by Edison Wong <hswong3i@pantarei-design.com>.
8 *
9 * @sa <a href="https://github.com/facebook/php-sdk">Facebook PHP SDK</a>.
10 */
11abstract class OAuth2Client {
12       
13        /**
14         * The default Cache Lifetime (in seconds).
15         */
16        const DEFAULT_EXPIRES_IN = 3600;
17       
18        /**
19         * The default Base domain for the Cookie.
20         */
21        const DEFAULT_BASE_DOMAIN = '';
22       
23        /**
24         * Array of persistent variables stored.
25         */
26        protected $conf = array();
27
28        /**
29         * Returns a persistent variable.
30         *
31         * To avoid problems, always use lower case for persistent variable names.
32         *
33         * @param $name
34         * The name of the variable to return.
35         * @param $default
36         * The default value to use if this variable has never been set.
37         *
38         * @return
39         * The value of the variable.
40         */
41        public function getVariable($name, $default = NULL) {
42                return isset($this->conf[$name]) ? $this->conf[$name] : $default;
43        }
44
45        /**
46         * Sets a persistent variable.
47         *
48         * To avoid problems, always use lower case for persistent variable names.
49         *
50         * @param $name
51         * The name of the variable to set.
52         * @param $value
53         * The value to set.
54         */
55        public function setVariable($name, $value) {
56                $this->conf[$name] = $value;
57                return $this;
58        }
59
60        // Stuff that should get overridden by subclasses.
61        //
62        // I don't want to make these abstract, because then subclasses would have
63        // to implement all of them, which is too much work.
64        //
65        // So they're just stubs. Override the ones you need.
66       
67
68        /**
69         * Initialize a Drupal OAuth2.0 Application.
70         *
71         * @param $config
72         * An associative array as below:
73         * - base_uri: The base URI for the OAuth2.0 endpoints.
74         * - code: (optional) The authorization code.
75         * - username: (optional) The username.
76         * - password: (optional) The password.
77         * - client_id: (optional) The application ID.
78         * - client_secret: (optional) The application secret.
79         * - authorize_uri: (optional) The end-user authorization endpoint URI.
80         * - access_token_uri: (optional) The token endpoint URI.
81         * - services_uri: (optional) The services endpoint URI.
82         * - cookie_support: (optional) TRUE to enable cookie support.
83         * - base_domain: (optional) The domain for the cookie.
84         * - file_upload_support: (optional) TRUE if file uploads are enabled.
85         */
86        public function __construct($config = array()) {
87                // We must set base_uri first.
88                $this->setVariable('base_uri', $config['base_uri']);
89                unset($config['base_uri']);
90               
91                // Use predefined OAuth2.0 params, or get it from $_REQUEST.
92                foreach ( array('code', 'username', 'password') as $name ) {
93                        if (isset($config[$name])) {
94                                $this->setVariable($name, $config[$name]);
95                        } else if (isset($_REQUEST[$name]) && !empty($_REQUEST[$name])) {
96                                $this->setVariable($name, $_REQUEST[$name]);
97                        }
98                        unset($config[$name]);
99                }
100               
101                // Endpoint URIs.
102                foreach ( array('authorize_uri', 'access_token_uri', 'services_uri') as $name ) {
103                        if (isset($config[$name]))
104                                if (substr($config[$name], 0, 4) == "http") {
105                                        $this->setVariable($name, $config[$name]);
106                                } else {
107                                        $this->setVariable($name, $this->getVariable('base_uri') . $config[$name]);
108                                }
109                        unset($config[$name]);
110                }
111               
112                // Other else configurations.
113                foreach ( $config as $name => $value ) {
114                        $this->setVariable($name, $value);
115                }
116        }
117
118        /**
119         * Try to get session object from custom method.
120         *
121         * By default we generate session object based on access_token response, or
122         * if it is provided from server with $_REQUEST. For sure, if it is provided
123         * by server it should follow our session object format.
124         *
125         * Session object provided by server can ensure the correct expires and
126         * base_domain setup as predefined in server, also you may get more useful
127         * information for custom functionality, too. BTW, this may require for
128         * additional remote call overhead.
129         *
130         * You may wish to override this function with your custom version due to
131         * your own server-side implementation.
132         *
133         * @param $access_token
134         * (optional) A valid access token in associative array as below:
135         * - access_token: A valid access_token generated by OAuth2.0
136         * authorization endpoint.
137         * - expires_in: (optional) A valid expires_in generated by OAuth2.0
138         * authorization endpoint.
139         * - refresh_token: (optional) A valid refresh_token generated by OAuth2.0
140         * authorization endpoint.
141         * - scope: (optional) A valid scope generated by OAuth2.0
142         * authorization endpoint.
143         *
144         * @return
145         * A valid session object in associative array for setup cookie, and
146         * NULL if not able to generate it with custom method.
147         */
148        protected function getSessionObject($access_token = NULL) {
149                $session = NULL;
150               
151                // Try generate local version of session cookie.
152                if (!empty($access_token) && isset($access_token['access_token'])) {
153                        $session['access_token'] = $access_token['access_token'];
154                        $session['base_domain'] = $this->getVariable('base_domain', self::DEFAULT_BASE_DOMAIN);
155                        $session['expires'] = isset($access_token['expires_in']) ? time() + $access_token['expires_in'] : time() + $this->getVariable('expires_in', self::DEFAULT_EXPIRES_IN);
156                        $session['refresh_token'] = isset($access_token['refresh_token']) ? $access_token['refresh_token'] : '';
157                        $session['scope'] = isset($access_token['scope']) ? $access_token['scope'] : '';
158                        $session['secret'] = md5(base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), uniqid())));
159                       
160                        // Provide our own signature.
161                        $sig = self::generateSignature($session, $this->getVariable('client_secret'));
162                        $session['sig'] = $sig;
163                }
164               
165                // Try loading session from $_REQUEST.
166                if (!$session && isset($_REQUEST['session'])) {
167                        $session = json_decode(get_magic_quotes_gpc() ? stripslashes($_REQUEST['session']) : $_REQUEST['session'], TRUE);
168                }
169               
170                return $session;
171        }
172
173        /**
174         * Make an API call.
175         *
176         * Support both OAuth2.0 or normal GET/POST API call, with relative
177         * or absolute URI.
178         *
179         * If no valid OAuth2.0 access token found in session object, this function
180         * will automatically switch as normal remote API call without "oauth_token"
181         * parameter.
182         *
183         * Assume server reply in JSON object and always decode during return. If
184         * you hope to issue a raw query, please use makeRequest().
185         *
186         * @param $path
187         * The target path, relative to base_path/service_uri or an absolute URI.
188         * @param $method
189         * (optional) The HTTP method (default 'GET').
190         * @param $params
191         * (optional The GET/POST parameters.
192         *
193         * @return
194         * The JSON decoded response object.
195         *
196         * @throws OAuth2Exception
197         */
198        public function api($path, $method = 'GET', $params = array()) {
199                if (is_array($method) && empty($params)) {
200                        $params = $method;
201                        $method = 'GET';
202                }
203               
204                // json_encode all params values that are not strings.
205                foreach ( $params as $key => $value ) {
206                        if (!is_string($value)) {
207                                $params[$key] = json_encode($value);
208                        }
209                }
210               
211                $result = json_decode($this->makeOAuth2Request($this->getUri($path), $method, $params), TRUE);
212               
213                // Results are returned, errors are thrown.
214                if (is_array($result) && isset($result['error'])) {
215                        $e = new OAuth2Exception($result);
216                        switch ($e->getType()) {
217                                // OAuth 2.0 Draft 10 style.
218                                case 'invalid_token':
219                                        $this->setSession(NULL);
220                                default :
221                                        $this->setSession(NULL);
222                        }
223                        throw $e;
224                }
225                return $result;
226        }
227       
228        // End stuff that should get overridden.
229       
230
231        /**
232         * Default options for cURL.
233         */
234        public static $CURL_OPTS = array(
235                CURLOPT_CONNECTTIMEOUT => 10,
236                CURLOPT_RETURNTRANSFER => TRUE,
237                CURLOPT_HEADER => TRUE,
238                CURLOPT_TIMEOUT => 60,
239                CURLOPT_USERAGENT => 'oauth2-draft-v10',
240                CURLOPT_HTTPHEADER => array("Accept: application/json")
241        );
242
243        /**
244         * Set the Session.
245         *
246         * @param $session
247         * (optional) The session object to be set. NULL if hope to frush existing
248         * session object.
249         * @param $write_cookie
250         * (optional) TRUE if a cookie should be written. This value is ignored
251         * if cookie support has been disabled.
252         *
253         * @return
254         * The current OAuth2.0 client-side instance.
255         */
256        public function setSession($session = NULL, $write_cookie = TRUE) {
257                $this->setVariable('_session', $this->validateSessionObject($session));
258                $this->setVariable('_session_loaded', TRUE);
259                if ($write_cookie) {
260                        $this->setCookieFromSession($this->getVariable('_session'));
261                }
262                return $this;
263        }
264
265        /**
266         * Get the session object.
267         *
268         * This will automatically look for a signed session via custom method,
269         * OAuth2.0 grant type with authorization_code, OAuth2.0 grant type with
270         * password, or cookie that we had already setup.
271         *
272         * @return
273         * The valid session object with OAuth2.0 infomration, and NULL if not
274         * able to discover any cases.
275         */
276        public function getSession() {
277                if (!$this->getVariable('_session_loaded')) {
278                        $session = NULL;
279                        $write_cookie = TRUE;
280                       
281                        // Try obtain login session by custom method.
282                        $session = $this->getSessionObject(NULL);
283                        $session = $this->validateSessionObject($session);
284                       
285                        // grant_type == authorization_code.
286                        if (!$session && $this->getVariable('code')) {
287                                $access_token = $this->getAccessTokenFromAuthorizationCode($this->getVariable('code'));
288                                $session = $this->getSessionObject($access_token);
289                                $session = $this->validateSessionObject($session);
290                        }
291                       
292                        // grant_type == password.
293                        if (!$session && $this->getVariable('username') && $this->getVariable('password')) {
294                                $access_token = $this->getAccessTokenFromPassword($this->getVariable('username'), $this->getVariable('password'));
295                                $session = $this->getSessionObject($access_token);
296                                $session = $this->validateSessionObject($session);
297                        }
298                       
299                        // Try loading session from cookie if necessary.
300                        if (!$session && $this->getVariable('cookie_support')) {
301                                $cookie_name = $this->getSessionCookieName();
302                                if (isset($_COOKIE[$cookie_name])) {
303                                        $session = array();
304                                        parse_str(trim(get_magic_quotes_gpc() ? stripslashes($_COOKIE[$cookie_name]) : $_COOKIE[$cookie_name], '"'), $session);
305                                        $session = $this->validateSessionObject($session);
306                                        // Write only if we need to delete a invalid session cookie.
307                                        $write_cookie = empty($session);
308                                }
309                        }
310                       
311                        $this->setSession($session, $write_cookie);
312                }
313               
314                return $this->getVariable('_session');
315        }
316
317        /**
318         * Gets an OAuth2.0 access token from session.
319         *
320         * This will trigger getSession() and so we MUST initialize with required
321         * configuration.
322         *
323         * @return
324         * The valid OAuth2.0 access token, and NULL if not exists in session.
325         */
326        public function getAccessToken() {
327                $session = $this->getSession();
328                return isset($session['access_token']) ? $session['access_token'] : NULL;
329        }
330
331        /**
332         * Get access token from OAuth2.0 token endpoint with authorization code.
333         *
334         * This function will only be activated if both access token URI, client
335         * identifier and client secret are setup correctly.
336         *
337         * @param $code
338         * Authorization code issued by authorization server's authorization
339         * endpoint.
340         *
341         * @return
342         * A valid OAuth2.0 JSON decoded access token in associative array, and
343         * NULL if not enough parameters or JSON decode failed.
344         */
345        private function getAccessTokenFromAuthorizationCode($code) {
346                if ($this->getVariable('access_token_uri') && $this->getVariable('client_id') && $this->getVariable('client_secret')) {
347                        return json_decode($this->makeRequest(
348                                $this->getVariable('access_token_uri'),
349                                'POST',
350                                array(
351                                        'grant_type' => 'authorization_code',
352                                        'client_id' => $this->getVariable('client_id'),
353                                        'client_secret' => $this->getVariable('client_secret'),
354                                        'code' => $code,
355                                        'redirect_uri' => $this->getCurrentUri()
356                                )
357                        ), TRUE);
358                }
359                return NULL;
360        }
361
362        /**
363         * Get access token from OAuth2.0 token endpoint with basic user
364         * credentials.
365         *
366         * This function will only be activated if both username and password
367         * are setup correctly.
368         *
369         * @param $username
370         * Username to be check with.
371         * @param $password
372         * Password to be check with.
373         *
374         * @return
375         * A valid OAuth2.0 JSON decoded access token in associative array, and
376         * NULL if not enough parameters or JSON decode failed.
377         */
378        private function getAccessTokenFromPassword($username, $password) {
379                if ($this->getVariable('access_token_uri') && $this->getVariable('client_id') && $this->getVariable('client_secret')) {
380                        return json_decode($this->makeRequest(
381                                $this->getVariable('access_token_uri'),
382                                'POST',
383                                array(
384                                        'grant_type' => 'password',
385                                        'client_id' => $this->getVariable('client_id'),
386                                        'client_secret' => $this->getVariable('client_secret'),
387                                        'username' => $username,
388                                        'password' => $password
389                                )
390                        ), TRUE);
391                }
392                return NULL;
393        }
394
395        /**
396         * Make an OAuth2.0 Request.
397         *
398         * Automatically append "oauth_token" in query parameters if not yet
399         * exists and able to discover a valid access token from session. Otherwise
400         * just ignore setup with "oauth_token" and handle the API call AS-IS, and
401         * so may issue a plain API call without OAuth2.0 protection.
402         *
403         * @param $path
404         * The target path, relative to base_path/service_uri or an absolute URI.
405         * @param $method
406         * (optional) The HTTP method (default 'GET').
407         * @param $params
408         * (optional The GET/POST parameters.
409         *
410         * @return
411         * The JSON decoded response object.
412         *
413         * @throws OAuth2Exception
414         */
415        protected function makeOAuth2Request($path, $method = 'GET', $params = array()) {
416                if ((!isset($params['oauth_token']) || empty($params['oauth_token'])) && $oauth_token = $this->getAccessToken()) {
417                        $params['oauth_token'] = $oauth_token;
418                }
419                return $this->makeRequest($path, $method, $params);
420        }
421
422        /**
423         * Makes an HTTP request.
424         *
425         * This method can be overriden by subclasses if developers want to do
426         * fancier things or use something other than cURL to make the request.
427         *
428         * @param $path
429         * The target path, relative to base_path/service_uri or an absolute URI.
430         * @param $method
431         * (optional) The HTTP method (default 'GET').
432         * @param $params
433         * (optional The GET/POST parameters.
434         * @param $ch
435         * (optional) An initialized curl handle
436         *
437         * @return
438         * The JSON decoded response object.
439         */
440        protected function makeRequest($path, $method = 'GET', $params = array(), $ch = NULL) {
441                if (!$ch)
442                        $ch = curl_init();
443               
444                $opts = self::$CURL_OPTS;
445                if ($params) {
446                        switch ($method) {
447                                case 'GET':
448                                        $path .= '?' . http_build_query($params, NULL, '&');
449                                        break;
450                                // Method override as we always do a POST.
451                                default :
452                                        if ($this->getVariable('file_upload_support')) {
453                                                $opts[CURLOPT_POSTFIELDS] = $params;
454                                        } else {
455                                                $opts[CURLOPT_POSTFIELDS] = http_build_query($params, NULL, '&');
456                                        }
457                        }
458                }
459                $opts[CURLOPT_URL] = $path;
460               
461                // Disable the 'Expect: 100-continue' behaviour. This causes CURL to wait
462                // for 2 seconds if the server does not support this header.
463                if (isset($opts[CURLOPT_HTTPHEADER])) {
464                        $existing_headers = $opts[CURLOPT_HTTPHEADER];
465                        $existing_headers[] = 'Expect:';
466                        $opts[CURLOPT_HTTPHEADER] = $existing_headers;
467                } else {
468                        $opts[CURLOPT_HTTPHEADER] = array('Expect:');
469                }
470               
471                curl_setopt_array($ch, $opts);
472                $result = curl_exec($ch);
473               
474                if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT
475                        error_log('Invalid or no certificate authority found, using bundled information');
476                        curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__) . '/fb_ca_chain_bundle.crt');
477                        $result = curl_exec($ch);
478                }
479               
480                if ($result === FALSE) {
481                        $e = new OAuth2Exception(array('code' => curl_errno($ch), 'message' => curl_error($ch)));
482                        curl_close($ch);
483                        throw $e;
484                }
485                curl_close($ch);
486               
487                // Split the HTTP response into header and body.
488                list($headers, $body) = explode("\r\n\r\n", $result);
489                $headers = explode("\r\n", $headers);
490               
491                // We catch HTTP/1.1 4xx or HTTP/1.1 5xx error response.
492                if (strpos($headers[0], 'HTTP/1.1 4') !== FALSE || strpos($headers[0], 'HTTP/1.1 5') !== FALSE) {
493                        $result = array('code' => 0, 'message' => '');
494                       
495                        if (preg_match('/^HTTP\/1.1 ([0-9]{3,3}) (.*)$/', $headers[0], $matches)) {
496                                $result['code'] = $matches[1];
497                                $result['message'] = $matches[2];
498                        }
499                       
500                        // In case retrun with WWW-Authenticate replace the description.
501                        foreach ( $headers as $header ) {
502                                if (preg_match("/^WWW-Authenticate:.*error='(.*)'/", $header, $matches)) {
503                                        $result['error'] = $matches[1];
504                                }
505                        }
506                       
507                        return json_encode($result);
508                }
509               
510                return $body;
511        }
512
513        /**
514         * The name of the cookie that contains the session object.
515         *
516         * @return
517         * The cookie name.
518         */
519        private function getSessionCookieName() {
520                return 'oauth2_' . $this->getVariable('client_id');
521        }
522
523        /**
524         * Set a JS Cookie based on the _passed in_ session.
525         *
526         * It does not use the currently stored session - you need to explicitly
527         * pass it in.
528         *
529         * @param $session
530         * The session to use for setting the cookie.
531         */
532        protected function setCookieFromSession($session = NULL) {
533                if (!$this->getVariable('cookie_support'))
534                        return;
535               
536                $cookie_name = $this->getSessionCookieName();
537                $value = 'deleted';
538                $expires = time() - 3600;
539                $base_domain = $this->getVariable('base_domain', self::DEFAULT_BASE_DOMAIN);
540                if ($session) {
541                        $value = '"' . http_build_query($session, NULL, '&') . '"';
542                        $base_domain = isset($session['base_domain']) ? $session['base_domain'] : $base_domain;
543                        $expires = isset($session['expires']) ? $session['expires'] : time() + $this->getVariable('expires_in', self::DEFAULT_EXPIRES_IN);
544                }
545               
546                // Prepend dot if a domain is found.
547                if ($base_domain)
548                        $base_domain = '.' . $base_domain;
549               
550                // If an existing cookie is not set, we dont need to delete it.
551                if ($value == 'deleted' && empty($_COOKIE[$cookie_name]))
552                        return;
553               
554                if (headers_sent())
555                        error_log('Could not set cookie. Headers already sent.');
556                else
557                        setcookie($cookie_name, $value, $expires, '/', $base_domain);
558        }
559
560        /**
561         * Validates a session_version = 3 style session object.
562         *
563         * @param $session
564         * The session object.
565         *
566         * @return
567         * The session object if it validates, NULL otherwise.
568         */
569        protected function validateSessionObject($session) {
570                // Make sure some essential fields exist.
571                if (is_array($session) && isset($session['access_token']) && isset($session['sig'])) {
572                        // Validate the signature.
573                        $session_without_sig = $session;
574                        unset($session_without_sig['sig']);
575                       
576                        $expected_sig = self::generateSignature($session_without_sig, $this->getVariable('client_secret'));
577                       
578                        if ($session['sig'] != $expected_sig) {
579                                error_log('Got invalid session signature in cookie.');
580                                $session = NULL;
581                        }
582                } else {
583                        $session = NULL;
584                }
585                return $session;
586        }
587
588        /**
589         * Since $_SERVER['REQUEST_URI'] is only available on Apache, we
590         * generate an equivalent using other environment variables.
591         */
592        function getRequestUri() {
593                if (isset($_SERVER['REQUEST_URI'])) {
594                        $uri = $_SERVER['REQUEST_URI'];
595                } else {
596                        if (isset($_SERVER['argv'])) {
597                                $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['argv'][0];
598                        } elseif (isset($_SERVER['QUERY_STRING'])) {
599                                $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['QUERY_STRING'];
600                        } else {
601                                $uri = $_SERVER['SCRIPT_NAME'];
602                        }
603                }
604                // Prevent multiple slashes to avoid cross site requests via the Form API.
605                $uri = '/' . ltrim($uri, '/');
606               
607                return $uri;
608        }
609
610        /**
611         * Returns the Current URL.
612         *
613         * @return
614         * The current URL.
615         */
616        protected function getCurrentUri() {
617                $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 'https://' : 'http://';
618                $current_uri = $protocol . $_SERVER['HTTP_HOST'] . $this->getRequestUri();
619                $parts = parse_url($current_uri);
620               
621                $query = '';
622                if (!empty($parts['query'])) {
623                        $params = array();
624                        parse_str($parts['query'], $params);
625                        $params = array_filter($params);
626                        if (!empty($params)) {
627                                $query = '?' . http_build_query($params, NULL, '&');
628                        }
629                }
630               
631                // Use port if non default.
632                $port = '';
633                if (isset($parts['port']) && (($protocol === 'http://' && $parts['port'] !== 80) || ($protocol === 'https://' && $parts['port'] !== 443))) {
634                        $port = ':' . $parts['port'];
635                }
636               
637               
638                // Rebuild.
639                return $protocol . $parts['host'] . $port . $parts['path'] . $query;
640        }
641
642        /**
643         * Build the URL for given path and parameters.
644         *
645         * @param $path
646         * (optional) The path.
647         * @param $params
648         * (optional) The query parameters in associative array.
649         *
650         * @return
651         * The URL for the given parameters.
652         */
653        protected function getUri($path = '', $params = array()) {
654                $url = $this->getVariable('services_uri') ? $this->getVariable('services_uri') : $this->getVariable('base_uri');
655               
656                if (!empty($path))
657                        if (substr($path, 0, 4) == "http")
658                                $url = $path;
659                        else
660                                $url = rtrim($url, '/') . '/' . ltrim($path, '/');
661               
662                if (!empty($params))
663                        $url .= '?' . http_build_query($params, NULL, '&');
664               
665                return $url;
666        }
667
668        /**
669         * Generate a signature for the given params and secret.
670         *
671         * @param $params
672         * The parameters to sign.
673         * @param $secret
674         * The secret to sign with.
675         *
676         * @return
677         * The generated signature
678         */
679        protected function generateSignature($params, $secret) {
680                // Work with sorted data.
681                ksort($params);
682               
683                // Generate the base string.
684                $base_string = '';
685                foreach ( $params as $key => $value ) {
686                        $base_string .= $key . '=' . $value;
687                }
688                $base_string .= $secret;
689               
690                return md5($base_string);
691        }
692}
Note: See TracBrowser for help on using the repository browser.