diff options
28 files changed, 3197 insertions, 10 deletions
@@ -11,7 +11,7 @@ require_once('include/cache.php'); define ( 'FRIENDIKA_PLATFORM', 'Friendica'); define ( 'FRIENDIKA_VERSION', '2.3.1158' ); define ( 'DFRN_PROTOCOL_VERSION', '2.22' ); -define ( 'DB_UPDATE_VERSION', 1102 ); +define ( 'DB_UPDATE_VERSION', 1103 ); define ( 'EOL', "<br />\r\n" ); define ( 'ATOM_TIME', 'Y-m-d\TH:i:s\Z' ); @@ -654,7 +654,7 @@ function get_guid($size=16) { // returns the complete html for inserting into the page if(! function_exists('login')) { -function login($register = false) { +function login($register = false, $hiddens=false) { $o = ""; $reg = false; if ($register) { @@ -685,6 +685,7 @@ function login($register = false) { '$openid' => !$noid, '$lopenid' => array('openid_url', t('OpenID: '),'',''), + '$hiddens' => $hiddens, '$register' => $reg, diff --git a/database.sql b/database.sql index 3d11ff4b7..58b81b0ac 100644 --- a/database.sql +++ b/database.sql @@ -460,14 +460,19 @@ CREATE TABLE IF NOT EXISTS `clients` ( `client_id` VARCHAR( 20 ) NOT NULL , `pw` VARCHAR( 20 ) NOT NULL , `redirect_uri` VARCHAR( 200 ) NOT NULL , +`name` VARCHAR( 128 ) NULL DEFAULT NULL, +`icon` VARCHAR( 255 ) NULL DEFAULT NULL, +`uid` INT NOT NULL DEFAULT 0, PRIMARY KEY ( `client_id` ) ) ENGINE = MyISAM DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `tokens` ( `id` VARCHAR( 40 ) NOT NULL , +`secret` VARCHAR( 40 ) NOT NULL , `client_id` VARCHAR( 20 ) NOT NULL , `expires` INT NOT NULL , `scope` VARCHAR( 200 ) NOT NULL , +`uid` INT NOT NULL , PRIMARY KEY ( `id` ) ) ENGINE = MyISAM DEFAULT CHARSET=utf8; diff --git a/images/icons/10/plugin.png b/images/icons/10/plugin.png Binary files differnew file mode 100644 index 000000000..6cfc40786 --- /dev/null +++ b/images/icons/10/plugin.png diff --git a/images/icons/16/plugin.png b/images/icons/16/plugin.png Binary files differnew file mode 100644 index 000000000..b11d92082 --- /dev/null +++ b/images/icons/16/plugin.png diff --git a/images/icons/22/plugin.png b/images/icons/22/plugin.png Binary files differnew file mode 100644 index 000000000..cf4421125 --- /dev/null +++ b/images/icons/22/plugin.png diff --git a/images/icons/48/plugin.png b/images/icons/48/plugin.png Binary files differnew file mode 100644 index 000000000..c74c6bf46 --- /dev/null +++ b/images/icons/48/plugin.png diff --git a/images/icons/Makefile b/images/icons/Makefile index 6a96fb676..ec57b943e 100644 --- a/images/icons/Makefile +++ b/images/icons/Makefile @@ -2,7 +2,7 @@ IMAGES=add.png edit.png gear.png info.png menu.png \ notify_off.png star.png delete.png feed.png group.png \ lock.png notice.png notify_on.png user.png link.png \ - play.png + play.png plugin.png DESTS=10/ 16/ 22/ 48/ \ $(addprefix 10/, $(IMAGES)) \ diff --git a/images/icons/plugin.png b/images/icons/plugin.png Binary files differnew file mode 100644 index 000000000..943be0d93 --- /dev/null +++ b/images/icons/plugin.png diff --git a/include/api.php b/include/api.php index d94cc2942..1196e0aac 100644 --- a/include/api.php +++ b/include/api.php @@ -2,7 +2,7 @@ require_once("bbcode.php"); require_once("datetime.php"); require_once("conversation.php"); - + require_once("oauth.php"); /* * Twitter-Like API * @@ -27,6 +27,23 @@ * Simple HTTP Login */ function api_login(&$a){ + // login with oauth + try{ + $oauth = new FKOAuth1(); + list($consumer,$token) = $oauth->verify_request(OAuthRequest::from_request()); + if (!is_null($token)){ + $oauth->loginUser($token->uid); + call_hooks('logged_in', $a->user); + return; + } + echo __file__.__line__.__function__."<pre>"; var_dump($consumer, $token); die(); + }catch(Exception $e){ + logger(__file__.__line__.__function__."\n".$e); + //die(__file__.__line__.__function__."<pre>".$e); die(); + } + + + // workaround for HTTP-auth in CGI mode if(x($_SERVER,'REDIRECT_REMOTE_USER')) { $userpass = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"],6)) ; @@ -1127,3 +1144,31 @@ } api_register_func('api/direct_messages/sent','api_direct_messages_sentbox',true); api_register_func('api/direct_messages','api_direct_messages_inbox',true); + + + + function api_oauth_request_token(&$a, $type){ + try{ + $oauth = new FKOAuth1(); + $r = $oauth->fetch_request_token(OAuthRequest::from_request()); + }catch(Exception $e){ + echo "error=". OAuthUtil::urlencode_rfc3986($e->getMessage()); killme(); + } + echo $r; + killme(); + } + function api_oauth_access_token(&$a, $type){ + try{ + $oauth = new FKOAuth1(); + $r = $oauth->fetch_access_token(OAuthRequest::from_request()); + }catch(Exception $e){ + echo "error=". OAuthUtil::urlencode_rfc3986($e->getMessage()); killme(); + } + echo $r; + killme(); + } + + api_register_func('api/oauth/request_token', 'api_oauth_request_token', false); + api_register_func('api/oauth/access_token', 'api_oauth_access_token', false); + + diff --git a/include/oauth.php b/include/oauth.php new file mode 100644 index 000000000..2724dcf7c --- /dev/null +++ b/include/oauth.php @@ -0,0 +1,266 @@ +<?php +/** + * OAuth server + * Based on oauth2-php <http://code.google.com/p/oauth2-php/> + * + */ + +define('REQUEST_TOKEN_DURATION', 300); +define('ACCESS_TOKEN_DURATION', 31536000); + +require_once("library/OAuth1.php"); +require_once("library/oauth2-php/lib/OAuth2.inc"); + +class FKOAuthDataStore extends OAuthDataStore { + function gen_token(){ + return md5(base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), uniqid()))); + } + + function lookup_consumer($consumer_key) { + logger(__function__.":".$consumer_key); + //echo "<pre>"; var_dump($consumer_key); killme(); + + $r = q("SELECT client_id, pw, redirect_uri FROM clients WHERE client_id='%s'", + dbesc($consumer_key) + ); + if (count($r)) + return new OAuthConsumer($r[0]['client_id'],$r[0]['pw'],$r[0]['redirect_uri']); + return null; + } + + function lookup_token($consumer, $token_type, $token) { + logger(__function__.":".$consumer.", ". $token_type.", ".$token); + $r = q("SELECT id, secret,scope, expires, uid FROM tokens WHERE client_id='%s' AND scope='%s' AND id='%s'", + dbesc($consumer->key), + dbesc($token_type), + dbesc($token) + ); + if (count($r)){ + $ot=new OAuthToken($r[0]['id'],$r[0]['secret']); + $ot->scope=$r[0]['scope']; + $ot->expires = $r[0]['expires']; + $ot->uid = $r[0]['uid']; + return $ot; + } + return null; + } + + function lookup_nonce($consumer, $token, $nonce, $timestamp) { + //echo __file__.":".__line__."<pre>"; var_dump($consumer,$key); killme(); + $r = q("SELECT id, secret FROM tokens WHERE client_id='%s' AND id='%s' AND expires=%d", + dbesc($consumer->key), + dbesc($nonce), + intval($timestamp) + ); + if (count($r)) + return new OAuthToken($r[0]['id'],$r[0]['secret']); + return null; + } + + function new_request_token($consumer, $callback = null) { + logger(__function__.":".$consumer.", ". $callback); + $key = $this->gen_token(); + $sec = $this->gen_token(); + + if ($consumer->key){ + $k = $consumer->key; + } else { + $k = $consumer; + } + + $r = q("INSERT INTO tokens (id, secret, client_id, scope, expires) VALUES ('%s','%s','%s','%s', UNIX_TIMESTAMP()+%d)", + dbesc($key), + dbesc($sec), + dbesc($k), + 'request', + intval(REQUEST_TOKEN_DURATION)); + if (!$r) return null; + return new OAuthToken($key,$sec); + } + + function new_access_token($token, $consumer, $verifier = null) { + logger(__function__.":".$token.", ". $consumer.", ". $verifier); + + // return a new access token attached to this consumer + // for the user associated with this token if the request token + // is authorized + // should also invalidate the request token + + $ret=Null; + + // get user for this verifier + $uverifier = get_config("oauth", $verifier); + logger(__function__.":".$verifier.",".$uverifier); + if (is_null($verifier) || ($uverifier!==false)){ + + $key = $this->gen_token(); + $sec = $this->gen_token(); + $r = q("INSERT INTO tokens (id, secret, client_id, scope, expires, uid) VALUES ('%s','%s','%s','%s', UNIX_TIMESTAMP()+%d, %d)", + dbesc($key), + dbesc($sec), + dbesc($consumer->key), + 'access', + intval(ACCESS_TOKEN_DURATION), + intval($uverifier)); + if ($r) + $ret = new OAuthToken($key,$sec); + } + + + q("DELETE FROM tokens WHERE id='%s'", $token->key); + + + if (!is_null($ret) && $uverifier!==false){ + del_config("oauth", $verifier); + /* $apps = get_pconfig($uverifier, "oauth", "apps"); + if ($apps===false) $apps=array(); + $apps[] = $consumer->key; + set_pconfig($uverifier, "oauth", "apps", $apps);*/ + } + + return $ret; + + } +} + +class FKOAuth1 extends OAuthServer { + function __construct() { + parent::__construct(new FKOAuthDataStore()); + $this->add_signature_method(new OAuthSignatureMethod_PLAINTEXT()); + $this->add_signature_method(new OAuthSignatureMethod_HMAC_SHA1()); + } + + function loginUser($uid){ + logger("FKOAuth1::loginUser $uid"); + $a = get_app(); + $r = q("SELECT * FROM `user` WHERE uid=%d AND `blocked` = 0 AND `account_expired` = 0 AND `verified` = 1 LIMIT 1", + intval($uid) + ); + if(count($r)){ + $record = $r[0]; + } else { + logger('FKOAuth1::loginUser failure: ' . print_r($_SERVER,true), LOGGER_DEBUG); + header('HTTP/1.0 401 Unauthorized'); + die('This api requires login'); + } + $_SESSION['uid'] = $record['uid']; + $_SESSION['theme'] = $record['theme']; + $_SESSION['authenticated'] = 1; + $_SESSION['page_flags'] = $record['page-flags']; + $_SESSION['my_url'] = $a->get_baseurl() . '/profile/' . $record['nickname']; + $_SESSION['addr'] = $_SERVER['REMOTE_ADDR']; + + //notice( t("Welcome back ") . $record['username'] . EOL); + $a->user = $record; + + if(strlen($a->user['timezone'])) { + date_default_timezone_set($a->user['timezone']); + $a->timezone = $a->user['timezone']; + } + + $r = q("SELECT * FROM `contact` WHERE `uid` = %s AND `self` = 1 LIMIT 1", + intval($_SESSION['uid'])); + if(count($r)) { + $a->contact = $r[0]; + $a->cid = $r[0]['id']; + $_SESSION['cid'] = $a->cid; + } + q("UPDATE `user` SET `login_date` = '%s' WHERE `uid` = %d LIMIT 1", + dbesc(datetime_convert()), + intval($_SESSION['uid']) + ); + + call_hooks('logged_in', $a->user); + } + +} +/* +class FKOAuth2 extends OAuth2 { + + private function db_secret($client_secret){ + return hash('whirlpool',$client_secret); + } + + public function addClient($client_id, $client_secret, $redirect_uri) { + $client_secret = $this->db_secret($client_secret); + $r = q("INSERT INTO clients (client_id, pw, redirect_uri) VALUES ('%s', '%s', '%s')", + dbesc($client_id), + dbesc($client_secret), + dbesc($redirect_uri) + ); + + return $r; + } + + protected function checkClientCredentials($client_id, $client_secret = NULL) { + $client_secret = $this->db_secret($client_secret); + + $r = q("SELECT pw FROM clients WHERE client_id = '%s'", + dbesc($client_id)); + + if ($client_secret === NULL) + return $result !== FALSE; + + return $result["client_secret"] == $client_secret; + } + + protected function getRedirectUri($client_id) { + $r = q("SELECT redirect_uri FROM clients WHERE client_id = '%s'", + dbesc($client_id)); + if ($r === FALSE) + return FALSE; + + return isset($r[0]["redirect_uri"]) && $r[0]["redirect_uri"] ? $r[0]["redirect_uri"] : NULL; + } + + protected function getAccessToken($oauth_token) { + $r = q("SELECT client_id, expires, scope FROM tokens WHERE id = '%s'", + dbesc($oauth_token)); + + if (count($r)) + return $r[0]; + return null; + } + + + + protected function setAccessToken($oauth_token, $client_id, $expires, $scope = NULL) { + $r = q("INSERT INTO tokens (id, client_id, expires, scope) VALUES ('%s', '%s', %d, '%s')", + dbesc($oauth_token), + dbesc($client_id), + intval($expires), + dbesc($scope)); + + return $r; + } + + protected function getSupportedGrantTypes() { + return array( + OAUTH2_GRANT_TYPE_AUTH_CODE, + ); + } + + + protected function getAuthCode($code) { + $r = q("SELECT id, client_id, redirect_uri, expires, scope FROM auth_codes WHERE id = '%s'", + dbesc($code)); + + if (count($r)) + return $r[0]; + return null; + } + + protected function setAuthCode($code, $client_id, $redirect_uri, $expires, $scope = NULL) { + $r = q("INSERT INTO auth_codes + (id, client_id, redirect_uri, expires, scope) VALUES + ('%s', '%s', '%s', %d, '%s')", + dbesc($code), + dbesc($client_id), + dbesc($redirect_uri), + intval($expires), + dbesc($scope)); + return $r; + } + +} +*/ diff --git a/library/OAuth1.php b/library/OAuth1.php index 67a94c479..3b211b146 100644 --- a/library/OAuth1.php +++ b/library/OAuth1.php @@ -27,6 +27,10 @@ class OAuthToken { public $key; public $secret; + public $expires; + public $scope; + public $uid; + /** * key = the token * secret = the token secret @@ -85,7 +89,8 @@ abstract class OAuthSignatureMethod { */ public function check_signature($request, $consumer, $token, $signature) { $built = $this->build_signature($request, $consumer, $token); - return $built == $signature; + //echo "<pre>"; var_dump($signature, $built, ($built == $signature)); killme(); + return ($built == $signature); } } @@ -113,7 +118,9 @@ class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod { $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); $key = implode('&', $key_parts); - return base64_encode(hash_hmac('sha1', $base_string, $key, true)); + + $r = base64_encode(hash_hmac('sha1', $base_string, $key, true)); + return $r; } } @@ -282,7 +289,12 @@ class OAuthRequest { } } - + // fix for friendika redirect system + + $http_url = substr($http_url, 0, strpos($http_url,$parameters['q'])+strlen($parameters['q'])); + unset( $parameters['q'] ); + + //echo "<pre>".__function__."\n"; var_dump($http_method, $http_url, $parameters, $_SERVER['REQUEST_URI']); killme(); return new OAuthRequest($http_method, $http_url, $parameters); } @@ -544,6 +556,7 @@ class OAuthServer { public function verify_request(&$request) { $this->get_version($request); $consumer = $this->get_consumer($request); + //echo __file__.__line__.__function__."<pre>"; var_dump($consumer); die(); $token = $this->get_token($request, $consumer, "access"); $this->check_signature($request, $consumer, $token); return array($consumer, $token); @@ -642,6 +655,7 @@ class OAuthServer { $token, $signature ); + if (!$valid_sig) { throw new OAuthException("Invalid signature"); diff --git a/library/oauth2-php/CHANGELOG.txt b/library/oauth2-php/CHANGELOG.txt new file mode 100644 index 000000000..d854598b6 --- /dev/null +++ b/library/oauth2-php/CHANGELOG.txt @@ -0,0 +1,98 @@ +oauth2-php revision xxx, xxxx-xx-xx (development version) +---------------------- + +oauth2-php revision 23, 2011-01-25 +---------------------- +* introduce Drupal style getVariable() and setVariable, replace legacy + variable get/set functions. +* remove hardcode PHP display_error and errror_reporting, as this should + be manually implement within 3rd party integration. +* make verbose error as configurable and default disable, as this should + be manually enable within 3rd party integration. +* add lib/OAuth2Client.inc and lib/OAuth2Exception.inc for client-side + implementation. + +oauth2-php revision 21, 2010-12-18 +---------------------- +* cleanup tabs and trailing whitespace at the end. +* remove server/examples/mongo/lib/oauth.php and + server/examples/pdo/lib/oauth.php, so only keep single copy as + lib /oauth.php. +* issue #5: Wrong variable name in get_access_token() in pdo_oatuh.php. +* issue #6: mysql_create_tables.sql should allow scope to be NULL. +* issue #7: authorize_client_response_type() is never used. +* issue #9: Change "redirect_uri" filtering from FILTER_VALIDATE_URL to + FILTER_SANITIZE_URL. +* better coding syntax for error() and callback_error(). +* better pdo_oauth2.php variable naming with change to + mysql_create_tables.sql. +* change REGEX_CLIENT_ID as 3-32 characters long, so will work with md5() + result directly. +* debug linkage to oauth2.php during previous commit. +* debug redirect_uri check for AUTH_CODE_GRANT_TYPE, clone from + get_authorize_params(). +* update mysql_create_tables.sql with phpmyadmin export format. +* rename library files, prepare for adding client-side implementation. +* code cleanup with indent and spacing. +* code cleanup true/false/null with TRUE/FALSE/NULL. +* rename constants with OAUTH2_ prefix, prevent 3rd party integration + conflict. +* remove HTTP 400 response constant, as useless refer to draft v10. +* merge ERROR_INVALID_CLIENT_ID and ERROR_UNAUTHORIZED_CLIENT as + OAUTH2_ERROR_INVALID_CLIENT, as refer to that of draft v9 to v10 changes. +* improve constants comment with doxygen syntax. +* update class function call naming. +* coding style clean up. +* update part of documents. +* change expirseRefreshToken() as unsetRefreshToken(). +* update token and auth code generation as md5() result, simpler for manual + debug with web browser. +* update all documents. +* restructure @ingroup. +* rename checkRestrictedClientResponseTypes() as + checkRestrictedAuthResponseType(). +* rename checkRestrictedClientGrantTypes() as checkRestrictedGrantType(). +* rename error() as errorJsonResponse(). +* rename errorCallback() as errorDoRedirectUriCallback(). +* rename send401Unauthorized() as errorWWWAuthenticateResponseHeader(), + update support with different HTTP status code. +* update __construct() with array input. +* update finishClientAuthorization() with array input. +* add get/set functions for $access_token_lifetime, $auth_code_lifetime and + $refresh_token_lifetime. +* fix a lots of typos. +* document all sample server implementation. +* more documents. +* add config.doxy for doxygen default setup. +* add MIT LICENSE.txt. +* add CHANGELOG.txt. + +oauth2-php revision 9, 2010-09-04 +---------------------- +- fixes for issues #2 and #4, updates oauth lib in the example folders to + the latest version in the 'lib' folder. +- updates server library to revision 10 of the OAuth 2.0 spec. +- adds an option for more verbose error messages to be returned in the JSON + response. +- adds method to be overridden for expiring used refresh tokens. +- fixes bug checking token expiration. +- makes some more methods protected instead of private so they can be + overridden. +- fixes issue #1 http://code.google.com/p/oauth2-php/issues/detail?id=1 + +oauth2-php revision 7, 2010-06-29 +---------------------- +- fixed mongo connection constants. +- updated store_refresh_token to include expires time. +- changed example server directory structure +- corrected "false" return result on get_stored_auth_code. +- implemented PDO example adapter. +- corrected an error in assertion grant type. +- updated for ietf draft v9: + http://tools.ietf.org/html/draft-ietf-oauth-v2-09. +- updated updated to support v9 lib. +- added mysql table creation script. + +oauth2-php revision 0, 2010-06-27 +---------------------- +- initial commit. diff --git a/library/oauth2-php/LICENSE.txt b/library/oauth2-php/LICENSE.txt new file mode 100644 index 000000000..7979b1c85 --- /dev/null +++ b/library/oauth2-php/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2010 Tim Ridgely <tim@opendining.net> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/library/oauth2-php/lib/OAuth2.inc b/library/oauth2-php/lib/OAuth2.inc new file mode 100644 index 000000000..e10e0f26d --- /dev/null +++ b/library/oauth2-php/lib/OAuth2.inc @@ -0,0 +1,1560 @@ +<?php + +/** + * @mainpage + * OAuth 2.0 server in PHP, originally written for + * <a href="http://www.opendining.net/"> Open Dining</a>. Supports + * <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-10">IETF draft v10</a>. + * + * Source repo has sample servers implementations for + * <a href="http://php.net/manual/en/book.pdo.php"> PHP Data Objects</a> and + * <a href="http://www.mongodb.org/">MongoDB</a>. Easily adaptable to other + * storage engines. + * + * PHP Data Objects supports a variety of databases, including MySQL, + * Microsoft SQL Server, SQLite, and Oracle, so you can try out the sample + * to see how it all works. + * + * We're expanding the wiki to include more helpful documentation, but for + * now, your best bet is to view the oauth.php source - it has lots of + * comments. + * + * @author Tim Ridgely <tim.ridgely@gmail.com> + * @author Aaron Parecki <aaron@parecki.com> + * @author Edison Wong <hswong3i@pantarei-design.com> + * + * @see http://code.google.com/p/oauth2-php/ + */ + + +/** + * The default duration in seconds of the access token lifetime. + */ +define("OAUTH2_DEFAULT_ACCESS_TOKEN_LIFETIME", 3600); + +/** + * The default duration in seconds of the authorization code lifetime. + */ +define("OAUTH2_DEFAULT_AUTH_CODE_LIFETIME", 30); + +/** + * The default duration in seconds of the refresh token lifetime. + */ +define("OAUTH2_DEFAULT_REFRESH_TOKEN_LIFETIME", 1209600); + + +/** + * @defgroup oauth2_section_2 Client Credentials + * @{ + * + * When interacting with the authorization server, the client identifies + * itself using a client identifier and authenticates using a set of + * client credentials. This specification provides one mechanism for + * authenticating the client using password credentials. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-2 + */ + +/** + * Regex to filter out the client identifier (described in Section 2 of IETF draft). + * + * IETF draft does not prescribe a format for these, however I've arbitrarily + * chosen alphanumeric strings with hyphens and underscores, 3-32 characters + * long. + * + * Feel free to change. + */ +define("OAUTH2_CLIENT_ID_REGEXP", "/^[a-z0-9-_]{3,32}$/i"); + +/** + * @} + */ + + +/** + * @defgroup oauth2_section_3 Obtaining End-User Authorization + * @{ + * + * When the client interacts with an end-user, the end-user MUST first + * grant the client authorization to access its protected resources. + * Once obtained, the end-user access grant is expressed as an + * authorization code which the client uses to obtain an access token. + * To obtain an end-user authorization, the client sends the end-user to + * the end-user authorization endpoint. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3 + */ + +/** + * Denotes "token" authorization response type. + */ +define("OAUTH2_AUTH_RESPONSE_TYPE_ACCESS_TOKEN", "token"); + +/** + * Denotes "code" authorization response type. + */ +define("OAUTH2_AUTH_RESPONSE_TYPE_AUTH_CODE", "code"); + +/** + * Denotes "code-and-token" authorization response type. + */ +define("OAUTH2_AUTH_RESPONSE_TYPE_CODE_AND_TOKEN", "code-and-token"); + +/** + * Regex to filter out the authorization response type. + */ +define("OAUTH2_AUTH_RESPONSE_TYPE_REGEXP", "/^(token|code|code-and-token)$/"); + +/** + * @} + */ + + +/** + * @defgroup oauth2_section_4 Obtaining an Access Token + * @{ + * + * The client obtains an access token by authenticating with the + * authorization server and presenting its access grant (in the form of + * an authorization code, resource owner credentials, an assertion, or a + * refresh token). + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4 + */ + +/** + * Denotes "authorization_code" grant types (for token obtaining). + */ +define("OAUTH2_GRANT_TYPE_AUTH_CODE", "authorization_code"); + +/** + * Denotes "password" grant types (for token obtaining). + */ +define("OAUTH2_GRANT_TYPE_USER_CREDENTIALS", "password"); + +/** + * Denotes "assertion" grant types (for token obtaining). + */ +define("OAUTH2_GRANT_TYPE_ASSERTION", "assertion"); + +/** + * Denotes "refresh_token" grant types (for token obtaining). + */ +define("OAUTH2_GRANT_TYPE_REFRESH_TOKEN", "refresh_token"); + +/** + * Denotes "none" grant types (for token obtaining). + */ +define("OAUTH2_GRANT_TYPE_NONE", "none"); + +/** + * Regex to filter out the grant type. + */ +define("OAUTH2_GRANT_TYPE_REGEXP", "/^(authorization_code|password|assertion|refresh_token|none)$/"); + +/** + * @} + */ + + +/** + * @defgroup oauth2_section_5 Accessing a Protected Resource + * @{ + * + * Clients access protected resources by presenting an access token to + * the resource server. Access tokens act as bearer tokens, where the + * token string acts as a shared symmetric secret. This requires + * treating the access token with the same care as other secrets (e.g. + * end-user passwords). Access tokens SHOULD NOT be sent in the clear + * over an insecure channel. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5 + */ + +/** + * Used to define the name of the OAuth access token parameter (POST/GET/etc.). + * + * IETF Draft sections 5.1.2 and 5.1.3 specify that it should be called + * "oauth_token" but other implementations use things like "access_token". + * + * I won't be heartbroken if you change it, but it might be better to adhere + * to the spec. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.1.2 + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.1.3 + */ +define("OAUTH2_TOKEN_PARAM_NAME", "oauth_token"); + +/** + * @} + */ + + +/** + * @defgroup oauth2_http_status HTTP status code + * @{ + */ + +/** + * "Found" HTTP status code. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3 + */ +define("OAUTH2_HTTP_FOUND", "302 Found"); + +/** + * "Bad Request" HTTP status code. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3 + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1 + */ +define("OAUTH2_HTTP_BAD_REQUEST", "400 Bad Request"); + +/** + * "Unauthorized" HTTP status code. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3 + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1 + */ +define("OAUTH2_HTTP_UNAUTHORIZED", "401 Unauthorized"); + +/** + * "Forbidden" HTTP status code. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1 + */ +define("OAUTH2_HTTP_FORBIDDEN", "403 Forbidden"); + +/** + * @} + */ + + +/** + * @defgroup oauth2_error Error handling + * @{ + * + * @todo Extend for i18n. + */ + +/** + * The request is missing a required parameter, includes an unsupported + * parameter or parameter value, or is otherwise malformed. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1 + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1 + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1 + */ +define("OAUTH2_ERROR_INVALID_REQUEST", "invalid_request"); + +/** + * The client identifier provided is invalid. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1 + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1 + */ +define("OAUTH2_ERROR_INVALID_CLIENT", "invalid_client"); + +/** + * The client is not authorized to use the requested response type. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1 + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1 + */ +define("OAUTH2_ERROR_UNAUTHORIZED_CLIENT", "unauthorized_client"); + +/** + * The redirection URI provided does not match a pre-registered value. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1 + */ +define("OAUTH2_ERROR_REDIRECT_URI_MISMATCH", "redirect_uri_mismatch"); + +/** + * The end-user or authorization server denied the request. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1 + */ +define("OAUTH2_ERROR_USER_DENIED", "access_denied"); + +/** + * The requested response type is not supported by the authorization server. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1 + */ +define("OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE", "unsupported_response_type"); + +/** + * The requested scope is invalid, unknown, or malformed. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2.1 + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1 + */ +define("OAUTH2_ERROR_INVALID_SCOPE", "invalid_scope"); + +/** + * The provided access grant is invalid, expired, or revoked (e.g. invalid + * assertion, expired authorization token, bad end-user password credentials, + * or mismatching authorization code and redirection URI). + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1 + */ +define("OAUTH2_ERROR_INVALID_GRANT", "invalid_grant"); + +/** + * The access grant included - its type or another attribute - is not + * supported by the authorization server. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3.1 + */ +define("OAUTH2_ERROR_UNSUPPORTED_GRANT_TYPE", "unsupported_grant_type"); + +/** + * The access token provided is invalid. Resource servers SHOULD use this + * error code when receiving an expired token which cannot be refreshed to + * indicate to the client that a new authorization is necessary. The resource + * server MUST respond with the HTTP 401 (Unauthorized) status code. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1 + */ +define("OAUTH2_ERROR_INVALID_TOKEN", "invalid_token"); + +/** + * The access token provided has expired. Resource servers SHOULD only use + * this error code when the client is expected to be able to handle the + * response and request a new access token using the refresh token issued + * with the expired access token. The resource server MUST respond with the + * HTTP 401 (Unauthorized) status code. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1 + */ +define("OAUTH2_ERROR_EXPIRED_TOKEN", "expired_token"); + +/** + * The request requires higher privileges than provided by the access token. + * The resource server SHOULD respond with the HTTP 403 (Forbidden) status + * code and MAY include the "scope" attribute with the scope necessary to + * access the protected resource. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2.1 + */ +define("OAUTH2_ERROR_INSUFFICIENT_SCOPE", "insufficient_scope"); + +/** + * @} + */ + +/** + * OAuth2.0 draft v10 server-side implementation. + * + * @author Originally written by Tim Ridgely <tim.ridgely@gmail.com>. + * @author Updated to draft v10 by Aaron Parecki <aaron@parecki.com>. + * @author Debug, coding style clean up and documented by Edison Wong <hswong3i@pantarei-design.com>. + */ +abstract class OAuth2 { + + /** + * Array of persistent variables stored. + */ + protected $conf = array(); + + /** + * Returns a persistent variable. + * + * To avoid problems, always use lower case for persistent variable names. + * + * @param $name + * The name of the variable to return. + * @param $default + * The default value to use if this variable has never been set. + * + * @return + * The value of the variable. + */ + public function getVariable($name, $default = NULL) { + return isset($this->conf[$name]) ? $this->conf[$name] : $default; + } + + /** + * Sets a persistent variable. + * + * To avoid problems, always use lower case for persistent variable names. + * + * @param $name + * The name of the variable to set. + * @param $value + * The value to set. + */ + public function setVariable($name, $value) { + $this->conf[$name] = $value; + return $this; + } + + // Subclasses must implement the following functions. + + /** + * Make sure that the client credentials is valid. + * + * @param $client_id + * Client identifier to be check with. + * @param $client_secret + * (optional) If a secret is required, check that they've given the right one. + * + * @return + * TRUE if client credentials are valid, and MUST return FALSE if invalid. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-2.1 + * + * @ingroup oauth2_section_2 + */ + abstract protected function checkClientCredentials($client_id, $client_secret = NULL); + + /** + * Get the registered redirect URI of corresponding client_id. + * + * OAuth says we should store request URIs for each registered client. + * Implement this function to grab the stored URI for a given client id. + * + * @param $client_id + * Client identifier to be check with. + * + * @return + * Registered redirect URI of corresponding client identifier, and MUST + * return FALSE if the given client does not exist or is invalid. + * + * @ingroup oauth2_section_3 + */ + abstract protected function getRedirectUri($client_id); + + /** + * Look up the supplied oauth_token from storage. + * + * We need to retrieve access token data as we create and verify tokens. + * + * @param $oauth_token + * oauth_token to be check with. + * + * @return + * An associative array as below, and return NULL if the supplied oauth_token + * is invalid: + * - client_id: Stored client identifier. + * - expires: Stored expiration in unix timestamp. + * - scope: (optional) Stored scope values in space-separated string. + * + * @ingroup oauth2_section_5 + */ + abstract protected function getAccessToken($oauth_token); + + /** + * Store the supplied access token values to storage. + * + * We need to store access token data as we create and verify tokens. + * + * @param $oauth_token + * oauth_token to be stored. + * @param $client_id + * Client identifier to be stored. + * @param $expires + * Expiration to be stored. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_4 + */ + abstract protected function setAccessToken($oauth_token, $client_id, $expires, $scope = NULL); + + // Stuff that should get overridden by subclasses. + // + // I don't want to make these abstract, because then subclasses would have + // to implement all of them, which is too much work. + // + // So they're just stubs. Override the ones you need. + + /** + * Return supported grant types. + * + * You should override this function with something, or else your OAuth + * provider won't support any grant types! + * + * @return + * A list as below. If you support all grant types, then you'd do: + * @code + * return array( + * OAUTH2_GRANT_TYPE_AUTH_CODE, + * OAUTH2_GRANT_TYPE_USER_CREDENTIALS, + * OAUTH2_GRANT_TYPE_ASSERTION, + * OAUTH2_GRANT_TYPE_REFRESH_TOKEN, + * OAUTH2_GRANT_TYPE_NONE, + * ); + * @endcode + * + * @ingroup oauth2_section_4 + */ + protected function getSupportedGrantTypes() { + return array(); + } + + /** + * Return supported authorization response types. + * + * You should override this function with your supported response types. + * + * @return + * A list as below. If you support all authorization response types, + * then you'd do: + * @code + * return array( + * OAUTH2_AUTH_RESPONSE_TYPE_AUTH_CODE, + * OAUTH2_AUTH_RESPONSE_TYPE_ACCESS_TOKEN, + * OAUTH2_AUTH_RESPONSE_TYPE_CODE_AND_TOKEN, + * ); + * @endcode + * + * @ingroup oauth2_section_3 + */ + protected function getSupportedAuthResponseTypes() { + return array( + OAUTH2_AUTH_RESPONSE_TYPE_AUTH_CODE, + OAUTH2_AUTH_RESPONSE_TYPE_ACCESS_TOKEN, + OAUTH2_AUTH_RESPONSE_TYPE_CODE_AND_TOKEN + ); + } + + /** + * Return supported scopes. + * + * If you want to support scope use, then have this function return a list + * of all acceptable scopes (used to throw the invalid-scope error). + * + * @return + * A list as below, for example: + * @code + * return array( + * 'my-friends', + * 'photos', + * 'whatever-else', + * ); + * @endcode + * + * @ingroup oauth2_section_3 + */ + protected function getSupportedScopes() { + return array(); + } + + /** + * Check restricted authorization response types of corresponding Client + * identifier. + * + * If you want to restrict clients to certain authorization response types, + * override this function. + * + * @param $client_id + * Client identifier to be check with. + * @param $response_type + * Authorization response type to be check with, would be one of the + * values contained in OAUTH2_AUTH_RESPONSE_TYPE_REGEXP. + * + * @return + * TRUE if the authorization response type is supported by this + * client identifier, and FALSE if it isn't. + * + * @ingroup oauth2_section_3 + */ + protected function checkRestrictedAuthResponseType($client_id, $response_type) { + return TRUE; + } + + /** + * Check restricted grant types of corresponding client identifier. + * + * If you want to restrict clients to certain grant types, override this + * function. + * + * @param $client_id + * Client identifier to be check with. + * @param $grant_type + * Grant type to be check with, would be one of the values contained in + * OAUTH2_GRANT_TYPE_REGEXP. + * + * @return + * TRUE if the grant type is supported by this client identifier, and + * FALSE if it isn't. + * + * @ingroup oauth2_section_4 + */ + protected function checkRestrictedGrantType($client_id, $grant_type) { + return TRUE; + } + + // Functions that help grant access tokens for various grant types. + + /** + * Fetch authorization code data (probably the most common grant type). + * + * Retrieve the stored data for the given authorization code. + * + * Required for OAUTH2_GRANT_TYPE_AUTH_CODE. + * + * @param $code + * Authorization code to be check with. + * + * @return + * An associative array as below, and NULL if the code is invalid: + * - client_id: Stored client identifier. + * - redirect_uri: Stored redirect URI. + * - expires: Stored expiration in unix timestamp. + * - scope: (optional) Stored scope values in space-separated string. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.1 + * + * @ingroup oauth2_section_4 + */ + protected function getAuthCode($code) { + return NULL; + } + + /** + * Take the provided authorization code values and store them somewhere. + * + * This function should be the storage counterpart to getAuthCode(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAUTH2_GRANT_TYPE_AUTH_CODE. + * + * @param $code + * Authorization code to be stored. + * @param $client_id + * Client identifier to be stored. + * @param $redirect_uri + * Redirect URI to be stored. + * @param $expires + * Expiration to be stored. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_4 + */ + protected function setAuthCode($code, $client_id, $redirect_uri, $expires, $scope = NULL) { + } + + /** + * Grant access tokens for basic user credentials. + * + * Check the supplied username and password for validity. + * + * You can also use the $client_id param to do any checks required based + * on a client, if you need that. + * + * Required for OAUTH2_GRANT_TYPE_USER_CREDENTIALS. + * + * @param $client_id + * Client identifier to be check with. + * @param $username + * Username to be check with. + * @param $password + * Password to be check with. + * + * @return + * TRUE if the username and password are valid, and FALSE if it isn't. + * Moreover, if the username and password are valid, and you want to + * verify the scope of a user's access, return an associative array + * with the scope values as below. We'll check the scope you provide + * against the requested scope before providing an access token: + * @code + * return array( + * 'scope' => <stored scope values (space-separated string)>, + * ); + * @endcode + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.2 + * + * @ingroup oauth2_section_4 + */ + protected function checkUserCredentials($client_id, $username, $password) { + return FALSE; + } + + /** + * Grant access tokens for assertions. + * + * Check the supplied assertion for validity. + * + * You can also use the $client_id param to do any checks required based + * on a client, if you need that. + * + * Required for OAUTH2_GRANT_TYPE_ASSERTION. + * + * @param $client_id + * Client identifier to be check with. + * @param $assertion_type + * The format of the assertion as defined by the authorization server. + * @param $assertion + * The assertion. + * + * @return + * TRUE if the assertion is valid, and FALSE if it isn't. Moreover, if + * the assertion is valid, and you want to verify the scope of an access + * request, return an associative array with the scope values as below. + * We'll check the scope you provide against the requested scope before + * providing an access token: + * @code + * return array( + * 'scope' => <stored scope values (space-separated string)>, + * ); + * @endcode + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.3 + * + * @ingroup oauth2_section_4 + */ + protected function checkAssertion($client_id, $assertion_type, $assertion) { + return FALSE; + } + + /** + * Grant refresh access tokens. + * + * Retrieve the stored data for the given refresh token. + * + * Required for OAUTH2_GRANT_TYPE_REFRESH_TOKEN. + * + * @param $refresh_token + * Refresh token to be check with. + * + * @return + * An associative array as below, and NULL if the refresh_token is + * invalid: + * - client_id: Stored client identifier. + * - expires: Stored expiration unix timestamp. + * - scope: (optional) Stored scope values in space-separated string. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.4 + * + * @ingroup oauth2_section_4 + */ + protected function getRefreshToken($refresh_token) { + return NULL; + } + + /** + * Take the provided refresh token values and store them somewhere. + * + * This function should be the storage counterpart to getRefreshToken(). + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * Required for OAUTH2_GRANT_TYPE_REFRESH_TOKEN. + * + * @param $refresh_token + * Refresh token to be stored. + * @param $client_id + * Client identifier to be stored. + * @param $expires + * expires to be stored. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_4 + */ + protected function setRefreshToken($refresh_token, $client_id, $expires, $scope = NULL) { + return; + } + + /** + * Expire a used refresh token. + * + * This is not explicitly required in the spec, but is almost implied. + * After granting a new refresh token, the old one is no longer useful and + * so should be forcibly expired in the data store so it can't be used again. + * + * If storage fails for some reason, we're not currently checking for + * any sort of success/failure, so you should bail out of the script + * and provide a descriptive fail message. + * + * @param $refresh_token + * Refresh token to be expirse. + * + * @ingroup oauth2_section_4 + */ + protected function unsetRefreshToken($refresh_token) { + return; + } + + /** + * Grant access tokens for the "none" grant type. + * + * Not really described in the IETF Draft, so I just left a method + * stub... Do whatever you want! + * + * Required for OAUTH2_GRANT_TYPE_NONE. + * + * @ingroup oauth2_section_4 + */ + protected function checkNoneAccess($client_id) { + return FALSE; + } + + /** + * Get default authentication realm for WWW-Authenticate header. + * + * Change this to whatever authentication realm you want to send in a + * WWW-Authenticate header. + * + * @return + * A string that you want to send in a WWW-Authenticate header. + * + * @ingroup oauth2_error + */ + protected function getDefaultAuthenticationRealm() { + return "Service"; + } + + // End stuff that should get overridden. + + /** + * Creates an OAuth2.0 server-side instance. + * + * @param $config + * An associative array as below: + * - access_token_lifetime: (optional) The lifetime of access token in + * seconds. + * - auth_code_lifetime: (optional) The lifetime of authorization code in + * seconds. + * - refresh_token_lifetime: (optional) The lifetime of refresh token in + * seconds. + * - display_error: (optional) Whether to show verbose error messages in + * the response. + */ + public function __construct($config = array()) { + foreach ($config as $name => $value) { + $this->setVariable($name, $value); + } + } + + // Resource protecting (Section 5). + + /** + * Check that a valid access token has been provided. + * + * The scope parameter defines any required scope that the token must have. + * If a scope param is provided and the token does not have the required + * scope, we bounce the request. + * + * Some implementations may choose to return a subset of the protected + * resource (i.e. "public" data) if the user has not provided an access + * token or if the access token is invalid or expired. + * + * The IETF spec says that we should send a 401 Unauthorized header and + * bail immediately so that's what the defaults are set to. + * + * @param $scope + * A space-separated string of required scope(s), if you want to check + * for scope. + * @param $exit_not_present + * If TRUE and no access token is provided, send a 401 header and exit, + * otherwise return FALSE. + * @param $exit_invalid + * If TRUE and the implementation of getAccessToken() returns NULL, exit, + * otherwise return FALSE. + * @param $exit_expired + * If TRUE and the access token has expired, exit, otherwise return FALSE. + * @param $exit_scope + * If TRUE the access token does not have the required scope(s), exit, + * otherwise return FALSE. + * @param $realm + * If you want to specify a particular realm for the WWW-Authenticate + * header, supply it here. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5 + * + * @ingroup oauth2_section_5 + */ + public function verifyAccessToken($scope = NULL, $exit_not_present = TRUE, $exit_invalid = TRUE, $exit_expired = TRUE, $exit_scope = TRUE, $realm = NULL) { + $token_param = $this->getAccessTokenParams(); + if ($token_param === FALSE) // Access token was not provided + return $exit_not_present ? $this->errorWWWAuthenticateResponseHeader(OAUTH2_HTTP_BAD_REQUEST, $realm, OAUTH2_ERROR_INVALID_REQUEST, 'The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats the same parameter, uses more than one method for including an access token, or is otherwise malformed.', NULL, $scope) : FALSE; + // Get the stored token data (from the implementing subclass) + $token = $this->getAccessToken($token_param); + if ($token === NULL) + return $exit_invalid ? $this->errorWWWAuthenticateResponseHeader(OAUTH2_HTTP_UNAUTHORIZED, $realm, OAUTH2_ERROR_INVALID_TOKEN, 'The access token provided is invalid.', NULL, $scope) : FALSE; + + // Check token expiration (I'm leaving this check separated, later we'll fill in better error messages) + if (isset($token["expires"]) && time() > $token["expires"]) + return $exit_expired ? $this->errorWWWAuthenticateResponseHeader(OAUTH2_HTTP_UNAUTHORIZED, $realm, OAUTH2_ERROR_EXPIRED_TOKEN, 'The access token provided has expired.', NULL, $scope) : FALSE; + + // Check scope, if provided + // If token doesn't have a scope, it's NULL/empty, or it's insufficient, then throw an error + if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->checkScope($scope, $token["scope"]))) + return $exit_scope ? $this->errorWWWAuthenticateResponseHeader(OAUTH2_HTTP_FORBIDDEN, $realm, OAUTH2_ERROR_INSUFFICIENT_SCOPE, 'The request requires higher privileges than provided by the access token.', NULL, $scope) : FALSE; + + return TRUE; + } + + /** + * Check if everything in required scope is contained in available scope. + * + * @param $required_scope + * Required scope to be check with. + * @param $available_scope + * Available scope to be compare with. + * + * @return + * TRUE if everything in required scope is contained in available scope, + * and False if it isn't. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5 + * + * @ingroup oauth2_section_5 + */ + private function checkScope($required_scope, $available_scope) { + // The required scope should match or be a subset of the available scope + if (!is_array($required_scope)) + $required_scope = explode(" ", $required_scope); + + if (!is_array($available_scope)) + $available_scope = explode(" ", $available_scope); + + return (count(array_diff($required_scope, $available_scope)) == 0); + } + + /** + * Pulls the access token out of the HTTP request. + * + * Either from the Authorization header or GET/POST/etc. + * + * @return + * Access token value if present, and FALSE if it isn't. + * + * @todo Support PUT or DELETE. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.1 + * + * @ingroup oauth2_section_5 + */ + private function getAccessTokenParams() { + $auth_header = $this->getAuthorizationHeader(); + + if ($auth_header !== FALSE) { + // Make sure only the auth header is set + if (isset($_GET[OAUTH2_TOKEN_PARAM_NAME]) || isset($_POST[OAUTH2_TOKEN_PARAM_NAME])) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Auth token found in GET or POST when token present in header'); + + $auth_header = trim($auth_header); + + // Make sure it's Token authorization + if (strcmp(substr($auth_header, 0, 5), "OAuth ") !== 0) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Auth header found that doesn\'t start with "OAuth"'); + + // Parse the rest of the header + if (preg_match('/\s*OAuth\s*="(.+)"/', substr($auth_header, 5), $matches) == 0 || count($matches) < 2) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Malformed auth header'); + + return $matches[1]; + } + + if (isset($_GET[OAUTH2_TOKEN_PARAM_NAME])) { + if (isset($_POST[OAUTH2_TOKEN_PARAM_NAME])) // Both GET and POST are not allowed + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Only send the token in GET or POST, not both'); + + return $_GET[OAUTH2_TOKEN_PARAM_NAME]; + } + + if (isset($_POST[OAUTH2_TOKEN_PARAM_NAME])) + return $_POST[OAUTH2_TOKEN_PARAM_NAME]; + + return FALSE; + } + + // Access token granting (Section 4). + + /** + * Grant or deny a requested access token. + * + * This would be called from the "/token" endpoint as defined in the spec. + * Obviously, you can call your endpoint whatever you want. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4 + * + * @ingroup oauth2_section_4 + */ + public function grantAccessToken() { + $filters = array( + "grant_type" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => OAUTH2_GRANT_TYPE_REGEXP), "flags" => FILTER_REQUIRE_SCALAR), + "scope" => array("flags" => FILTER_REQUIRE_SCALAR), + "code" => array("flags" => FILTER_REQUIRE_SCALAR), + "redirect_uri" => array("filter" => FILTER_SANITIZE_URL), + "username" => array("flags" => FILTER_REQUIRE_SCALAR), + "password" => array("flags" => FILTER_REQUIRE_SCALAR), + "assertion_type" => array("flags" => FILTER_REQUIRE_SCALAR), + "assertion" => array("flags" => FILTER_REQUIRE_SCALAR), + "refresh_token" => array("flags" => FILTER_REQUIRE_SCALAR), + ); + + $input = filter_input_array(INPUT_POST, $filters); + + // Grant Type must be specified. + if (!$input["grant_type"]) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Invalid grant_type parameter or parameter missing'); + + // Make sure we've implemented the requested grant type + if (!in_array($input["grant_type"], $this->getSupportedGrantTypes())) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_UNSUPPORTED_GRANT_TYPE); + + // Authorize the client + $client = $this->getClientCredentials(); + + if ($this->checkClientCredentials($client[0], $client[1]) === FALSE) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_CLIENT); + + if (!$this->checkRestrictedGrantType($client[0], $input["grant_type"])) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_UNAUTHORIZED_CLIENT); + + // Do the granting + switch ($input["grant_type"]) { + case OAUTH2_GRANT_TYPE_AUTH_CODE: + if (!$input["code"] || !$input["redirect_uri"]) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST); + + $stored = $this->getAuthCode($input["code"]); + + // Ensure that the input uri starts with the stored uri + if ($stored === NULL || (strcasecmp(substr($input["redirect_uri"], 0, strlen($stored["redirect_uri"])), $stored["redirect_uri"]) !== 0) || $client[0] != $stored["client_id"]) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_GRANT); + + if ($stored["expires"] < time()) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_EXPIRED_TOKEN); + + break; + case OAUTH2_GRANT_TYPE_USER_CREDENTIALS: + if (!$input["username"] || !$input["password"]) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'Missing parameters. "username" and "password" required'); + + $stored = $this->checkUserCredentials($client[0], $input["username"], $input["password"]); + + if ($stored === FALSE) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_GRANT); + + break; + case OAUTH2_GRANT_TYPE_ASSERTION: + if (!$input["assertion_type"] || !$input["assertion"]) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST); + + $stored = $this->checkAssertion($client[0], $input["assertion_type"], $input["assertion"]); + + if ($stored === FALSE) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_GRANT); + + break; + case OAUTH2_GRANT_TYPE_REFRESH_TOKEN: + if (!$input["refresh_token"]) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST, 'No "refresh_token" parameter found'); + + $stored = $this->getRefreshToken($input["refresh_token"]); + + if ($stored === NULL || $client[0] != $stored["client_id"]) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_GRANT); + + if ($stored["expires"] < time()) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_EXPIRED_TOKEN); + + // store the refresh token locally so we can delete it when a new refresh token is generated + $this->setVariable('_old_refresh_token', $stored["token"]); + + break; + case OAUTH2_GRANT_TYPE_NONE: + $stored = $this->checkNoneAccess($client[0]); + + if ($stored === FALSE) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_REQUEST); + } + + // Check scope, if provided + if ($input["scope"] && (!is_array($stored) || !isset($stored["scope"]) || !$this->checkScope($input["scope"], $stored["scope"]))) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_SCOPE); + + if (!$input["scope"]) + $input["scope"] = NULL; + + $token = $this->createAccessToken($client[0], $input["scope"]); + + $this->sendJsonHeaders(); + echo json_encode($token); + } + + /** + * Internal function used to get the client credentials from HTTP basic + * auth or POST data. + * + * @return + * A list containing the client identifier and password, for example + * @code + * return array( + * $_POST["client_id"], + * $_POST["client_secret"], + * ); + * @endcode + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-2 + * + * @ingroup oauth2_section_2 + */ + protected function getClientCredentials() { + if (isset($_SERVER["PHP_AUTH_USER"]) && $_POST && isset($_POST["client_id"])) + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_CLIENT); + + // Try basic auth + if (isset($_SERVER["PHP_AUTH_USER"])) + return array($_SERVER["PHP_AUTH_USER"], $_SERVER["PHP_AUTH_PW"]); + + // Try POST + if ($_POST && isset($_POST["client_id"])) { + if (isset($_POST["client_secret"])) + return array($_POST["client_id"], $_POST["client_secret"]); + + return array($_POST["client_id"], NULL); + } + + // No credentials were specified + $this->errorJsonResponse(OAUTH2_HTTP_BAD_REQUEST, OAUTH2_ERROR_INVALID_CLIENT); + } + + // End-user/client Authorization (Section 3 of IETF Draft). + + /** + * Pull the authorization request data out of the HTTP request. + * + * @return + * The authorization parameters so the authorization server can prompt + * the user for approval if valid. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3 + * + * @ingroup oauth2_section_3 + */ + public function getAuthorizeParams() { + $filters = array( + "client_id" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => OAUTH2_CLIENT_ID_REGEXP), "flags" => FILTER_REQUIRE_SCALAR), + "response_type" => array("filter" => FILTER_VALIDATE_REGEXP, "options" => array("regexp" => OAUTH2_AUTH_RESPONSE_TYPE_REGEXP), "flags" => FILTER_REQUIRE_SCALAR), + "redirect_uri" => array("filter" => FILTER_SANITIZE_URL), + "state" => array("flags" => FILTER_REQUIRE_SCALAR), + "scope" => array("flags" => FILTER_REQUIRE_SCALAR), + ); + + $input = filter_input_array(INPUT_GET, $filters); + + // Make sure a valid client id was supplied + if (!$input["client_id"]) { + if ($input["redirect_uri"]) + $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_INVALID_CLIENT, NULL, NULL, $input["state"]); + + $this->errorJsonResponse(OAUTH2_HTTP_FOUND, OAUTH2_ERROR_INVALID_CLIENT); // We don't have a good URI to use + } + + // redirect_uri is not required if already established via other channels + // check an existing redirect URI against the one supplied + $redirect_uri = $this->getRedirectUri($input["client_id"]); + + // At least one of: existing redirect URI or input redirect URI must be specified + if (!$redirect_uri && !$input["redirect_uri"]) + $this->errorJsonResponse(OAUTH2_HTTP_FOUND, OAUTH2_ERROR_INVALID_REQUEST); + + // getRedirectUri() should return FALSE if the given client ID is invalid + // this probably saves us from making a separate db call, and simplifies the method set + if ($redirect_uri === FALSE) + $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_INVALID_CLIENT, NULL, NULL, $input["state"]); + + // If there's an existing uri and one from input, verify that they match + if ($redirect_uri && $input["redirect_uri"]) { + // Ensure that the input uri starts with the stored uri + if (strcasecmp(substr($input["redirect_uri"], 0, strlen($redirect_uri)), $redirect_uri) !== 0) + $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_REDIRECT_URI_MISMATCH, NULL, NULL, $input["state"]); + } + elseif ($redirect_uri) { // They did not provide a uri from input, so use the stored one + $input["redirect_uri"] = $redirect_uri; + } + + // type and client_id are required + if (!$input["response_type"]) + $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_INVALID_REQUEST, 'Invalid response type.', NULL, $input["state"]); + + // Check requested auth response type against the list of supported types + if (array_search($input["response_type"], $this->getSupportedAuthResponseTypes()) === FALSE) + $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE, NULL, NULL, $input["state"]); + + // Restrict clients to certain authorization response types + if ($this->checkRestrictedAuthResponseType($input["client_id"], $input["response_type"]) === FALSE) + $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_UNAUTHORIZED_CLIENT, NULL, NULL, $input["state"]); + + // Validate that the requested scope is supported + if ($input["scope"] && !$this->checkScope($input["scope"], $this->getSupportedScopes())) + $this->errorDoRedirectUriCallback($input["redirect_uri"], OAUTH2_ERROR_INVALID_SCOPE, NULL, NULL, $input["state"]); + + return $input; + } + + /** + * Redirect the user appropriately after approval. + * + * After the user has approved or denied the access request the + * authorization server should call this function to redirect the user + * appropriately. + * + * @param $is_authorized + * TRUE or FALSE depending on whether the user authorized the access. + * @param $params + * An associative array as below: + * - response_type: The requested response: an access token, an + * authorization code, or both. + * - client_id: The client identifier as described in Section 2. + * - redirect_uri: An absolute URI to which the authorization server + * will redirect the user-agent to when the end-user authorization + * step is completed. + * - scope: (optional) The scope of the access request expressed as a + * list of space-delimited strings. + * - state: (optional) An opaque value used by the client to maintain + * state between the request and callback. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3 + * + * @ingroup oauth2_section_3 + */ + public function finishClientAuthorization($is_authorized, $params = array()) { + $params += array( + 'scope' => NULL, + 'state' => NULL, + ); + extract($params); + + if ($state !== NULL) + $result["query"]["state"] = $state; + + if ($is_authorized === FALSE) { + $result["query"]["error"] = OAUTH2_ERROR_USER_DENIED; + } + else { + if ($response_type == OAUTH2_AUTH_RESPONSE_TYPE_AUTH_CODE || $response_type == OAUTH2_AUTH_RESPONSE_TYPE_CODE_AND_TOKEN) + $result["query"]["code"] = $this->createAuthCode($client_id, $redirect_uri, $scope); + + if ($response_type == OAUTH2_AUTH_RESPONSE_TYPE_ACCESS_TOKEN || $response_type == OAUTH2_AUTH_RESPONSE_TYPE_CODE_AND_TOKEN) + $result["fragment"] = $this->createAccessToken($client_id, $scope); + } + + $this->doRedirectUriCallback($redirect_uri, $result); + } + + // Other/utility functions. + + /** + * Redirect the user agent. + * + * Handle both redirect for success or error response. + * + * @param $redirect_uri + * An absolute URI to which the authorization server will redirect + * the user-agent to when the end-user authorization step is completed. + * @param $params + * Parameters to be pass though buildUri(). + * + * @ingroup oauth2_section_3 + */ + private function doRedirectUriCallback($redirect_uri, $params) { + header("HTTP/1.1 ". OAUTH2_HTTP_FOUND); + header("Location: " . $this->buildUri($redirect_uri, $params)); + exit; + } + + /** + * Build the absolute URI based on supplied URI and parameters. + * + * @param $uri + * An absolute URI. + * @param $params + * Parameters to be append as GET. + * + * @return + * An absolute URI with supplied parameters. + * + * @ingroup oauth2_section_3 + */ + private function buildUri($uri, $params) { + $parse_url = parse_url($uri); + + // Add our params to the parsed uri + foreach ($params as $k => $v) { + if (isset($parse_url[$k])) + $parse_url[$k] .= "&" . http_build_query($v); + else + $parse_url[$k] = http_build_query($v); + } + + // Put humpty dumpty back together + return + ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "") + . ((isset($parse_url["user"])) ? $parse_url["user"] . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "") + . ((isset($parse_url["host"])) ? $parse_url["host"] : "") + . ((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "") + . ((isset($parse_url["path"])) ? $parse_url["path"] : "") + . ((isset($parse_url["query"])) ? "?" . $parse_url["query"] : "") + . ((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : ""); + } + + /** + * Handle the creation of access token, also issue refresh token if support. + * + * This belongs in a separate factory, but to keep it simple, I'm just + * keeping it here. + * + * @param $client_id + * Client identifier related to the access token. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_4 + */ + protected function createAccessToken($client_id, $scope = NULL) { + $token = array( + "access_token" => $this->genAccessToken(), + "expires_in" => $this->getVariable('access_token_lifetime', OAUTH2_DEFAULT_ACCESS_TOKEN_LIFETIME), + "scope" => $scope + ); + + $this->setAccessToken($token["access_token"], $client_id, time() + $this->getVariable('access_token_lifetime', OAUTH2_DEFAULT_ACCESS_TOKEN_LIFETIME), $scope); + + // Issue a refresh token also, if we support them + if (in_array(OAUTH2_GRANT_TYPE_REFRESH_TOKEN, $this->getSupportedGrantTypes())) { + $token["refresh_token"] = $this->genAccessToken(); + $this->setRefreshToken($token["refresh_token"], $client_id, time() + $this->getVariable('refresh_token_lifetime', OAUTH2_DEFAULT_REFRESH_TOKEN_LIFETIME), $scope); + // If we've granted a new refresh token, expire the old one + if ($this->getVariable('_old_refresh_token')) + $this->unsetRefreshToken($this->getVariable('_old_refresh_token')); + } + + return $token; + } + + /** + * Handle the creation of auth code. + * + * This belongs in a separate factory, but to keep it simple, I'm just + * keeping it here. + * + * @param $client_id + * Client identifier related to the access token. + * @param $redirect_uri + * An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $scope + * (optional) Scopes to be stored in space-separated string. + * + * @ingroup oauth2_section_3 + */ + private function createAuthCode($client_id, $redirect_uri, $scope = NULL) { + $code = $this->genAuthCode(); + $this->setAuthCode($code, $client_id, $redirect_uri, time() + $this->getVariable('auth_code_lifetime', OAUTH2_DEFAULT_AUTH_CODE_LIFETIME), $scope); + return $code; + } + + /** + * Generate unique access token. + * + * Implementing classes may want to override these function to implement + * other access token or auth code generation schemes. + * + * @return + * An unique access token. + * + * @ingroup oauth2_section_4 + */ + protected function genAccessToken() { + return md5(base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), uniqid()))); + } + + /** + * Generate unique auth code. + * + * Implementing classes may want to override these function to implement + * other access token or auth code generation schemes. + * + * @return + * An unique auth code. + * + * @ingroup oauth2_section_3 + */ + protected function genAuthCode() { + return md5(base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), uniqid()))); + } + + /** + * Pull out the Authorization HTTP header and return it. + * + * Implementing classes may need to override this function for use on + * non-Apache web servers. + * + * @return + * The Authorization HTTP header, and FALSE if does not exist. + * + * @todo Handle Authorization HTTP header for non-Apache web servers. + * + * @ingroup oauth2_section_5 + */ + private function getAuthorizationHeader() { + if (array_key_exists("HTTP_AUTHORIZATION", $_SERVER)) + return $_SERVER["HTTP_AUTHORIZATION"]; + + if (function_exists("apache_request_headers")) { + $headers = apache_request_headers(); + + if (array_key_exists("Authorization", $headers)) + return $headers["Authorization"]; + } + + return FALSE; + } + + /** + * Send out HTTP headers for JSON. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.2 + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3 + * + * @ingroup oauth2_section_4 + */ + private function sendJsonHeaders() { + header("Content-Type: application/json"); + header("Cache-Control: no-store"); + } + + /** + * Redirect the end-user's user agent with error message. + * + * @param $redirect_uri + * An absolute URI to which the authorization server will redirect the + * user-agent to when the end-user authorization step is completed. + * @param $error + * A single error code as described in Section 3.2.1. + * @param $error_description + * (optional) A human-readable text providing additional information, + * used to assist in the understanding and resolution of the error + * occurred. + * @param $error_uri + * (optional) A URI identifying a human-readable web page with + * information about the error, used to provide the end-user with + * additional information about the error. + * @param $state + * (optional) REQUIRED if the "state" parameter was present in the client + * authorization request. Set to the exact value received from the client. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-3.2 + * + * @ingroup oauth2_error + */ + private function errorDoRedirectUriCallback($redirect_uri, $error, $error_description = NULL, $error_uri = NULL, $state = NULL) { + $result["query"]["error"] = $error; + + if ($state) + $result["query"]["state"] = $state; + + if ($this->getVariable('display_error') && $error_description) + $result["query"]["error_description"] = $error_description; + + if ($this->getVariable('display_error') && $error_uri) + $result["query"]["error_uri"] = $error_uri; + + $this->doRedirectUriCallback($redirect_uri, $result); + } + + /** + * Send out error message in JSON. + * + * @param $http_status_code + * HTTP status code message as predefined. + * @param $error + * A single error code. + * @param $error_description + * (optional) A human-readable text providing additional information, + * used to assist in the understanding and resolution of the error + * occurred. + * @param $error_uri + * (optional) A URI identifying a human-readable web page with + * information about the error, used to provide the end-user with + * additional information about the error. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.3 + * + * @ingroup oauth2_error + */ + private function errorJsonResponse($http_status_code, $error, $error_description = NULL, $error_uri = NULL) { + $result['error'] = $error; + + if ($this->getVariable('display_error') && $error_description) + $result["error_description"] = $error_description; + + if ($this->getVariable('display_error') && $error_uri) + $result["error_uri"] = $error_uri; + + header("HTTP/1.1 " . $http_status_code); + $this->sendJsonHeaders(); + echo json_encode($result); + + exit; + } + + /** + * Send a 401 unauthorized header with the given realm and an error, if + * provided. + * + * @param $http_status_code + * HTTP status code message as predefined. + * @param $realm + * The "realm" attribute is used to provide the protected resources + * partition as defined by [RFC2617]. + * @param $scope + * A space-delimited list of scope values indicating the required scope + * of the access token for accessing the requested resource. + * @param $error + * The "error" attribute is used to provide the client with the reason + * why the access request was declined. + * @param $error_description + * (optional) The "error_description" attribute provides a human-readable text + * containing additional information, used to assist in the understanding + * and resolution of the error occurred. + * @param $error_uri + * (optional) The "error_uri" attribute provides a URI identifying a human-readable + * web page with information about the error, used to offer the end-user + * with additional information about the error. If the value is not an + * absolute URI, it is relative to the URI of the requested protected + * resource. + * + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-5.2 + * + * @ingroup oauth2_error + */ + private function errorWWWAuthenticateResponseHeader($http_status_code, $realm, $error, $error_description = NULL, $error_uri = NULL, $scope = NULL) { + $realm = $realm === NULL ? $this->getDefaultAuthenticationRealm() : $realm; + + $result = "WWW-Authenticate: OAuth realm='" . $realm . "'"; + + if ($error) + $result .= ", error='" . $error . "'"; + + if ($this->getVariable('display_error') && $error_description) + $result .= ", error_description='" . $error_description . "'"; + + if ($this->getVariable('display_error') && $error_uri) + $result .= ", error_uri='" . $error_uri . "'"; + + if ($scope) + $result .= ", scope='" . $scope . "'"; + + header("HTTP/1.1 ". $http_status_code); + header($result); + + exit; + } +} diff --git a/library/oauth2-php/lib/OAuth2Client.inc b/library/oauth2-php/lib/OAuth2Client.inc new file mode 100644 index 000000000..e87d723de --- /dev/null +++ b/library/oauth2-php/lib/OAuth2Client.inc @@ -0,0 +1,721 @@ +<?php + +/** + * The default Cache Lifetime (in seconds). + */ +define("OAUTH2_DEFAULT_EXPIRES_IN", 3600); + +/** + * The default Base domain for the Cookie. + */ +define("OAUTH2_DEFAULT_BASE_DOMAIN", ''); + +/** + * OAuth2.0 draft v10 client-side implementation. + * + * @author Originally written by Naitik Shah <naitik@facebook.com>. + * @author Update to draft v10 by Edison Wong <hswong3i@pantarei-design.com>. + * + * @sa <a href="https://github.com/facebook/php-sdk">Facebook PHP SDK</a>. + */ +abstract class OAuth2Client { + + /** + * Array of persistent variables stored. + */ + protected $conf = array(); + + /** + * Returns a persistent variable. + * + * To avoid problems, always use lower case for persistent variable names. + * + * @param $name + * The name of the variable to return. + * @param $default + * The default value to use if this variable has never been set. + * + * @return + * The value of the variable. + */ + public function getVariable($name, $default = NULL) { + return isset($this->conf[$name]) ? $this->conf[$name] : $default; + } + + /** + * Sets a persistent variable. + * + * To avoid problems, always use lower case for persistent variable names. + * + * @param $name + * The name of the variable to set. + * @param $value + * The value to set. + */ + public function setVariable($name, $value) { + $this->conf[$name] = $value; + return $this; + } + + // Stuff that should get overridden by subclasses. + // + // I don't want to make these abstract, because then subclasses would have + // to implement all of them, which is too much work. + // + // So they're just stubs. Override the ones you need. + + /** + * Initialize a Drupal OAuth2.0 Application. + * + * @param $config + * An associative array as below: + * - base_uri: The base URI for the OAuth2.0 endpoints. + * - code: (optional) The authorization code. + * - username: (optional) The username. + * - password: (optional) The password. + * - client_id: (optional) The application ID. + * - client_secret: (optional) The application secret. + * - authorize_uri: (optional) The end-user authorization endpoint URI. + * - access_token_uri: (optional) The token endpoint URI. + * - services_uri: (optional) The services endpoint URI. + * - cookie_support: (optional) TRUE to enable cookie support. + * - base_domain: (optional) The domain for the cookie. + * - file_upload_support: (optional) TRUE if file uploads are enabled. + */ + public function __construct($config = array()) { + // We must set base_uri first. + $this->setVariable('base_uri', $config['base_uri']); + unset($config['base_uri']); + + // Use predefined OAuth2.0 params, or get it from $_REQUEST. + foreach (array('code', 'username', 'password') as $name) { + if (isset($config[$name])) + $this->setVariable($name, $config[$name]); + else if (isset($_REQUEST[$name]) && !empty($_REQUEST[$name])) + $this->setVariable($name, $_REQUEST[$name]); + unset($config[$name]); + } + + // Endpoint URIs. + foreach (array('authorize_uri', 'access_token_uri', 'services_uri') as $name) { + if (isset($config[$name])) + if (substr($config[$name], 0, 4) == "http") + $this->setVariable($name, $config[$name]); + else + $this->setVariable($name, $this->getVariable('base_uri') . $config[$name]); + unset($config[$name]); + } + + // Other else configurations. + foreach ($config as $name => $value) { + $this->setVariable($name, $value); + } + } + + /** + * Try to get session object from custom method. + * + * By default we generate session object based on access_token response, or + * if it is provided from server with $_REQUEST. For sure, if it is provided + * by server it should follow our session object format. + * + * Session object provided by server can ensure the correct expirse and + * base_domain setup as predefined in server, also you may get more useful + * information for custom functionality, too. BTW, this may require for + * additional remote call overhead. + * + * You may wish to override this function with your custom version due to + * your own server-side implementation. + * + * @param $access_token + * (optional) A valid access token in associative array as below: + * - access_token: A valid access_token generated by OAuth2.0 + * authorization endpoint. + * - expires_in: (optional) A valid expires_in generated by OAuth2.0 + * authorization endpoint. + * - refresh_token: (optional) A valid refresh_token generated by OAuth2.0 + * authorization endpoint. + * - scope: (optional) A valid scope generated by OAuth2.0 + * authorization endpoint. + * + * @return + * A valid session object in associative array for setup cookie, and + * NULL if not able to generate it with custom method. + */ + protected function getSessionObject($access_token = NULL) { + $session = NULL; + + // Try generate local version of session cookie. + if (!empty($access_token) && isset($access_token['access_token'])) { + $session['access_token'] = $access_token['access_token']; + $session['base_domain'] = $this->getVariable('base_domain', OAUTH2_DEFAULT_BASE_DOMAIN); + $session['expirse'] = isset($access_token['expires_in']) ? time() + $access_token['expires_in'] : time() + $this->getVariable('expires_in', OAUTH2_DEFAULT_EXPIRES_IN); + $session['refresh_token'] = isset($access_token['refresh_token']) ? $access_token['refresh_token'] : ''; + $session['scope'] = isset($access_token['scope']) ? $access_token['scope'] : ''; + $session['secret'] = md5(base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), uniqid()))); + + // Provide our own signature. + $sig = self::generateSignature( + $session, + $this->getVariable('client_secret') + ); + $session['sig'] = $sig; + } + + // Try loading session from $_REQUEST. + if (!$session && isset($_REQUEST['session'])) { + $session = json_decode( + get_magic_quotes_gpc() + ? stripslashes($_REQUEST['session']) + : $_REQUEST['session'], + TRUE + ); + } + + return $session; + } + + /** + * Make an API call. + * + * Support both OAuth2.0 or normal GET/POST API call, with relative + * or absolute URI. + * + * If no valid OAuth2.0 access token found in session object, this function + * will automatically switch as normal remote API call without "oauth_token" + * parameter. + * + * Assume server reply in JSON object and always decode during return. If + * you hope to issue a raw query, please use makeRequest(). + * + * @param $path + * The target path, relative to base_path/service_uri or an absolute URI. + * @param $method + * (optional) The HTTP method (default 'GET'). + * @param $params + * (optional The GET/POST parameters. + * + * @return + * The JSON decoded response object. + * + * @throws OAuth2Exception + */ + public function api($path, $method = 'GET', $params = array()) { + if (is_array($method) && empty($params)) { + $params = $method; + $method = 'GET'; + } + + // json_encode all params values that are not strings. + foreach ($params as $key => $value) { + if (!is_string($value)) { + $params[$key] = json_encode($value); + } + } + + $result = json_decode($this->makeOAuth2Request( + $this->getUri($path), + $method, + $params + ), TRUE); + + // Results are returned, errors are thrown. + if (is_array($result) && isset($result['error'])) { + $e = new OAuth2Exception($result); + switch ($e->getType()) { + // OAuth 2.0 Draft 10 style. + case 'invalid_token': + $this->setSession(NULL); + default: + $this->setSession(NULL); + } + throw $e; + } + return $result; + } + + // End stuff that should get overridden. + + /** + * Default options for cURL. + */ + public static $CURL_OPTS = array( + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_HEADER => TRUE, + CURLOPT_TIMEOUT => 60, + CURLOPT_USERAGENT => 'oauth2-draft-v10', + CURLOPT_HTTPHEADER => array("Accept: application/json"), + ); + + /** + * Set the Session. + * + * @param $session + * (optional) The session object to be set. NULL if hope to frush existing + * session object. + * @param $write_cookie + * (optional) TRUE if a cookie should be written. This value is ignored + * if cookie support has been disabled. + * + * @return + * The current OAuth2.0 client-side instance. + */ + public function setSession($session = NULL, $write_cookie = TRUE) { + $this->setVariable('_session', $this->validateSessionObject($session)); + $this->setVariable('_session_loaded', TRUE); + if ($write_cookie) { + $this->setCookieFromSession($this->getVariable('_session')); + } + return $this; + } + + /** + * Get the session object. + * + * This will automatically look for a signed session via custom method, + * OAuth2.0 grant type with authorization_code, OAuth2.0 grant type with + * password, or cookie that we had already setup. + * + * @return + * The valid session object with OAuth2.0 infomration, and NULL if not + * able to discover any cases. + */ + public function getSession() { + if (!$this->getVariable('_session_loaded')) { + $session = NULL; + $write_cookie = TRUE; + + // Try obtain login session by custom method. + $session = $this->getSessionObject(NULL); + $session = $this->validateSessionObject($session); + + // grant_type == authorization_code. + if (!$session && $this->getVariable('code')) { + $access_token = $this->getAccessTokenFromAuthorizationCode($this->getVariable('code')); + $session = $this->getSessionObject($access_token); + $session = $this->validateSessionObject($session); + } + + // grant_type == password. + if (!$session && $this->getVariable('username') && $this->getVariable('password')) { + $access_token = $this->getAccessTokenFromPassword($this->getVariable('username'), $this->getVariable('password')); + $session = $this->getSessionObject($access_token); + $session = $this->validateSessionObject($session); + } + + // Try loading session from cookie if necessary. + if (!$session && $this->getVariable('cookie_support')) { + $cookie_name = $this->getSessionCookieName(); + if (isset($_COOKIE[$cookie_name])) { + $session = array(); + parse_str(trim( + get_magic_quotes_gpc() + ? stripslashes($_COOKIE[$cookie_name]) + : $_COOKIE[$cookie_name], + '"' + ), $session); + $session = $this->validateSessionObject($session); + // Write only if we need to delete a invalid session cookie. + $write_cookie = empty($session); + } + } + + $this->setSession($session, $write_cookie); + } + + return $this->getVariable('_session'); + } + + /** + * Gets an OAuth2.0 access token from session. + * + * This will trigger getSession() and so we MUST initialize with required + * configuration. + * + * @return + * The valid OAuth2.0 access token, and NULL if not exists in session. + */ + public function getAccessToken() { + $session = $this->getSession(); + return isset($session['access_token']) ? $session['access_token'] : NULL; + } + + /** + * Get access token from OAuth2.0 token endpoint with authorization code. + * + * This function will only be activated if both access token URI, client + * identifier and client secret are setup correctly. + * + * @param $code + * Authorization code issued by authorization server's authorization + * endpoint. + * + * @return + * A valid OAuth2.0 JSON decoded access token in associative array, and + * NULL if not enough parameters or JSON decode failed. + */ + private function getAccessTokenFromAuthorizationCode($code) { + if ($this->getVariable('access_token_uri') && $this->getVariable('client_id') && $this->getVariable('client_secret')) { + return json_decode($this->makeRequest( + $this->getVariable('access_token_uri'), + 'POST', + array( + 'grant_type' => 'authorization_code', + 'client_id' => $this->getVariable('client_id'), + 'client_secret' => $this->getVariable('client_secret'), + 'code' => $code, + 'redirect_uri' => $this->getCurrentUri(), + ) + ), TRUE); + } + return NULL; + } + + /** + * Get access token from OAuth2.0 token endpoint with basic user + * credentials. + * + * This function will only be activated if both username and password + * are setup correctly. + * + * @param $username + * Username to be check with. + * @param $password + * Password to be check with. + * + * @return + * A valid OAuth2.0 JSON decoded access token in associative array, and + * NULL if not enough parameters or JSON decode failed. + */ + private function getAccessTokenFromPassword($username, $password) { + if ($this->getVariable('access_token_uri') && $this->getVariable('client_id') && $this->getVariable('client_secret')) { + return json_decode($this->makeRequest( + $this->getVariable('access_token_uri'), + 'POST', + array( + 'grant_type' => 'password', + 'client_id' => $this->getVariable('client_id'), + 'client_secret' => $this->getVariable('client_secret'), + 'username' => $username, + 'password' => $password, + ) + ), TRUE); + } + return NULL; + } + + /** + * Make an OAuth2.0 Request. + * + * Automatically append "oauth_token" in query parameters if not yet + * exists and able to discover a valid access token from session. Otherwise + * just ignore setup with "oauth_token" and handle the API call AS-IS, and + * so may issue a plain API call without OAuth2.0 protection. + * + * @param $path + * The target path, relative to base_path/service_uri or an absolute URI. + * @param $method + * (optional) The HTTP method (default 'GET'). + * @param $params + * (optional The GET/POST parameters. + * + * @return + * The JSON decoded response object. + * + * @throws OAuth2Exception + */ + protected function makeOAuth2Request($path, $method = 'GET', $params = array()) { + if ((!isset($params['oauth_token']) || empty($params['oauth_token'])) && $oauth_token = $this->getAccessToken()) { + $params['oauth_token'] = $oauth_token; + } + return $this->makeRequest($path, $method, $params); + } + + /** + * Makes an HTTP request. + * + * This method can be overriden by subclasses if developers want to do + * fancier things or use something other than cURL to make the request. + * + * @param $path + * The target path, relative to base_path/service_uri or an absolute URI. + * @param $method + * (optional) The HTTP method (default 'GET'). + * @param $params + * (optional The GET/POST parameters. + * @param $ch + * (optional) An initialized curl handle + * + * @return + * The JSON decoded response object. + */ + protected function makeRequest($path, $method = 'GET', $params = array(), $ch = NULL) { + if (!$ch) + $ch = curl_init(); + + $opts = self::$CURL_OPTS; + if ($params) { + switch ($method) { + case 'GET': + $path .= '?' . http_build_query($params, NULL, '&'); + break; + // Method override as we always do a POST. + default: + if ($this->getVariable('file_upload_support')) { + $opts[CURLOPT_POSTFIELDS] = $params; + } + else { + $opts[CURLOPT_POSTFIELDS] = http_build_query($params, NULL, '&'); + } + } + } + $opts[CURLOPT_URL] = $path; + + // Disable the 'Expect: 100-continue' behaviour. This causes CURL to wait + // for 2 seconds if the server does not support this header. + if (isset($opts[CURLOPT_HTTPHEADER])) { + $existing_headers = $opts[CURLOPT_HTTPHEADER]; + $existing_headers[] = 'Expect:'; + $opts[CURLOPT_HTTPHEADER] = $existing_headers; + } + else { + $opts[CURLOPT_HTTPHEADER] = array('Expect:'); + } + + curl_setopt_array($ch, $opts); + $result = curl_exec($ch); + + if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT + error_log('Invalid or no certificate authority found, using bundled information'); + curl_setopt($ch, CURLOPT_CAINFO, + dirname(__FILE__) . '/fb_ca_chain_bundle.crt'); + $result = curl_exec($ch); + } + + if ($result === FALSE) { + $e = new OAuth2Exception(array( + 'code' => curl_errno($ch), + 'message' => curl_error($ch), + )); + curl_close($ch); + throw $e; + } + curl_close($ch); + + // Split the HTTP response into header and body. + list($headers, $body) = explode("\r\n\r\n", $result); + $headers = explode("\r\n", $headers); + + // We catch HTTP/1.1 4xx or HTTP/1.1 5xx error response. + if (strpos($headers[0], 'HTTP/1.1 4') !== FALSE || strpos($headers[0], 'HTTP/1.1 5') !== FALSE) { + $result = array( + 'code' => 0, + 'message' => '', + ); + + if (preg_match('/^HTTP\/1.1 ([0-9]{3,3}) (.*)$/', $headers[0], $matches)) { + $result['code'] = $matches[1]; + $result['message'] = $matches[2]; + } + + // In case retrun with WWW-Authenticate replace the description. + foreach ($headers as $header) { + if (preg_match("/^WWW-Authenticate:.*error='(.*)'/", $header, $matches)) { + $result['error'] = $matches[1]; + } + } + + return json_encode($result); + } + + return $body; + } + + /** + * The name of the cookie that contains the session object. + * + * @return + * The cookie name. + */ + private function getSessionCookieName() { + return 'oauth2_' . $this->getVariable('client_id'); + } + + /** + * Set a JS Cookie based on the _passed in_ session. + * + * It does not use the currently stored session - you need to explicitly + * pass it in. + * + * @param $session + * The session to use for setting the cookie. + */ + protected function setCookieFromSession($session = NULL) { + if (!$this->getVariable('cookie_support')) + return; + + $cookie_name = $this->getSessionCookieName(); + $value = 'deleted'; + $expires = time() - 3600; + $base_domain = $this->getVariable('base_domain', OAUTH2_DEFAULT_BASE_DOMAIN); + if ($session) { + $value = '"' . http_build_query($session, NULL, '&') . '"'; + $base_domain = isset($session['base_domain']) ? $session['base_domain'] : $base_domain; + $expires = isset($session['expires']) ? $session['expires'] : time() + $this->getVariable('expires_in', OAUTH2_DEFAULT_EXPIRES_IN); + } + + // Prepend dot if a domain is found. + if ($base_domain) + $base_domain = '.' . $base_domain; + + // If an existing cookie is not set, we dont need to delete it. + if ($value == 'deleted' && empty($_COOKIE[$cookie_name])) + return; + + if (headers_sent()) + error_log('Could not set cookie. Headers already sent.'); + else + setcookie($cookie_name, $value, $expires, '/', $base_domain); + } + + /** + * Validates a session_version = 3 style session object. + * + * @param $session + * The session object. + * + * @return + * The session object if it validates, NULL otherwise. + */ + protected function validateSessionObject($session) { + // Make sure some essential fields exist. + if (is_array($session) && isset($session['access_token']) && isset($session['sig'])) { + // Validate the signature. + $session_without_sig = $session; + unset($session_without_sig['sig']); + + $expected_sig = self::generateSignature( + $session_without_sig, + $this->getVariable('client_secret') + ); + + if ($session['sig'] != $expected_sig) { + error_log('Got invalid session signature in cookie.'); + $session = NULL; + } + } + else { + $session = NULL; + } + return $session; + } + + /** + * Since $_SERVER['REQUEST_URI'] is only available on Apache, we + * generate an equivalent using other environment variables. + */ + function getRequestUri() { + if (isset($_SERVER['REQUEST_URI'])) { + $uri = $_SERVER['REQUEST_URI']; + } + else { + if (isset($_SERVER['argv'])) { + $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['argv'][0]; + } + elseif (isset($_SERVER['QUERY_STRING'])) { + $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['QUERY_STRING']; + } + else { + $uri = $_SERVER['SCRIPT_NAME']; + } + } + // Prevent multiple slashes to avoid cross site requests via the Form API. + $uri = '/' . ltrim($uri, '/'); + + return $uri; + } + + /** + * Returns the Current URL. + * + * @return + * The current URL. + */ + protected function getCurrentUri() { + $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' + ? 'https://' + : 'http://'; + $current_uri = $protocol . $_SERVER['HTTP_HOST'] . $this->getRequestUri(); + $parts = parse_url($current_uri); + + $query = ''; + if (!empty($parts['query'])) { + $params = array(); + parse_str($parts['query'], $params); + $params = array_filter($params); + if (!empty($params)) { + $query = '?' . http_build_query($params, NULL, '&'); + } + } + + // Use port if non default. + $port = isset($parts['port']) && + (($protocol === 'http://' && $parts['port'] !== 80) || ($protocol === 'https://' && $parts['port'] !== 443)) + ? ':' . $parts['port'] : ''; + + // Rebuild. + return $protocol . $parts['host'] . $port . $parts['path'] . $query; + } + + /** + * Build the URL for given path and parameters. + * + * @param $path + * (optional) The path. + * @param $params + * (optional) The query parameters in associative array. + * + * @return + * The URL for the given parameters. + */ + protected function getUri($path = '', $params = array()) { + $url = $this->getVariable('services_uri') ? $this->getVariable('services_uri') : $this->getVariable('base_uri'); + + if (!empty($path)) + if (substr($path, 0, 4) == "http") + $url = $path; + else + $url = rtrim($url, '/') . '/' . ltrim($path, '/'); + + if (!empty($params)) + $url .= '?' . http_build_query($params, NULL, '&'); + + return $url; + } + + /** + * Generate a signature for the given params and secret. + * + * @param $params + * The parameters to sign. + * @param $secret + * The secret to sign with. + * + * @return + * The generated signature + */ + protected function generateSignature($params, $secret) { + // Work with sorted data. + ksort($params); + + // Generate the base string. + $base_string = ''; + foreach ($params as $key => $value) { + $base_string .= $key . '=' . $value; + } + $base_string .= $secret; + + return md5($base_string); + } +} diff --git a/library/oauth2-php/lib/OAuth2Exception.inc b/library/oauth2-php/lib/OAuth2Exception.inc new file mode 100644 index 000000000..8dc046974 --- /dev/null +++ b/library/oauth2-php/lib/OAuth2Exception.inc @@ -0,0 +1,85 @@ +<?php + +/** + * OAuth2.0 draft v10 exception handling. + * + * @author Originally written by Naitik Shah <naitik@facebook.com>. + * @author Update to draft v10 by Edison Wong <hswong3i@pantarei-design.com>. + * + * @sa <a href="https://github.com/facebook/php-sdk">Facebook PHP SDK</a>. + */ +class OAuth2Exception extends Exception { + + /** + * The result from the API server that represents the exception information. + */ + protected $result; + + /** + * Make a new API Exception with the given result. + * + * @param $result + * The result from the API server. + */ + public function __construct($result) { + $this->result = $result; + + $code = isset($result['code']) ? $result['code'] : 0; + + if (isset($result['error'])) { + // OAuth 2.0 Draft 10 style + $message = $result['error']; + } + elseif (isset($result['message'])) { + // cURL style + $message = $result['message']; + } + else { + $message = 'Unknown Error. Check getResult()'; + } + + parent::__construct($message, $code); + } + + /** + * Return the associated result object returned by the API server. + * + * @returns + * The result from the API server. + */ + public function getResult() { + return $this->result; + } + + /** + * Returns the associated type for the error. This will default to + * 'Exception' when a type is not available. + * + * @return + * The type for the error. + */ + public function getType() { + if (isset($this->result['error'])) { + $message = $this->result['error']; + if (is_string($message)) { + // OAuth 2.0 Draft 10 style + return $message; + } + } + return 'Exception'; + } + + /** + * To make debugging easier. + * + * @returns + * The string representation of the error. + */ + public function __toString() { + $str = $this->getType() . ': '; + if ($this->code != 0) { + $str .= $this->code . ': '; + } + return $str . $this->message; + } +} diff --git a/library/stanlemon-jgrowl-tip.tar.gz b/library/stanlemon-jgrowl-tip.tar.gz Binary files differdeleted file mode 100644 index 07a5fb097..000000000 --- a/library/stanlemon-jgrowl-tip.tar.gz +++ /dev/null diff --git a/mod/api.php b/mod/api.php index fa5e43de9..ad75e6620 100644 --- a/mod/api.php +++ b/mod/api.php @@ -2,7 +2,115 @@ require_once('include/api.php'); +function oauth_get_client($request){ + + + $params = $request->get_parameters(); + $token = $params['oauth_token']; + + $r = q("SELECT `clients`.* + FROM `clients`, `tokens` + WHERE `clients`.`client_id`=`tokens`.`client_id` + AND `tokens`.`id`='%s' AND `tokens`.`scope`='request'", + dbesc($token)); + + if (!count($r)) + return null; + + return $r[0]; +} + +function api_post(&$a) { + + if(! local_user()) { + notice( t('Permission denied.') . EOL); + return; + } + + if(count($a->user) && x($a->user,'uid') && $a->user['uid'] != local_user()) { + notice( t('Permission denied.') . EOL); + return; + } + +} + function api_content(&$a) { + if ($a->cmd=='api/oauth/authorize'){ + /* + * api/oauth/authorize interact with the user. return a standard page + */ + + $a->page['template'] = "minimal"; + + + // get consumer/client from request token + try { + $request = OAuthRequest::from_request(); + } catch(Exception $e) { + echo "<pre>"; var_dump($e); killme(); + } + + + if (x($_POST,'oauth_yes')){ + + $app = oauth_get_client($request); + if (is_null($app)) return "Invalid request. Unknown token."; + $consumer = new OAuthConsumer($app['client_id'], $app['pw'], $app['redirect_uri']); + + $verifier = md5($app['secret'].local_user()); + set_config("oauth", $verifier, local_user()); + + + if ($consumer->callback_url!=null) { + $params = $request->get_parameters(); + $glue="?"; + if (strstr($consumer->callback_url,$glue)) $glue="?"; + goaway($consumer->callback_url.$glue."oauth_token=".OAuthUtil::urlencode_rfc3986($params['oauth_token'])."&oauth_verifier=".OAuthUtil::urlencode_rfc3986($verifier)); + killme(); + } + + + + $tpl = get_markup_template("oauth_authorize_done.tpl"); + $o = replace_macros($tpl, array( + '$title' => t('Authorize application connection'), + '$info' => t('Return to your app and insert this Securty Code:'), + '$code' => $verifier, + )); + + return $o; + + + } + + + if(! local_user()) { + //TODO: we need login form to redirect to this page + notice( t('Please login to continue.') . EOL ); + return login(false,$request->get_parameters()); + } + //FKOAuth1::loginUser(4); + + $app = oauth_get_client($request); + if (is_null($app)) return "Invalid request. Unknown token."; + + + + + $tpl = get_markup_template('oauth_authorize.tpl'); + $o = replace_macros($tpl, array( + '$title' => t('Authorize application connection'), + '$app' => $app, + '$authorize' => t('Do you want to authorize this application to access your posts and contacts, and/or create new posts for you?'), + '$yes' => t('Yes'), + '$no' => t('No'), + )); + + //echo "<pre>"; var_dump($app); killme(); + + return $o; + } + echo api_call($a); killme(); } diff --git a/mod/notice.php b/mod/notice.php new file mode 100644 index 000000000..9d8aeed70 --- /dev/null +++ b/mod/notice.php @@ -0,0 +1,20 @@ +<?php + /* identi.ca -> friendika items permanent-url compatibility */ + + function notice_init(&$a){ + $id = $a->argv[1]; + $r = q("SELECT user.nickname FROM user LEFT JOIN item ON item.uid=user.uid WHERE item.id=%d", + intval($id) + ); + if (count($r)){ + $nick = $r[0]['nickname']; + $url = $a->get_baseurl()."/display/$nick/$id"; + goaway($url); + } else { + $a->error = 404; + notice( t('Item not found.') . EOL); + + } + return; + + } diff --git a/mod/settings.php b/mod/settings.php index 522ae52de..6cc80e3a6 100644 --- a/mod/settings.php +++ b/mod/settings.php @@ -47,6 +47,58 @@ function settings_post(&$a) { return; } + if(($a->argc > 1) && ($a->argv[1] === 'oauth') && x($_POST,'remove')){ + $key = $_POST['remove']; + q("DELETE FROM tokens WHERE id='%s' AND uid=%d", + dbesc($key), + local_user()); + goaway($a->get_baseurl()."/settings/oauth/"); + return; + } + + if(($a->argc > 2) && ($a->argv[1] === 'oauth') && ($a->argv[2] === 'edit'||($a->argv[2] === 'add')) && x($_POST,'submit')) { + + $name = ((x($_POST,'name')) ? $_POST['name'] : ''); + $key = ((x($_POST,'key')) ? $_POST['key'] : ''); + $secret = ((x($_POST,'secret')) ? $_POST['secret'] : ''); + $redirect = ((x($_POST,'redirect')) ? $_POST['redirect'] : ''); + $icon = ((x($_POST,'icon')) ? $_POST['icon'] : ''); + if ($name=="" || $key=="" || $secret==""){ + notice(t("Missing some important data!")); + + } else { + if ($_POST['submit']==t("Update")){ + $r = q("UPDATE clients SET + client_id='%s', + pw='%s', + name='%s', + redirect_uri='%s', + icon='%s', + uid=%d + WHERE client_id='%s'", + dbesc($key), + dbesc($secret), + dbesc($name), + dbesc($redirect), + dbesc($icon), + local_user(), + dbesc($key)); + } else { + $r = q("INSERT INTO clients + (client_id, pw, name, redirect_uri, icon, uid) + VALUES ('%s','%s','%s','%s','%s',%d)", + dbesc($key), + dbesc($secret), + dbesc($name), + dbesc($redirect), + dbesc($icon), + local_user()); + } + } + goaway($a->get_baseurl()."/settings/oauth/"); + return; + } + if(($a->argc > 1) && ($a->argv[1] == 'addon')) { call_hooks('plugin_settings_post', $_POST); return; @@ -342,6 +394,11 @@ function settings_content(&$a) { 'sel' => (($a->argc > 1) && ($a->argv[1] === 'addon')?'active':''), ), array( + 'label' => t('Connections'), + 'url' => $a->get_baseurl() . '/settings/oauth', + 'sel' => (($a->argc > 1) && ($a->argv[1] === 'oauth')?'active':''), + ), + array( 'label' => t('Export personal data'), 'url' => $a->get_baseurl() . '/uexport', 'sel' => '' @@ -353,8 +410,83 @@ function settings_content(&$a) { '$tabs' => $tabs, )); - - + if(($a->argc > 1) && ($a->argv[1] === 'oauth')) { + + if(($a->argc > 2) && ($a->argv[2] === 'add')) { + $tpl = get_markup_template("settings_oauth_edit.tpl"); + $o .= replace_macros($tpl, array( + '$tabs' => $tabs, + '$title' => t('Add application'), + '$submit' => t('Submit'), + '$cancel' => t('Cancel'), + '$name' => array('name', t('Name'), '', ''), + '$key' => array('key', t('Consumer Key'), '', ''), + '$secret' => array('secret', t('Consumer Secret'), '', ''), + '$redirect' => array('redirect', t('Redirect'), '', ''), + '$icon' => array('icon', t('Icon url'), '', ''), + )); + return $o; + } + + if(($a->argc > 3) && ($a->argv[2] === 'edit')) { + $r = q("SELECT * FROM clients WHERE client_id='%s' AND uid=%d", + dbesc($a->argv[3]), + local_user()); + + if (!count($r)){ + notice(t("You can't edit this application.")); + return; + } + $app = $r[0]; + + $tpl = get_markup_template("settings_oauth_edit.tpl"); + $o .= replace_macros($tpl, array( + '$tabs' => $tabs, + '$title' => t('Add application'), + '$submit' => t('Update'), + '$cancel' => t('Cancel'), + '$name' => array('name', t('Name'), $app['name'] , ''), + '$key' => array('key', t('Consumer Key'), $app['client_id'], ''), + '$secret' => array('secret', t('Consumer Secret'), $app['pw'], ''), + '$redirect' => array('redirect', t('Redirect'), $app['redirect_uri'], ''), + '$icon' => array('icon', t('Icon url'), $app['icon'], ''), + )); + return $o; + } + + if(($a->argc > 3) && ($a->argv[2] === 'delete')) { + $r = q("DELETE FROM clients WHERE client_id='%s' AND uid=%d", + dbesc($a->argv[3]), + local_user()); + goaway($a->get_baseurl()."/settings/oauth/"); + return; + } + + + $r = q("SELECT clients.*, tokens.id as oauth_token, (clients.uid=%d) AS my + FROM clients + LEFT JOIN tokens ON clients.client_id=tokens.client_id + WHERE clients.uid IN (%d,0)", + local_user(), + local_user()); + + + $tpl = get_markup_template("settings_oauth.tpl"); + $o .= replace_macros($tpl, array( + '$baseurl' => $a->get_baseurl(), + '$title' => t('Connected Apps'), + '$add' => t('Add application'), + '$edit' => t('Edit'), + '$delete' => t('Delete'), + '$consumerkey' => t('Client key starts with'), + '$noname' => t('No name'), + '$remove' => t('Remove authorization'), + '$tabs' => $tabs, + '$apps' => $r, + )); + return $o; + + } if(($a->argc > 1) && ($a->argv[1] === 'addon')) { $settings_addons = ""; diff --git a/update.php b/update.php index 67017fa03..362935c38 100644 --- a/update.php +++ b/update.php @@ -1,6 +1,6 @@ <?php -define( 'UPDATE_VERSION' , 1102 ); +define( 'UPDATE_VERSION' , 1103 ); /** * @@ -874,7 +874,14 @@ function update_1101() { q("ALTER TABLE `gcign` ADD INDEX (`uid`), ADD INDEX (`gcid`) "); } +function update_1102() { + q("ALTER TABLE `clients` ADD `name` TEXT NULL DEFAULT NULL AFTER `redirect_uri` "); + q("ALTER TABLE `clients` ADD `icon` TEXT NULL DEFAULT NULL AFTER `name` "); + q("ALTER TABLE `clients` ADD `uid` INT NOT NULL DEFAULT 0 AFTER `icon` "); + q("ALTER TABLE `tokens` ADD `secret` TEXT NOT NULL AFTER `id` "); + q("ALTER TABLE `tokens` ADD `uid` INT NOT NULL AFTER `scope` "); +} diff --git a/view/login.tpl b/view/login.tpl index 5240bb9ad..5349fa3d8 100644 --- a/view/login.tpl +++ b/view/login.tpl @@ -22,6 +22,11 @@ <input type="submit" name="submit" id="login-submit-button" value="$login" /> </div> + {{ for $hiddens as $k=>$v }} + <input type="hidden" name="$k" value="$v" /> + {{ endfor }} + + </form> diff --git a/view/minimal.php b/view/minimal.php new file mode 100644 index 000000000..a8c693985 --- /dev/null +++ b/view/minimal.php @@ -0,0 +1,14 @@ +<!DOCTYPE html >
+<html>
+<head>
+ <title><?php if(x($page,'title')) echo $page['title'] ?></title>
+ <script>var baseurl="<?php echo $a->get_baseurl() ?>";</script>
+ <?php if(x($page,'htmlhead')) echo $page['htmlhead'] ?>
+</head>
+<body>
+ <section style="margin:0px!important; padding:0px!important; float:none!important;display:block!important;"><?php if(x($page,'content')) echo $page['content']; ?>
+ <div id="page-footer"></div>
+ </section>
+</body>
+</html>
+
diff --git a/view/oauth_authorize.tpl b/view/oauth_authorize.tpl new file mode 100644 index 000000000..31f02ac50 --- /dev/null +++ b/view/oauth_authorize.tpl @@ -0,0 +1,10 @@ +<h1>$title</h1> + +<div class='oauthapp'> + <img src='$app.icon'> + <h4>$app.name</h4> +</div> +<h3>$authorize</h3> +<form method="POST"> +<div class="settings-submit-wrapper"><input class="settings-submit" type="submit" name="oauth_yes" value="$yes" /></div> +</form> diff --git a/view/oauth_authorize_done.tpl b/view/oauth_authorize_done.tpl new file mode 100644 index 000000000..51eaea248 --- /dev/null +++ b/view/oauth_authorize_done.tpl @@ -0,0 +1,4 @@ +<h1>$title</h1> + +<p>$info</p> +<code>$code</code> diff --git a/view/settings_oauth.tpl b/view/settings_oauth.tpl new file mode 100644 index 000000000..bc5866bec --- /dev/null +++ b/view/settings_oauth.tpl @@ -0,0 +1,32 @@ +$tabs + +<h1>$title</h1> + + +<form action="settings/oauth" method="post" autocomplete="off"> + + <div id="profile-edit-links"> + <ul> + <li> + <a id="profile-edit-view-link" href="$baseurl/settings/oauth/add">$add</a> + </li> + </ul> + </div> + + {{ for $apps as $app }} + <div class='oauthapp'> + <img src='$app.icon' class="{{ if $app.icon }} {{ else }}noicon{{ endif }}"> + {{ if $app.name }}<h4>$app.name</h4>{{ else }}<h4>$noname</h4>{{ endif }} + {{ if $app.my }} + {{ if $app.oauth_token }} + <div class="settings-submit-wrapper" ><button class="settings-submit" type="submit" name="remove" value="$app.oauth_token">$remove</button></div> + {{ endif }} + {{ endif }} + {{ if $app.my }} + <a href="$baseurl/settings/oauth/edit/$app.client_id" class="icon edit" title="$edit"> </a> + <a href="$baseurl/settings/oauth/delete/$app.client_id" class="icon drop" title="$delete"> </a> + {{ endif }} + </div> + {{ endfor }} + +</form> diff --git a/view/settings_oauth_edit.tpl b/view/settings_oauth_edit.tpl new file mode 100644 index 000000000..98b7457aa --- /dev/null +++ b/view/settings_oauth_edit.tpl @@ -0,0 +1,17 @@ +$tabs + +<h1>$title</h1> + +<form method="POST"> +{{ inc field_input.tpl with $field=$name }}{{ endinc }} +{{ inc field_input.tpl with $field=$key }}{{ endinc }} +{{ inc field_input.tpl with $field=$secret }}{{ endinc }} +{{ inc field_input.tpl with $field=$redirect }}{{ endinc }} +{{ inc field_input.tpl with $field=$icon }}{{ endinc }} + +<div class="settings-submit-wrapper" > +<input type="submit" name="submit" class="settings-submit" value="$submit" /> +<input type="submit" name="cancel" class="settings-submit" value="$cancel" /> +</div> + +</form> diff --git a/view/theme/duepuntozero/style.css b/view/theme/duepuntozero/style.css index eb73bfb97..e9c0817f4 100644 --- a/view/theme/duepuntozero/style.css +++ b/view/theme/duepuntozero/style.css @@ -2779,6 +2779,28 @@ a.mail-list-link { .panel_text .progress { width: 50%; overflow: hidden; height: auto; border: 1px solid #cccccc; margin-bottom: 5px} .panel_text .progress span {float: right; display: block; width: 25%; background-color: #eeeeee; text-align: right;} +/** + * OAuth + */ +.oauthapp { + height: auto; overflow: auto; + border-bottom: 2px solid #cccccc; + padding-bottom: 1em; + margin-bottom: 1em; +} +.oauthapp img { + float: left; + width: 48px; height: 48px; + margin: 10px; +} +.oauthapp img.noicon { + background-image: url("../../../images/icons/48/plugin.png"); + background-position: center center; + background-repeat: no-repeat; +} +.oauthapp a { + float: left; +} /** * ICONS |