Kaynağa Gözat

#252 Trident Connect + OAuth2 server modules

Alex Trofimov 10 yıl önce
ebeveyn
işleme
782d458f0a
100 değiştirilmiş dosya ile 5009 ekleme ve 33 silme
  1. 2 0
      inc/classes/BxDol.php
  2. 9 1
      inc/classes/BxDolAccount.php
  3. 2 2
      inc/classes/BxDolGrid.php
  4. 10 1
      inc/classes/BxDolProfile.php
  5. 8 0
      inc/classes/BxDolProfileUndefined.php
  6. 2 2
      inc/profiles.inc.php
  7. 32 12
      inc/utils.inc.php
  8. 0 2
      modules/base/connect/classes/BxBaseModConnectTemplate.php
  9. 1 1
      modules/base/general/classes/BxBaseModGeneralModule.php
  10. 10 0
      modules/base/profile/classes/BxBaseModProfileModule.php
  11. 1 1
      modules/base/profile/classes/BxBaseModProfileTemplate.php
  12. 1 3
      modules/boonex/facebook_connect/classes/BxFaceBookConnectConfig.php
  13. 1 3
      modules/boonex/facebook_connect/classes/BxFaceBookConnectModule.php
  14. 2 2
      modules/boonex/facebook_connect/install/sql/enable.sql
  15. 166 0
      modules/boonex/oauth2/classes/BxOAuthAPI.php
  16. 21 0
      modules/boonex/oauth2/classes/BxOAuthConfig.php
  17. 34 0
      modules/boonex/oauth2/classes/BxOAuthDb.php
  18. 99 0
      modules/boonex/oauth2/classes/BxOAuthFormAdd.php
  19. 78 0
      modules/boonex/oauth2/classes/BxOAuthGrid.php
  20. 163 0
      modules/boonex/oauth2/classes/BxOAuthModule.php
  21. 21 0
      modules/boonex/oauth2/classes/BxOAuthPage.php
  22. 45 0
      modules/boonex/oauth2/classes/BxOAuthStudioPage.php
  23. 20 0
      modules/boonex/oauth2/classes/BxOAuthTemplate.php
  24. 68 0
      modules/boonex/oauth2/install/config.php
  25. 20 0
      modules/boonex/oauth2/install/installer.php
  26. 21 0
      modules/boonex/oauth2/install/langs/en.xml
  27. 18 0
      modules/boonex/oauth2/install/sql/disable.sql
  28. 42 0
      modules/boonex/oauth2/install/sql/enable.sql
  29. 60 0
      modules/boonex/oauth2/install/sql/install.sql
  30. 9 0
      modules/boonex/oauth2/install/sql/uninstall.sql
  31. 18 0
      modules/boonex/oauth2/request.php
  32. 21 0
      modules/boonex/oauth2/template/clients.html
  33. 9 0
      modules/boonex/oauth2/template/css/main.css
  34. BIN
      modules/boonex/oauth2/template/images/icons/std-mi.png
  35. BIN
      modules/boonex/oauth2/template/images/icons/std-pi.png
  36. BIN
      modules/boonex/oauth2/template/images/icons/std-si.png
  37. BIN
      modules/boonex/oauth2/template/images/icons/std-wi.png
  38. 16 0
      modules/boonex/oauth2/template/page_auth.html
  39. 1 1
      modules/boonex/organizations/classes/BxOrgsModule.php
  40. 21 0
      modules/boonex/russian/data/langs/bx_oauth/ru.xml
  41. 37 0
      modules/boonex/russian/data/langs/bx_tricon/ru.xml
  42. 2 2
      modules/boonex/russian/data/langs/system/ru.xml
  43. 21 0
      modules/boonex/trident_connect/classes/BxTriConAlerts.php
  44. 49 0
      modules/boonex/trident_connect/classes/BxTriConConfig.php
  45. 20 0
      modules/boonex/trident_connect/classes/BxTriConDb.php
  46. 179 0
      modules/boonex/trident_connect/classes/BxTriConModule.php
  47. 27 0
      modules/boonex/trident_connect/classes/BxTriConStudioPage.php
  48. 20 0
      modules/boonex/trident_connect/classes/BxTriConTemplate.php
  49. 68 0
      modules/boonex/trident_connect/install/config.php
  50. 20 0
      modules/boonex/trident_connect/install/installer.php
  51. 38 0
      modules/boonex/trident_connect/install/langs/en.xml
  52. 25 0
      modules/boonex/trident_connect/install/sql/disable.sql
  53. 51 0
      modules/boonex/trident_connect/install/sql/enable.sql
  54. 21 0
      modules/boonex/trident_connect/install/sql/install.sql
  55. 9 0
      modules/boonex/trident_connect/install/sql/uninstall.sql
  56. 18 0
      modules/boonex/trident_connect/request.php
  57. BIN
      modules/boonex/trident_connect/template/images/icons/std-mi.png
  58. BIN
      modules/boonex/trident_connect/template/images/icons/std-pi.png
  59. BIN
      modules/boonex/trident_connect/template/images/icons/std-si.png
  60. BIN
      modules/boonex/trident_connect/template/images/icons/std-wi.png
  61. 48 0
      plugins/OAuth2/Autoloader.php
  62. 15 0
      plugins/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php
  63. 123 0
      plugins/OAuth2/ClientAssertionType/HttpBasic.php
  64. 383 0
      plugins/OAuth2/Controller/AuthorizeController.php
  65. 43 0
      plugins/OAuth2/Controller/AuthorizeControllerInterface.php
  66. 111 0
      plugins/OAuth2/Controller/ResourceController.php
  67. 26 0
      plugins/OAuth2/Controller/ResourceControllerInterface.php
  68. 274 0
      plugins/OAuth2/Controller/TokenController.php
  69. 32 0
      plugins/OAuth2/Controller/TokenControllerInterface.php
  70. 11 0
      plugins/OAuth2/Encryption/EncryptionInterface.php
  71. 47 0
      plugins/OAuth2/Encryption/FirebaseJwt.php
  72. 173 0
      plugins/OAuth2/Encryption/Jwt.php
  73. 100 0
      plugins/OAuth2/GrantType/AuthorizationCode.php
  74. 67 0
      plugins/OAuth2/GrantType/ClientCredentials.php
  75. 20 0
      plugins/OAuth2/GrantType/GrantTypeInterface.php
  76. 226 0
      plugins/OAuth2/GrantType/JwtBearer.php
  77. 111 0
      plugins/OAuth2/GrantType/RefreshToken.php
  78. 83 0
      plugins/OAuth2/GrantType/UserCredentials.php
  79. 21 0
      plugins/OAuth2/LICENSE
  80. 106 0
      plugins/OAuth2/OpenID/Controller/AuthorizeController.php
  81. 10 0
      plugins/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php
  82. 58 0
      plugins/OAuth2/OpenID/Controller/UserInfoController.php
  83. 23 0
      plugins/OAuth2/OpenID/Controller/UserInfoControllerInterface.php
  84. 33 0
      plugins/OAuth2/OpenID/GrantType/AuthorizationCode.php
  85. 60 0
      plugins/OAuth2/OpenID/ResponseType/AuthorizationCode.php
  86. 27 0
      plugins/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php
  87. 24 0
      plugins/OAuth2/OpenID/ResponseType/CodeIdToken.php
  88. 9 0
      plugins/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php
  89. 124 0
      plugins/OAuth2/OpenID/ResponseType/IdToken.php
  90. 29 0
      plugins/OAuth2/OpenID/ResponseType/IdTokenInterface.php
  91. 27 0
      plugins/OAuth2/OpenID/ResponseType/IdTokenToken.php
  92. 9 0
      plugins/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php
  93. 37 0
      plugins/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php
  94. 38 0
      plugins/OAuth2/OpenID/Storage/UserClaimsInterface.php
  95. 8 0
      plugins/OAuth2/README.md
  96. 213 0
      plugins/OAuth2/Request.php
  97. 16 0
      plugins/OAuth2/RequestInterface.php
  98. 369 0
      plugins/OAuth2/Response.php
  99. 24 0
      plugins/OAuth2/ResponseInterface.php
  100. 194 0
      plugins/OAuth2/ResponseType/AccessToken.php

+ 2 - 0
inc/classes/BxDol.php

@@ -58,6 +58,7 @@ interface iBxDolProfile
     public function getDisplayName();
     public function getUrl();
     public function getUnit();
+    public function getPicture();
     public function getAvatar();
     public function getThumb();
     public function getIcon();
@@ -72,6 +73,7 @@ interface iBxDolProfile
 interface iBxDolProfileService
 {
     public function serviceProfileUnit ($iContentId);
+    public function serviceProfilePicture ($iContentId);
     public function serviceProfileAvatar ($iContentId);
     public function serviceProfileThumb ($iContentId);
     public function serviceProfileIcon ($iContentId);

+ 9 - 1
inc/classes/BxDolAccount.php

@@ -186,7 +186,15 @@ class BxDolAccount extends BxDol
     }
 
     /**
-     * Get avatar picture url
+     * Get picture url
+     */
+    public function getPicture($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-picture.png');
+    }
+
+    /**
+     * Get avatar url
      */
     public function getAvatar($iAccountId = false)
     {

+ 2 - 2
inc/classes/BxDolGrid.php

@@ -270,7 +270,7 @@ class BxDolGrid extends BxDol implements iBxDolFactoryObject, iBxDolReplaceable
      * @param $sObject object name
      * @return object instance or false on error
      */
-    public static function getObjectInstance($sObject)
+    public static function getObjectInstance($sObject, $oTemplate = false)
     {
         if (isset($GLOBALS['bxDolClasses']['BxDolGrid!'.$sObject]))
             return $GLOBALS['bxDolClasses']['BxDolGrid!'.$sObject];
@@ -286,7 +286,7 @@ class BxDolGrid extends BxDol implements iBxDolFactoryObject, iBxDolReplaceable
                 require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
         }
 
-        $o = new $sClass($aObject);
+        $o = new $sClass($aObject, $oTemplate);
 
         if (!$o->_isVisibleGrid($aObject))
             return false;

+ 10 - 1
inc/classes/BxDolProfile.php

@@ -213,7 +213,16 @@ class BxDolProfile extends BxDol implements iBxDolProfile
     }
 
     /**
-     * Get thumbnail url
+     * Get picture url
+     */
+    public function getPicture($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_picture', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get avatar url
      */
     public function getAvatar($iProfileId = 0)
     {

+ 8 - 0
inc/classes/BxDolProfileUndefined.php

@@ -82,6 +82,14 @@ class BxDolProfileUndefined extends BxDol implements iBxDolSingleton, iBxDolProf
         return '<div>' . $this->getDisplayName() . '</div>';
     }
 
+    /**
+     * Get picture url
+     */
+    public function getPicture()
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-picture.png');
+    }
+
     /**
      * Get profile avatar
      */

+ 2 - 2
inc/profiles.inc.php

@@ -248,14 +248,14 @@ function bx_require_authentication ($bStudio = false)
  * @param $bStudio display login form for studio
  * @param $bAjaxMode login form displayed via AJAX
  */
-function bx_login_form($bStudio = false, $bAjaxMode = false)
+function bx_login_form($bStudio = false, $bAjaxMode = false, $sForceRelocate = '')
 {
     if ($bStudio == 1) {
         BxTemplStudioFunctions::getInstance()->getLoginForm();
         exit;
     }
 
-    $sFormCode = BxDolService::call('system', 'login_form', array(), 'TemplServiceLogin');
+    $sFormCode = BxDolService::call('system', 'login_form', array('', $sForceRelocate), 'TemplServiceLogin');
 
     if ($bAjaxMode) {
         echo $GLOBALS['oFunctions']->transBox('', $sFormCode, false, true);

+ 32 - 12
inc/utils.inc.php

@@ -728,41 +728,61 @@ function bx_php_string_quot ($mixedInput)
  *
  * @param string $sFileUrl - file URL to be read.
  * @param array $aParams - an array of parameters to be pathed with URL.
+ * @param string $sMethod - post or get.
+ * @param array $aHeaders - custom headers.
  * @return string the file's contents.
  */
-function bx_file_get_contents($sFileUrl, $aParams = array(), $bChangeTimeout = false)
+function bx_file_get_contents($sFileUrl, $aParams = array(), $sMethod = 'get', $aHeaders = array(), &$sHttpCode = null)
 {
-    if ($aParams)
-        $sFileUrl = bx_append_url_params($sFileUrl, $aParams);
+    $bChangeTimeout = false;
+
+    if ('post' != $sMethod)
+    	$sFileUrl = bx_append_url_params($sFileUrl, $aParams);
 
     $sResult = '';
     if(function_exists('curl_init')) {
         $rConnect = curl_init();
 
+        curl_setopt($rConnect, CURLOPT_TIMEOUT, 10);
         curl_setopt($rConnect, CURLOPT_URL, $sFileUrl);
-        curl_setopt($rConnect, CURLOPT_HEADER, 0);
+        curl_setopt($rConnect, CURLOPT_HEADER, NULL === $sHttpCode ? false : true);
         curl_setopt($rConnect, CURLOPT_RETURNTRANSFER, 1);
-        curl_setopt($rConnect, CURLOPT_FOLLOWLOCATION, 1);
-
-        if(bx_mb_strpos($sFileUrl, 'https') !== false) {
-	        curl_setopt($rConnect, CURLOPT_SSL_VERIFYPEER, 0);
-			curl_setopt($rConnect, CURLOPT_SSL_VERIFYHOST, 0);
-        }
 
         if ($bChangeTimeout) {
             curl_setopt($rConnect, CURLOPT_CONNECTTIMEOUT, 3);
             curl_setopt($rConnect, CURLOPT_TIMEOUT, 3);
         }
 
+        if (!ini_get('open_basedir'))
+            curl_setopt($rConnect, CURLOPT_FOLLOWLOCATION, 1);
+
+        if ($aHeaders)
+            curl_setopt($rConnect, CURLOPT_HTTPHEADER, $aHeaders);
+
+        if ('post' == $sMethod) {
+            curl_setopt($rConnect, CURLOPT_POST, true);
+            curl_setopt($rConnect, CURLOPT_POSTFIELDS, $aParams);
+        }
+
         $sAllCookies = '';
         foreach($_COOKIE as $sKey=>$sValue){
-            $sAllCookies .= $sKey."=".$sValue.";";
+            $sAllCookies .= $sKey . '=' . $sValue . ';';
         }
         curl_setopt($rConnect, CURLOPT_COOKIE, $sAllCookies);
 
         $sResult = curl_exec($rConnect);
+
+        if (curl_errno($rConnect) == 60) { // CURLE_SSL_CACERT
+            curl_setopt($rConnect, CURLOPT_CAINFO, BX_DIRECTORY_PATH_PLUGINS . 'curl/cacert.pem');
+            $sResult = curl_exec($rConnect);
+        }
+
+        if (NULL !== $sHttpCode)
+            $sHttpCode = curl_getinfo($rConnect, CURLINFO_HTTP_CODE);
+
         curl_close($rConnect);
-    } else {
+    }
+    else {
 
         $iSaveTimeout = false;
         if ($bChangeTimeout) {

+ 0 - 2
modules/base/connect/classes/BxBaseModConnectTemplate.php

@@ -11,8 +11,6 @@
 
 class BxBaseModConnectTemplate extends BxDolModuleTemplate
 {
-    protected $_sPageIcon;
-
     function __construct(&$oConfig, &$oDb)
     {
         parent::__construct($oConfig, $oDb);

+ 1 - 1
modules/base/general/classes/BxBaseModGeneralModule.php

@@ -90,7 +90,7 @@ class BxBaseModGeneralModule extends BxDolModule
     	return 0;
     }
 
-    public function serviceFormsHelper ()    
+    public function serviceFormsHelper ()
     {
         bx_import('FormsEntryHelper', $this->_aModule);
         $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';

+ 10 - 0
modules/base/profile/classes/BxBaseModProfileModule.php

@@ -108,6 +108,11 @@ class BxBaseModProfileModule extends BxBaseModGeneralModule implements iBxDolPro
         return $this->_serviceTemplateFunc('unit', $iContentId);
     }
 
+    public function serviceProfilePicture ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlPicture', $iContentId);
+    }
+
     public function serviceProfileAvatar ($iContentId)
     {
         return $this->_serviceTemplateFunc('urlAvatar', $iContentId);
@@ -154,6 +159,11 @@ class BxBaseModProfileModule extends BxBaseModGeneralModule implements iBxDolPro
         return $aFieldsProfile;
     }
 
+    public function serviceFormsHelper ()
+    {
+        return parent::serviceFormsHelper ();
+    }
+
     public function serviceBrowseRecentProfiles ($bDisplayEmptyMsg = false)
     {
         return $this->_serviceBrowse ('recent', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg);

+ 1 - 1
modules/base/profile/classes/BxBaseModProfileTemplate.php

@@ -130,7 +130,7 @@ class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
     }
 
     /**
-     * Get profile picture preview url
+     * Get profile avatar url
      */
     function urlAvatar ($aData, $bSubstituteNoImage = true)
     {

+ 1 - 3
modules/boonex/facebook_connect/classes/BxFaceBookConnectConfig.php

@@ -41,9 +41,7 @@ class BxFaceBookConnectConfig extends BxBaseModConnectConfig
         $this -> isAlwaysConfirmEmail = (bool)getParam('bx_facebook_connect_confirm_email'); 
         $this -> isAlwaysAutoApprove = (bool)getParam('bx_facebook_connect_approve');
 
-        $this -> bAutoFriends = 'on' == getParam('bx_facebook_connect_auto_friends')
-            ? true
-            : false;
+        $this -> bAutoFriends = 'on' == getParam('bx_facebook_connect_auto_friends');
 
         $this -> aFaceBookReqParams = array(
             'scope' => getParam('bx_facebook_connect_extended_info') 

+ 1 - 3
modules/boonex/facebook_connect/classes/BxFaceBookConnectModule.php

@@ -93,8 +93,6 @@ class BxFaceBookConnectModule extends BxBaseModConnectModule
                     $this -> setLogged($iProfileId);
                 } 
                 else {
-                    $sAlternativeNickName = '';
-
                     // process profile's nickname
                     $aFacebookProfileInfo['nick_name'] = $this
                         -> _proccesNickName($aFacebookProfileInfo['first_name']);
@@ -109,7 +107,7 @@ class BxFaceBookConnectModule extends BxBaseModConnectModule
                     }
 
                     // create new profile
-                    $this -> _createProfile($aFacebookProfileInfo, $sAlternativeNickName);
+                    $this -> _createProfile($aFacebookProfileInfo);
                 }
             } else {
                 // FB profile info is not defined;

+ 2 - 2
modules/boonex/facebook_connect/install/sql/enable.sql

@@ -6,12 +6,12 @@ INSERT INTO `sys_email_templates` (`Module`, `NameSystem`, `Name`, `Subject`, `B
 
 -- OBJECTS: AUTH
 
-INSERT INTO  `sys_objects_auths` (`Name`, `Title`, `Link`, `Icon`) VALUES
+INSERT INTO `sys_objects_auths` (`Name`, `Title`, `Link`, `Icon`) VALUES
 ('bx_facebook', '_bx_facebook_auth_title', 'modules/?r=facebook_connect/login_form', 'facebook-square');
 
 -- ALERTS
 
-INSERT INTO `sys_alerts_handlers` SET `name`  = 'bx_facebook_connect', `class` = 'BxFaceBookConnectAlerts', `file` = 'modules/boonex/facebook_connect/classes/BxFaceBookConnectAlerts.php';
+INSERT INTO `sys_alerts_handlers` SET `name` = 'bx_facebook_connect', `class` = 'BxFaceBookConnectAlerts', `file` = 'modules/boonex/facebook_connect/classes/BxFaceBookConnectAlerts.php';
 
 SET @iHandlerId := (SELECT `id` FROM `sys_alerts_handlers`  WHERE `name` = 'bx_facebook_connect');
 

+ 166 - 0
modules/boonex/oauth2/classes/BxOAuthAPI.php

@@ -0,0 +1,166 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+use OAuth2\Response;
+
+class BxOAuthAPI extends BxDol
+{
+    protected $_oModule;
+    protected $_oDb;
+    public $aAction2Scope = array (
+        'me' => 'basic',
+        'user' => 'basic',
+        'friends' => 'basic',
+        'service' => 'basic', // service
+    );
+
+    function __construct($oModule)
+    {
+        $this->_oModule = $oModule;
+        $this->_oDb = $oModule->_oDb;
+    }
+    
+    function me($aToken)
+    {        
+        if (!($oProfile = BxDolProfile::getInstance($aToken['user_id']))) {
+            $this->errorOutput('404', 'not_found', 'Profile was not found');
+            return;
+        }
+    
+        $this->output($this->_prepareProfileArray($oProfile, false));
+    }
+
+    function user($aToken)
+    {
+        $iProfileId = (int)bx_get('id');
+
+        if ($iProfileId == $aToken['user_id']) {
+            $this->me($aToken);
+            return;
+        }
+
+        if (!($oProfile = $this->_getProfileWithAccessChecking($iProfileId)))
+            return;
+        
+        $this->output($this->_prepareProfileArray($oProfile, !isAdmin($aToken['user_id'])));
+    }
+
+    function friends($aToken)
+    {
+        $iProfileId = (int)bx_get('id');
+
+        if (!($oProfile = $this->_getProfileWithAccessChecking($iProfileId)))
+            return;
+
+        $this->output(array(
+            'user_id' => $iProfileId,
+            'friends' => getMyFriendsEx($iProfileId),
+        ));
+    }
+
+    function service($aToken) 
+    {
+        if (!isAdmin($aToken['user_id'])) {
+            $this->errorOutput(403, 'access_denied', 'Only admin can access service endpoint');
+            return false;
+        }
+
+        bx_login($aToken['user_id'], false, false);
+
+        $sUri = bx_get('uri');
+        $sMethod = bx_get('method');
+
+        if (!($aParams = bx_get('params')))
+            $aParams = array();
+        elseif (is_string($aParams) && preg_match('/^a:[\d+]:\{/', $aParams))
+            $aParams = @unserialize($aParams);
+        if (!is_array($aParams))
+            $aParams = array($aParams);
+
+        if (!($sClass = bx_get('class')))
+            $sClass = 'Module';
+
+        if (!BxDolRequest::serviceExists($sUri, $sMethod, $sClass)) {
+            $this->errorOutput(404, 'not_found', 'Service was not found');
+            return false;
+        }
+
+        $mixedRet = BxDolService::call($sUri, $sMethod, $aParams, $sClass);
+
+        $this->output(array(
+            'uri' => $sUri,
+            'method' => $sMethod,
+            'data' => $mixedRet,
+        ));
+    }
+
+    function errorOutput($iHttpCode, $sError, $sErrorDesc)
+    {
+        $oReponse = new Response();
+        $oReponse->setError($iHttpCode, $sError, $sErrorDesc);
+        $oReponse->send();
+    }
+
+    function output($a)
+    {
+        $oReponse = new Response();
+        $oReponse->setParameters($a);
+        $oReponse->send();
+    }
+
+    protected function _getProfileWithAccessChecking ($iProfileId) 
+    {
+        if (!$oProfile = BxDolProfile::getInstance($iProfileId)) {
+            $this->errorOutput('404', 'not_found', 'Profile was not found');
+            return false;
+        }
+/*        
+        // TODO: check visibility
+        if (!bx_check_profile_visibility($iProfileId, $aToken['user_id'], true)) {
+            $this->errorOutput(403, 'access_denied', 'You have no rights to view this user info');
+            return false;
+        }
+*/
+        return $oProfile;
+    }
+
+    protected function _prepareProfileArray ($oProfile, $bPublicFieldsOnly = true) 
+    {
+        $oAccount = $oProfile->getAccountObject();
+        $aProfileInfo = $oProfile->getInfo();
+
+        if ($bPublicFieldsOnly) {
+            $aProfileInfo = array(
+                'id' => $aProfileInfo['id'],
+            );
+        } 
+        else {
+            unset($aProfileInfo['account_id']);
+            unset($aProfileInfo['content_id']);
+            unset($aProfileInfo['status']);
+
+            $aProfileInfo['email'] = $oAccount->getEmail();
+
+            // TODO: fetch extended info from profile module
+        }
+
+
+        $aProfileInfo['name'] = $oAccount->getDisplayName();
+        $aProfileInfo['profile_display_name'] = $oProfile->getDisplayName();
+        $aProfileInfo['profile_link'] = $oProfile->getUrl();
+        $aProfileInfo['picture'] = $oProfile->getPicture();
+
+        return $aProfileInfo;
+    }
+
+}
+
+/** @} */

+ 21 - 0
modules/boonex/oauth2/classes/BxOAuthConfig.php

@@ -0,0 +1,21 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxOAuthConfig extends BxDolModuleConfig
+{
+    function __construct($aModule)
+    {
+        parent::__construct($aModule);
+    }
+
+}
+
+/** @} */

+ 34 - 0
modules/boonex/oauth2/classes/BxOAuthDb.php

@@ -0,0 +1,34 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxOAuthDb extends BxDolModuleDb
+{
+    function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+    }
+
+    function getClientTitle($sClientId)
+    {
+        $sQuery = $this->prepare("SELECT `title` FROM `bx_oauth_clients` WHERE `client_id` = ?", $sClientId);
+        return $this->getOne($sQuery);
+    }
+
+    function deleteClients($aClients)
+    {        
+        foreach ($aClients as $sClientId) {
+            $sQuery = $this->prepare("DELETE FROM `bx_oauth_clients` WHERE `client_id` = ?", $sClientId);
+            $this->query($sQuery);
+        }
+    }
+}
+
+/** @} */

+ 99 - 0
modules/boonex/oauth2/classes/BxOAuthFormAdd.php

@@ -0,0 +1,99 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxOAuthFormAdd extends BxTemplFormView
+{
+     protected static $LENGTH_ID = 10;
+     protected static $LENGTH_SECRET = 32;
+
+    function __construct ($oModule)
+    {
+        $aCustomForm = array(
+
+            'form_attrs' => array(
+                'id' => 'bx-oauth-add',
+                'name' => 'bx-oauth-add',
+                'action' => BX_DOL_URL_ROOT . 'grid.php?' . bx_encode_url_params($_GET, array('ids', '_r')),
+                'method' => 'post',
+            ),
+
+            'params' => array (
+                'db' => array(
+                    'table' => 'bx_oauth_clients',
+                    'key' => 'id',
+                    'submit_name' => 'client_add',
+                ),
+            ),
+
+            'inputs' => array(
+
+                'title' => array(
+                    'type' => 'text',
+                    'name' => 'title',
+                    'caption' => _t('_Title'),
+                    'required' => true,
+                    'checker' => array (
+                        'func' => 'avail',
+                        'error' => _t ('_sys_adm_form_err_required_field'),
+                    ),
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+
+                'redirect_uri' => array(
+                    'type' => 'text',
+                    'name' => 'redirect_uri',
+                    'caption' => _t('_bx_oauth_client_url'),
+                    'required' => true,
+                    'checker' => array (
+                        'func' => 'avail',
+                        'error' => _t ('_sys_adm_form_err_required_field'),
+                    ),
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+
+                'Submit' => array (
+                    'type' => 'input_set',
+                    0 => array (
+                        'type' => 'submit',
+                        'name' => 'client_add',
+                        'value' => _t('_Submit'),
+                    ),
+                    1 => array (
+                        'type' => 'reset',
+                        'name' => 'close',
+                        'value' => _t('Close'),
+                        'attrs' => array(
+                            'onclick' => "$('.bx-popup-active').dolPopupHide()",
+                            'class' => 'bx-def-margin-sec-left',
+                        ),
+                    ),
+                ),
+            ),
+        );
+
+        parent::__construct ($aCustomForm);
+    }
+
+    function insert ($aValsToAdd = array(), $isIgnore = false)
+    {
+        $aValsToAdd['client_id'] = strtolower(genRndPwd(self::$LENGTH_ID, false));
+        $aValsToAdd['client_secret'] = strtolower(genRndPwd(self::$LENGTH_SECRET, false));
+        $aValsToAdd['scope'] = 'basic';
+        $aValsToAdd['user_id'] = getLoggedId();
+        return parent::insert($aValsToAdd, $isIgnore);
+    }
+}
+
+/** @} */

+ 78 - 0
modules/boonex/oauth2/classes/BxOAuthGrid.php

@@ -0,0 +1,78 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxOAuthGrid extends BxTemplGrid
+{
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+    }
+
+    /**
+     * add js file for AJAX form submission
+     */
+    protected function _addJsCss()
+    {
+        parent::_addJsCss();
+        $this->_oTemplate->addJs('jquery.form.min.js');
+
+        $oForm = new BxTemplFormView(array());
+        $oForm->addCssJs();
+    }
+
+    /**
+     * 'add' action handler
+     */
+    public function performActionAdd()
+    {
+        $oModule = BxDolModule::getInstance('bx_oauth');
+
+        bx_import('FormAdd', $oModule->_aModule);
+        $oForm = new BxOAuthFormAdd($oModule);
+        $oForm->initChecker();
+
+        if ($oForm->isSubmittedAndValid()) { // if form is submitted and all fields are valid
+
+            $iNewId = $oForm->insert (array(), true); // insert record to database
+            if ($iNewId)
+                $aRes = array('grid' => $this->getCode(false), 'blink' => $iNewId); // if record is successfully added, reload grid and highlight added row
+            else
+                $aRes = array('msg' => "Error occured"); // if record adding failed, display error message
+
+            $this->_echoResultJson($aRes, true);
+
+        } else { // if form is not submitted or some fields are invalid, display popup with form
+
+            // we need to use 'transBox' function to properly display 'popup'
+            $s = BxTemplFunctions::getInstance()->transBox('', '
+                <div class="bx-def-padding bx-def-color-bg-block">' . $oForm->getCode() . '</div>
+                <script>
+                    $(document).ready(function () {
+                        $("#bx-oauth-add").ajaxForm({
+                            dataType: "json",
+                            beforeSubmit: function (formData, jqForm, options) {
+                                bx_loading($("#' . $oForm->aFormAttrs['id'] . '"), true);
+                            },
+                            success: function (data) {
+                                $(".bx-popup-active").dolPopupHide();
+                                glGrids.' . $this->_sObject . '.processJson(data, "add");
+                            }
+                        });
+                    });
+                </script>');
+
+            $this->_echoResultJson(array('popup' => array('html' => $s, 'options' => array('closeOnOuterClick' => false))), true);
+
+        }
+    }
+}
+
+/** @} */

+ 163 - 0
modules/boonex/oauth2/classes/BxOAuthModule.php

@@ -0,0 +1,163 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+require_once (BX_DIRECTORY_PATH_PLUGINS . 'OAuth2/Autoloader.php');
+OAuth2\Autoloader::register();
+
+class BxOAuthModule extends BxDolModule
+{
+    protected $_oStorage;
+    protected $_oServer;
+    protected $_oAPI;
+
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+
+        $aConfig = array (
+            'client_table' => 'bx_oauth_clients',
+            'access_token_table' => 'bx_oauth_access_tokens',
+            'refresh_token_table' => 'bx_oauth_refresh_tokens',
+            'code_table' => 'bx_oauth_authorization_codes',
+            'user_table' => 'Profiles',
+            'jwt_table'  => '',
+            'jti_table'  => '',
+            'scope_table'  => 'bx_oauth_scopes',
+            'public_key_table'  => '',
+        );
+
+        $this->_oStorage = new OAuth2\Storage\Pdo(array(
+            'dsn' => $this->_buildDSN(), 
+            'username' => BX_DATABASE_USER, 
+            'password' => BX_DATABASE_PASS,
+            'options' => array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'"),
+        ), $aConfig);
+
+        $this->_oServer = new OAuth2\Server($this->_oStorage, array (
+            'require_exact_redirect_uri' => false,
+        ));
+
+        // Add the "Client Credentials" grant type (it is the simplest of the grant types)
+        $this->_oServer->addGrantType(new OAuth2\GrantType\ClientCredentials($this->_oStorage));
+
+        // Add the "Authorization Code" grant type (this is where the oauth magic happens)
+        $this->_oServer->addGrantType(new OAuth2\GrantType\AuthorizationCode($this->_oStorage));
+
+    }
+
+    protected function _buildDSN () 
+    {
+        $sDSN = 'mysql:';
+        if (!empty(BX_DATABASE_HOST))
+            $sDSN .= 'host=' . BX_DATABASE_HOST . ';';
+        if (!empty(BX_DATABASE_PORT))
+            $sDSN .= 'port=' . BX_DATABASE_PORT . ';';
+        if (!empty(BX_DATABASE_SOCK))
+            $sDSN .= 'unix_socket=' . BX_DATABASE_SOCK . ';';
+        $sDSN .= 'dbname=' . BX_DATABASE_NAME . ';charset=UTF8';
+        return $sDSN;
+    }
+
+    function actionToken ()
+    {
+        // Handle a request for an OAuth2.0 Access Token and send the response to the client
+        $this->_oServer->handleTokenRequest(OAuth2\Request::createFromGlobals())->send();
+    }
+
+    function actionApi ($sAction)
+    {
+        // Handle a request to a resource and authenticate the access token
+        if (!$this->_oServer->verifyResourceRequest(OAuth2\Request::createFromGlobals())) {
+            $this->_oServer->getResponse()->send();
+            return;
+        }
+
+        $aToken = $this->_oServer->getAccessTokenData(OAuth2\Request::createFromGlobals());
+
+        if (!$this->_oAPI) {
+            bx_import('API', $this->_aModule);
+            $this->_oAPI = new BxOAuthAPI($this);
+        }
+
+        if (!$sAction || !method_exists($this->_oAPI, $sAction) || 0 == strcasecmp('errorOutput', $sAction) || 0 == strcasecmp('output', $sAction)) {
+            $this->_oAPI->errorOutput(404, 'not_found', 'No such API endpoint available');
+            return;
+        }
+
+        $sScope = $this->_oAPI->aAction2Scope[$sAction];
+        if (false === strpos($sScope, $aToken['scope'])) {
+            $this->_oAPI->errorOutput(403, 'insufficient_scope', 'The request requires higher privileges than provided by the access token');
+            return;
+        }
+
+        $this->_oAPI->$sAction($aToken);
+    }
+
+    function actionAuth ()
+    {
+        $oRequest = OAuth2\Request::createFromGlobals();
+        $oResponse = new OAuth2\Response();
+
+        // validate the authorize request
+        if (!$this->_oServer->validateAuthorizeRequest($oRequest, $oResponse)) {
+            $o = json_decode($oResponse->getResponseBody());
+            $this->_oTemplate->pageError($o->error_description);
+        }
+
+        if (!isLogged()) {
+            require_once(BX_DIRECTORY_PATH_INC . 'design.inc.php');
+            $sForceRelocate = BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'auth/?client_id=' . bx_get('client_id') . '&response_type=' . bx_get('response_type') . '&state=' . bx_get('state') . '&redirect_uri=' . bx_get('redirect_uri');
+            bx_login_form(false, false, $sForceRelocate);
+            return;
+        }
+
+        if (empty($_POST)) {
+            $oPage = BxDolPage::getObjectInstanceByURI('oauth-authorization');
+            $this->_oTemplate->getPage(false, $oPage->getCode());
+            return;
+        }
+
+        $this->_oServer->handleAuthorizeRequest($oRequest, $oResponse, (bool)bx_get('profile_id'), bx_get('profile_id'));
+
+        $oResponse->send();
+    }
+
+    function serviceAuthorization ()
+    {
+        $sTitle = $this->_oDb->getClientTitle(bx_get('client_id'));
+        $this->_oTemplate->addCss('main.css');
+        return $this->_oTemplate->parseHtmlByName('page_auth.html', array(
+            'text' => _t('_bx_oauth_authorize_app', htmlspecialchars_adv($sTitle)),
+            'url' => BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'auth',
+            'client_id' => bx_get('client_id'),
+            'response_type' => bx_get('response_type'),
+            'redirect_uri' => bx_get('redirect_uri'),
+            'state' => bx_get('state'),
+            'profiles' => BxDolService::call('system', 'account_profile_switcher', array(getLoggedId(), false, "javascript: $('#bx-auth-profile-id').val('{profile_id}'); $('#bx-auth-form form').submit(); void(0);"), 'TemplServiceProfiles')['content'],
+        ));
+    }
+
+    function studioSettings ()
+    {
+        if (!isAdmin()) {
+            $this->_oTemplate->displayAccessDenied ();
+            return;
+        }
+
+        $oGrid = BxDolGrid::getObjectInstance('bx_oauth', BxDolStudioTemplate::getInstance());
+        if ($oGrid)
+            return $oGrid->getCode();
+
+        $this->_oTemplate->displayPageNotFound();
+    }
+}
+
+/** @} */

+ 21 - 0
modules/boonex/oauth2/classes/BxOAuthPage.php

@@ -0,0 +1,21 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxOAuthPage extends BxTemplPage
+{
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+    }
+
+}
+
+/** @} */

+ 45 - 0
modules/boonex/oauth2/classes/BxOAuthStudioPage.php

@@ -0,0 +1,45 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxOAuthStudioPage extends BxTemplStudioModule
+{
+    protected $oModule;
+
+    function __construct($sModule = "", $sPage = "")
+    {
+        parent::__construct($sModule, $sPage);
+
+        $this->oModule = BxDolModule::getInstance('bx_oauth');
+
+        $this->aMenuItems = array(
+            array('name' => 'settings', 'icon' => 'cogs', 'title' => '_adm_lmi_cpt_settings'),
+        );
+    }
+
+    function getSettings ()
+    {
+        return $this->oModule->studioSettings();
+    }
+
+    function getHelp ()
+    {
+        return _t('_bx_oauth_help_text', BX_DOL_URL_ROOT);
+    }
+
+    protected function getPageCaptionHelp()
+    {
+        $oTemplate = BxDolStudioTemplate::getInstance();
+        $sContent = '<a href="' . BX_DOL_URL_STUDIO . 'module.php?name=bx_oauth&page=help">' . _t('_bx_oauth_help') . "</a>";
+        return $oTemplate->parseHtmlByName('page_caption_help.html', array('content' => $sContent));
+    }
+}
+
+/** @} */

+ 20 - 0
modules/boonex/oauth2/classes/BxOAuthTemplate.php

@@ -0,0 +1,20 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxOAuthTemplate extends BxBaseModConnectTemplate
+{
+    function __construct($oConfig, $oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+    }
+}
+
+/** @} */

+ 68 - 0
modules/boonex/oauth2/install/config.php

@@ -0,0 +1,68 @@
+<?php
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+$aConfig = array(
+    /**
+     * Main Section.
+     */
+    'type' => BX_DOL_MODULE_TYPE_MODULE,
+    'name' => 'bx_oauth',
+    'title' => 'OAuth2 Server',
+    'note' => 'Allow to use site user credentials and basic API',
+    'version' => '8.0.0.DEV',
+    'vendor' => 'BoonEx',
+    'help_url' => 'http://feed.boonex.com/?section={module_name}',
+
+    'compatible_with' => array(
+        '8.0.x'
+    ),
+
+    /**
+     * 'home_dir' and 'home_uri' - should be unique. Don't use spaces in 'home_uri' and the other special chars.
+     */
+    'home_dir' => 'boonex/oauth2/',
+    'home_uri' => 'oauth2',
+
+    'db_prefix' => 'bx_oauth_',
+    'class_prefix' => 'BxOAuth',
+
+    /**
+     * Category for language keys.
+     */
+    'language_category' => 'OAuth2 Server',
+
+    /**
+     * Installation/Uninstallation Section.
+     */
+    'install' => array(
+        'execute_sql' => 1,
+        'update_languages' => 1,
+    ),
+    'uninstall' => array (
+        'execute_sql' => 1,
+        'update_languages' => 1,
+    ),
+    'enable' => array(
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'disable' => array (
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+
+    /**
+     * Dependencies Section
+     */
+    'dependencies' => array(),
+);
+
+/** @} */

+ 20 - 0
modules/boonex/oauth2/install/installer.php

@@ -0,0 +1,20 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxOAuthInstaller extends BxDolStudioInstaller
+{
+    function __construct($aConfig)
+    {
+        parent::__construct($aConfig);
+    }
+}
+
+/** @} */

+ 21 - 0
modules/boonex/oauth2/install/langs/en.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources name="en" flag="gb" title="English">
+	<string name="_sys_module_oauth2"><![CDATA[OAuth2 Server]]></string>
+	<string name="_bx_oauth_administration"><![CDATA[OAuth2 Server]]></string>
+	<string name="_bx_oauth"><![CDATA[OAuth2 Server]]></string>
+	<string name="_bx_oauth_help"><![CDATA[Information]]></string>
+	<string name="_bx_oauth_help_text"><![CDATA[Add clients to allow them to use profile information from this site on client's sites. <br />
+Client's sites must have "Dolphin Connect" module installed. <br />
+Specify the following "Dolphin Connect URL" on the client sites:<br />
+<strong>{0}</strong>]]></string>
+    <string name="_bx_oauth_client_url"><![CDATA[Client Site URL]]></string>
+	<string name="_bx_oauth_client_id"><![CDATA[Client Key]]></string>
+	<string name="_bx_oauth_client_secret"><![CDATA[Client Secret]]></string>
+	<string name="_bx_oauth_add"><![CDATA[Add new client]]></string>
+	<string name="_bx_oauth_clients"><![CDATA[OAuth2 clients]]></string>
+	<string name="_bx_oauth_authorization"><![CDATA[Authorization]]></string>
+	<string name="_bx_oauth_authorize_app"><![CDATA[Choose profile to use on "{0}" site:]]></string>
+
+	<string name="_bx_oauth_adm_stg_cpt_type"><![CDATA[OAuth2 Server]]></string>
+	<string name="_bx_oauth_adm_stg_cpt_category_general"><![CDATA[General]]></string>
+</resources>

+ 18 - 0
modules/boonex/oauth2/install/sql/disable.sql

@@ -0,0 +1,18 @@
+
+TRUNCATE TABLE `bx_oauth_scopes`;
+
+-- GRID
+
+DELETE FROM `sys_objects_grid` WHERE `object` = 'bx_oauth';
+DELETE FROM `sys_grid_fields` WHERE `object` = 'bx_oauth';
+DELETE FROM `sys_grid_actions` WHERE `object` = 'bx_oauth';
+
+-- SETTINGS
+
+DELETE FROM `top`, `toc`, `to` USING `sys_options_types` AS `top` LEFT JOIN `sys_options_categories` AS `toc` ON `top`.`id`=`toc`.`type_id` LEFT JOIN `sys_options` AS `to` ON `toc`.`id`=`to`.`category_id` WHERE `top`.`name` = 'bx_oauth';
+
+-- PAGES
+
+DELETE FROM `sys_objects_page` WHERE `module` = 'bx_oauth_authorization';
+DELETE FROM `sys_pages_blocks` WHERE `module` = 'bx_oauth_authorization';
+

+ 42 - 0
modules/boonex/oauth2/install/sql/enable.sql

@@ -0,0 +1,42 @@
+
+INSERT INTO `bx_oauth_scopes` (`scope`, `is_default`) VALUES
+('basic', 1),
+('service', 0);
+
+-- GRID
+
+INSERT INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `field_id`, `field_order`, `paginate_url`, `paginate_per_page`, `paginate_simple`, `paginate_get_start`, `paginate_get_per_page`, `filter_fields`, `filter_mode`, `sorting_fields`, `override_class_name`, `override_class_file`) VALUES
+('bx_oauth', 'Sql', 'SELECT * FROM `bx_oauth_clients`', 'bx_oauth_clients', 'id', '', '', 10, NULL, 'start', '', 'title,client_id,client_secret', 'auto', 'title,client_id,client_secret', 'BxOAuthGrid', 'modules/boonex/oauth2/classes/BxOAuthGrid.php');
+
+INSERT INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `params`, `order`) VALUES
+('bx_oauth', 'checkbox', 'Select', '2%', '', 10),
+('bx_oauth', 'title', '_Title', '28%', '', 20),
+('bx_oauth', 'client_id', '_bx_oauth_client_id', '35%', '', 30),
+('bx_oauth', 'client_secret', '_bx_oauth_client_secret', '35%', '', 40);
+
+INSERT INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `confirm`, `order`) VALUES
+('bx_oauth', 'bulk', 'delete', '_Delete', 1, 1),
+('bx_oauth', 'independent', 'add', '_bx_oauth_add', 0, 1);
+
+-- SETTINGS
+
+SET @iTypeOrder = (SELECT MAX(`order`) FROM `sys_options_types` WHERE `group` = 'modules');
+INSERT INTO `sys_options_types` (`group`, `name`, `caption`, `icon`, `order`) VALUES 
+('modules', 'bx_oauth', '_bx_oauth_adm_stg_cpt_type', 'bx_oauth@modules/boonex/oauth2/|std-mi.png', IF(NOT ISNULL(@iTypeOrder), @iTypeOrder + 1, 1));
+SET @iTypeId = LAST_INSERT_ID();
+
+INSERT INTO `sys_options_categories` (`type_id`, `name`, `caption`, `order` )  
+VALUES (@iTypeId, 'bx_oauth_general', '_bx_oauth_adm_stg_cpt_category_general', 1);
+SET @iCategId = LAST_INSERT_ID();
+
+-- INSERT INTO `sys_options` (`name`, `value`, `category_id`, `caption`, `type`, `check`, `check_error`, `order`, `extra`) VALUES
+-- ('bx_oauth2_on', '', @iCategId, 'Enable OAuth2 Server', 'checkbox', '', '', '10', '');
+
+-- Pages
+
+INSERT INTO `sys_objects_page`(`object`, `uri`, `title_system`, `title`, `module`, `layout_id`, `visible_for_levels`, `visible_for_levels_editable`, `url`, `meta_description`, `meta_keywords`, `meta_robots`, `cache_lifetime`, `cache_editable`, `deletable`, `override_class_name`, `override_class_file`) VALUES 
+('bx_oauth_authorization', 'oauth-authorization', '_bx_oauth_authorization', '_bx_oauth_authorization', 'bx_oauth', 5, 2147483647, 0, '', '', '', '', 0, 1, 0, 'BxOAuthPage', 'modules/boonex/oauth2/classes/BxOAuthPage.php');
+
+INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `active`, `order`) VALUES 
+('bx_oauth_authorization', 1, 'bx_oauth', '_bx_oauth_authorization', 11, 2147483647, 'service', 'a:2:{s:6:\"module\";s:8:\"bx_oauth\";s:6:\"method\";s:13:\"authorization\";}', 0, 0, 1, 1);
+

+ 60 - 0
modules/boonex/oauth2/install/sql/install.sql

@@ -0,0 +1,60 @@
+
+CREATE TABLE IF NOT EXISTS `bx_oauth_access_tokens` (
+  `access_token` varchar(40) NOT NULL,
+  `client_id` varchar(80) NOT NULL,
+  `user_id` int(10) unsigned DEFAULT NULL,
+  `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `scope` varchar(255) DEFAULT NULL,
+  PRIMARY KEY (`access_token`)
+);
+
+CREATE TABLE IF NOT EXISTS `bx_oauth_authorization_codes` (
+  `authorization_code` varchar(40) NOT NULL,
+  `client_id` varchar(80) NOT NULL,
+  `user_id` int(10) unsigned DEFAULT NULL,
+  `redirect_uri` varchar(255) DEFAULT NULL,
+  `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `scope` varchar(255) DEFAULT NULL,
+  PRIMARY KEY (`authorization_code`)
+);
+
+CREATE TABLE IF NOT EXISTS `bx_oauth_clients` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `title` varchar(255) NOT NULL,
+  `client_id` varchar(80) NOT NULL,
+  `client_secret` varchar(80) DEFAULT NULL,
+  `redirect_uri` varchar(255) NOT NULL,
+  `grant_types` varchar(80) DEFAULT NULL,
+  `scope` varchar(255) DEFAULT NULL,
+  `user_id` int(10) unsigned DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `client_id` (`client_id`)
+);
+
+CREATE TABLE IF NOT EXISTS `bx_oauth_refresh_tokens` (
+  `refresh_token` varchar(40) NOT NULL,
+  `client_id` varchar(80) NOT NULL,
+  `user_id` int(10) unsigned DEFAULT NULL,
+  `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `scope` varchar(255) DEFAULT NULL,
+  PRIMARY KEY (`refresh_token`)
+);
+
+CREATE TABLE IF NOT EXISTS `bx_oauth_scopes` (
+  `scope` varchar(255)  DEFAULT NULL,
+  `is_default` tinyint(1) DEFAULT NULL
+);
+
+-- Studio page and widget
+
+INSERT INTO `sys_std_pages`(`index`, `name`, `header`, `caption`, `icon`) VALUES
+(3, 'bx_oauth', '_bx_oauth', '_bx_oauth', 'bx_oauth@modules/boonex/oauth2/|std-pi.png');
+SET @iPageId = LAST_INSERT_ID();
+
+SET @iParentPageId = (SELECT `id` FROM `sys_std_pages` WHERE `name` = 'home');
+SET @iParentPageOrder = (SELECT MAX(`order`) FROM `sys_std_pages_widgets` WHERE `page_id` = @iParentPageId);
+INSERT INTO `sys_std_widgets` (`page_id`, `module`, `url`, `click`, `icon`, `caption`, `cnt_notices`, `cnt_actions`) VALUES
+(@iPageId, 'bx_oauth', '{url_studio}module.php?name=bx_oauth&page=settings', '', 'bx_oauth@modules/boonex/oauth2/|std-wi.png', '_bx_oauth', '', 'a:4:{s:6:"module";s:6:"system";s:6:"method";s:11:"get_actions";s:6:"params";a:0:{}s:5:"class";s:18:"TemplStudioModules";}');
+INSERT INTO `sys_std_pages_widgets` (`page_id`, `widget_id`, `order`) VALUES
+(@iParentPageId, LAST_INSERT_ID(), IF(ISNULL(@iParentPageOrder), 1, @iParentPageOrder + 1));
+

+ 9 - 0
modules/boonex/oauth2/install/sql/uninstall.sql

@@ -0,0 +1,9 @@
+
+DROP TABLE IF EXISTS `bx_oauth_access_tokens`, `bx_oauth_authorization_codes`, `bx_oauth_clients`, `bx_oauth_refresh_tokens`, `bx_oauth_scopes`;
+
+-- Studio page and widget
+
+DELETE FROM `tp`, `tw`, `tpw`
+USING `sys_std_pages` AS `tp`, `sys_std_widgets` AS `tw`, `sys_std_pages_widgets` AS `tpw`
+WHERE `tp`.`id` = `tw`.`page_id` AND `tw`.`id` = `tpw`.`widget_id` AND `tp`.`name` = 'bx_oauth';
+

+ 18 - 0
modules/boonex/oauth2/request.php

@@ -0,0 +1,18 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    OAuth2 OAuth2 server
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+require_once(BX_DIRECTORY_PATH_INC . 'profiles.inc.php');
+
+check_logged();
+
+BxDolRequest::processAsAction($GLOBALS['aModule'], $GLOBALS['aRequest']);
+
+/** @} */

+ 21 - 0
modules/boonex/oauth2/template/clients.html

@@ -0,0 +1,21 @@
+<form id="bx-oauth-form-add" name="bx-oauth-form-add" method="post">
+    <table class="bx-def-bc-margin-thd" width="100%" cellpadding="5" cellspacing="0">
+        <tr>
+            <th></th>
+            <th><bx_text:_Title /></th>
+            <th><bx_text:_bx_oauth_client_id /></th>
+            <th><bx_text:_bx_oauth_client_secret /></th>
+            <th><bx_text:_URL /></th>
+        </tr>
+        <bx_repeat:clients>
+            <tr>
+                <td><input type="checkbox" class="form_input_checkbox" id="bx-oauth-client-__client_id__" name="clients[]" value="__client_id__" /></td>
+                <td>__title__</td>
+                <td>__client_id__</td>
+                <td>__client_secret__</td>
+                <td><a target="_blank" href="__redirect_uri__">__redirect_uri__</a></td>
+            </tr>
+        </bx_repeat:clients>
+    </table>
+    __controls__
+</form>

+ 9 - 0
modules/boonex/oauth2/template/css/main.css

@@ -0,0 +1,9 @@
+#bx-auth-form h3,
+#bx-auth-form .bx-auth-form-buttons {
+    text-align:center;
+}
+
+.bx-auth-form-buttons .bx-btn {
+    float: none;
+    display: inline-block;
+}

BIN
modules/boonex/oauth2/template/images/icons/std-mi.png


BIN
modules/boonex/oauth2/template/images/icons/std-pi.png


BIN
modules/boonex/oauth2/template/images/icons/std-si.png


BIN
modules/boonex/oauth2/template/images/icons/std-wi.png


+ 16 - 0
modules/boonex/oauth2/template/page_auth.html

@@ -0,0 +1,16 @@
+<div id="bx-auth-form" class="controlsDiv">
+    <h3>__text__</h3>
+    <form method="post" action="__url__">
+        <input type="hidden" name="profile_id" id="bx-auth-profile-id" value="0" />
+        <input type="hidden" name="client_id" value="__client_id__" />
+        <input type="hidden" name="response_type" value="__response_type__" />
+        <input type="hidden" name="redirect_uri" value="__redirect_uri__" />
+        <input type="hidden" name="state" value="__state__" />
+        <div class="bx-auth-form-profiles">
+            __profiles__
+        </div>
+        <div class="bx-auth-form-buttons bx-def-margin-top">
+            <button class="bx-btn" onclick="$('#bx-auth-profile-id').val(0); $('#bx-auth-form form').submit(); return false;"><bx_text:_Cancel /></button>
+        </div>
+    </form>
+</div>

+ 1 - 1
modules/boonex/organizations/classes/BxOrgsModule.php

@@ -22,7 +22,7 @@ class BxOrgsModule extends BxBaseModProfileModule
     public function servicePrepareFields ($aFieldsProfile)
     {
         $aFieldsProfile['org_name'] = $aFieldsProfile['name'];
-        $aFieldsProfile['org_desc'] = $aFieldsProfile['description'];
+        $aFieldsProfile['org_desc'] = isset($aFieldsProfile['description']) ? $aFieldsProfile['description'] : '';
         unset($aFieldsProfile['name']);
         unset($aFieldsProfile['description']);
         return $aFieldsProfile;

+ 21 - 0
modules/boonex/russian/data/langs/bx_oauth/ru.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources name="ru" flag="ru" title="Russian">
+	<string name="_sys_module_oauth2"><![CDATA[OAuth2 сервер]]></string>
+	<string name="_bx_oauth_administration"><![CDATA[OAuth2 сервер]]></string>
+	<string name="_bx_oauth"><![CDATA[OAuth2 сервер]]></string>
+	<string name="_bx_oauth_help"><![CDATA[Информация]]></string>
+	<string name="_bx_oauth_help_text"><![CDATA[Что-бы использовать информацию из профайла на клиентских сайтах, нужно добавить новый клиентский сайт. <br />
+На клиентском сайте должен быть установлен "Dolphin Connect" модуль.<br />
+Нужно указать следующий "Dolphin Connect URL" на клиентских сайтах:<br />
+<strong>{0}</strong>]]></string>
+    <string name="_bx_oauth_client_url"><![CDATA[Ссылка на клиентский сайт]]></string>
+	<string name="_bx_oauth_client_id"><![CDATA[Ключ клиента]]></string>
+	<string name="_bx_oauth_client_secret"><![CDATA[Секретный код]]></string>
+	<string name="_bx_oauth_add"><![CDATA[Добавить]]></string>
+	<string name="_bx_oauth_clients"><![CDATA[Клиенты OAuth2]]></string>
+	<string name="_bx_oauth_authorization"><![CDATA[Authorization]]></string>
+	<string name="_bx_oauth_authorize_app"><![CDATA[Выбери профайл, для использования на "{0}" сайте:]]></string>
+
+	<string name="_bx_oauth_adm_stg_cpt_type"><![CDATA[OAuth2 сервер]]></string>
+	<string name="_bx_oauth_adm_stg_cpt_category_general"><![CDATA[Общие]]></string>
+</resources>

+ 37 - 0
modules/boonex/russian/data/langs/bx_tricon/ru.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources name="ru" flag="ru" title="Russian">
+	<string name="_sys_module_tricon"><![CDATA[Trident-соединение]]></string>
+	<string name="_bx_tricon"><![CDATA[Trident-соединение]]></string>
+	<string name="_bx_tricon_settings"><![CDATA[Trident-соединение]]></string>
+	<string name="_bx_tricon_information"><![CDATA[Trident-соединение, информация]]></string>
+	<string name="_bx_tricon_information_block"><![CDATA[
+Этот модуль может использоваться для захода на сайт используя другой Trident сайт (главный сайт). <br />
+Для этого нужно зайти на главный сайт, установить модуль "OAuth2 Server", добавить новый клиентский сайт, указав любой заголовок и следующий URL: <br />
+<strong>{0}</strong><br />
+Затем нужно ввести полученные ключ и секретный код в настройках этого модуля]]></string>
+	<string name="_bx_tricon_error_join_unavailable"><![CDATA[Извините, но в данный момент регистрация не доступна. Пожалуйста, попробуйте позже.]]></string>
+	<string name="_bx_tricon_profile_error_info"><![CDATA[Невозможно получить информацию о Вашем профиле]]></string>
+	<string name="_bx_tricon_profile_error_api_keys"><![CDATA[Пожалуйста, настройте модуль в административной панели (невозможно определить ключи)]]></string>
+	<string name="_bx_tricon_chart"><![CDATA[Trident профили]]></string>
+	<string name="_bx_tricon_auth_title"><![CDATA[Trident-соединение]]></string>
+	<string name="_bx_tricon_state_invalid"><![CDATA['State' параметр не совпадает]]></string>
+
+    <string name="_bx_tricon_adm_stg_cpt_type"><![CDATA[Trident-соединение]]></string>
+    <string name="_bx_tricon_adm_stg_cpt_category_general"><![CDATA[Общие]]></string>
+    <string name="_bx_tricon_option_app_id"><![CDATA[Trident App ID]]></string>
+    <string name="_bx_tricon_option_app_secret"><![CDATA[Trident App Secret]]></string>
+    <string name="_bx_tricon_option_app_url"><![CDATA[URL главного Trident сайта]]></string>
+    <string name="_bx_tricon_option_redirect"><![CDATA[Перенаправления после захода на сайт]]></string>
+    <string name="_bx_tricon_option_auto_friends"><![CDATA[Автоматически делать друзьями тех, что уже друг на другом Trident сайте]]></string>
+    <string name="_bx_tricon_option_module"><![CDATA[Модуль в котором создавать профили]]></string>
+    <string name="_bx_tricon_option_confirm_email"><![CDATA[Всегда подтверждать емейл, когда заходят через Trident-соединение]]></string>
+    <string name="_bx_tricon_option_approve"><![CDATA[Всегда одобрять профиль, когда заходят через Trident-соединение]]></string>
+    <string name="_bx_tricon_option_rewrite"><![CDATA[Использовать преобразованные ссылки на главном Trident сайта]]></string>
+
+	<string name="_bx_tricon_et_password_generated"><![CDATA[Trident-соединение: временный пароль]]></string>
+	<string name="_bx_tricon_et_password_generated_subject"><![CDATA[Временный пароль]]></string>
+	<string name="_bx_tricon_et_password_generated_body"><![CDATA[<bx_include_auto:_email_header.html />
+<p>Временный пароль - <b>{password}</b></p>
+<p>Его нужно изменить, после первого захода на сайт.</p>
+<bx_include_auto:_email_footer.html />]]></string>
+</resources>

+ 2 - 2
modules/boonex/russian/data/langs/system/ru.xml

@@ -1496,7 +1496,7 @@
 <p>Ваш профиль был проверен и активирован!</p>
 <p>Просто перейдите по ссылке внизу, чтобы воспользоваться нашими услугами:<br />
 <a href="{site_url}member.php">{site_url}member.php</a></p>
-<p>Ваш имейл, использованнй для регистрации: <span style="color:#FF6633">{account_email}</span></p>
+<p>Ваш имейл, использованный для регистрации: <span style="color:#FF6633">{account_email}</span></p>
 <p><b>Спасибо за использование нашего сервиса!</b></p>
 <bx_include_auto:_email_footer.html />]]></string>
 	<string name="_sys_et_txt_body_profile_change_status_pending"><![CDATA[<bx_include_auto:_email_header.html />
@@ -1887,7 +1887,7 @@
     <string name="_sys_on"><![CDATA[вкл.]]></string>
     <string name="_sys_off"><![CDATA[выкл.]]></string>
 	<string name="_sys_page_block_title_about"><![CDATA[Об...]]></string>
-	<string name="_sys_page_block_title_account_profile_switcher"><![CDATA[Использовать сайт для ...]]></string>
+	<string name="_sys_page_block_title_account_profile_switcher"><![CDATA[Использовать сайт как ...]]></string>
 	<string name="_sys_page_block_title_account_settings_delete"><![CDATA[Удалить Аккаунт]]></string>
 	<string name="_sys_page_block_title_account_settings_email"><![CDATA[Имейл - Настройки]]></string>
 	<string name="_sys_page_block_title_account_settings_info"><![CDATA[Редактировать Аккаунт]]></string>

+ 21 - 0
modules/boonex/trident_connect/classes/BxTriConAlerts.php

@@ -0,0 +1,21 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentConnect Trident Connect
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxTriConAlerts extends BxBaseModConnectAlerts
+{
+    function __construct()
+    {
+        parent::__construct();
+        $this -> oModule = BxDolModule::getInstance('bx_tricon');
+    }
+}
+
+/** @} */

+ 49 - 0
modules/boonex/trident_connect/classes/BxTriConConfig.php

@@ -0,0 +1,49 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentConnect Trident Connect
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxTriConConfig extends BxBaseModConnectConfig
+{
+    public $sApiID;
+    public $sApiSecret;
+    public $sApiUrl;
+
+    public $sPageStart;
+    public $sPageHandle;
+
+    public $sScope = 'basic';
+
+    function __construct($aModule)
+    {
+        parent::__construct($aModule);
+
+        $this -> sApiID = getParam('bx_tricon_api_key');
+        $this -> sApiSecret = getParam('bx_tricon_secret');
+        $this -> sApiUrl = trim(getParam('bx_tricon_url'), '/') . (getParam('bx_tricon_url_rewrite') ? '/m/oauth2/' : '/modules/?r=oauth2/');
+
+        $this -> sSessionUid = 'tricon_session';
+        $this -> sSessionProfile = 'tricon_session_profile';
+
+        $this -> sEmailTemplatePasswordGenerated = 'bx_tricon_password_generated';
+        $this -> sDefaultTitleLangKey = '_bx_tricon';
+
+        $this -> sRedirectPage = getParam('bx_tricon_redirect_page');
+        $this -> sProfilesModule = getParam('bx_tricon_module');
+        $this -> isAlwaysConfirmEmail = (bool)getParam('bx_tricon_confirm_email'); 
+        $this -> isAlwaysAutoApprove = (bool)getParam('bx_tricon_approve');
+
+        $this -> bAutoFriends = 'on' == getParam('bx_tricon_auto_friends');
+
+        $this -> sPageStart = BX_DOL_URL_ROOT . $this -> getBaseUri() . 'start';
+        $this -> sPageHandle = BX_DOL_URL_ROOT . $this -> getBaseUri() . 'handle';
+    }
+}
+
+/** @} */

+ 20 - 0
modules/boonex/trident_connect/classes/BxTriConDb.php

@@ -0,0 +1,20 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentConnect Trident Connect
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxTriConDb extends BxBaseModConnectDb
+{
+    function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+    }
+}
+
+/** @} */

+ 179 - 0
modules/boonex/trident_connect/classes/BxTriConModule.php

@@ -0,0 +1,179 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentConnect Trident Connect
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxTriConModule extends BxBaseModConnectModule
+{
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+    }
+
+    /**
+     * Generate admin page;
+     *
+     * @return : (text) - html presentation data;
+     */
+    function actionAdministration()
+    {
+        parent::_actionAdministration('bx_tricon_api_key', '_bx_tricon_settings', '_bx_tricon_information', '_bx_tricon_information_block');
+    }
+
+    /**
+     * Redirect to remote Trident site login form
+     *
+     * @return n/a/ - redirect or HTML page in case of error
+     */
+    function actionStart()
+    {
+        if (isLogged())
+            $this->_redirect ($this -> _oConfig -> sDefaultRedirectUrl);
+
+        if (!$this->_oConfig->sApiID || !$this->_oConfig->sApiSecret || !$this->_oConfig->sApiUrl) {
+            $sCode =  MsgBox( _t('_bx_tricon_profile_error_api_keys') );
+            $this->_oTemplate->getPage(_t('_bx_tricon'), $sCode);            
+        } 
+        else {
+
+            // define redirect URL to the remote Trident site                
+            $sUrl = bx_append_url_params($this->_oConfig->sApiUrl . 'auth', array(
+                'response_type' => 'code',
+                'client_id' => $this->_oConfig->sApiID,
+                'redirect_uri' => $this->_oConfig->sPageHandle,
+                'scope' => $this->_oConfig->sScope,
+                'state' => $this->_genCsrfToken(),
+            ));
+            $this->_redirect($sUrl);
+        }
+    }
+
+    function actionHandle()
+    {
+        require_once(BX_DIRECTORY_PATH_INC . 'design.inc.php');
+
+        // check CSRF token
+        if ($this->_getCsrfToken() != bx_get('state')) {
+            $this->_oTemplate->getPage(_t('_Error'), MsgBox(_t('_bx_tricon_state_invalid')));
+            return;
+        }
+
+        // check code
+        $sCode = bx_get('code');
+        if (!$sCode) {
+            $sErrorDescription = bx_get('error_description') ? bx_get('error_description') : _t('_error occured');
+            $this->_oTemplate->getPage(_t('_Error'), MsgBox($sErrorDescription));
+            return;
+        }
+
+        // make request for token
+        $s = bx_file_get_contents($this->_oConfig->sApiUrl . 'token', array(
+            'client_id'     => $this->_oConfig->sApiID,
+            'client_secret' => $this->_oConfig->sApiSecret,
+            'grant_type'    => 'authorization_code',
+            'code'          => $sCode,
+            'redirect_uri'  => $this->_oConfig->sPageHandle,
+        ), 'post');
+
+        // handle error
+        if (!$s || NULL === ($aResponse = json_decode($s, true)) || !isset($aResponse['access_token']) || isset($aResponse['error'])) {
+            $sErrorDescription = isset($aResponse['error_description']) ? $aResponse['error_description'] : _t('_error occured');
+            $this->_oTemplate->getPage(_t('_Error'), MsgBox($sErrorDescription));
+            return;
+        }
+
+        // get the data, especially access_token
+        $sAccessToken = $aResponse['access_token'];
+        $sExpiresIn = $aResponse['expires_in'];
+        $sExpiresAt = new \DateTime('+' . $sExpiresIn . ' seconds');
+        $sRefreshToken = $aResponse['refresh_token'];
+
+        // request info about profile
+        $s = bx_file_get_contents($this->_oConfig->sApiUrl . 'api/me', array(), 'get', array(
+            'Authorization: Bearer ' . $sAccessToken,
+        ));
+
+        // handle error
+        if (!$s || NULL === ($aResponse = json_decode($s, true)) || !$aResponse || isset($aResponse['error'])) {
+            $sErrorDescription = isset($aResponse['error_description']) ? $aResponse['error_description'] : _t('_error occured'); 
+            $this->_oTemplate->getPage(_t('_Error'), MsgBox($sErrorDescription));
+            return;
+        }
+
+        $aRemoteProfileInfo = $aResponse;
+
+        if ($aRemoteProfileInfo) {
+
+            // check if user logged in before
+            $iLocalProfileId = $this->_oDb->getProfileId($aRemoteProfileInfo['id']);
+            
+            if ($iLocalProfileId && $oProfile = BxDolProfile::getInstance($iLocalProfileId)) {
+                // user already exists
+                $this->setLogged($oProfile ->id());
+            }             
+            else {  
+                // register new user
+                $this->_createProfile($aRemoteProfileInfo);
+            }
+        } 
+        else {
+            $this->_oTemplate->getPage(_t('_Error'), MsgBox(_t('_bx_tricon_profile_error_info')));
+        }
+    }
+
+    protected function _redirect($sUrl, $iStatus = 302)
+    {
+        header("Location:{$sUrl}", true, $iStatus);
+        exit;
+    }
+
+    /**
+     * @param $aProfileInfo - remote profile info
+     * @param $sAlternativeName - suffix to add to NickName to make it unique
+     * @return profile array info, ready for the local database
+     */
+    protected function _convertRemoteFields($aProfileInfo, $sAlternativeName = '')
+    {
+        $aProfileFields = $aProfileInfo;
+
+        $aProfileFields['name'] = $aProfileInfo['profile_display_name'];
+        $aProfileFields['fullname'] = $aProfileInfo['profile_display_name'];
+
+        return $aProfileFields;
+    }
+
+    protected function _genCsrfToken($bReturn = false)
+    {
+        if (getParam('sys_security_form_token_enable') != 'on' || defined('BX_DOL_CRON_EXECUTE'))
+            return false;
+
+        $oSession = BxDolSession::getInstance();
+
+        $iCsrfTokenLifetime = (int)$this->_oDb->getParam('sys_security_form_token_lifetime');
+        if ($oSession->getValue('bx_tricon_csrf_token') === false || ($iCsrfTokenLifetime != 0 && time() - (int)$oSession->getValue('csrf_token_time') > $iCsrfTokenLifetime)) {
+            $sToken = genRndPwd(20, false);
+            $oSession->setValue('bx_tricon_csrf_token', $sToken);
+            $oSession->setValue('bx_tricon_csrf_token_time', time());
+        }
+        else {
+            $sToken = $oSession->getValue('bx_tricon_csrf_token');
+        }
+
+        return $sToken;
+    }
+
+    protected function _getCsrfToken()
+    {
+        $oSession = BxDolSession::getInstance();
+        return $oSession->getValue('bx_tricon_csrf_token');
+    }
+
+}
+
+/** @} */

+ 27 - 0
modules/boonex/trident_connect/classes/BxTriConStudioPage.php

@@ -0,0 +1,27 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentConnect Trident Connect
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxTriConStudioPage extends BxTemplStudioModule
+{
+    function getHelp ()
+    {
+        return _t('_bx_tricon_information_block', BX_DOL_URL_ROOT);
+    }
+
+    protected function getPageCaptionHelp()
+    {
+        $oTemplate = BxDolStudioTemplate::getInstance();
+        $sContent = '<a href="' . BX_DOL_URL_STUDIO . 'module.php?name=bx_tricon&page=help">' . _t('_bx_tricon_information') . "</a>";
+        return $oTemplate->parseHtmlByName('page_caption_help.html', array('content' => $sContent));
+    }
+}
+
+/** @} */

+ 20 - 0
modules/boonex/trident_connect/classes/BxTriConTemplate.php

@@ -0,0 +1,20 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentConnect Trident Connect
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxTriConTemplate extends BxBaseModConnectTemplate
+{
+    function __construct(&$oConfig, &$oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+    }
+}
+
+/** @} */

+ 68 - 0
modules/boonex/trident_connect/install/config.php

@@ -0,0 +1,68 @@
+<?php 
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentConnect Trident Connect
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+$aConfig = array(
+    /**
+     * Main Section.
+     */
+    'type' => BX_DOL_MODULE_TYPE_MODULE,
+    'name' => 'bx_tricon',
+    'title' => 'Trident connect',
+    'note' => 'Join the site using another Trident site account credentials.',
+    'version' => '8.0.0.DEV',
+    'vendor' => 'Boonex',
+    'help_url' => 'http://feed.boonex.com/?section={module_name}',
+
+    'compatible_with' => array(
+        '8.0.x'
+    ),
+
+    /**
+     * 'home_dir' and 'home_uri' - should be unique. Don't use spaces in 'home_uri' and the other special chars.
+     */
+    'home_dir' => 'boonex/trident_connect/',
+    'home_uri' => 'tricon',
+
+    'db_prefix' => 'bx_tricon_',
+    'class_prefix' => 'BxTriCon',
+
+    /**
+     * Category for language keys.
+     */
+    'language_category' => 'Trident Connect',
+
+    /**
+     * Installation/Uninstallation Section.
+     */
+    'install' => array(
+        'execute_sql' => 1,
+        'update_languages' => 1,
+    ),
+    'uninstall' => array (
+        'execute_sql' => 1,
+        'update_languages' => 1,
+    ),
+    'enable' => array(
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+    'disable' => array (
+        'execute_sql' => 1,
+        'clear_db_cache' => 1,
+    ),
+
+    /**
+     * Dependencies Section
+     */
+    'dependencies' => array(),
+);
+
+/** @} */

+ 20 - 0
modules/boonex/trident_connect/install/installer.php

@@ -0,0 +1,20 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentConnect Trident Connect
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+class BxTriConInstaller extends BxDolStudioInstaller
+{
+    function __construct($aConfig)
+    {
+        parent::__construct($aConfig);
+    }
+}
+
+/** @} */

+ 38 - 0
modules/boonex/trident_connect/install/langs/en.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources name="en" flag="gb" title="English">
+	<string name="_sys_module_tricon"><![CDATA[Trident Connect]]></string>
+	<string name="_bx_tricon"><![CDATA[Trident Connect]]></string>
+	<string name="_bx_tricon_settings"><![CDATA[Trident Connect]]></string>
+	<string name="_bx_tricon_information"><![CDATA[Trident connect information]]></string>
+	<string name="_bx_tricon_information_block"><![CDATA[
+This module can be used to login to this site using another Trident site (master site) credentials. <br />
+To make it work, go to the master site, install "OAuth2 Server" module and add new client site, specify the desired title and the following URL:<br />
+<strong>{0}</strong><br />
+Then enter generated on the master site the Key & Server pair in this module settings.]]></string>
+	<string name="_bx_tricon_error_join_unavailable"><![CDATA[Sorry but join is unavailable now. Please try again later.]]></string>
+	<string name="_bx_tricon_profile_error_info"><![CDATA[Can't define your profile information]]></string>
+	<string name="_bx_tricon_profile_error_api_keys"><![CDATA[Please configure the module in admin panel (can't define API key, secret & URL)]]></string>
+	<string name="_bx_tricon_chart"><![CDATA[Trident Profiles]]></string>
+	<string name="_bx_tricon_auth_title"><![CDATA[Trident Connect]]></string>
+	<string name="_bx_tricon_state_invalid"><![CDATA[State parameter isn't matching]]></string>
+
+    <string name="_bx_tricon_adm_stg_cpt_type"><![CDATA[Trident Connect]]></string>
+    <string name="_bx_tricon_adm_stg_cpt_category_general"><![CDATA[General]]></string>
+    <string name="_bx_tricon_option_app_id"><![CDATA[Trident App ID]]></string>
+    <string name="_bx_tricon_option_app_secret"><![CDATA[Trident App Secret]]></string>
+    <string name="_bx_tricon_option_app_url"><![CDATA[Main Trident Site URL]]></string>
+    <string name="_bx_tricon_option_redirect"><![CDATA[Redirect page after first sign in]]></string>
+    <string name="_bx_tricon_option_auto_friends"><![CDATA[Auto-friend members if they are already friends on other Trident site]]></string>
+    <string name="_bx_tricon_option_module"><![CDATA[Module to create profile in]]></string>
+    <string name="_bx_tricon_option_confirm_email"><![CDATA[Always confirm emails when joined using Trident Connect]]></string>
+    <string name="_bx_tricon_option_approve"><![CDATA[Always approve profiles when joined using Trident Connect]]></string>
+    <string name="_bx_tricon_option_rewrite"><![CDATA[Use rewrite URLs on main Trident site]]></string>
+
+	<string name="_bx_tricon_et_password_generated"><![CDATA[Trident connect temporary password]]></string>
+	<string name="_bx_tricon_et_password_generated_subject"><![CDATA[Temporary password]]></string>
+	<string name="_bx_tricon_et_password_generated_body"><![CDATA[<bx_include_auto:_email_header.html />
+<p>Temporary password - <b>{password}</b></p>
+<p>Change it after logging-in.</p>
+<bx_include_auto:_email_footer.html />]]></string>
+    
+</resources>

+ 25 - 0
modules/boonex/trident_connect/install/sql/disable.sql

@@ -0,0 +1,25 @@
+
+-- Email template
+
+DELETE FROM `sys_email_templates` WHERE `Name` = 'bx_tricon_password_generated';
+
+-- Auth objects
+
+DELETE FROM `sys_objects_auths` WHERE `Name` = 'bx_tricon';
+
+-- Alerts
+
+SET @iHandlerId := (SELECT `id` FROM `sys_alerts_handlers`  WHERE `name`  =  'bx_tricon');
+
+DELETE FROM `sys_alerts_handlers` WHERE `id` = @iHandlerId;
+DELETE FROM `sys_alerts` WHERE `handler_id` =  @iHandlerId;
+
+-- Options
+
+DELETE FROM `top`, `toc`, `to` USING `sys_options_types` AS `top` LEFT JOIN `sys_options_categories` AS `toc` ON `top`.`id`=`toc`.`type_id` LEFT JOIN `sys_options` AS `to` ON `toc`.`id`=`to`.`category_id` WHERE `top`.`name` = 'bx_tricon';
+
+-- Pages
+
+DELETE FROM `sys_objects_page` WHERE `module` = 'bx_tricon';
+DELETE FROM `sys_pages_blocks` WHERE `module` = 'bx_tricon';
+

+ 51 - 0
modules/boonex/trident_connect/install/sql/enable.sql

@@ -0,0 +1,51 @@
+
+-- Email template
+
+INSERT INTO `sys_email_templates` (`Module`, `NameSystem`, `Name`, `Subject`, `Body`) VALUES
+('bx_tricon', '_bx_tricon_et_password_generated', 'bx_tricon_password_generated', '_bx_tricon_et_password_generated_subject', '_bx_tricon_et_password_generated_body');
+
+-- Auth objects
+
+INSERT INTO `sys_objects_auths` (`Name`, `Title`, `Link`, `Icon`) VALUES
+('bx_tricon', '_bx_tricon_auth_title', 'modules/?r=tricon/start', 'sign-in');
+
+-- Alerts
+
+INSERT INTO `sys_alerts_handlers` SET `name` = 'bx_tricon', `class` = 'BxTriConAlerts', `file` = 'modules/boonex/dolphin_connect/classes/BxTriConAlerts.php';
+
+SET @iHandlerId := (SELECT `id` FROM `sys_alerts_handlers`  WHERE `name` = 'bx_tricon');
+
+INSERT INTO `sys_alerts` (`unit`, `action`, `handler_id`) VALUES
+('account', 'logout', @iHandlerId),
+('profile', 'delete', @iHandlerId),
+('profile', 'add', @iHandlerId);
+
+-- Options
+
+SET @iTypeOrder = (SELECT MAX(`order`) FROM `sys_options_types` WHERE `group` = 'modules');
+INSERT INTO `sys_options_types` (`group`, `name`, `caption`, `icon`, `order`) VALUES 
+('modules', 'bx_tricon', '_bx_tricon_adm_stg_cpt_type', 'bx_tricon@modules/boonex/tricon/|std-mi.png', IF(NOT ISNULL(@iTypeOrder), @iTypeOrder + 1, 1));
+SET @iTypeId = LAST_INSERT_ID();
+
+INSERT INTO `sys_options_categories` (`type_id`, `name`, `caption`, `order` )  
+VALUES (@iTypeId, 'bx_tricon_general', '_bx_tricon_adm_stg_cpt_category_general', 1);
+SET @iCategId = LAST_INSERT_ID();
+
+INSERT INTO `sys_options` (`name`, `value`, `category_id`, `caption`, `type`, `check`, `check_error`, `order`, `extra`) VALUES
+('bx_tricon_api_key', '', @iCategId, '_bx_tricon_option_app_id', 'digit', '', '', 10, ''),
+('bx_tricon_secret', '', @iCategId, '_bx_tricon_option_app_secret', 'digit', '', '', 20, ''),
+('bx_tricon_url', '', @iCategId, '_bx_tricon_option_app_url', 'digit', '', '', 30, ''),
+('bx_tricon_redirect_page', 'dashboard', @iCategId, '_bx_tricon_option_redirect', 'select', '', '', 40, 'join,settings,dashboard,index'),
+('bx_tricon_module', 'bx_persons', @iCategId, '_bx_tricon_option_module', 'select', '', '', 50, 'a:2:{s:6:"module";s:9:"bx_tricon";s:6:"method";s:20:"get_profiles_modules";}'),
+('bx_tricon_auto_friends', 'on', @iCategId, '_bx_tricon_option_auto_friends', 'checkbox', '', '', 60, ''),
+('bx_tricon_confirm_email', 'on', @iCategId, '_bx_tricon_option_confirm_email', 'checkbox', '', '', 70, ''),
+('bx_tricon_approve', '', @iCategId, '_bx_tricon_option_approve', 'checkbox', '', '', 80, ''),
+('bx_tricon_url_rewrite', 'on', @iCategId, '_bx_tricon_option_rewrite', 'checkbox', '', '', 90, '');
+
+-- Pages
+
+INSERT INTO `sys_objects_page`(`object`, `uri`, `title_system`, `title`, `module`, `layout_id`, `visible_for_levels`, `visible_for_levels_editable`, `url`, `meta_description`, `meta_keywords`, `meta_robots`, `cache_lifetime`, `cache_editable`, `deletable`, `override_class_name`, `override_class_file`) VALUES 
+('bx_tricon_error', 'tricon-error', '_bx_tricon_error', '_bx_tricon_error', 'bx_tricon', 5, 2147483647, 0, '', '', '', '', 0, 1, 0, 'BxTriConPage', 'modules/boonex/tricon/classes/BxTriConPage.php');
+
+INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `active`, `order`) VALUES 
+('bx_tricon_error', 1, 'bx_tricon', '_bx_tricon_error', 11, 2147483647, 'service', 'a:2:{s:6:\"module\";s:9:\"bx_tricon\";s:6:\"method\";s:10:\"last_error\";}', 0, 0, 1, 1);

+ 21 - 0
modules/boonex/trident_connect/install/sql/install.sql

@@ -0,0 +1,21 @@
+
+CREATE TABLE `bx_tricon_accounts` (
+  `local_profile` int(10) unsigned NOT NULL,
+  `remote_profile` int(10) unsigned NOT NULL,
+  PRIMARY KEY (`local_profile`),
+  KEY `remote_profile` (`remote_profile`)
+) ENGINE=MyISAM;
+
+-- Studio page and widget
+
+INSERT INTO `sys_std_pages`(`index`, `name`, `header`, `caption`, `icon`) VALUES
+(3, 'bx_tricon', '_bx_tricon', '_bx_tricon', 'bx_tricon@modules/boonex/trident_connect/|std-pi.png');
+SET @iPageId = LAST_INSERT_ID();
+
+SET @iParentPageId = (SELECT `id` FROM `sys_std_pages` WHERE `name` = 'home');
+SET @iParentPageOrder = (SELECT MAX(`order`) FROM `sys_std_pages_widgets` WHERE `page_id` = @iParentPageId);
+INSERT INTO `sys_std_widgets` (`page_id`, `module`, `url`, `click`, `icon`, `caption`, `cnt_notices`, `cnt_actions`) VALUES
+(@iPageId, 'bx_tricon', '{url_studio}module.php?name=bx_tricon', '', 'bx_tricon@modules/boonex/trident_connect/|std-wi.png', '_bx_tricon', '', 'a:4:{s:6:"module";s:6:"system";s:6:"method";s:11:"get_actions";s:6:"params";a:0:{}s:5:"class";s:18:"TemplStudioModules";}');
+INSERT INTO `sys_std_pages_widgets` (`page_id`, `widget_id`, `order`) VALUES
+(@iParentPageId, LAST_INSERT_ID(), IF(ISNULL(@iParentPageOrder), 1, @iParentPageOrder + 1));
+

+ 9 - 0
modules/boonex/trident_connect/install/sql/uninstall.sql

@@ -0,0 +1,9 @@
+
+DROP TABLE IF EXISTS `bx_tricon_accounts`;
+
+-- Studio page and widget
+
+DELETE FROM `tp`, `tw`, `tpw`
+USING `sys_std_pages` AS `tp`, `sys_std_widgets` AS `tw`, `sys_std_pages_widgets` AS `tpw`
+WHERE `tp`.`id` = `tw`.`page_id` AND `tw`.`id` = `tpw`.`widget_id` AND `tp`.`name` = 'bx_tricon';
+

+ 18 - 0
modules/boonex/trident_connect/request.php

@@ -0,0 +1,18 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentConnect Trident Connect
+ * @ingroup     TridentModules
+ *
+ * @{
+ */
+
+require_once(BX_DIRECTORY_PATH_INC . 'profiles.inc.php');
+
+check_logged();
+
+BxDolRequest::processAsAction($GLOBALS['aModule'], $GLOBALS['aRequest']);
+
+/** @} */

BIN
modules/boonex/trident_connect/template/images/icons/std-mi.png


BIN
modules/boonex/trident_connect/template/images/icons/std-pi.png


BIN
modules/boonex/trident_connect/template/images/icons/std-si.png


BIN
modules/boonex/trident_connect/template/images/icons/std-wi.png


+ 48 - 0
plugins/OAuth2/Autoloader.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace OAuth2;
+
+/**
+ * Autoloads OAuth2 classes
+ *
+ * @author    Brent Shaffer <bshafs at gmail dot com>
+ * @license   MIT License
+ */
+class Autoloader
+{
+    private $dir;
+
+    public function __construct($dir = null)
+    {
+        if (is_null($dir)) {
+            $dir = dirname(__FILE__).'/..';
+        }
+        $this->dir = $dir;
+    }
+    /**
+     * Registers OAuth2\Autoloader as an SPL autoloader.
+     */
+    public static function register($dir = null)
+    {
+        ini_set('unserialize_callback_func', 'spl_autoload_call');
+        spl_autoload_register(array(new self($dir), 'autoload'));
+    }
+
+    /**
+     * Handles autoloading of classes.
+     *
+     * @param string $class A class name.
+     *
+     * @return boolean Returns true if the class has been loaded
+     */
+    public function autoload($class)
+    {
+        if (0 !== strpos($class, 'OAuth2')) {
+            return;
+        }
+
+        if (file_exists($file = $this->dir.'/'.str_replace('\\', '/', $class).'.php')) {
+            require $file;
+        }
+    }
+}

+ 15 - 0
plugins/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace OAuth2\ClientAssertionType;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * Interface for all OAuth2 Client Assertion Types
+ */
+interface ClientAssertionTypeInterface
+{
+    public function validateRequest(RequestInterface $request, ResponseInterface $response);
+    public function getClientId();
+}

+ 123 - 0
plugins/OAuth2/ClientAssertionType/HttpBasic.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace OAuth2\ClientAssertionType;
+
+use OAuth2\Storage\ClientCredentialsInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * Validate a client via Http Basic authentication
+ *
+ * @author    Brent Shaffer <bshafs at gmail dot com>
+ */
+class HttpBasic implements ClientAssertionTypeInterface
+{
+    private $clientData;
+
+    protected $storage;
+    protected $config;
+
+    /**
+     * @param OAuth2\Storage\ClientCredentialsInterface $clientStorage REQUIRED Storage class for retrieving client credentials information
+     * @param array                                     $config        OPTIONAL Configuration options for the server
+     *                                                                 <code>
+     *                                                                 $config = array(
+     *                                                                 'allow_credentials_in_request_body' => true, // whether to look for credentials in the POST body in addition to the Authorize HTTP Header
+     *                                                                 'allow_public_clients'  => true              // if true, "public clients" (clients without a secret) may be authenticated
+     *                                                                 );
+     *                                                                 </code>
+     */
+    public function __construct(ClientCredentialsInterface $storage, array $config = array())
+    {
+        $this->storage = $storage;
+        $this->config = array_merge(array(
+            'allow_credentials_in_request_body' => true,
+            'allow_public_clients' => true,
+        ), $config);
+    }
+
+    public function validateRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if (!$clientData = $this->getClientCredentials($request, $response)) {
+            return false;
+        }
+
+        if (!isset($clientData['client_id'])) {
+            throw new \LogicException('the clientData array must have "client_id" set');
+        }
+
+        if (!isset($clientData['client_secret']) || $clientData['client_secret'] == '') {
+            if (!$this->config['allow_public_clients']) {
+                $response->setError(400, 'invalid_client', 'client credentials are required');
+
+                return false;
+            }
+
+            if (!$this->storage->isPublicClient($clientData['client_id'])) {
+                $response->setError(400, 'invalid_client', 'This client is invalid or must authenticate using a client secret');
+
+                return false;
+            }
+        } elseif ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) {
+            $response->setError(400, 'invalid_client', 'The client credentials are invalid');
+
+            return false;
+        }
+
+        $this->clientData = $clientData;
+
+        return true;
+    }
+
+    public function getClientId()
+    {
+        return $this->clientData['client_id'];
+    }
+
+    /**
+     * Internal function used to get the client credentials from HTTP basic
+     * auth or POST data.
+     *
+     * According to the spec (draft 20), the client_id can be provided in
+     * the Basic Authorization header (recommended) or via GET/POST.
+     *
+     * @return
+     * A list containing the client identifier and password, for example
+     * @code
+     * return array(
+     *     "client_id"     => CLIENT_ID,        // REQUIRED the client id
+     *     "client_secret" => CLIENT_SECRET,    // OPTIONAL the client secret (may be omitted for public clients)
+     * );
+     * @endcode
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-2.3.1
+     *
+     * @ingroup oauth2_section_2
+     */
+    public function getClientCredentials(RequestInterface $request, ResponseInterface $response = null)
+    {
+        if (!is_null($request->headers('PHP_AUTH_USER')) && !is_null($request->headers('PHP_AUTH_PW'))) {
+            return array('client_id' => $request->headers('PHP_AUTH_USER'), 'client_secret' => $request->headers('PHP_AUTH_PW'));
+        }
+
+        if ($this->config['allow_credentials_in_request_body']) {
+            // Using POST for HttpBasic authorization is not recommended, but is supported by specification
+            if (!is_null($request->request('client_id'))) {
+                /**
+                 * client_secret can be null if the client's password is an empty string
+                 * @see http://tools.ietf.org/html/rfc6749#section-2.3.1
+                 */
+
+                return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret'));
+            }
+        }
+
+        if ($response) {
+            $message = $this->config['allow_credentials_in_request_body'] ? ' or body' : '';
+            $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers'.$message);
+        }
+
+        return null;
+    }
+}

+ 383 - 0
plugins/OAuth2/Controller/AuthorizeController.php

@@ -0,0 +1,383 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\Storage\ClientInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use OAuth2\Scope;
+
+/**
+ * @see OAuth2\Controller\AuthorizeControllerInterface
+ */
+class AuthorizeController implements AuthorizeControllerInterface
+{
+    private $scope;
+    private $state;
+    private $client_id;
+    private $redirect_uri;
+    private $response_type;
+
+    protected $clientStorage;
+    protected $responseTypes;
+    protected $config;
+    protected $scopeUtil;
+
+    /**
+     * @param OAuth2\Storage\ClientInterface $clientStorage REQUIRED Instance of OAuth2\Storage\ClientInterface to retrieve client information
+     * @param array                          $responseTypes OPTIONAL Array of OAuth2\ResponseType\ResponseTypeInterface objects.  Valid array
+     *                                                      keys are "code" and "token"
+     * @param array                          $config        OPTIONAL Configuration options for the server
+     *                                                      <code>
+     *                                                      $config = array(
+     *                                                      'allow_implicit' => false,            // if the controller should allow the "implicit" grant type
+     *                                                      'enforce_state'  => true              // if the controller should require the "state" parameter
+     *                                                      'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter
+     *                                                      'redirect_status_code' => 302,        // HTTP status code to use for redirect responses
+     *                                                      );
+     *                                                      </code>
+     * @param OAuth2\ScopeInterface          $scopeUtil     OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope
+     */
+    public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ScopeInterface $scopeUtil = null)
+    {
+        $this->clientStorage = $clientStorage;
+        $this->responseTypes = $responseTypes;
+        $this->config = array_merge(array(
+            'allow_implicit' => false,
+            'enforce_state'  => true,
+            'require_exact_redirect_uri' => true,
+            'redirect_status_code' => 302,
+        ), $config);
+
+        if (is_null($scopeUtil)) {
+            $scopeUtil = new Scope();
+        }
+        $this->scopeUtil = $scopeUtil;
+    }
+
+    public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null)
+    {
+        if (!is_bool($is_authorized)) {
+            throw new \InvalidArgumentException('Argument "is_authorized" must be a boolean.  This method must know if the user has granted access to the client.');
+        }
+
+        // We repeat this, because we need to re-validate. The request could be POSTed
+        // by a 3rd-party (because we are not internally enforcing NONCEs, etc)
+        if (!$this->validateAuthorizeRequest($request, $response)) {
+            return;
+        }
+
+        // If no redirect_uri is passed in the request, use client's registered one
+        if (empty($this->redirect_uri)) {
+            $clientData              = $this->clientStorage->getClientDetails($this->client_id);
+            $registered_redirect_uri = $clientData['redirect_uri'];
+        }
+
+        // the user declined access to the client's application
+        if ($is_authorized === false) {
+            $redirect_uri = $this->redirect_uri ?: $registered_redirect_uri;
+            $this->setNotAuthorizedResponse($request, $response, $redirect_uri, $user_id);
+
+            return;
+        }
+
+        // build the parameters to set in the redirect URI
+        if (!$params = $this->buildAuthorizeParameters($request, $response, $user_id)) {
+            return;
+        }
+
+        $authResult = $this->responseTypes[$this->response_type]->getAuthorizeResponse($params, $user_id);
+
+        list($redirect_uri, $uri_params) = $authResult;
+
+        if (empty($redirect_uri) && !empty($registered_redirect_uri)) {
+            $redirect_uri = $registered_redirect_uri;
+        }
+
+        $uri = $this->buildUri($redirect_uri, $uri_params);
+
+        // return redirect response
+        $response->setRedirect($this->config['redirect_status_code'], $uri);
+    }
+
+    protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null)
+    {
+        $error = 'access_denied';
+        $error_message = 'The user denied access to your application';
+        $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->state, $error, $error_message);
+    }
+
+    /*
+     * We have made this protected so this class can be extended to add/modify
+     * these parameters
+     */
+    protected function buildAuthorizeParameters($request, $response, $user_id)
+    {
+        // @TODO: we should be explicit with this in the future
+        $params = array(
+            'scope'         => $this->scope,
+            'state'         => $this->state,
+            'client_id'     => $this->client_id,
+            'redirect_uri'  => $this->redirect_uri,
+            'response_type' => $this->response_type,
+        );
+
+        return $params;
+    }
+
+    public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        // Make sure a valid client id was supplied (we can not redirect because we were unable to verify the URI)
+        if (!$client_id = $request->query('client_id', $request->request('client_id'))) {
+            // We don't have a good URI to use
+            $response->setError(400, 'invalid_client', "No client id supplied");
+
+            return false;
+        }
+
+        // Get client details
+        if (!$clientData = $this->clientStorage->getClientDetails($client_id)) {
+            $response->setError(400, 'invalid_client', 'The client id supplied is invalid');
+
+            return false;
+        }
+
+        $registered_redirect_uri = isset($clientData['redirect_uri']) ? $clientData['redirect_uri'] : '';
+
+        // Make sure a valid redirect_uri was supplied. If specified, it must match the clientData URI.
+        // @see http://tools.ietf.org/html/rfc6749#section-3.1.2
+        // @see http://tools.ietf.org/html/rfc6749#section-4.1.2.1
+        // @see http://tools.ietf.org/html/rfc6749#section-4.2.2.1
+        if ($supplied_redirect_uri = $request->query('redirect_uri', $request->request('redirect_uri'))) {
+            // validate there is no fragment supplied
+            $parts = parse_url($supplied_redirect_uri);
+            if (isset($parts['fragment']) && $parts['fragment']) {
+                $response->setError(400, 'invalid_uri', 'The redirect URI must not contain a fragment');
+
+                return false;
+            }
+
+            // validate against the registered redirect uri(s) if available
+            if ($registered_redirect_uri && !$this->validateRedirectUri($supplied_redirect_uri, $registered_redirect_uri)) {
+                $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI provided is missing or does not match', '#section-3.1.2');
+
+                return false;
+            }
+            $redirect_uri = $supplied_redirect_uri;
+        } else {
+            // use the registered redirect_uri if none has been supplied, if possible
+            if (!$registered_redirect_uri) {
+                $response->setError(400, 'invalid_uri', 'No redirect URI was supplied or stored');
+
+                return false;
+            }
+
+            if (count(explode(' ', $registered_redirect_uri)) > 1) {
+                $response->setError(400, 'invalid_uri', 'A redirect URI must be supplied when multiple redirect URIs are registered', '#section-3.1.2.3');
+
+                return false;
+            }
+            $redirect_uri = $registered_redirect_uri;
+        }
+
+        // Select the redirect URI
+        $response_type = $request->query('response_type', $request->request('response_type'));
+
+        // for multiple-valued response types - make them alphabetical
+        if (false !== strpos($response_type, ' ')) {
+            $types = explode(' ', $response_type);
+            sort($types);
+            $response_type = ltrim(implode(' ', $types));
+        }
+
+        $state = $request->query('state', $request->request('state'));
+
+        // type and client_id are required
+        if (!$response_type || !in_array($response_type, $this->getValidResponseTypes())) {
+            $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_request', 'Invalid or missing response type', null);
+
+            return false;
+        }
+
+        if ($response_type == self::RESPONSE_TYPE_AUTHORIZATION_CODE) {
+            if (!isset($this->responseTypes['code'])) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'authorization code grant type not supported', null);
+
+                return false;
+            }
+            if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'authorization_code')) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null);
+
+                return false;
+            }
+            if ($this->responseTypes['code']->enforceRedirect() && !$redirect_uri) {
+                $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI is mandatory and was not supplied');
+
+                return false;
+            }
+        } else {
+            if (!$this->config['allow_implicit']) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'implicit grant type not supported', null);
+
+                return false;
+            }
+            if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'implicit')) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null);
+
+                return false;
+            }
+        }
+
+        // validate requested scope if it exists
+        $requestedScope = $this->scopeUtil->getScopeFromRequest($request);
+
+        if ($requestedScope) {
+            // restrict scope by client specific scope if applicable,
+            // otherwise verify the scope exists
+            $clientScope = $this->clientStorage->getClientScope($client_id);
+            if ((is_null($clientScope) && !$this->scopeUtil->scopeExists($requestedScope))
+                || ($clientScope && !$this->scopeUtil->checkScope($requestedScope, $clientScope))) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_scope', 'An unsupported scope was requested', null);
+
+                return false;
+            }
+        } else {
+            // use a globally-defined default scope
+            $defaultScope = $this->scopeUtil->getDefaultScope($client_id);
+
+            if (false === $defaultScope) {
+                $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_client', 'This application requires you specify a scope parameter', null);
+
+                return false;
+            }
+
+            $requestedScope = $defaultScope;
+        }
+
+        // Validate state parameter exists (if configured to enforce this)
+        if ($this->config['enforce_state'] && !$state) {
+            $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, null, 'invalid_request', 'The state parameter is required');
+
+            return false;
+        }
+
+        // save the input data and return true
+        $this->scope         = $requestedScope;
+        $this->state         = $state;
+        $this->client_id     = $client_id;
+        // Only save the SUPPLIED redirect URI (@see http://tools.ietf.org/html/rfc6749#section-4.1.3)
+        $this->redirect_uri  = $supplied_redirect_uri;
+        $this->response_type = $response_type;
+
+        return true;
+    }
+
+    /**
+     * 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_4
+     */
+    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"]) && !empty($parse_url['query'])) ? "?" . $parse_url["query"] : "")
+            . ((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : "")
+        ;
+    }
+
+    protected function getValidResponseTypes()
+    {
+        return array(
+            self::RESPONSE_TYPE_ACCESS_TOKEN,
+            self::RESPONSE_TYPE_AUTHORIZATION_CODE,
+        );
+    }
+
+    /**
+     * Internal method for validating redirect URI supplied
+     *
+     * @param string $inputUri            The submitted URI to be validated
+     * @param string $registeredUriString The allowed URI(s) to validate against.  Can be a space-delimited string of URIs to
+     *                                    allow for multiple URIs
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-3.1.2
+     */
+    protected function validateRedirectUri($inputUri, $registeredUriString)
+    {
+        if (!$inputUri || !$registeredUriString) {
+            return false; // if either one is missing, assume INVALID
+        }
+
+        $registered_uris = preg_split('/\s+/', $registeredUriString);
+        foreach ($registered_uris as $registered_uri) {
+            if ($this->config['require_exact_redirect_uri']) {
+                // the input uri is validated against the registered uri using exact match
+                if (strcmp($inputUri, $registered_uri) === 0) {
+                    return true;
+                }
+            } else {
+                // the input uri is validated against the registered uri using case-insensitive match of the initial string
+                // i.e. additional query parameters may be applied
+                if (strcasecmp(substr($inputUri, 0, strlen($registered_uri)), $registered_uri) === 0) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Convenience methods to access the parameters derived from the validated request
+     */
+
+    public function getScope()
+    {
+        return $this->scope;
+    }
+
+    public function getState()
+    {
+        return $this->state;
+    }
+
+    public function getClientId()
+    {
+        return $this->client_id;
+    }
+
+    public function getRedirectUri()
+    {
+        return $this->redirect_uri;
+    }
+
+    public function getResponseType()
+    {
+        return $this->response_type;
+    }
+}

+ 43 - 0
plugins/OAuth2/Controller/AuthorizeControllerInterface.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *  This controller is called when a user should be authorized
+ *  by an authorization server.  As OAuth2 does not handle
+ *  authorization directly, this controller ensures the request is valid, but
+ *  requires the application to determine the value of $is_authorized
+ *
+ *  ex:
+ *  > $user_id = $this->somehowDetermineUserId();
+ *  > $is_authorized = $this->somehowDetermineUserAuthorization();
+ *  > $response = new OAuth2\Response();
+ *  > $authorizeController->handleAuthorizeRequest(
+ *  >     OAuth2\Request::createFromGlobals(),
+ *  >     $response,
+ *  >     $is_authorized,
+ *  >     $user_id);
+ *  > $response->send();
+ *
+ */
+interface AuthorizeControllerInterface
+{
+    /**
+     * List of possible authentication response types.
+     * The "authorization_code" mechanism exclusively supports 'code'
+     * and the "implicit" mechanism exclusively supports 'token'.
+     *
+     * @var string
+     * @see http://tools.ietf.org/html/rfc6749#section-4.1.1
+     * @see http://tools.ietf.org/html/rfc6749#section-4.2.1
+     */
+    const RESPONSE_TYPE_AUTHORIZATION_CODE = 'code';
+    const RESPONSE_TYPE_ACCESS_TOKEN = 'token';
+
+    public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null);
+
+    public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response);
+}

+ 111 - 0
plugins/OAuth2/Controller/ResourceController.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\TokenType\TokenTypeInterface;
+use OAuth2\Storage\AccessTokenInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use OAuth2\Scope;
+
+/**
+ * @see OAuth2\Controller\ResourceControllerInterface
+ */
+class ResourceController implements ResourceControllerInterface
+{
+    private $token;
+
+    protected $tokenType;
+    protected $tokenStorage;
+    protected $config;
+    protected $scopeUtil;
+
+    public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, $config = array(), ScopeInterface $scopeUtil = null)
+    {
+        $this->tokenType = $tokenType;
+        $this->tokenStorage = $tokenStorage;
+
+        $this->config = array_merge(array(
+            'www_realm' => 'Service',
+        ), $config);
+
+        if (is_null($scopeUtil)) {
+            $scopeUtil = new Scope();
+        }
+        $this->scopeUtil = $scopeUtil;
+    }
+
+    public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null)
+    {
+        $token = $this->getAccessTokenData($request, $response);
+
+        // Check if we have token data
+        if (is_null($token)) {
+            return false;
+        }
+
+        /**
+         * Check scope, if provided
+         * If token doesn't have a scope, it's null/empty, or it's insufficient, then throw 403
+         * @see http://tools.ietf.org/html/rfc6750#section-3.1
+         */
+        if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->scopeUtil->checkScope($scope, $token["scope"]))) {
+            $response->setError(403, 'insufficient_scope', 'The request requires higher privileges than provided by the access token');
+            $response->addHttpHeaders(array(
+                'WWW-Authenticate' => sprintf('%s realm="%s", scope="%s", error="%s", error_description="%s"',
+                    $this->tokenType->getTokenType(),
+                    $this->config['www_realm'],
+                    $scope,
+                    $response->getParameter('error'),
+                    $response->getParameter('error_description')
+                )
+            ));
+
+            return false;
+        }
+
+        // allow retrieval of the token
+        $this->token = $token;
+
+        return (bool) $token;
+    }
+
+    public function getAccessTokenData(RequestInterface $request, ResponseInterface $response)
+    {
+        // Get the token parameter
+        if ($token_param = $this->tokenType->getAccessTokenParameter($request, $response)) {
+            // Get the stored token data (from the implementing subclass)
+            // Check we have a well formed token
+            // Check token expiration (expires is a mandatory paramter)
+            if (!$token = $this->tokenStorage->getAccessToken($token_param)) {
+                $response->setError(401, 'invalid_token', 'The access token provided is invalid');
+            } elseif (!isset($token["expires"]) || !isset($token["client_id"])) {
+                $response->setError(401, 'malformed_token', 'Malformed token (missing "expires")');
+            } elseif (time() > $token["expires"]) {
+                $response->setError(401, 'expired_token', 'The access token provided has expired');
+            } else {
+                return $token;
+            }
+        }
+
+        $authHeader = sprintf('%s realm="%s"', $this->tokenType->getTokenType(), $this->config['www_realm']);
+
+        if ($error = $response->getParameter('error')) {
+            $authHeader = sprintf('%s, error="%s"', $authHeader, $error);
+            if ($error_description = $response->getParameter('error_description')) {
+                $authHeader = sprintf('%s, error_description="%s"', $authHeader, $error_description);
+            }
+        }
+
+        $response->addHttpHeaders(array('WWW-Authenticate' => $authHeader));
+
+        return null;
+    }
+
+    // convenience method to allow retrieval of the token
+    public function getToken()
+    {
+        return $this->token;
+    }
+}

+ 26 - 0
plugins/OAuth2/Controller/ResourceControllerInterface.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *  This controller is called when a "resource" is requested.
+ *  call verifyResourceRequest in order to determine if the request
+ *  contains a valid token.
+ *
+ *  ex:
+ *  > if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) {
+ *  >     $response->send(); // authorization failed
+ *  >     die();
+ *  > }
+ *  > return json_encode($resource); // valid token!  Send the stuff!
+ *
+ */
+interface ResourceControllerInterface
+{
+    public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null);
+
+    public function getAccessTokenData(RequestInterface $request, ResponseInterface $response);
+}

+ 274 - 0
plugins/OAuth2/Controller/TokenController.php

@@ -0,0 +1,274 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\ClientAssertionType\ClientAssertionTypeInterface;
+use OAuth2\GrantType\GrantTypeInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\Scope;
+use OAuth2\Storage\ClientInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * @see OAuth2\Controller\TokenControllerInterface
+ */
+class TokenController implements TokenControllerInterface
+{
+    protected $accessToken;
+    protected $grantTypes;
+    protected $clientAssertionType;
+    protected $scopeUtil;
+    protected $clientStorage;
+
+    public function __construct(AccessTokenInterface $accessToken, ClientInterface $clientStorage, array $grantTypes = array(), ClientAssertionTypeInterface $clientAssertionType = null, ScopeInterface $scopeUtil = null)
+    {
+        if (is_null($clientAssertionType)) {
+            foreach ($grantTypes as $grantType) {
+                if (!$grantType instanceof ClientAssertionTypeInterface) {
+                    throw new \InvalidArgumentException('You must supply an instance of OAuth2\ClientAssertionType\ClientAssertionTypeInterface or only use grant types which implement OAuth2\ClientAssertionType\ClientAssertionTypeInterface');
+                }
+            }
+        }
+        $this->clientAssertionType = $clientAssertionType;
+        $this->accessToken = $accessToken;
+        $this->clientStorage = $clientStorage;
+        foreach ($grantTypes as $grantType) {
+            $this->addGrantType($grantType);
+        }
+
+        if (is_null($scopeUtil)) {
+            $scopeUtil = new Scope();
+        }
+        $this->scopeUtil = $scopeUtil;
+    }
+
+    public function handleTokenRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if ($token = $this->grantAccessToken($request, $response)) {
+            // @see http://tools.ietf.org/html/rfc6749#section-5.1
+            // server MUST disable caching in headers when tokens are involved
+            $response->setStatusCode(200);
+            $response->addParameters($token);
+            $response->addHttpHeaders(array('Cache-Control' => 'no-store', 'Pragma' => 'no-cache'));
+        }
+    }
+
+    /**
+     * Grant or deny a requested access token.
+     * This would be called from the "/token" endpoint as defined in the spec.
+     * You can call your endpoint whatever you want.
+     *
+     * @param $request - RequestInterface
+     * Request object to grant access token
+     *
+     * @throws InvalidArgumentException
+     * @throws LogicException
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4
+     * @see http://tools.ietf.org/html/rfc6749#section-10.6
+     * @see http://tools.ietf.org/html/rfc6749#section-4.1.3
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function grantAccessToken(RequestInterface $request, ResponseInterface $response)
+    {
+        if (strtolower($request->server('REQUEST_METHOD')) != 'post') {
+            $response->setError(405, 'invalid_request', 'The request method must be POST when requesting an access token', '#section-3.2');
+            $response->addHttpHeaders(array('Allow' => 'POST'));
+
+            return null;
+        }
+
+        /**
+         * Determine grant type from request
+         * and validate the request for that grant type
+         */
+        if (!$grantTypeIdentifier = $request->request('grant_type')) {
+            $response->setError(400, 'invalid_request', 'The grant type was not specified in the request');
+
+            return null;
+        }
+
+        if (!isset($this->grantTypes[$grantTypeIdentifier])) {
+            /* TODO: If this is an OAuth2 supported grant type that we have chosen not to implement, throw a 501 Not Implemented instead */
+            $response->setError(400, 'unsupported_grant_type', sprintf('Grant type "%s" not supported', $grantTypeIdentifier));
+
+            return null;
+        }
+
+        $grantType = $this->grantTypes[$grantTypeIdentifier];
+
+        /**
+         * Retrieve the client information from the request
+         * ClientAssertionTypes allow for grant types which also assert the client data
+         * in which case ClientAssertion is handled in the validateRequest method
+         *
+         * @see OAuth2\GrantType\JWTBearer
+         * @see OAuth2\GrantType\ClientCredentials
+         */
+        if (!$grantType instanceof ClientAssertionTypeInterface) {
+            if (!$this->clientAssertionType->validateRequest($request, $response)) {
+                return null;
+            }
+            $clientId = $this->clientAssertionType->getClientId();
+        }
+
+        /**
+         * Retrieve the grant type information from the request
+         * The GrantTypeInterface object handles all validation
+         * If the object is an instance of ClientAssertionTypeInterface,
+         * That logic is handled here as well
+         */
+        if (!$grantType->validateRequest($request, $response)) {
+            return null;
+        }
+
+        if ($grantType instanceof ClientAssertionTypeInterface) {
+            $clientId = $grantType->getClientId();
+        } else {
+            // validate the Client ID (if applicable)
+            if (!is_null($storedClientId = $grantType->getClientId()) && $storedClientId != $clientId) {
+                $response->setError(400, 'invalid_grant', sprintf('%s doesn\'t exist or is invalid for the client', $grantTypeIdentifier));
+
+                return null;
+            }
+        }
+
+        /**
+         * Validate the client can use the requested grant type
+         */
+        if (!$this->clientStorage->checkRestrictedGrantType($clientId, $grantTypeIdentifier)) {
+            $response->setError(400, 'unauthorized_client', 'The grant type is unauthorized for this client_id');
+
+            return false;
+        }
+
+        /**
+         * Validate the scope of the token
+         *
+         * requestedScope - the scope specified in the token request
+         * availableScope - the scope associated with the grant type
+         *  ex: in the case of the "Authorization Code" grant type,
+         *  the scope is specified in the authorize request
+         *
+         * @see http://tools.ietf.org/html/rfc6749#section-3.3
+         */
+
+        $requestedScope = $this->scopeUtil->getScopeFromRequest($request);
+        $availableScope = $grantType->getScope();
+
+        if ($requestedScope) {
+            // validate the requested scope
+            if ($availableScope) {
+                if (!$this->scopeUtil->checkScope($requestedScope, $availableScope)) {
+                    $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this request');
+
+                    return null;
+                }
+            } else {
+                // validate the client has access to this scope
+                if ($clientScope = $this->clientStorage->getClientScope($clientId)) {
+                    if (!$this->scopeUtil->checkScope($requestedScope, $clientScope)) {
+                        $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this client');
+
+                        return false;
+                    }
+                } elseif (!$this->scopeUtil->scopeExists($requestedScope)) {
+                    $response->setError(400, 'invalid_scope', 'An unsupported scope was requested');
+
+                    return null;
+                }
+            }
+        } elseif ($availableScope) {
+            // use the scope associated with this grant type
+            $requestedScope = $availableScope;
+        } else {
+            // use a globally-defined default scope
+            $defaultScope = $this->scopeUtil->getDefaultScope($clientId);
+
+            // "false" means default scopes are not allowed
+            if (false === $defaultScope) {
+                $response->setError(400, 'invalid_scope', 'This application requires you specify a scope parameter');
+
+                return null;
+            }
+
+            $requestedScope = $defaultScope;
+        }
+
+        return $grantType->createAccessToken($this->accessToken, $clientId, $grantType->getUserId(), $requestedScope);
+    }
+
+    /**
+     * addGrantType
+     *
+     * @param grantType - OAuth2\GrantTypeInterface
+     * the grant type to add for the specified identifier
+     * @param identifier - string
+     * a string passed in as "grant_type" in the response that will call this grantType
+     */
+    public function addGrantType(GrantTypeInterface $grantType, $identifier = null)
+    {
+        if (is_null($identifier) || is_numeric($identifier)) {
+            $identifier = $grantType->getQuerystringIdentifier();
+        }
+
+        $this->grantTypes[$identifier] = $grantType;
+    }
+
+    public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if ($this->revokeToken($request, $response)) {
+            $response->setStatusCode(200);
+            $response->addParameters(array('revoked' => true));
+        }
+    }
+
+    /**
+     * Revoke a refresh or access token. Returns true on success and when tokens are invalid
+     *
+     * Note: invalid tokens do not cause an error response since the client
+     * cannot handle such an error in a reasonable way.  Moreover, the
+     * purpose of the revocation request, invalidating the particular token,
+     * is already achieved.
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return bool|null
+     */
+    public function revokeToken(RequestInterface $request, ResponseInterface $response)
+    {
+        if (strtolower($request->server('REQUEST_METHOD')) != 'post') {
+            $response->setError(405, 'invalid_request', 'The request method must be POST when revoking an access token', '#section-3.2');
+            $response->addHttpHeaders(array('Allow' => 'POST'));
+
+            return null;
+        }
+
+        $token_type_hint = $request->request('token_type_hint');
+        if (!in_array($token_type_hint, array(null, 'access_token', 'refresh_token'), true)) {
+            $response->setError(400, 'invalid_request', 'Token type hint must be either \'access_token\' or \'refresh_token\'');
+
+            return null;
+        }
+
+        $token = $request->request('token');
+        if ($token === null) {
+            $response->setError(400, 'invalid_request', 'Missing token parameter to revoke');
+
+            return null;
+        }
+
+        // @todo remove this check for v2.0
+        if (!method_exists($this->accessToken, 'revokeToken')) {
+            $class = get_class($this->accessToken);
+            throw new \RuntimeException("AccessToken {$class} does not implement required revokeToken method");
+        }
+
+        $this->accessToken->revokeToken($token, $token_type_hint);
+
+        return true;
+    }
+}

+ 32 - 0
plugins/OAuth2/Controller/TokenControllerInterface.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *  This controller is called when a token is being requested.
+ *  it is called to handle all grant types the application supports.
+ *  It also validates the client's credentials
+ *
+ *  ex:
+ *  > $tokenController->handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response());
+ *  > $response->send();
+ *
+ */
+interface TokenControllerInterface
+{
+    /**
+     * handleTokenRequest
+     *
+     * @param $request
+     * OAuth2\RequestInterface - The current http request
+     * @param $response
+     * OAuth2\ResponseInterface - An instance of OAuth2\ResponseInterface to contain the response data
+     *
+     */
+    public function handleTokenRequest(RequestInterface $request, ResponseInterface $response);
+
+    public function grantAccessToken(RequestInterface $request, ResponseInterface $response);
+}

+ 11 - 0
plugins/OAuth2/Encryption/EncryptionInterface.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace OAuth2\Encryption;
+
+interface EncryptionInterface
+{
+    public function encode($payload, $key, $algorithm = null);
+    public function decode($payload, $key, $algorithm = null);
+    public function urlSafeB64Encode($data);
+    public function urlSafeB64Decode($b64);
+}

+ 47 - 0
plugins/OAuth2/Encryption/FirebaseJwt.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace OAuth2\Encryption;
+
+/**
+ * Bridge file to use the firebase/php-jwt package for JWT encoding and decoding.
+ * @author Francis Chuang <francis.chuang@gmail.com>
+ */
+class FirebaseJwt implements EncryptionInterface
+{
+    public function __construct()
+    {
+        if (!class_exists('\JWT')) {
+            throw new \ErrorException('firebase/php-jwt must be installed to use this feature. You can do this by running "composer require firebase/php-jwt"');
+        }
+    }
+
+    public function encode($payload, $key, $alg = 'HS256', $keyId = null)
+    {
+        return \JWT::encode($payload, $key, $alg, $keyId);
+    }
+
+    public function decode($jwt, $key = null, $allowedAlgorithms = null)
+    {
+        try {
+
+            //Maintain BC: Do not verify if no algorithms are passed in.
+            if (!$allowedAlgorithms) {
+                $key = null;
+            }
+
+            return (array)\JWT::decode($jwt, $key, $allowedAlgorithms);
+        } catch (\Exception $e) {
+            return false;
+        }
+    }
+
+    public function urlSafeB64Encode($data)
+    {
+        return \JWT::urlsafeB64Encode($data);
+    }
+
+    public function urlSafeB64Decode($b64)
+    {
+        return \JWT::urlsafeB64Decode($b64);
+    }
+}

+ 173 - 0
plugins/OAuth2/Encryption/Jwt.php

@@ -0,0 +1,173 @@
+<?php
+
+namespace OAuth2\Encryption;
+
+/**
+ * @link https://github.com/F21/jwt
+ * @author F21
+ */
+class Jwt implements EncryptionInterface
+{
+    public function encode($payload, $key, $algo = 'HS256')
+    {
+        $header = $this->generateJwtHeader($payload, $algo);
+
+        $segments = array(
+            $this->urlSafeB64Encode(json_encode($header)),
+            $this->urlSafeB64Encode(json_encode($payload))
+        );
+
+        $signing_input = implode('.', $segments);
+
+        $signature = $this->sign($signing_input, $key, $algo);
+        $segments[] = $this->urlsafeB64Encode($signature);
+
+        return implode('.', $segments);
+    }
+
+    public function decode($jwt, $key = null, $allowedAlgorithms = true)
+    {
+        if (!strpos($jwt, '.')) {
+            return false;
+        }
+
+        $tks = explode('.', $jwt);
+
+        if (count($tks) != 3) {
+            return false;
+        }
+
+        list($headb64, $payloadb64, $cryptob64) = $tks;
+
+        if (null === ($header = json_decode($this->urlSafeB64Decode($headb64), true))) {
+            return false;
+        }
+
+        if (null === $payload = json_decode($this->urlSafeB64Decode($payloadb64), true)) {
+            return false;
+        }
+
+        $sig = $this->urlSafeB64Decode($cryptob64);
+
+        if ((bool) $allowedAlgorithms) {
+            if (!isset($header['alg'])) {
+                return false;
+            }
+
+            // check if bool arg supplied here to maintain BC
+            if (is_array($allowedAlgorithms) && !in_array($header['alg'], $allowedAlgorithms)) {
+                return false;
+            }
+
+            if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header['alg'])) {
+                return false;
+            }
+        }
+
+        return $payload;
+    }
+
+    private function verifySignature($signature, $input, $key, $algo = 'HS256')
+    {
+        // use constants when possible, for HipHop support
+        switch ($algo) {
+            case'HS256':
+            case'HS384':
+            case'HS512':
+                return $this->hash_equals(
+                    $this->sign($input, $key, $algo),
+                    $signature
+                );
+
+            case 'RS256':
+                return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256')  === 1;
+
+            case 'RS384':
+                return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384') === 1;
+
+            case 'RS512':
+                return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512') === 1;
+
+            default:
+                throw new \InvalidArgumentException("Unsupported or invalid signing algorithm.");
+        }
+    }
+
+    private function sign($input, $key, $algo = 'HS256')
+    {
+        switch ($algo) {
+            case 'HS256':
+                return hash_hmac('sha256', $input, $key, true);
+
+            case 'HS384':
+                return hash_hmac('sha384', $input, $key, true);
+
+            case 'HS512':
+                return hash_hmac('sha512', $input, $key, true);
+
+            case 'RS256':
+                return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256');
+
+            case 'RS384':
+                return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384');
+
+            case 'RS512':
+                return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512');
+
+            default:
+                throw new \Exception("Unsupported or invalid signing algorithm.");
+        }
+    }
+
+    private function generateRSASignature($input, $key, $algo)
+    {
+        if (!openssl_sign($input, $signature, $key, $algo)) {
+            throw new \Exception("Unable to sign data.");
+        }
+
+        return $signature;
+    }
+
+    public function urlSafeB64Encode($data)
+    {
+        $b64 = base64_encode($data);
+        $b64 = str_replace(array('+', '/', "\r", "\n", '='),
+                array('-', '_'),
+                $b64);
+
+        return $b64;
+    }
+
+    public function urlSafeB64Decode($b64)
+    {
+        $b64 = str_replace(array('-', '_'),
+                array('+', '/'),
+                $b64);
+
+        return base64_decode($b64);
+    }
+
+    /**
+     * Override to create a custom header
+     */
+    protected function generateJwtHeader($payload, $algorithm)
+    {
+        return array(
+            'typ' => 'JWT',
+            'alg' => $algorithm,
+        );
+    }
+
+    protected function hash_equals($a, $b)
+    {
+        if (function_exists('hash_equals')) {
+            return hash_equals($a, $b);
+        }
+        $diff = strlen($a) ^ strlen($b);
+        for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) {
+            $diff |= ord($a[$i]) ^ ord($b[$i]);
+        }
+
+        return $diff === 0;
+    }
+}

+ 100 - 0
plugins/OAuth2/GrantType/AuthorizationCode.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\AuthorizationCodeInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode implements GrantTypeInterface
+{
+    protected $storage;
+    protected $authCode;
+
+    /**
+     * @param OAuth2\Storage\AuthorizationCodeInterface $storage REQUIRED Storage class for retrieving authorization code information
+     */
+    public function __construct(AuthorizationCodeInterface $storage)
+    {
+        $this->storage = $storage;
+    }
+
+    public function getQuerystringIdentifier()
+    {
+        return 'authorization_code';
+    }
+
+    public function validateRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if (!$request->request('code')) {
+            $response->setError(400, 'invalid_request', 'Missing parameter: "code" is required');
+
+            return false;
+        }
+
+        $code = $request->request('code');
+        if (!$authCode = $this->storage->getAuthorizationCode($code)) {
+            $response->setError(400, 'invalid_grant', 'Authorization code doesn\'t exist or is invalid for the client');
+
+            return false;
+        }
+
+        /*
+         * 4.1.3 - ensure that the "redirect_uri" parameter is present if the "redirect_uri" parameter was included in the initial authorization request
+         * @uri - http://tools.ietf.org/html/rfc6749#section-4.1.3
+         */
+        if (isset($authCode['redirect_uri']) && $authCode['redirect_uri']) {
+            if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != $authCode['redirect_uri']) {
+                $response->setError(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3");
+
+                return false;
+            }
+        }
+
+        if (!isset($authCode['expires'])) {
+            throw new \Exception('Storage must return authcode with a value for "expires"');
+        }
+
+        if ($authCode["expires"] < time()) {
+            $response->setError(400, 'invalid_grant', "The authorization code has expired");
+
+            return false;
+        }
+
+        if (!isset($authCode['code'])) {
+            $authCode['code'] = $code; // used to expire the code after the access token is granted
+        }
+
+        $this->authCode = $authCode;
+
+        return true;
+    }
+
+    public function getClientId()
+    {
+        return $this->authCode['client_id'];
+    }
+
+    public function getScope()
+    {
+        return isset($this->authCode['scope']) ? $this->authCode['scope'] : null;
+    }
+
+    public function getUserId()
+    {
+        return isset($this->authCode['user_id']) ? $this->authCode['user_id'] : null;
+    }
+
+    public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+    {
+        $token = $accessToken->createAccessToken($client_id, $user_id, $scope);
+        $this->storage->expireAuthorizationCode($this->authCode['code']);
+
+        return $token;
+    }
+}

+ 67 - 0
plugins/OAuth2/GrantType/ClientCredentials.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\ClientAssertionType\HttpBasic;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\Storage\ClientCredentialsInterface;
+
+/**
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ *
+ * @see OAuth2\ClientAssertionType_HttpBasic
+ */
+class ClientCredentials extends HttpBasic implements GrantTypeInterface
+{
+    private $clientData;
+
+    public function __construct(ClientCredentialsInterface $storage, array $config = array())
+    {
+        /**
+         * The client credentials grant type MUST only be used by confidential clients
+         *
+         * @see http://tools.ietf.org/html/rfc6749#section-4.4
+         */
+        $config['allow_public_clients'] = false;
+
+        parent::__construct($storage, $config);
+    }
+
+    public function getQuerystringIdentifier()
+    {
+        return 'client_credentials';
+    }
+
+    public function getScope()
+    {
+        $this->loadClientData();
+
+        return isset($this->clientData['scope']) ? $this->clientData['scope'] : null;
+    }
+
+    public function getUserId()
+    {
+        $this->loadClientData();
+
+        return isset($this->clientData['user_id']) ? $this->clientData['user_id'] : null;
+    }
+
+    public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+    {
+        /**
+         * Client Credentials Grant does NOT include a refresh token
+         *
+         * @see http://tools.ietf.org/html/rfc6749#section-4.4.3
+         */
+        $includeRefreshToken = false;
+
+        return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken);
+    }
+
+    private function loadClientData()
+    {
+        if (!$this->clientData) {
+            $this->clientData = $this->storage->getClientDetails($this->getClientId());
+        }
+    }
+}

+ 20 - 0
plugins/OAuth2/GrantType/GrantTypeInterface.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * Interface for all OAuth2 Grant Types
+ */
+interface GrantTypeInterface
+{
+    public function getQuerystringIdentifier();
+    public function validateRequest(RequestInterface $request, ResponseInterface $response);
+    public function getClientId();
+    public function getUserId();
+    public function getScope();
+    public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope);
+}

+ 226 - 0
plugins/OAuth2/GrantType/JwtBearer.php

@@ -0,0 +1,226 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\ClientAssertionType\ClientAssertionTypeInterface;
+use OAuth2\Storage\JwtBearerInterface;
+use OAuth2\Encryption\Jwt;
+use OAuth2\Encryption\EncryptionInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * The JWT bearer authorization grant implements JWT (JSON Web Tokens) as a grant type per the IETF draft.
+ *
+ * @see http://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-04#section-4
+ *
+ * @author F21
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface
+{
+    private $jwt;
+
+    protected $storage;
+    protected $audience;
+    protected $jwtUtil;
+    protected $allowedAlgorithms;
+
+    /**
+     * Creates an instance of the JWT bearer grant type.
+     *
+     * @param OAuth2\Storage\JWTBearerInterface|JwtBearerInterface $storage A valid storage interface that implements storage hooks for the JWT bearer grant type.
+     * @param string $audience The audience to validate the token against. This is usually the full URI of the OAuth token requests endpoint.
+     * @param EncryptionInterface|OAuth2\Encryption\JWT $jwtUtil OPTONAL The class used to decode, encode and verify JWTs.
+     * @param array $config
+     */
+    public function __construct(JwtBearerInterface $storage, $audience, EncryptionInterface $jwtUtil = null, array $config = array())
+    {
+        $this->storage = $storage;
+        $this->audience = $audience;
+
+        if (is_null($jwtUtil)) {
+            $jwtUtil = new Jwt();
+        }
+
+        $this->config = array_merge(array(
+            'allowed_algorithms' => array('RS256', 'RS384', 'RS512')
+        ), $config);
+
+        $this->jwtUtil = $jwtUtil;
+
+        $this->allowedAlgorithms = $this->config['allowed_algorithms'];
+    }
+
+    /**
+     * Returns the grant_type get parameter to identify the grant type request as JWT bearer authorization grant.
+     *
+     * @return
+     * The string identifier for grant_type.
+     *
+     * @see OAuth2\GrantType\GrantTypeInterface::getQuerystringIdentifier()
+     */
+    public function getQuerystringIdentifier()
+    {
+        return 'urn:ietf:params:oauth:grant-type:jwt-bearer';
+    }
+
+    /**
+     * Validates the data from the decoded JWT.
+     *
+     * @return
+     * TRUE if the JWT request is valid and can be decoded. Otherwise, FALSE is returned.
+     *
+     * @see OAuth2\GrantType\GrantTypeInterface::getTokenData()
+     */
+    public function validateRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if (!$request->request("assertion")) {
+            $response->setError(400, 'invalid_request', 'Missing parameters: "assertion" required');
+
+            return null;
+        }
+
+        // Store the undecoded JWT for later use
+        $undecodedJWT = $request->request('assertion');
+
+        // Decode the JWT
+        $jwt = $this->jwtUtil->decode($request->request('assertion'), null, false);
+
+        if (!$jwt) {
+            $response->setError(400, 'invalid_request', "JWT is malformed");
+
+            return null;
+        }
+
+        // ensure these properties contain a value
+        // @todo: throw malformed error for missing properties
+        $jwt = array_merge(array(
+            'scope' => null,
+            'iss' => null,
+            'sub' => null,
+            'aud' => null,
+            'exp' => null,
+            'nbf' => null,
+            'iat' => null,
+            'jti' => null,
+            'typ' => null,
+        ), $jwt);
+
+        if (!isset($jwt['iss'])) {
+            $response->setError(400, 'invalid_grant', "Invalid issuer (iss) provided");
+
+            return null;
+        }
+
+        if (!isset($jwt['sub'])) {
+            $response->setError(400, 'invalid_grant', "Invalid subject (sub) provided");
+
+            return null;
+        }
+
+        if (!isset($jwt['exp'])) {
+            $response->setError(400, 'invalid_grant', "Expiration (exp) time must be present");
+
+            return null;
+        }
+
+        // Check expiration
+        if (ctype_digit($jwt['exp'])) {
+            if ($jwt['exp'] <= time()) {
+                $response->setError(400, 'invalid_grant', "JWT has expired");
+
+                return null;
+            }
+        } else {
+            $response->setError(400, 'invalid_grant', "Expiration (exp) time must be a unix time stamp");
+
+            return null;
+        }
+
+        // Check the not before time
+        if ($notBefore = $jwt['nbf']) {
+            if (ctype_digit($notBefore)) {
+                if ($notBefore > time()) {
+                    $response->setError(400, 'invalid_grant', "JWT cannot be used before the Not Before (nbf) time");
+
+                    return null;
+                }
+            } else {
+                $response->setError(400, 'invalid_grant', "Not Before (nbf) time must be a unix time stamp");
+
+                return null;
+            }
+        }
+
+        // Check the audience if required to match
+        if (!isset($jwt['aud']) || ($jwt['aud'] != $this->audience)) {
+            $response->setError(400, 'invalid_grant', "Invalid audience (aud)");
+
+            return null;
+        }
+
+        // Check the jti (nonce)
+        // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-13#section-4.1.7
+        if (isset($jwt['jti'])) {
+            $jti = $this->storage->getJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']);
+
+            //Reject if jti is used and jwt is still valid (exp parameter has not expired).
+            if ($jti && $jti['expires'] > time()) {
+                $response->setError(400, 'invalid_grant', "JSON Token Identifier (jti) has already been used");
+
+                return null;
+            } else {
+                $this->storage->setJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']);
+            }
+        }
+
+        // Get the iss's public key
+        // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06#section-4.1.1
+        if (!$key = $this->storage->getClientKey($jwt['iss'], $jwt['sub'])) {
+            $response->setError(400, 'invalid_grant', "Invalid issuer (iss) or subject (sub) provided");
+
+            return null;
+        }
+
+        // Verify the JWT
+        if (!$this->jwtUtil->decode($undecodedJWT, $key, $this->allowedAlgorithms)) {
+            $response->setError(400, 'invalid_grant', "JWT failed signature verification");
+
+            return null;
+        }
+
+        $this->jwt = $jwt;
+
+        return true;
+    }
+
+    public function getClientId()
+    {
+        return $this->jwt['iss'];
+    }
+
+    public function getUserId()
+    {
+        return $this->jwt['sub'];
+    }
+
+    public function getScope()
+    {
+        return null;
+    }
+
+    /**
+     * Creates an access token that is NOT associated with a refresh token.
+     * If a subject (sub) the name of the user/account we are accessing data on behalf of.
+     *
+     * @see OAuth2\GrantType\GrantTypeInterface::createAccessToken()
+     */
+    public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+    {
+        $includeRefreshToken = false;
+
+        return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken);
+    }
+}

+ 111 - 0
plugins/OAuth2/GrantType/RefreshToken.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\RefreshTokenInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class RefreshToken implements GrantTypeInterface
+{
+    private $refreshToken;
+
+    protected $storage;
+    protected $config;
+
+    /**
+     * @param OAuth2\Storage\RefreshTokenInterface $storage REQUIRED Storage class for retrieving refresh token information
+     * @param array                                $config  OPTIONAL Configuration options for the server
+     *                                                      <code>
+     *                                                      $config = array(
+     *                                                      'always_issue_new_refresh_token' => true, // whether to issue a new refresh token upon successful token request
+     *                                                      'unset_refresh_token_after_use' => true // whether to unset the refresh token after after using
+     *                                                      );
+     *                                                      </code>
+     */
+    public function __construct(RefreshTokenInterface $storage, $config = array())
+    {
+        $this->config = array_merge(array(
+            'always_issue_new_refresh_token' => false,
+            'unset_refresh_token_after_use' => true
+        ), $config);
+
+        // to preserve B.C. with v1.6
+        // @see https://github.com/bshaffer/oauth2-server-php/pull/580
+        // @todo - remove in v2.0
+        if (isset($config['always_issue_new_refresh_token']) && !isset($config['unset_refresh_token_after_use'])) {
+            $this->config['unset_refresh_token_after_use'] = $config['always_issue_new_refresh_token'];
+        }
+
+        $this->storage = $storage;
+    }
+
+    public function getQuerystringIdentifier()
+    {
+        return 'refresh_token';
+    }
+
+    public function validateRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if (!$request->request("refresh_token")) {
+            $response->setError(400, 'invalid_request', 'Missing parameter: "refresh_token" is required');
+
+            return null;
+        }
+
+        if (!$refreshToken = $this->storage->getRefreshToken($request->request("refresh_token"))) {
+            $response->setError(400, 'invalid_grant', 'Invalid refresh token');
+
+            return null;
+        }
+
+        if ($refreshToken['expires'] > 0 && $refreshToken["expires"] < time()) {
+            $response->setError(400, 'invalid_grant', 'Refresh token has expired');
+
+            return null;
+        }
+
+        // store the refresh token locally so we can delete it when a new refresh token is generated
+        $this->refreshToken = $refreshToken;
+
+        return true;
+    }
+
+    public function getClientId()
+    {
+        return $this->refreshToken['client_id'];
+    }
+
+    public function getUserId()
+    {
+        return isset($this->refreshToken['user_id']) ? $this->refreshToken['user_id'] : null;
+    }
+
+    public function getScope()
+    {
+        return isset($this->refreshToken['scope']) ? $this->refreshToken['scope'] : null;
+    }
+
+    public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+    {
+        /*
+         * It is optional to force a new refresh token when a refresh token is used.
+         * However, if a new refresh token is issued, the old one MUST be expired
+         * @see http://tools.ietf.org/html/rfc6749#section-6
+         */
+        $issueNewRefreshToken = $this->config['always_issue_new_refresh_token'];
+        $unsetRefreshToken = $this->config['unset_refresh_token_after_use'];
+        $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $issueNewRefreshToken);
+
+        if ($unsetRefreshToken) {
+            $this->storage->unsetRefreshToken($this->refreshToken['refresh_token']);
+        }
+
+        return $token;
+    }
+}

+ 83 - 0
plugins/OAuth2/GrantType/UserCredentials.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\UserCredentialsInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class UserCredentials implements GrantTypeInterface
+{
+    private $userInfo;
+
+    protected $storage;
+
+    /**
+     * @param OAuth2\Storage\UserCredentialsInterface $storage REQUIRED Storage class for retrieving user credentials information
+     */
+    public function __construct(UserCredentialsInterface $storage)
+    {
+        $this->storage = $storage;
+    }
+
+    public function getQuerystringIdentifier()
+    {
+        return 'password';
+    }
+
+    public function validateRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if (!$request->request("password") || !$request->request("username")) {
+            $response->setError(400, 'invalid_request', 'Missing parameters: "username" and "password" required');
+
+            return null;
+        }
+
+        if (!$this->storage->checkUserCredentials($request->request("username"), $request->request("password"))) {
+            $response->setError(401, 'invalid_grant', 'Invalid username and password combination');
+
+            return null;
+        }
+
+        $userInfo = $this->storage->getUserDetails($request->request("username"));
+
+        if (empty($userInfo)) {
+            $response->setError(400, 'invalid_grant', 'Unable to retrieve user information');
+
+            return null;
+        }
+
+        if (!isset($userInfo['user_id'])) {
+            throw new \LogicException("you must set the user_id on the array returned by getUserDetails");
+        }
+
+        $this->userInfo = $userInfo;
+
+        return true;
+    }
+
+    public function getClientId()
+    {
+        return null;
+    }
+
+    public function getUserId()
+    {
+        return $this->userInfo['user_id'];
+    }
+
+    public function getScope()
+    {
+        return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null;
+    }
+
+    public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+    {
+        return $accessToken->createAccessToken($client_id, $user_id, $scope);
+    }
+}

+ 21 - 0
plugins/OAuth2/LICENSE

@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2014 Brent Shaffer
+
+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.

+ 106 - 0
plugins/OAuth2/OpenID/Controller/AuthorizeController.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\Controller\AuthorizeController as BaseAuthorizeController;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * @see OAuth2\Controller\AuthorizeControllerInterface
+ */
+class AuthorizeController extends BaseAuthorizeController implements AuthorizeControllerInterface
+{
+    private $nonce;
+
+    protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null)
+    {
+        $prompt = $request->query('prompt', 'consent');
+        if ($prompt == 'none') {
+            if (is_null($user_id)) {
+                $error = 'login_required';
+                $error_message = 'The user must log in';
+            } else {
+                $error = 'interaction_required';
+                $error_message = 'The user must grant access to your application';
+            }
+        } else {
+            $error = 'consent_required';
+            $error_message = 'The user denied access to your application';
+        }
+
+        $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->getState(), $error, $error_message);
+    }
+
+    protected function buildAuthorizeParameters($request, $response, $user_id)
+    {
+        if (!$params = parent::buildAuthorizeParameters($request, $response, $user_id)) {
+            return;
+        }
+
+        // Generate an id token if needed.
+        if ($this->needsIdToken($this->getScope()) && $this->getResponseType() == self::RESPONSE_TYPE_AUTHORIZATION_CODE) {
+            $params['id_token'] = $this->responseTypes['id_token']->createIdToken($this->getClientId(), $user_id, $this->nonce);
+        }
+
+        // add the nonce to return with the redirect URI
+        $params['nonce'] = $this->nonce;
+
+        return $params;
+    }
+
+    public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if (!parent::validateAuthorizeRequest($request, $response)) {
+            return false;
+        }
+
+        $nonce = $request->query('nonce');
+
+        // Validate required nonce for "id_token" and "id_token token"
+        if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_ID_TOKEN_TOKEN))) {
+            $response->setError(400, 'invalid_nonce', 'This application requires you specify a nonce parameter');
+
+            return false;
+        }
+
+        $this->nonce = $nonce;
+
+        return true;
+    }
+
+    protected function getValidResponseTypes()
+    {
+        return array(
+            self::RESPONSE_TYPE_ACCESS_TOKEN,
+            self::RESPONSE_TYPE_AUTHORIZATION_CODE,
+            self::RESPONSE_TYPE_ID_TOKEN,
+            self::RESPONSE_TYPE_ID_TOKEN_TOKEN,
+            self::RESPONSE_TYPE_CODE_ID_TOKEN,
+        );
+    }
+
+    /**
+     * Returns whether the current request needs to generate an id token.
+     *
+     * ID Tokens are a part of the OpenID Connect specification, so this
+     * method checks whether OpenID Connect is enabled in the server settings
+     * and whether the openid scope was requested.
+     *
+     * @param $request_scope
+     *  A space-separated string of scopes.
+     *
+     * @return
+     *   TRUE if an id token is needed, FALSE otherwise.
+     */
+    public function needsIdToken($request_scope)
+    {
+        // see if the "openid" scope exists in the requested scope
+        return $this->scopeUtil->checkScope('openid', $request_scope);
+    }
+
+    public function getNonce()
+    {
+        return $this->nonce;
+    }
+}

+ 10 - 0
plugins/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+interface AuthorizeControllerInterface
+{
+    const RESPONSE_TYPE_ID_TOKEN = 'id_token';
+    const RESPONSE_TYPE_ID_TOKEN_TOKEN = 'id_token token';
+    const RESPONSE_TYPE_CODE_ID_TOKEN  = 'code id_token';
+}

+ 58 - 0
plugins/OAuth2/OpenID/Controller/UserInfoController.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\Scope;
+use OAuth2\TokenType\TokenTypeInterface;
+use OAuth2\Storage\AccessTokenInterface;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\Controller\ResourceController;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * @see OAuth2\Controller\UserInfoControllerInterface
+ */
+class UserInfoController extends ResourceController implements UserInfoControllerInterface
+{
+    private $token;
+
+    protected $tokenType;
+    protected $tokenStorage;
+    protected $userClaimsStorage;
+    protected $config;
+    protected $scopeUtil;
+
+    public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, UserClaimsInterface $userClaimsStorage, $config = array(), ScopeInterface $scopeUtil = null)
+    {
+        $this->tokenType = $tokenType;
+        $this->tokenStorage = $tokenStorage;
+        $this->userClaimsStorage = $userClaimsStorage;
+
+        $this->config = array_merge(array(
+            'www_realm' => 'Service',
+        ), $config);
+
+        if (is_null($scopeUtil)) {
+            $scopeUtil = new Scope();
+        }
+        $this->scopeUtil = $scopeUtil;
+    }
+
+    public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response)
+    {
+        if (!$this->verifyResourceRequest($request, $response, 'openid')) {
+            return;
+        }
+
+        $token = $this->getToken();
+        $claims = $this->userClaimsStorage->getUserClaims($token['user_id'], $token['scope']);
+        // The sub Claim MUST always be returned in the UserInfo Response.
+        // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
+        $claims += array(
+            'sub' => $token['user_id'],
+        );
+        $response->addParameters($claims);
+    }
+}

+ 23 - 0
plugins/OAuth2/OpenID/Controller/UserInfoControllerInterface.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *  This controller is called when the user claims for OpenID Connect's
+ *  UserInfo endpoint should be returned.
+ *
+ *  ex:
+ *  > $response = new OAuth2\Response();
+ *  > $userInfoController->handleUserInfoRequest(
+ *  >     OAuth2\Request::createFromGlobals(),
+ *  >     $response;
+ *  > $response->send();
+ *
+ */
+interface UserInfoControllerInterface
+{
+    public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response);
+}

+ 33 - 0
plugins/OAuth2/OpenID/GrantType/AuthorizationCode.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace OAuth2\OpenID\GrantType;
+
+use OAuth2\GrantType\AuthorizationCode as BaseAuthorizationCode;
+use OAuth2\ResponseType\AccessTokenInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode extends BaseAuthorizationCode
+{
+    public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+    {
+        $includeRefreshToken = true;
+        if (isset($this->authCode['id_token'])) {
+            // OpenID Connect requests include the refresh token only if the
+            // offline_access scope has been requested and granted.
+            $scopes = explode(' ', trim($scope));
+            $includeRefreshToken = in_array('offline_access', $scopes);
+        }
+
+        $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken);
+        if (isset($this->authCode['id_token'])) {
+            $token['id_token'] = $this->authCode['id_token'];
+        }
+
+        $this->storage->expireAuthorizationCode($this->authCode['code']);
+
+        return $token;
+    }
+}

+ 60 - 0
plugins/OAuth2/OpenID/ResponseType/AuthorizationCode.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\AuthorizationCode as BaseAuthorizationCode;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as AuthorizationCodeStorageInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode extends BaseAuthorizationCode implements AuthorizationCodeInterface
+{
+    public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array())
+    {
+        parent::__construct($storage, $config);
+    }
+
+    public function getAuthorizeResponse($params, $user_id = null)
+    {
+        // build the URL to redirect to
+        $result = array('query' => array());
+
+        $params += array('scope' => null, 'state' => null, 'id_token' => null);
+
+        $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token']);
+
+        if (isset($params['state'])) {
+            $result['query']['state'] = $params['state'];
+        }
+
+        return array($params['redirect_uri'], $result);
+    }
+
+    /**
+     * Handle the creation of the authorization code.
+     *
+     * @param $client_id
+     * Client identifier related to the authorization code
+     * @param $user_id
+     * User ID associated with the authorization code
+     * @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.
+     * @param $id_token
+     * (optional) The OpenID Connect id_token.
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4
+     * @ingroup oauth2_section_4
+     */
+    public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null)
+    {
+        $code = $this->generateAuthorizationCode();
+        $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token);
+
+        return $code;
+    }
+}

+ 27 - 0
plugins/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\AuthorizationCodeInterface as BaseAuthorizationCodeInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface
+{
+    /**
+     * Handle the creation of the authorization code.
+     *
+     * @param $client_id                Client identifier related to the authorization code
+     * @param $user_id                  User ID associated with the authorization code
+     * @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.
+     * @param $id_token     OPTIONAL    The OpenID Connect id_token.
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4
+     * @ingroup oauth2_section_4
+     */
+    public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null);
+}

+ 24 - 0
plugins/OAuth2/OpenID/ResponseType/CodeIdToken.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+class CodeIdToken implements CodeIdTokenInterface
+{
+    protected $authCode;
+    protected $idToken;
+
+    public function __construct(AuthorizationCodeInterface $authCode, IdTokenInterface $idToken)
+    {
+        $this->authCode = $authCode;
+        $this->idToken = $idToken;
+    }
+
+    public function getAuthorizeResponse($params, $user_id = null)
+    {
+        $result = $this->authCode->getAuthorizeResponse($params, $user_id);
+        $id_token = $this->idToken->createIdToken($params['client_id'], $user_id, $params['nonce']);
+        $result[1]['query']['id_token'] = $id_token;
+
+        return $result;
+    }
+}

+ 9 - 0
plugins/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\ResponseTypeInterface;
+
+interface CodeIdTokenInterface extends ResponseTypeInterface
+{
+}

+ 124 - 0
plugins/OAuth2/OpenID/ResponseType/IdToken.php

@@ -0,0 +1,124 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\Encryption\EncryptionInterface;
+use OAuth2\Encryption\Jwt;
+use OAuth2\Storage\PublicKeyInterface;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+
+class IdToken implements IdTokenInterface
+{
+    protected $userClaimsStorage;
+    protected $publicKeyStorage;
+    protected $config;
+    protected $encryptionUtil;
+
+    public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInterface $publicKeyStorage, array $config = array(), EncryptionInterface $encryptionUtil = null)
+    {
+        $this->userClaimsStorage = $userClaimsStorage;
+        $this->publicKeyStorage = $publicKeyStorage;
+        if (is_null($encryptionUtil)) {
+            $encryptionUtil = new Jwt();
+        }
+        $this->encryptionUtil = $encryptionUtil;
+
+        if (!isset($config['issuer'])) {
+            throw new \LogicException('config parameter "issuer" must be set');
+        }
+        $this->config = array_merge(array(
+            'id_lifetime' => 3600,
+        ), $config);
+    }
+
+    public function getAuthorizeResponse($params, $userInfo = null)
+    {
+        // build the URL to redirect to
+        $result = array('query' => array());
+        $params += array('scope' => null, 'state' => null, 'nonce' => null);
+
+        // create the id token.
+        list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo);
+        $userClaims = $this->userClaimsStorage->getUserClaims($user_id, $params['scope']);
+
+        $id_token = $this->createIdToken($params['client_id'], $userInfo, $params['nonce'], $userClaims, null);
+        $result["fragment"] = array('id_token' => $id_token);
+        if (isset($params['state'])) {
+            $result["fragment"]["state"] = $params['state'];
+        }
+
+        return array($params['redirect_uri'], $result);
+    }
+
+    public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null)
+    {
+        // pull auth_time from user info if supplied
+        list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo);
+
+        $token = array(
+            'iss'        => $this->config['issuer'],
+            'sub'        => $user_id,
+            'aud'        => $client_id,
+            'iat'        => time(),
+            'exp'        => time() + $this->config['id_lifetime'],
+            'auth_time'  => $auth_time,
+        );
+
+        if ($nonce) {
+            $token['nonce'] = $nonce;
+        }
+
+        if ($userClaims) {
+            $token += $userClaims;
+        }
+
+        if ($access_token) {
+            $token['at_hash'] = $this->createAtHash($access_token, $client_id);
+        }
+
+        return $this->encodeToken($token, $client_id);
+    }
+
+    protected function createAtHash($access_token, $client_id = null)
+    {
+        // maps HS256 and RS256 to sha256, etc.
+        $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id);
+        $hash_algorithm = 'sha' . substr($algorithm, 2);
+        $hash = hash($hash_algorithm, $access_token);
+        $at_hash = substr($hash, 0, strlen($hash) / 2);
+
+        return $this->encryptionUtil->urlSafeB64Encode($at_hash);
+    }
+
+    protected function encodeToken(array $token, $client_id = null)
+    {
+        $private_key = $this->publicKeyStorage->getPrivateKey($client_id);
+        $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id);
+
+        return $this->encryptionUtil->encode($token, $private_key, $algorithm);
+    }
+
+    private function getUserIdAndAuthTime($userInfo)
+    {
+        $auth_time = null;
+
+        // support an array for user_id / auth_time
+        if (is_array($userInfo)) {
+            if (!isset($userInfo['user_id'])) {
+                throw new \LogicException('if $user_id argument is an array, user_id index must be set');
+            }
+
+            $auth_time = isset($userInfo['auth_time']) ? $userInfo['auth_time'] : null;
+            $user_id = $userInfo['user_id'];
+        } else {
+            $user_id = $userInfo;
+        }
+
+        if (is_null($auth_time)) {
+            $auth_time = time();
+        }
+
+        // userInfo is a scalar, and so this is the $user_id. Auth Time is null
+        return array($user_id, $auth_time);
+    }
+}

+ 29 - 0
plugins/OAuth2/OpenID/ResponseType/IdTokenInterface.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\ResponseTypeInterface;
+
+interface IdTokenInterface extends ResponseTypeInterface
+{
+    /**
+     * Create the id token.
+     *
+     * If Authorization Code Flow is used, the id_token is generated when the
+     * authorization code is issued, and later returned from the token endpoint
+     * together with the access_token.
+     * If the Implicit Flow is used, the token and id_token are generated and
+     * returned together.
+     *
+     * @param string $client_id    The client id.
+     * @param string $user_id      The user id.
+     * @param string $nonce        OPTIONAL The nonce.
+     * @param string $userClaims   OPTIONAL Claims about the user.
+     * @param string $access_token OPTIONAL The access token, if known.
+     *
+     * @return string The ID Token represented as a JSON Web Token (JWT).
+     *
+     * @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken
+     */
+    public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null);
+}

+ 27 - 0
plugins/OAuth2/OpenID/ResponseType/IdTokenToken.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\AccessTokenInterface;
+
+class IdTokenToken implements IdTokenTokenInterface
+{
+    protected $accessToken;
+    protected $idToken;
+
+    public function __construct(AccessTokenInterface $accessToken, IdTokenInterface $idToken)
+    {
+        $this->accessToken = $accessToken;
+        $this->idToken = $idToken;
+    }
+
+    public function getAuthorizeResponse($params, $user_id = null)
+    {
+        $result = $this->accessToken->getAuthorizeResponse($params, $user_id);
+        $access_token = $result[1]['fragment']['access_token'];
+        $id_token = $this->idToken->createIdToken($params['client_id'], $user_id, $params['nonce'], null, $access_token);
+        $result[1]['fragment']['id_token'] = $id_token;
+
+        return $result;
+    }
+}

+ 9 - 0
plugins/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\ResponseTypeInterface;
+
+interface IdTokenTokenInterface extends ResponseTypeInterface
+{
+}

+ 37 - 0
plugins/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace OAuth2\OpenID\Storage;
+
+use OAuth2\Storage\AuthorizationCodeInterface as BaseAuthorizationCodeInterface;
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should get/save authorization codes for the "Authorization Code"
+ * grant type
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface
+{
+    /**
+     * 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 $user_id             user identifier to be stored.
+     * @param string $redirect_uri redirect URI(s) to be stored in a space-separated string.
+     * @param int    $expires      expiration to be stored as a Unix timestamp.
+     * @param string $scope        OPTIONAL scopes to be stored in space-separated string.
+     * @param string $id_token     OPTIONAL the OpenID Connect id_token.
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null);
+}

+ 38 - 0
plugins/OAuth2/OpenID/Storage/UserClaimsInterface.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace OAuth2\OpenID\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should retrieve user claims for the OpenID Connect id_token.
+ */
+interface UserClaimsInterface
+{
+    // valid scope values to pass into the user claims API call
+    const VALID_CLAIMS = 'profile email address phone';
+
+    // fields returned for the claims above
+    const PROFILE_CLAIM_VALUES  = 'name family_name given_name middle_name nickname preferred_username profile picture website gender birthdate zoneinfo locale updated_at';
+    const EMAIL_CLAIM_VALUES    = 'email email_verified';
+    const ADDRESS_CLAIM_VALUES  = 'formatted street_address locality region postal_code country';
+    const PHONE_CLAIM_VALUES    = 'phone_number phone_number_verified';
+
+    /**
+     * Return claims about the provided user id.
+     *
+     * Groups of claims are returned based on the requested scopes. No group
+     * is required, and no claim is required.
+     *
+     * @param $user_id
+     * The id of the user for which claims should be returned.
+     * @param $scope
+     * The requested scope.
+     * Scopes with matching claims: profile, email, address, phone.
+     *
+     * @return
+     * An array in the claim => value format.
+     *
+     * @see http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
+     */
+    public function getUserClaims($user_id, $scope);
+}

+ 8 - 0
plugins/OAuth2/README.md

@@ -0,0 +1,8 @@
+oauth2-server-php
+=================
+
+[![Build Status](https://travis-ci.org/bshaffer/oauth2-server-php.svg?branch=develop)](https://travis-ci.org/bshaffer/oauth2-server-php)
+
+[![Total Downloads](https://poser.pugx.org/bshaffer/oauth2-server-php/downloads.png)](https://packagist.org/packages/bshaffer/oauth2-server-php)
+
+View the [complete documentation](http://bshaffer.github.io/oauth2-server-php-docs/)

+ 213 - 0
plugins/OAuth2/Request.php

@@ -0,0 +1,213 @@
+<?php
+
+namespace OAuth2;
+
+/**
+ * OAuth2\Request
+ * This class is taken from the Symfony2 Framework and is part of the Symfony package.
+ * See Symfony\Component\HttpFoundation\Request (https://github.com/symfony/symfony)
+ */
+class Request implements RequestInterface
+{
+    public $attributes;
+    public $request;
+    public $query;
+    public $server;
+    public $files;
+    public $cookies;
+    public $headers;
+    public $content;
+
+    /**
+     * Constructor.
+     *
+     * @param array  $query      The GET parameters
+     * @param array  $request    The POST parameters
+     * @param array  $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
+     * @param array  $cookies    The COOKIE parameters
+     * @param array  $files      The FILES parameters
+     * @param array  $server     The SERVER parameters
+     * @param string $content    The raw body data
+     *
+     * @api
+     */
+    public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null)
+    {
+        $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content, $headers);
+    }
+
+    /**
+     * Sets the parameters for this request.
+     *
+     * This method also re-initializes all properties.
+     *
+     * @param array  $query      The GET parameters
+     * @param array  $request    The POST parameters
+     * @param array  $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
+     * @param array  $cookies    The COOKIE parameters
+     * @param array  $files      The FILES parameters
+     * @param array  $server     The SERVER parameters
+     * @param string $content    The raw body data
+     *
+     * @api
+     */
+    public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null)
+    {
+        $this->request = $request;
+        $this->query = $query;
+        $this->attributes = $attributes;
+        $this->cookies = $cookies;
+        $this->files = $files;
+        $this->server = $server;
+        $this->content = $content;
+        $this->headers = is_null($headers) ? $this->getHeadersFromServer($this->server) : $headers;
+    }
+
+    public function query($name, $default = null)
+    {
+        return isset($this->query[$name]) ? $this->query[$name] : $default;
+    }
+
+    public function request($name, $default = null)
+    {
+        return isset($this->request[$name]) ? $this->request[$name] : $default;
+    }
+
+    public function server($name, $default = null)
+    {
+        return isset($this->server[$name]) ? $this->server[$name] : $default;
+    }
+
+    public function headers($name, $default = null)
+    {
+        $headers = array_change_key_case($this->headers);
+        $name = strtolower($name);
+
+        return isset($headers[$name]) ? $headers[$name] : $default;
+    }
+
+    public function getAllQueryParameters()
+    {
+        return $this->query;
+    }
+
+    /**
+     * Returns the request body content.
+     *
+     * @param Boolean $asResource If true, a resource will be returned
+     *
+     * @return string|resource The request body content or a resource to read the body stream.
+     */
+    public function getContent($asResource = false)
+    {
+        if (false === $this->content || (true === $asResource && null !== $this->content)) {
+            throw new \LogicException('getContent() can only be called once when using the resource return type.');
+        }
+
+        if (true === $asResource) {
+            $this->content = false;
+
+            return fopen('php://input', 'rb');
+        }
+
+        if (null === $this->content) {
+            $this->content = file_get_contents('php://input');
+        }
+
+        return $this->content;
+    }
+
+    private function getHeadersFromServer($server)
+    {
+        $headers = array();
+        foreach ($server as $key => $value) {
+            if (0 === strpos($key, 'HTTP_')) {
+                $headers[substr($key, 5)] = $value;
+            }
+            // CONTENT_* are not prefixed with HTTP_
+            elseif (in_array($key, array('CONTENT_LENGTH', 'CONTENT_MD5', 'CONTENT_TYPE'))) {
+                $headers[$key] = $value;
+            }
+        }
+
+        if (isset($server['PHP_AUTH_USER'])) {
+            $headers['PHP_AUTH_USER'] = $server['PHP_AUTH_USER'];
+            $headers['PHP_AUTH_PW'] = isset($server['PHP_AUTH_PW']) ? $server['PHP_AUTH_PW'] : '';
+        } else {
+            /*
+             * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default
+             * For this workaround to work, add this line to your .htaccess file:
+             * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+             *
+             * A sample .htaccess file:
+             * RewriteEngine On
+             * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+             * RewriteCond %{REQUEST_FILENAME} !-f
+             * RewriteRule ^(.*)$ app.php [QSA,L]
+             */
+
+            $authorizationHeader = null;
+            if (isset($server['HTTP_AUTHORIZATION'])) {
+                $authorizationHeader = $server['HTTP_AUTHORIZATION'];
+            } elseif (isset($server['REDIRECT_HTTP_AUTHORIZATION'])) {
+                $authorizationHeader = $server['REDIRECT_HTTP_AUTHORIZATION'];
+            } elseif (function_exists('apache_request_headers')) {
+                $requestHeaders = (array) apache_request_headers();
+
+                // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization)
+                $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
+
+                if (isset($requestHeaders['Authorization'])) {
+                    $authorizationHeader = trim($requestHeaders['Authorization']);
+                }
+            }
+
+            if (null !== $authorizationHeader) {
+                $headers['AUTHORIZATION'] = $authorizationHeader;
+                // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic
+                if (0 === stripos($authorizationHeader, 'basic')) {
+                    $exploded = explode(':', base64_decode(substr($authorizationHeader, 6)));
+                    if (count($exploded) == 2) {
+                        list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded;
+                    }
+                }
+            }
+        }
+
+        // PHP_AUTH_USER/PHP_AUTH_PW
+        if (isset($headers['PHP_AUTH_USER'])) {
+            $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.$headers['PHP_AUTH_PW']);
+        }
+
+        return $headers;
+    }
+
+    /**
+     * Creates a new request with values from PHP's super globals.
+     *
+     * @return Request A new request
+     *
+     * @api
+     */
+    public static function createFromGlobals()
+    {
+        $class = get_called_class();
+        $request = new $class($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER);
+
+        $contentType = $request->server('CONTENT_TYPE', '');
+        $requestMethod = $request->server('REQUEST_METHOD', 'GET');
+        if (0 === strpos($contentType, 'application/x-www-form-urlencoded')
+            && in_array(strtoupper($requestMethod), array('PUT', 'DELETE'))
+        ) {
+            parse_str($request->getContent(), $data);
+            $request->request = $data;
+        } elseif (0 === strpos($contentType, 'application/json')
+            && in_array(strtoupper($requestMethod), array('POST', 'PUT', 'DELETE'))
+        ) {
+            $data = json_decode($request->getContent(), true);
+            $request->request = $data;
+        }
+
+        return $request;
+    }
+}

+ 16 - 0
plugins/OAuth2/RequestInterface.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace OAuth2;
+
+interface RequestInterface
+{
+    public function query($name, $default = null);
+
+    public function request($name, $default = null);
+
+    public function server($name, $default = null);
+
+    public function headers($name, $default = null);
+
+    public function getAllQueryParameters();
+}

+ 369 - 0
plugins/OAuth2/Response.php

@@ -0,0 +1,369 @@
+<?php
+
+namespace OAuth2;
+
+/**
+ * Class to handle OAuth2 Responses in a graceful way.  Use this interface
+ * to output the proper OAuth2 responses.
+ *
+ * @see OAuth2\ResponseInterface
+ *
+ * This class borrows heavily from the Symfony2 Framework and is part of the symfony package
+ * @see Symfony\Component\HttpFoundation\Request (https://github.com/symfony/symfony)
+ */
+class Response implements ResponseInterface
+{
+    public $version;
+    protected $statusCode = 200;
+    protected $statusText;
+    protected $parameters = array();
+    protected $httpHeaders = array();
+
+    public static $statusTexts = array(
+        100 => 'Continue',
+        101 => 'Switching Protocols',
+        200 => 'OK',
+        201 => 'Created',
+        202 => 'Accepted',
+        203 => 'Non-Authoritative Information',
+        204 => 'No Content',
+        205 => 'Reset Content',
+        206 => 'Partial Content',
+        300 => 'Multiple Choices',
+        301 => 'Moved Permanently',
+        302 => 'Found',
+        303 => 'See Other',
+        304 => 'Not Modified',
+        305 => 'Use Proxy',
+        307 => 'Temporary Redirect',
+        400 => 'Bad Request',
+        401 => 'Unauthorized',
+        402 => 'Payment Required',
+        403 => 'Forbidden',
+        404 => 'Not Found',
+        405 => 'Method Not Allowed',
+        406 => 'Not Acceptable',
+        407 => 'Proxy Authentication Required',
+        408 => 'Request Timeout',
+        409 => 'Conflict',
+        410 => 'Gone',
+        411 => 'Length Required',
+        412 => 'Precondition Failed',
+        413 => 'Request Entity Too Large',
+        414 => 'Request-URI Too Long',
+        415 => 'Unsupported Media Type',
+        416 => 'Requested Range Not Satisfiable',
+        417 => 'Expectation Failed',
+        418 => 'I\'m a teapot',
+        500 => 'Internal Server Error',
+        501 => 'Not Implemented',
+        502 => 'Bad Gateway',
+        503 => 'Service Unavailable',
+        504 => 'Gateway Timeout',
+        505 => 'HTTP Version Not Supported',
+    );
+
+    public function __construct($parameters = array(), $statusCode = 200, $headers = array())
+    {
+        $this->setParameters($parameters);
+        $this->setStatusCode($statusCode);
+        $this->setHttpHeaders($headers);
+        $this->version = '1.1';
+    }
+
+    /**
+     * Converts the response object to string containing all headers and the response content.
+     *
+     * @return string The response with headers and content
+     */
+    public function __toString()
+    {
+        $headers = array();
+        foreach ($this->httpHeaders as $name => $value) {
+            $headers[$name] = (array) $value;
+        }
+
+        return
+            sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
+            $this->getHttpHeadersAsString($headers)."\r\n".
+            $this->getResponseBody();
+    }
+
+    /**
+     * Returns the build header line.
+     *
+     * @param string $name  The header name
+     * @param string $value The header value
+     *
+     * @return string The built header line
+     */
+    protected function buildHeader($name, $value)
+    {
+        return sprintf("%s: %s\n", $name, $value);
+    }
+
+    public function getStatusCode()
+    {
+        return $this->statusCode;
+    }
+
+    public function setStatusCode($statusCode, $text = null)
+    {
+        $this->statusCode = (int) $statusCode;
+        if ($this->isInvalid()) {
+            throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $statusCode));
+        }
+
+        $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text);
+    }
+
+    public function getStatusText()
+    {
+        return $this->statusText;
+    }
+
+    public function getParameters()
+    {
+        return $this->parameters;
+    }
+
+    public function setParameters(array $parameters)
+    {
+        $this->parameters = $parameters;
+    }
+
+    public function addParameters(array $parameters)
+    {
+        $this->parameters = array_merge($this->parameters, $parameters);
+    }
+
+    public function getParameter($name, $default = null)
+    {
+        return isset($this->parameters[$name]) ? $this->parameters[$name] : $default;
+    }
+
+    public function setParameter($name, $value)
+    {
+        $this->parameters[$name] = $value;
+    }
+
+    public function setHttpHeaders(array $httpHeaders)
+    {
+        $this->httpHeaders = $httpHeaders;
+    }
+
+    public function setHttpHeader($name, $value)
+    {
+        $this->httpHeaders[$name] = $value;
+    }
+
+    public function addHttpHeaders(array $httpHeaders)
+    {
+        $this->httpHeaders = array_merge($this->httpHeaders, $httpHeaders);
+    }
+
+    public function getHttpHeaders()
+    {
+        return $this->httpHeaders;
+    }
+
+    public function getHttpHeader($name, $default = null)
+    {
+        return isset($this->httpHeaders[$name]) ? $this->httpHeaders[$name] : $default;
+    }
+
+    public function getResponseBody($format = 'json')
+    {
+        switch ($format) {
+            case 'json':
+                return json_encode($this->parameters);
+            case 'xml':
+                // this only works for single-level arrays
+                $xml = new \SimpleXMLElement('<response/>');
+                foreach ($this->parameters as $key => $param) {
+                    $xml->addChild($key, $param);
+                }
+
+                return $xml->asXML();
+        }
+
+        throw new \InvalidArgumentException(sprintf('The format %s is not supported', $format));
+
+    }
+
+    public function send($format = 'json')
+    {
+        // headers have already been sent by the developer
+        if (headers_sent()) {
+            return;
+        }
+
+        switch ($format) {
+            case 'json':
+                $this->setHttpHeader('Content-Type', 'application/json');
+                break;
+            case 'xml':
+                $this->setHttpHeader('Content-Type', 'text/xml');
+                break;
+        }
+        // status
+        header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText));
+
+        foreach ($this->getHttpHeaders() as $name => $header) {
+            header(sprintf('%s: %s', $name, $header));
+        }
+        echo $this->getResponseBody($format);
+    }
+
+    public function setError($statusCode, $error, $errorDescription = null, $errorUri = null)
+    {
+        $parameters = array(
+            'error' => $error,
+            'error_description' => $errorDescription,
+        );
+
+        if (!is_null($errorUri)) {
+            if (strlen($errorUri) > 0 && $errorUri[0] == '#') {
+                // we are referencing an oauth bookmark (for brevity)
+                $errorUri = 'http://tools.ietf.org/html/rfc6749' . $errorUri;
+            }
+            $parameters['error_uri'] = $errorUri;
+        }
+
+        $httpHeaders = array(
+            'Cache-Control' => 'no-store'
+        );
+
+        $this->setStatusCode($statusCode);
+        $this->addParameters($parameters);
+        $this->addHttpHeaders($httpHeaders);
+
+        if (!$this->isClientError() && !$this->isServerError()) {
+            throw new \InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode));
+        }
+    }
+
+    public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null)
+    {
+        if (empty($url)) {
+            throw new \InvalidArgumentException('Cannot redirect to an empty URL.');
+        }
+
+        $parameters = array();
+
+        if (!is_null($state)) {
+            $parameters['state'] = $state;
+        }
+
+        if (!is_null($error)) {
+            $this->setError(400, $error, $errorDescription, $errorUri);
+        }
+        $this->setStatusCode($statusCode);
+        $this->addParameters($parameters);
+
+        if (count($this->parameters) > 0) {
+            // add parameters to URL redirection
+            $parts = parse_url($url);
+            $sep = isset($parts['query']) && count($parts['query']) > 0 ? '&' : '?';
+            $url .= $sep . http_build_query($this->parameters);
+        }
+
+        $this->addHttpHeaders(array('Location' =>  $url));
+
+        if (!$this->isRedirection()) {
+            throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode));
+        }
+    }
+
+    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isInvalid()
+    {
+        return $this->statusCode < 100 || $this->statusCode >= 600;
+    }
+
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isInformational()
+    {
+        return $this->statusCode >= 100 && $this->statusCode < 200;
+    }
+
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isSuccessful()
+    {
+        return $this->statusCode >= 200 && $this->statusCode < 300;
+    }
+
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isRedirection()
+    {
+        return $this->statusCode >= 300 && $this->statusCode < 400;
+    }
+
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isClientError()
+    {
+        return $this->statusCode >= 400 && $this->statusCode < 500;
+    }
+
+    /**
+     * @return Boolean
+     *
+     * @api
+     */
+    public function isServerError()
+    {
+        return $this->statusCode >= 500 && $this->statusCode < 600;
+    }
+
+    /*
+     * Functions from Symfony2 HttpFoundation - output pretty header
+     */
+    private function getHttpHeadersAsString($headers)
+    {
+        if (count($headers) == 0) {
+            return '';
+        }
+
+        $max = max(array_map('strlen', array_keys($headers))) + 1;
+        $content = '';
+        ksort($headers);
+        foreach ($headers as $name => $values) {
+            foreach ($values as $value) {
+                $content .= sprintf("%-{$max}s %s\r\n", $this->beautifyHeaderName($name).':', $value);
+            }
+        }
+
+        return $content;
+    }
+
+    private function beautifyHeaderName($name)
+    {
+        return preg_replace_callback('/\-(.)/', array($this, 'beautifyCallback'), ucfirst($name));
+    }
+
+    private function beautifyCallback($match)
+    {
+        return '-'.strtoupper($match[1]);
+    }
+}

+ 24 - 0
plugins/OAuth2/ResponseInterface.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace OAuth2;
+
+/**
+ * Interface which represents an object response.  Meant to handle and display the proper OAuth2 Responses
+ * for errors and successes
+ *
+ * @see OAuth2\Response
+ */
+interface ResponseInterface
+{
+    public function addParameters(array $parameters);
+
+    public function addHttpHeaders(array $httpHeaders);
+
+    public function setStatusCode($statusCode);
+
+    public function setError($statusCode, $name, $description = null, $uri = null);
+
+    public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null);
+
+    public function getParameter($name);
+}

+ 194 - 0
plugins/OAuth2/ResponseType/AccessToken.php

@@ -0,0 +1,194 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+use OAuth2\Storage\AccessTokenInterface as AccessTokenStorageInterface;
+use OAuth2\Storage\RefreshTokenInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AccessToken implements AccessTokenInterface
+{
+    protected $tokenStorage;
+    protected $refreshStorage;
+    protected $config;
+
+    /**
+     * @param OAuth2\Storage\AccessTokenInterface  $tokenStorage   REQUIRED Storage class for saving access token information
+     * @param OAuth2\Storage\RefreshTokenInterface $refreshStorage OPTIONAL Storage class for saving refresh token information
+     * @param array                                $config         OPTIONAL Configuration options for the server
+     *                                                             <code>
+     *                                                             $config = array(
+     *                                                             'token_type' => 'bearer',              // token type identifier
+     *                                                             'access_lifetime' => 3600,             // time before access token expires
+     *                                                             'refresh_token_lifetime' => 1209600,   // time before refresh token expires
+     *                                                             );
+     *                                                             </endcode>
+     */
+    public function __construct(AccessTokenStorageInterface $tokenStorage, RefreshTokenInterface $refreshStorage = null, array $config = array())
+    {
+        $this->tokenStorage = $tokenStorage;
+        $this->refreshStorage = $refreshStorage;
+
+        $this->config = array_merge(array(
+            'token_type'             => 'bearer',
+            'access_lifetime'        => 3600,
+            'refresh_token_lifetime' => 1209600,
+        ), $config);
+    }
+
+    public function getAuthorizeResponse($params, $user_id = null)
+    {
+        // build the URL to redirect to
+        $result = array('query' => array());
+
+        $params += array('scope' => null, 'state' => null);
+
+        /*
+         * a refresh token MUST NOT be included in the fragment
+         *
+         * @see http://tools.ietf.org/html/rfc6749#section-4.2.2
+         */
+        $includeRefreshToken = false;
+        $result["fragment"] = $this->createAccessToken($params['client_id'], $user_id, $params['scope'], $includeRefreshToken);
+
+        if (isset($params['state'])) {
+            $result["fragment"]["state"] = $params['state'];
+        }
+
+        return array($params['redirect_uri'], $result);
+    }
+
+    /**
+     * Handle the creation of access token, also issue refresh token if supported / desirable.
+     *
+     * @param $client_id                client identifier related to the access token.
+     * @param $user_id                  user ID associated with the access token
+     * @param $scope                    OPTIONAL scopes to be stored in space-separated string.
+     * @param bool $includeRefreshToken if true, a new refresh_token will be added to the response
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-5
+     * @ingroup oauth2_section_5
+     */
+    public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true)
+    {
+        $token = array(
+            "access_token" => $this->generateAccessToken(),
+            "expires_in" => $this->config['access_lifetime'],
+            "token_type" => $this->config['token_type'],
+            "scope" => $scope
+        );
+
+        $this->tokenStorage->setAccessToken($token["access_token"], $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope);
+
+        /*
+         * Issue a refresh token also, if we support them
+         *
+         * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface
+         * is supplied in the constructor
+         */
+        if ($includeRefreshToken && $this->refreshStorage) {
+            $token["refresh_token"] = $this->generateRefreshToken();
+            $expires = 0;
+            if ($this->config['refresh_token_lifetime'] > 0) {
+                $expires = time() + $this->config['refresh_token_lifetime'];
+            }
+            $this->refreshStorage->setRefreshToken($token['refresh_token'], $client_id, $user_id, $expires, $scope);
+        }
+
+        return $token;
+    }
+
+    /**
+     * Generates an unique access token.
+     *
+     * Implementing classes may want to override this function to implement
+     * other access token generation schemes.
+     *
+     * @return
+     * An unique access token.
+     *
+     * @ingroup oauth2_section_4
+     */
+    protected function generateAccessToken()
+    {
+        if (function_exists('mcrypt_create_iv')) {
+            $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM);
+            if ($randomData !== false && strlen($randomData) === 20) {
+                return bin2hex($randomData);
+            }
+        }
+        if (function_exists('openssl_random_pseudo_bytes')) {
+            $randomData = openssl_random_pseudo_bytes(20);
+            if ($randomData !== false && strlen($randomData) === 20) {
+                return bin2hex($randomData);
+            }
+        }
+        if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data
+            $randomData = file_get_contents('/dev/urandom', false, null, 0, 20);
+            if ($randomData !== false && strlen($randomData) === 20) {
+                return bin2hex($randomData);
+            }
+        }
+        // Last resort which you probably should just get rid of:
+        $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true);
+
+        return substr(hash('sha512', $randomData), 0, 40);
+    }
+
+    /**
+     * Generates an unique refresh token
+     *
+     * Implementing classes may want to override this function to implement
+     * other refresh token generation schemes.
+     *
+     * @return
+     * An unique refresh.
+     *
+     * @ingroup oauth2_section_4
+     * @see OAuth2::generateAccessToken()
+     */
+    protected function generateRefreshToken()
+    {
+        return $this->generateAccessToken(); // let's reuse the same scheme for token generation
+    }
+
+    /**
+     * Handle the revoking of refresh tokens, and access tokens if supported / desirable
+     * RFC7009 specifies that "If the server is unable to locate the token using
+     * the given hint, it MUST extend its search across all of its supported token types"
+     *
+     * @param $token
+     * @param null $tokenTypeHint
+     * @return boolean
+     */
+    public function revokeToken($token, $tokenTypeHint = null)
+    {
+        if ($tokenTypeHint == 'refresh_token') {
+            if ($this->refreshStorage && $revoked = $this->refreshStorage->unsetRefreshToken($token)) {
+                return true;
+            }
+        }
+
+        /** @TODO remove in v2 */
+        if (!method_exists($this->tokenStorage, 'unsetAccessToken')) {
+            throw new \RuntimeException(
+                sprintf('Token storage %s must implement unsetAccessToken method', get_class($this->tokenStorage)
+            ));
+        }
+
+        $revoked = $this->tokenStorage->unsetAccessToken($token);
+
+        // if a typehint is supplied and fails, try other storages 
+        // @see https://tools.ietf.org/html/rfc7009#section-2.1
+        if (!$revoked && $tokenTypeHint != 'refresh_token') {
+            if ($this->refreshStorage) {
+                $revoked = $this->refreshStorage->unsetRefreshToken($token);
+            }
+        }
+
+        return $revoked;
+    }
+}

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor