4 * The default Cache Lifetime (in seconds).
6 define("OAUTH2_DEFAULT_EXPIRES_IN", 3600);
9 * The default Base domain for the Cookie.
11 define("OAUTH2_DEFAULT_BASE_DOMAIN", '');
14 * OAuth2.0 draft v10 client-side implementation.
16 * @author Originally written by Naitik Shah <naitik@facebook.com>.
17 * @author Update to draft v10 by Edison Wong <hswong3i@pantarei-design.com>.
19 * @sa <a href="https://github.com/facebook/php-sdk">Facebook PHP SDK</a>.
21 abstract class OAuth2Client {
24 * Array of persistent variables stored.
26 protected $conf = array();
29 * Returns a persistent variable.
31 * To avoid problems, always use lower case for persistent variable names.
34 * The name of the variable to return.
36 * The default value to use if this variable has never been set.
39 * The value of the variable.
41 public function getVariable($name, $default = NULL) {
42 return isset($this->conf[$name]) ? $this->conf[$name] : $default;
46 * Sets a persistent variable.
48 * To avoid problems, always use lower case for persistent variable names.
51 * The name of the variable to set.
55 public function setVariable($name, $value) {
56 $this->conf[$name] = $value;
60 // Stuff that should get overridden by subclasses.
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.
65 // So they're just stubs. Override the ones you need.
68 * Initialize a Drupal OAuth2.0 Application.
71 * An associative array as below:
72 * - base_uri: The base URI for the OAuth2.0 endpoints.
73 * - code: (optional) The authorization code.
74 * - username: (optional) The username.
75 * - password: (optional) The password.
76 * - client_id: (optional) The application ID.
77 * - client_secret: (optional) The application secret.
78 * - authorize_uri: (optional) The end-user authorization endpoint URI.
79 * - access_token_uri: (optional) The token endpoint URI.
80 * - services_uri: (optional) The services endpoint URI.
81 * - cookie_support: (optional) TRUE to enable cookie support.
82 * - base_domain: (optional) The domain for the cookie.
83 * - file_upload_support: (optional) TRUE if file uploads are enabled.
85 public function __construct($config = array()) {
86 // We must set base_uri first.
87 $this->setVariable('base_uri', $config['base_uri']);
88 unset($config['base_uri']);
90 // Use predefined OAuth2.0 params, or get it from $_REQUEST.
91 foreach (array('code', 'username', 'password') as $name) {
92 if (isset($config[$name]))
93 $this->setVariable($name, $config[$name]);
94 else if (isset($_REQUEST[$name]) && !empty($_REQUEST[$name]))
95 $this->setVariable($name, $_REQUEST[$name]);
96 unset($config[$name]);
100 foreach (array('authorize_uri', 'access_token_uri', 'services_uri') as $name) {
101 if (isset($config[$name]))
102 if (substr($config[$name], 0, 4) == "http")
103 $this->setVariable($name, $config[$name]);
105 $this->setVariable($name, $this->getVariable('base_uri') . $config[$name]);
106 unset($config[$name]);
109 // Other else configurations.
110 foreach ($config as $name => $value) {
111 $this->setVariable($name, $value);
116 * Try to get session object from custom method.
118 * By default we generate session object based on access_token response, or
119 * if it is provided from server with $_REQUEST. For sure, if it is provided
120 * by server it should follow our session object format.
122 * Session object provided by server can ensure the correct expirse and
123 * base_domain setup as predefined in server, also you may get more useful
124 * information for custom functionality, too. BTW, this may require for
125 * additional remote call overhead.
127 * You may wish to override this function with your custom version due to
128 * your own server-side implementation.
130 * @param $access_token
131 * (optional) A valid access token in associative array as below:
132 * - access_token: A valid access_token generated by OAuth2.0
133 * authorization endpoint.
134 * - expires_in: (optional) A valid expires_in generated by OAuth2.0
135 * authorization endpoint.
136 * - refresh_token: (optional) A valid refresh_token generated by OAuth2.0
137 * authorization endpoint.
138 * - scope: (optional) A valid scope generated by OAuth2.0
139 * authorization endpoint.
142 * A valid session object in associative array for setup cookie, and
143 * NULL if not able to generate it with custom method.
145 protected function getSessionObject($access_token = NULL) {
148 // Try generate local version of session cookie.
149 if (!empty($access_token) && isset($access_token['access_token'])) {
150 $session['access_token'] = $access_token['access_token'];
151 $session['base_domain'] = $this->getVariable('base_domain', OAUTH2_DEFAULT_BASE_DOMAIN);
152 $session['expirse'] = isset($access_token['expires_in']) ? time() + $access_token['expires_in'] : time() + $this->getVariable('expires_in', OAUTH2_DEFAULT_EXPIRES_IN);
153 $session['refresh_token'] = isset($access_token['refresh_token']) ? $access_token['refresh_token'] : '';
154 $session['scope'] = isset($access_token['scope']) ? $access_token['scope'] : '';
155 $session['secret'] = md5(base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), uniqid())));
157 // Provide our own signature.
158 $sig = self::generateSignature(
160 $this->getVariable('client_secret')
162 $session['sig'] = $sig;
165 // Try loading session from $_REQUEST.
166 if (!$session && isset($_REQUEST['session'])) {
167 $session = json_decode(
168 get_magic_quotes_gpc()
169 ? stripslashes($_REQUEST['session'])
170 : $_REQUEST['session'],
181 * Support both OAuth2.0 or normal GET/POST API call, with relative
184 * If no valid OAuth2.0 access token found in session object, this function
185 * will automatically switch as normal remote API call without "oauth_token"
188 * Assume server reply in JSON object and always decode during return. If
189 * you hope to issue a raw query, please use makeRequest().
192 * The target path, relative to base_path/service_uri or an absolute URI.
194 * (optional) The HTTP method (default 'GET').
196 * (optional The GET/POST parameters.
199 * The JSON decoded response object.
201 * @throws OAuth2Exception
203 public function api($path, $method = 'GET', $params = array()) {
204 if (is_array($method) && empty($params)) {
209 // json_encode all params values that are not strings.
210 foreach ($params as $key => $value) {
211 if (!is_string($value)) {
212 $params[$key] = json_encode($value);
216 $result = json_decode($this->makeOAuth2Request(
217 $this->getUri($path),
222 // Results are returned, errors are thrown.
223 if (is_array($result) && isset($result['error'])) {
224 $e = new OAuth2Exception($result);
225 switch ($e->getType()) {
226 // OAuth 2.0 Draft 10 style.
227 case 'invalid_token':
228 $this->setSession(NULL);
230 $this->setSession(NULL);
237 // End stuff that should get overridden.
240 * Default options for cURL.
242 public static $CURL_OPTS = array(
243 CURLOPT_CONNECTTIMEOUT => 10,
244 CURLOPT_RETURNTRANSFER => TRUE,
245 CURLOPT_HEADER => TRUE,
246 CURLOPT_TIMEOUT => 60,
247 CURLOPT_USERAGENT => 'oauth2-draft-v10',
248 CURLOPT_HTTPHEADER => array("Accept: application/json"),
255 * (optional) The session object to be set. NULL if hope to frush existing
257 * @param $write_cookie
258 * (optional) TRUE if a cookie should be written. This value is ignored
259 * if cookie support has been disabled.
262 * The current OAuth2.0 client-side instance.
264 public function setSession($session = NULL, $write_cookie = TRUE) {
265 $this->setVariable('_session', $this->validateSessionObject($session));
266 $this->setVariable('_session_loaded', TRUE);
268 $this->setCookieFromSession($this->getVariable('_session'));
274 * Get the session object.
276 * This will automatically look for a signed session via custom method,
277 * OAuth2.0 grant type with authorization_code, OAuth2.0 grant type with
278 * password, or cookie that we had already setup.
281 * The valid session object with OAuth2.0 infomration, and NULL if not
282 * able to discover any cases.
284 public function getSession() {
285 if (!$this->getVariable('_session_loaded')) {
287 $write_cookie = TRUE;
289 // Try obtain login session by custom method.
290 $session = $this->getSessionObject(NULL);
291 $session = $this->validateSessionObject($session);
293 // grant_type == authorization_code.
294 if (!$session && $this->getVariable('code')) {
295 $access_token = $this->getAccessTokenFromAuthorizationCode($this->getVariable('code'));
296 $session = $this->getSessionObject($access_token);
297 $session = $this->validateSessionObject($session);
300 // grant_type == password.
301 if (!$session && $this->getVariable('username') && $this->getVariable('password')) {
302 $access_token = $this->getAccessTokenFromPassword($this->getVariable('username'), $this->getVariable('password'));
303 $session = $this->getSessionObject($access_token);
304 $session = $this->validateSessionObject($session);
307 // Try loading session from cookie if necessary.
308 if (!$session && $this->getVariable('cookie_support')) {
309 $cookie_name = $this->getSessionCookieName();
310 if (isset($_COOKIE[$cookie_name])) {
313 get_magic_quotes_gpc()
314 ? stripslashes($_COOKIE[$cookie_name])
315 : $_COOKIE[$cookie_name],
318 $session = $this->validateSessionObject($session);
319 // Write only if we need to delete a invalid session cookie.
320 $write_cookie = empty($session);
324 $this->setSession($session, $write_cookie);
327 return $this->getVariable('_session');
331 * Gets an OAuth2.0 access token from session.
333 * This will trigger getSession() and so we MUST initialize with required
337 * The valid OAuth2.0 access token, and NULL if not exists in session.
339 public function getAccessToken() {
340 $session = $this->getSession();
341 return isset($session['access_token']) ? $session['access_token'] : NULL;
345 * Get access token from OAuth2.0 token endpoint with authorization code.
347 * This function will only be activated if both access token URI, client
348 * identifier and client secret are setup correctly.
351 * Authorization code issued by authorization server's authorization
355 * A valid OAuth2.0 JSON decoded access token in associative array, and
356 * NULL if not enough parameters or JSON decode failed.
358 private function getAccessTokenFromAuthorizationCode($code) {
359 if ($this->getVariable('access_token_uri') && $this->getVariable('client_id') && $this->getVariable('client_secret')) {
360 return json_decode($this->makeRequest(
361 $this->getVariable('access_token_uri'),
364 'grant_type' => 'authorization_code',
365 'client_id' => $this->getVariable('client_id'),
366 'client_secret' => $this->getVariable('client_secret'),
368 'redirect_uri' => $this->getCurrentUri(),
376 * Get access token from OAuth2.0 token endpoint with basic user
379 * This function will only be activated if both username and password
380 * are setup correctly.
383 * Username to be check with.
385 * Password to be check with.
388 * A valid OAuth2.0 JSON decoded access token in associative array, and
389 * NULL if not enough parameters or JSON decode failed.
391 private function getAccessTokenFromPassword($username, $password) {
392 if ($this->getVariable('access_token_uri') && $this->getVariable('client_id') && $this->getVariable('client_secret')) {
393 return json_decode($this->makeRequest(
394 $this->getVariable('access_token_uri'),
397 'grant_type' => 'password',
398 'client_id' => $this->getVariable('client_id'),
399 'client_secret' => $this->getVariable('client_secret'),
400 'username' => $username,
401 'password' => $password,
409 * Make an OAuth2.0 Request.
411 * Automatically append "oauth_token" in query parameters if not yet
412 * exists and able to discover a valid access token from session. Otherwise
413 * just ignore setup with "oauth_token" and handle the API call AS-IS, and
414 * so may issue a plain API call without OAuth2.0 protection.
417 * The target path, relative to base_path/service_uri or an absolute URI.
419 * (optional) The HTTP method (default 'GET').
421 * (optional The GET/POST parameters.
424 * The JSON decoded response object.
426 * @throws OAuth2Exception
428 protected function makeOAuth2Request($path, $method = 'GET', $params = array()) {
429 if ((!isset($params['oauth_token']) || empty($params['oauth_token'])) && $oauth_token = $this->getAccessToken()) {
430 $params['oauth_token'] = $oauth_token;
432 return $this->makeRequest($path, $method, $params);
436 * Makes an HTTP request.
438 * This method can be overriden by subclasses if developers want to do
439 * fancier things or use something other than cURL to make the request.
442 * The target path, relative to base_path/service_uri or an absolute URI.
444 * (optional) The HTTP method (default 'GET').
446 * (optional The GET/POST parameters.
448 * (optional) An initialized curl handle
451 * The JSON decoded response object.
453 protected function makeRequest($path, $method = 'GET', $params = array(), $ch = NULL) {
457 $opts = self::$CURL_OPTS;
461 $path .= '?' . http_build_query($params, NULL, '&');
463 // Method override as we always do a POST.
465 if ($this->getVariable('file_upload_support')) {
466 $opts[CURLOPT_POSTFIELDS] = $params;
469 $opts[CURLOPT_POSTFIELDS] = http_build_query($params, NULL, '&');
473 $opts[CURLOPT_URL] = $path;
475 // Disable the 'Expect: 100-continue' behaviour. This causes CURL to wait
476 // for 2 seconds if the server does not support this header.
477 if (isset($opts[CURLOPT_HTTPHEADER])) {
478 $existing_headers = $opts[CURLOPT_HTTPHEADER];
479 $existing_headers[] = 'Expect:';
480 $opts[CURLOPT_HTTPHEADER] = $existing_headers;
483 $opts[CURLOPT_HTTPHEADER] = array('Expect:');
486 curl_setopt_array($ch, $opts);
487 $result = curl_exec($ch);
489 if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT
490 error_log('Invalid or no certificate authority found, using bundled information');
491 curl_setopt($ch, CURLOPT_CAINFO,
492 dirname(__FILE__) . '/fb_ca_chain_bundle.crt');
493 $result = curl_exec($ch);
496 if ($result === FALSE) {
497 $e = new OAuth2Exception(array(
498 'code' => curl_errno($ch),
499 'message' => curl_error($ch),
506 // Split the HTTP response into header and body.
507 list($headers, $body) = explode("\r\n\r\n", $result);
508 $headers = explode("\r\n", $headers);
510 // We catch HTTP/1.1 4xx or HTTP/1.1 5xx error response.
511 if (strpos($headers[0], 'HTTP/1.1 4') !== FALSE || strpos($headers[0], 'HTTP/1.1 5') !== FALSE) {
517 if (preg_match('/^HTTP\/1.1 ([0-9]{3,3}) (.*)$/', $headers[0], $matches)) {
518 $result['code'] = $matches[1];
519 $result['message'] = $matches[2];
522 // In case retrun with WWW-Authenticate replace the description.
523 foreach ($headers as $header) {
524 if (preg_match("/^WWW-Authenticate:.*error='(.*)'/", $header, $matches)) {
525 $result['error'] = $matches[1];
529 return json_encode($result);
536 * The name of the cookie that contains the session object.
541 private function getSessionCookieName() {
542 return 'oauth2_' . $this->getVariable('client_id');
546 * Set a JS Cookie based on the _passed in_ session.
548 * It does not use the currently stored session - you need to explicitly
552 * The session to use for setting the cookie.
554 protected function setCookieFromSession($session = NULL) {
555 if (!$this->getVariable('cookie_support'))
558 $cookie_name = $this->getSessionCookieName();
560 $expires = time() - 3600;
561 $base_domain = $this->getVariable('base_domain', OAUTH2_DEFAULT_BASE_DOMAIN);
563 $value = '"' . http_build_query($session, NULL, '&') . '"';
564 $base_domain = isset($session['base_domain']) ? $session['base_domain'] : $base_domain;
565 $expires = isset($session['expires']) ? $session['expires'] : time() + $this->getVariable('expires_in', OAUTH2_DEFAULT_EXPIRES_IN);
568 // Prepend dot if a domain is found.
570 $base_domain = '.' . $base_domain;
572 // If an existing cookie is not set, we dont need to delete it.
573 if ($value == 'deleted' && empty($_COOKIE[$cookie_name]))
577 error_log('Could not set cookie. Headers already sent.');
579 setcookie($cookie_name, $value, $expires, '/', $base_domain);
583 * Validates a session_version = 3 style session object.
586 * The session object.
589 * The session object if it validates, NULL otherwise.
591 protected function validateSessionObject($session) {
592 // Make sure some essential fields exist.
593 if (is_array($session) && isset($session['access_token']) && isset($session['sig'])) {
594 // Validate the signature.
595 $session_without_sig = $session;
596 unset($session_without_sig['sig']);
598 $expected_sig = self::generateSignature(
599 $session_without_sig,
600 $this->getVariable('client_secret')
603 if ($session['sig'] != $expected_sig) {
604 error_log('Got invalid session signature in cookie.');
615 * Since $_SERVER['REQUEST_URI'] is only available on Apache, we
616 * generate an equivalent using other environment variables.
618 function getRequestUri() {
619 if (isset($_SERVER['REQUEST_URI'])) {
620 $uri = $_SERVER['REQUEST_URI'];
623 if (isset($_SERVER['argv'])) {
624 $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['argv'][0];
626 elseif (isset($_SERVER['QUERY_STRING'])) {
627 $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['QUERY_STRING'];
630 $uri = $_SERVER['SCRIPT_NAME'];
633 // Prevent multiple slashes to avoid cross site requests via the Form API.
634 $uri = '/' . ltrim($uri, '/');
640 * Returns the Current URL.
645 protected function getCurrentUri() {
646 $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'
649 $current_uri = $protocol . $_SERVER['HTTP_HOST'] . $this->getRequestUri();
650 $parts = parse_url($current_uri);
653 if (!empty($parts['query'])) {
655 parse_str($parts['query'], $params);
656 $params = array_filter($params);
657 if (!empty($params)) {
658 $query = '?' . http_build_query($params, NULL, '&');
662 // Use port if non default.
663 $port = isset($parts['port']) &&
664 (($protocol === 'http://' && $parts['port'] !== 80) || ($protocol === 'https://' && $parts['port'] !== 443))
665 ? ':' . $parts['port'] : '';
668 return $protocol . $parts['host'] . $port . $parts['path'] . $query;
672 * Build the URL for given path and parameters.
675 * (optional) The path.
677 * (optional) The query parameters in associative array.
680 * The URL for the given parameters.
682 protected function getUri($path = '', $params = array()) {
683 $url = $this->getVariable('services_uri') ? $this->getVariable('services_uri') : $this->getVariable('base_uri');
686 if (substr($path, 0, 4) == "http")
689 $url = rtrim($url, '/') . '/' . ltrim($path, '/');
692 $url .= '?' . http_build_query($params, NULL, '&');
698 * Generate a signature for the given params and secret.
701 * The parameters to sign.
703 * The secret to sign with.
706 * The generated signature
708 protected function generateSignature($params, $secret) {
709 // Work with sorted data.
712 // Generate the base string.
714 foreach ($params as $key => $value) {
715 $base_string .= $key . '=' . $value;
717 $base_string .= $secret;
719 return md5($base_string);