Browse Source

[Web] Prepare for oauth2
[Web] Some lib updates
[Web] Allow to add a footer

andryyy 6 years ago
parent
commit
7a85abdb42
100 changed files with 14467 additions and 38 deletions
  1. 4 0
      data/web/admin.php
  2. 10 1
      data/web/inc/footer.inc.php
  3. 3 0
      data/web/inc/functions.customize.inc.php
  4. 208 0
      data/web/inc/functions.oauth2.inc.php
  5. 69 2
      data/web/inc/init_db.inc.php
  6. 3 2
      data/web/inc/lib/composer.json
  7. 94 33
      data/web/inc/lib/composer.lock
  8. 200 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/CHANGELOG.md
  9. 21 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/LICENSE
  10. 8 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/README.md
  11. 36 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/composer.json
  12. 54 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php
  13. 28 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php
  14. 139 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/HttpBasic.php
  15. 480 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeController.php
  16. 58 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeControllerInterface.php
  17. 156 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceController.php
  18. 41 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceControllerInterface.php
  19. 333 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenController.php
  20. 39 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenControllerInterface.php
  21. 34 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/EncryptionInterface.php
  22. 47 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/FirebaseJwt.php
  23. 223 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/Jwt.php
  24. 142 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php
  25. 98 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/ClientCredentials.php
  26. 59 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/GrantTypeInterface.php
  27. 247 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/JwtBearer.php
  28. 154 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/RefreshToken.php
  29. 123 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/UserCredentials.php
  30. 135 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeController.php
  31. 12 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php
  32. 62 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoController.php
  33. 30 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php
  34. 41 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/GrantType/AuthorizationCode.php
  35. 66 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php
  36. 27 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php
  37. 40 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdToken.php
  38. 9 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php
  39. 178 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdToken.php
  40. 30 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php
  41. 45 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenToken.php
  42. 9 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php
  43. 37 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php
  44. 35 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/UserClaimsInterface.php
  45. 252 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Request.php
  46. 39 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/RequestInterface.php
  47. 487 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Response.php
  48. 53 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseInterface.php
  49. 218 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessToken.php
  50. 33 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessTokenInterface.php
  51. 101 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCode.php
  52. 30 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCodeInterface.php
  53. 159 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/JwtAccessToken.php
  54. 13 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/ResponseTypeInterface.php
  55. 109 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Scope.php
  56. 35 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ScopeInterface.php
  57. 1019 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Server.php
  58. 65 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AccessTokenInterface.php
  59. 86 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AuthorizationCodeInterface.php
  60. 660 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Cassandra.php
  61. 49 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientCredentialsInterface.php
  62. 66 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientInterface.php
  63. 331 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/CouchbaseDB.php
  64. 540 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/DynamoDB.php
  65. 87 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessToken.php
  66. 14 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessTokenInterface.php
  67. 74 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtBearerInterface.php
  68. 381 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Memory.php
  69. 392 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Mongo.php
  70. 380 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/MongoDB.php
  71. 731 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php
  72. 30 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/PublicKeyInterface.php
  73. 321 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Redis.php
  74. 82 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/RefreshTokenInterface.php
  75. 46 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php
  76. 52 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/UserCredentialsInterface.php
  77. 130 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php
  78. 22 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php
  79. 21 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/TokenTypeInterface.php
  80. 18 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/AutoloadTest.php
  81. 493 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/AuthorizeControllerTest.php
  82. 177 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/ResourceControllerTest.php
  83. 332 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/TokenControllerTest.php
  84. 103 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/FirebaseJwtTest.php
  85. 103 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/JwtTest.php
  86. 224 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/AuthorizationCodeTest.php
  87. 160 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ClientCredentialsTest.php
  88. 144 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ImplicitTest.php
  89. 361 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/JwtBearerTest.php
  90. 205 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/RefreshTokenTest.php
  91. 173 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/UserCredentialsTest.php
  92. 183 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php
  93. 45 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php
  94. 58 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php
  95. 183 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php
  96. 185 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTest.php
  97. 92 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php
  98. 95 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php
  99. 41 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/UserClaimsTest.php
  100. 117 0
      data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/RequestTest.php

+ 4 - 0
data/web/admin.php

@@ -974,6 +974,10 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
               <label for="help_text"><?=$lang['admin']['help_text'];?>:</label>
               <label for="help_text"><?=$lang['admin']['help_text'];?>:</label>
               <textarea class="form-control" id="help_text" name="help_text" rows="7"><?=$ui_texts['help_text'];?></textarea>
               <textarea class="form-control" id="help_text" name="help_text" rows="7"><?=$ui_texts['help_text'];?></textarea>
             </div>
             </div>
+            <div class="form-group">
+              <label for="ui_impress"><?=$lang['admin']['ui_impress'];?>:</label>
+              <textarea class="form-control" id="ui_impress" name="ui_impress" rows="7"><?=$ui_texts['ui_impress'];?></textarea>
+            </div>
             <button class="btn btn-default" data-action="edit_selected" data-item="ui" data-id="uitexts" data-api-url='edit/ui_texts' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
             <button class="btn btn-default" data-action="edit_selected" data-item="ui" data-id="uitexts" data-api-url='edit/ui_texts' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
           </form>
           </form>
         </div>
         </div>

+ 10 - 1
data/web/inc/footer.inc.php

@@ -174,7 +174,16 @@ $(document).ready(function() {
   }
   }
 });
 });
 </script>
 </script>
-
+<?php
+if (!empty($UI_TEXTS['ui_impress'])):
+?>
+  <div class="container" style="margin-bottom:20px;color:#959595;">
+  <hr>
+    <?=$UI_TEXTS['ui_impress'];?>
+  </div>
+<?php
+endif;
+?>
 </body>
 </body>
 </html>
 </html>
 <?php
 <?php

+ 3 - 0
data/web/inc/functions.customize.inc.php

@@ -112,11 +112,13 @@ function customize($_action, $_item, $_data = null) {
           $main_name = $_data['main_name'];
           $main_name = $_data['main_name'];
           $apps_name = $_data['apps_name'];
           $apps_name = $_data['apps_name'];
           $help_text = $_data['help_text'];
           $help_text = $_data['help_text'];
+          $ui_impress = $_data['ui_impress'];
           try {
           try {
             $redis->set('TITLE_NAME', htmlspecialchars($title_name));
             $redis->set('TITLE_NAME', htmlspecialchars($title_name));
             $redis->set('MAIN_NAME', htmlspecialchars($main_name));
             $redis->set('MAIN_NAME', htmlspecialchars($main_name));
             $redis->set('APPS_NAME', htmlspecialchars($apps_name));
             $redis->set('APPS_NAME', htmlspecialchars($apps_name));
             $redis->set('HELP_TEXT', $help_text);
             $redis->set('HELP_TEXT', $help_text);
+            $redis->set('UI_IMPRESS', $ui_impress);
           }
           }
           catch (RedisException $e) {
           catch (RedisException $e) {
             $_SESSION['return'][] = array(
             $_SESSION['return'][] = array(
@@ -201,6 +203,7 @@ function customize($_action, $_item, $_data = null) {
             $data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : 'mailcow UI';
             $data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : 'mailcow UI';
             $data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : 'mailcow Apps';
             $data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : 'mailcow Apps';
             $data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false;
             $data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false;
+            $data['ui_impress'] = ($ui_impress = $redis->get('UI_IMPRESS')) ? $ui_impress : false;
             return $data;
             return $data;
           }
           }
           catch (RedisException $e) {
           catch (RedisException $e) {

+ 208 - 0
data/web/inc/functions.oauth2.inc.php

@@ -0,0 +1,208 @@
+<?php
+function oauth2($_action, $_type, $_data = null) {
+	global $pdo;
+	global $redis;
+	global $lang;
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+  switch ($_action) {
+    case 'add':
+      switch ($_type) {
+        case 'client':
+          $client_id = $_data['client_id'];
+          $client_secret = $_data['client_secret'];
+          $redirect_uri = $_data['redirect_uri'];
+          // $grant_type = isset($_data['grant_type']) ? $_data['grant_type'] : 'authorization_code';
+          // $scope = isset($_data['scope']) ? $_data['scope'] : 'profile';
+          if ($grant_type != "authorization_code" && $grant_type != "password") {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => sprintf($lang['danger']['access_denied'])
+            );
+            return false;
+          }
+          // For future use
+          if ($scope != "profile") {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => sprintf($lang['danger']['access_denied'])
+            );
+            return false;
+          }
+          if (!ctype_alnum($client_id) || !ctype_alnum($client_secret)) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => sprintf($lang['danger']['access_denied'])
+            );
+            return false;
+          }
+          $stmt = $pdo->prepare("SELECT 'client' FROM `oauth_clients`
+            WHERE `client_id` = :client_id");
+          $stmt->execute(array(':client_id' => $client_id));
+          $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+          if ($num_results != 0) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'Client ID exists'
+            );
+            return false;
+          }
+          $stmt = $pdo->prepare("INSERT INTO `oauth_clients` (`client_id`, `client_secret` ,`redirect_uri`)
+            VALUES (:client_id, :client_secret, :redirect_uri)");
+          $stmt->execute(array(
+            ':client_id' => $client_id,
+            ':client_secret' => $client_secret,
+            ':redirect_uri' => $redirect_uri
+          ));
+          $_SESSION['return'] = array(
+            'type' => 'success',
+            'msg' => 'Added client access'
+          );
+        break;
+      }
+    break;
+    case 'edit':
+      switch ($_type) {
+        case 'client':
+          $ids = (array)$_data['id'];
+          foreach ($ids as $id) {
+            $is_now = oauth2('details', 'client', $id);
+            if (!empty($is_now)) {
+              $client_id      = (!empty($_data['client_id'])) ? $_data['client_id'] : $is_now['client_id'];
+              $client_secret  = (!empty($_data['client_secret'])) ? $_data['client_secret'] : $is_now['client_secret'];
+              $redirect_uri   = (!empty($_data['redirect_uri'])) ? $_data['redirect_uri'] : $is_now['redirect_uri'];
+            }
+            else {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => sprintf($lang['danger']['access_denied'])
+              );
+              return false;
+            }
+            if (!ctype_alnum($client_id) || !ctype_alnum($client_secret)) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => 'Client ID and secret must be alphanumeric'
+              );
+              return false;
+            }
+            if (empty($redirect_uri)) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => 'Redirect/Callback URL cannot be empty'
+              );
+              return false;
+            }
+            $stmt = $pdo->prepare("UPDATE `oauth_clients` SET
+              `client_id` = :client_id,
+              `client_secret` = :client_secret,
+              `redirect_uri` = :redirect_uri
+                WHERE `id` = :id");
+            $stmt->execute(array(
+              ':id' => $id,
+              ':client_id' => $client_id,
+              ':client_secret' => $client_secret,
+              ':redirect_uri' => $redirect_uri
+            ));
+          }
+          $_SESSION['return'] = array(
+            'type' => 'success',
+            'msg' => sprintf($lang['success']['object_modified'], htmlspecialchars(implode(', ', $ids)))
+          );
+        break;
+      }
+    break;
+    case 'delete':
+      switch ($_type) {
+        case 'client':
+          (array)$ids = $_data['id'];
+          foreach ($ids as $id) {
+            if (!is_numeric($id)) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => sprintf($lang['danger']['access_denied'])
+              );
+              return false;
+            }
+            $stmt = $pdo->prepare("DELETE FROM `oauth_clients` WHERE `id` = :id");
+            $stmt->execute(array(
+              ':id' => $id
+            ));
+          }
+          $_SESSION['return'] = array(
+            'type' => 'success',
+            'msg' => sprintf($lang['success']['items_deleted'], implode(', ', $ids))
+          );
+        break;
+        case 'access_token':
+          (array)$access_tokens = $_data['access_token'];
+          foreach ($access_tokens as $access_token) {
+            if (!ctype_alnum($access_token)) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => sprintf($lang['danger']['access_denied'])
+              );
+              return false;
+            }
+            $stmt = $pdo->prepare("DELETE FROM `oauth_access_tokens` WHERE `access_token` = :access_token");
+            $stmt->execute(array(
+              ':access_token' => $access_token
+            ));
+          }
+          $_SESSION['return'] = array(
+            'type' => 'success',
+            'msg' => sprintf($lang['success']['items_deleted'], implode(', ', $access_tokens))
+          );
+        break;
+        case 'refresh_token':
+          (array)$refresh_tokens = $_data['refresh_token'];
+          foreach ($refresh_tokens as $refresh_token) {
+            if (!ctype_alnum($refresh_token)) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => sprintf($lang['danger']['access_denied'])
+              );
+              return false;
+            }
+            $stmt = $pdo->prepare("DELETE FROM `oauth_refresh_tokens` WHERE `refresh_token` = :refresh_token");
+            $stmt->execute(array(
+              ':refresh_token' => $refresh_token
+            ));
+          }
+          $_SESSION['return'] = array(
+            'type' => 'success',
+            'msg' => sprintf($lang['success']['items_deleted'], implode(', ', $refresh_tokens))
+          );
+        break;
+      }
+    break;
+    case 'get':
+      switch ($_type) {
+        case 'clients':
+          $stmt = $pdo->query("SELECT `id` FROM `oauth_clients`");
+          $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+          while ($row = array_shift($rows)) {
+            $oauth_clients[] = $row['id'];
+          }
+          return $oauth_clients;
+        break;
+      }
+    break;
+    case 'details':
+      switch ($_type) {
+        case 'client':
+          $stmt = $pdo->prepare("SELECT * FROM `oauth_clients`
+            WHERE `id` = :id");
+          $stmt->execute(array(':id' => $_data));
+          $oauth_client_details = $stmt->fetch(PDO::FETCH_ASSOC);
+          return $oauth_client_details;
+        break;
+      }
+    break;
+  }
+}

+ 69 - 2
data/web/inc/init_db.inc.php

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
   try {
     global $pdo;
     global $pdo;
 
 
-    $db_version = "22092019_0940";
+    $db_version = "27092019_1040";
 
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -330,7 +330,7 @@ function init_db_schema() {
           "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'",
-          "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "quarantine_attachments" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "quarantine_attachments" => "TINYINT(1) NOT NULL DEFAULT '1'",
@@ -783,6 +783,73 @@ function init_db_schema() {
           )
           )
         ),
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
+      "oauth_clients" => array(
+        "cols" => array(
+          "id" => "INT NOT NULL AUTO_INCREMENT",
+          "client_id" => "VARCHAR(80) NOT NULL",
+          "client_secret" => "VARCHAR(80)",
+          "redirect_uri" => "VARCHAR(2000)",
+          "grant_types" => "VARCHAR(80)",
+          "scope" => "VARCHAR(4000)",
+          "user_id" => "VARCHAR(80)"
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("client_id")
+          ),
+          "unique" => array(
+            "id" => array("id")
+          )
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
+      "oauth_access_tokens" => array(
+        "cols" => array(
+          "access_token" => "VARCHAR(40) NOT NULL",
+          "client_id" => "VARCHAR(80) NOT NULL",
+          "user_id" => "VARCHAR(80)",
+          "expires" => "TIMESTAMP NOT NULL",
+          "scope" => "VARCHAR(4000)"
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("access_token")
+          )
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
+      "oauth_authorization_codes" => array(
+        "cols" => array(
+          "authorization_code" => "VARCHAR(40) NOT NULL",
+          "client_id" => "VARCHAR(80) NOT NULL",
+          "user_id" => "VARCHAR(80)",
+          "redirect_uri" => "VARCHAR(2000)",
+          "expires" => "TIMESTAMP NOT NULL",
+          "scope" => "VARCHAR(4000)",
+          "id_token" => "VARCHAR(1000)"
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("authorization_code")
+          )
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
+      "oauth_refresh_tokens" => array(
+        "cols" => array(
+          "refresh_token" => "VARCHAR(40) NOT NULL",
+          "client_id" => "VARCHAR(80) NOT NULL",
+          "user_id" => "VARCHAR(80)",
+          "expires" => "TIMESTAMP NOT NULL",
+          "scope" => "VARCHAR(4000)"
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("refresh_token")
+          )
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
       )
       )
     );
     );
 
 

+ 3 - 2
data/web/inc/lib/composer.json

@@ -3,9 +3,10 @@
         "robthree/twofactorauth": "^1.6",
         "robthree/twofactorauth": "^1.6",
         "yubico/u2flib-server": "^1.0",
         "yubico/u2flib-server": "^1.0",
         "phpmailer/phpmailer": "^5.2",
         "phpmailer/phpmailer": "^5.2",
-        "php-mime-mail-parser/php-mime-mail-parser": "^5.1",
+        "php-mime-mail-parser/php-mime-mail-parser": "^5.0.5",
         "soundasleep/html2text": "^0.5.0",
         "soundasleep/html2text": "^0.5.0",
         "ddeboer/imap": "^1.5",
         "ddeboer/imap": "^1.5",
-        "matthiasmullie/minify": "^1.3"
+        "matthiasmullie/minify": "^1.3",
+        "bshaffer/oauth2-server-php": "^1.11"
     }
     }
 }
 }

+ 94 - 33
data/web/inc/lib/composer.lock

@@ -4,20 +4,78 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
         "This file is @generated automatically"
     ],
     ],
-    "content-hash": "e72f119b7f62fea0aa6123109abb9a35",
+    "content-hash": "c435e39c1b3cbefbecebc3dc6fcc8d89",
     "packages": [
     "packages": [
+        {
+            "name": "bshaffer/oauth2-server-php",
+            "version": "v1.11.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/bshaffer/oauth2-server-php.git",
+                "reference": "5a0c8000d4763b276919e2106f54eddda6bc50fa"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/bshaffer/oauth2-server-php/zipball/5a0c8000d4763b276919e2106f54eddda6bc50fa",
+                "reference": "5a0c8000d4763b276919e2106f54eddda6bc50fa",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.9"
+            },
+            "require-dev": {
+                "aws/aws-sdk-php": "~2.8",
+                "firebase/php-jwt": "~2.2",
+                "mongodb/mongodb": "^1.1",
+                "phpunit/phpunit": "^4.0",
+                "predis/predis": "dev-master",
+                "thobbs/phpcassa": "dev-master"
+            },
+            "suggest": {
+                "aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage",
+                "firebase/php-jwt": "~2.2 is required to use JWT features",
+                "mongodb/mongodb": "^1.1 is required to use MongoDB storage",
+                "predis/predis": "Required to use Redis storage",
+                "thobbs/phpcassa": "Required to use Cassandra storage"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "OAuth2": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Brent Shaffer",
+                    "email": "bshafs@gmail.com",
+                    "homepage": "http://brentertainment.com"
+                }
+            ],
+            "description": "OAuth2 Server for PHP",
+            "homepage": "http://github.com/bshaffer/oauth2-server-php",
+            "keywords": [
+                "auth",
+                "oauth",
+                "oauth2"
+            ],
+            "time": "2018-12-04T00:29:32+00:00"
+        },
         {
         {
             "name": "ddeboer/imap",
             "name": "ddeboer/imap",
-            "version": "1.6.0",
+            "version": "1.8.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/ddeboer/imap.git",
                 "url": "https://github.com/ddeboer/imap.git",
-                "reference": "4d3b31c7cc5eb3cf3a8a0369fabd0d6e3f39cede"
+                "reference": "ff985d72916267cba2f944e7c9ee654c69893219"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/ddeboer/imap/zipball/4d3b31c7cc5eb3cf3a8a0369fabd0d6e3f39cede",
-                "reference": "4d3b31c7cc5eb3cf3a8a0369fabd0d6e3f39cede",
+                "url": "https://api.github.com/repos/ddeboer/imap/zipball/ff985d72916267cba2f944e7c9ee654c69893219",
+                "reference": "ff985d72916267cba2f944e7c9ee654c69893219",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -27,10 +85,11 @@
                 "php": "^7.1"
                 "php": "^7.1"
             },
             },
             "require-dev": {
             "require-dev": {
-                "friendsofphp/php-cs-fixer": "^2.13",
-                "phpstan/phpstan": "^0.9.1",
-                "phpstan/phpstan-phpunit": "^0.9.3",
-                "phpunit/phpunit": "^7.4",
+                "friendsofphp/php-cs-fixer": "^2.14",
+                "phpstan/phpstan": "^0.11",
+                "phpstan/phpstan-phpunit": "^0.11",
+                "phpstan/phpstan-strict-rules": "^0.11.0",
+                "phpunit/phpunit": "^7.5",
                 "zendframework/zend-mail": "^2.10"
                 "zendframework/zend-mail": "^2.10"
             },
             },
             "type": "library",
             "type": "library",
@@ -63,7 +122,7 @@
                 "imap",
                 "imap",
                 "mail"
                 "mail"
             ],
             ],
-            "time": "2018-12-04T13:35:19+00:00"
+            "time": "2019-04-15T09:18:52+00:00"
         },
         },
         {
         {
             "name": "matthiasmullie/minify",
             "name": "matthiasmullie/minify",
@@ -221,31 +280,31 @@
         },
         },
         {
         {
             "name": "php-mime-mail-parser/php-mime-mail-parser",
             "name": "php-mime-mail-parser/php-mime-mail-parser",
-            "version": "2.11.1",
+            "version": "5.0.5",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/php-mime-mail-parser/php-mime-mail-parser.git",
                 "url": "https://github.com/php-mime-mail-parser/php-mime-mail-parser.git",
-                "reference": "4769e942ed0dbbdd7882fc390b119d625463c8af"
+                "reference": "27983433aabeccee832573c3c56e6a4855e57745"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-mime-mail-parser/php-mime-mail-parser/zipball/4769e942ed0dbbdd7882fc390b119d625463c8af",
-                "reference": "4769e942ed0dbbdd7882fc390b119d625463c8af",
+                "url": "https://api.github.com/repos/php-mime-mail-parser/php-mime-mail-parser/zipball/27983433aabeccee832573c3c56e6a4855e57745",
+                "reference": "27983433aabeccee832573c3c56e6a4855e57745",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
                 "ext-mailparse": "*",
                 "ext-mailparse": "*",
-                "php": "^5.4.0 || ^7.0"
+                "php": "^7.1"
             },
             },
             "replace": {
             "replace": {
                 "exorus/php-mime-mail-parser": "*",
                 "exorus/php-mime-mail-parser": "*",
                 "messaged/php-mime-mail-parser": "*"
                 "messaged/php-mime-mail-parser": "*"
             },
             },
             "require-dev": {
             "require-dev": {
-                "phpunit/php-token-stream": "^1.3.0",
-                "phpunit/phpunit": "^4.0 || ^5.0",
-                "satooshi/php-coveralls": "0.*",
-                "squizlabs/php_codesniffer": "2.*"
+                "php-coveralls/php-coveralls": "^2.1",
+                "phpunit/php-token-stream": "^3.0",
+                "phpunit/phpunit": "^7.0",
+                "squizlabs/php_codesniffer": "^3.4"
             },
             },
             "type": "library",
             "type": "library",
             "autoload": {
             "autoload": {
@@ -258,12 +317,6 @@
                 "MIT"
                 "MIT"
             ],
             ],
             "authors": [
             "authors": [
-                {
-                    "name": "bucabay",
-                    "email": "gabe@fijiwebdesign.com",
-                    "homepage": "http://www.fijiwebdesign.com",
-                    "role": "Developer"
-                },
                 {
                 {
                     "name": "eXorus",
                     "name": "eXorus",
                     "email": "exorus.spam@gmail.com",
                     "email": "exorus.spam@gmail.com",
@@ -287,17 +340,25 @@
                     "email": "alkne@gmail.com",
                     "email": "alkne@gmail.com",
                     "homepage": "https://code.google.com/p/php-mime-mail-parser",
                     "homepage": "https://code.google.com/p/php-mime-mail-parser",
                     "role": "Developer"
                     "role": "Developer"
+                },
+                {
+                    "name": "bucabay",
+                    "email": "gabe@fijiwebdesign.com",
+                    "homepage": "http://www.fijiwebdesign.com",
+                    "role": "Developer"
                 }
                 }
             ],
             ],
-            "description": "Fully Tested Mailparse Extension Wrapper for PHP 5.4+",
+            "description": "A fully tested email parser for PHP 7.1+ (mailparse extension wrapper).",
             "homepage": "https://github.com/php-mime-mail-parser/php-mime-mail-parser",
             "homepage": "https://github.com/php-mime-mail-parser/php-mime-mail-parser",
             "keywords": [
             "keywords": [
                 "MimeMailParser",
                 "MimeMailParser",
                 "mail",
                 "mail",
                 "mailparse",
                 "mailparse",
-                "mime"
+                "mime",
+                "parser",
+                "php"
             ],
             ],
-            "time": "2018-04-30T05:55:59+00:00"
+            "time": "2019-09-23T11:57:58+00:00"
         },
         },
         {
         {
             "name": "phpmailer/phpmailer",
             "name": "phpmailer/phpmailer",
@@ -378,16 +439,16 @@
         },
         },
         {
         {
             "name": "robthree/twofactorauth",
             "name": "robthree/twofactorauth",
-            "version": "1.6.5",
+            "version": "1.6.7",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/RobThree/TwoFactorAuth.git",
                 "url": "https://github.com/RobThree/TwoFactorAuth.git",
-                "reference": "f5f58a4c62d0336a0e6175856894a51f3565dad2"
+                "reference": "3407c33775391fa8c36f7d766f26c5e59a736374"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/f5f58a4c62d0336a0e6175856894a51f3565dad2",
-                "reference": "f5f58a4c62d0336a0e6175856894a51f3565dad2",
+                "url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/3407c33775391fa8c36f7d766f26c5e59a736374",
+                "reference": "3407c33775391fa8c36f7d766f26c5e59a736374",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
@@ -425,7 +486,7 @@
                 "php",
                 "php",
                 "tfa"
                 "tfa"
             ],
             ],
-            "time": "2018-06-09T10:09:59+00:00"
+            "time": "2019-06-21T08:51:04+00:00"
         },
         },
         {
         {
             "name": "soundasleep/html2text",
             "name": "soundasleep/html2text",

+ 200 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/CHANGELOG.md

@@ -0,0 +1,200 @@
+CHANGELOG for 1.x
+=================
+
+This changelog references the relevant changes (bug and security fixes) done
+in 1.x minor versions.
+
+To see the files changed for a given bug, go to https://github.com/bshaffer/oauth2-server-php/issues/### where ### is the bug number
+To get the diff between two versions, go to https://github.com/bshaffer/oauth2-server-php/compare/v1.0...v1.1
+To get the diff for a specific change, go to https://github.com/bshaffer/oauth2-server-php/commit/XXX where XXX is the change hash
+
+* 1.10.0 (2017-11-15)
+
+  PR: https://github.com/bshaffer/oauth2-server-php/pull/889
+
+  * #795 - [feature] added protected createPayload method to allow easier customization of JWT payload
+  * #807 - [refactor] simplifies UserInfoController constructor
+  * #814 - [docs] Adds https to README link
+  * #827 - [testing] Explicitly pulls in phpunit 4
+  * #828 - [docs] PHPDoc improvements and type hinting of variables.
+  * #829 - [bug] Fix CORS issue for revoking and requesting an access token
+  * #869 - [testing] Remove php 5.3 from travis and use vendored phpunit
+  * #834 - [feature] use random_bytes if available
+  * #851 - [docs] Fix PHPDoc
+  * #872 - [bug] Fix count() error on PHP 7.2
+  * #873 - [testing] adds php 7.2 to travis
+  * #794 - [docs] Fix typo in composer.json
+  * #885 - [testing] Use PHPUnit\Framework\TestCase instead of PHPUnit_Framework_TestCase
+
+* 1.9.0 (2017-01-06)
+
+  PR: https://github.com/bshaffer/oauth2-server-php/pull/788
+
+  * bug #645 - Allow null for client_secret
+  * bug #651 - Fix bug in isPublicClient of Cassandra Storage
+  * bug #670 - Bug in client's scope restriction
+  * bug #672 - Implemented method to override the password hashing algorithm
+  * bug #698 - Fix Token Response's Content-Type to application/json
+  * bug #729 - Ensures unsetAccessToken and unsetRefreshToken return a bool
+  * bug #749 - Fix UserClaims for CodeIdToken
+  * bug #784 - RFC6750 compatibility
+  * bug #776 - Fix "redirect_uri_mismatch" for URIs with encoded characters
+  * bug #759 - no access token supplied to resource controller results in empty request body
+  * bug #773 - Use OpenSSL random method before attempting Mcrypt's.
+  * bug #790 - Add mongo db
+
+* 1.8.0 (2015-09-18)
+
+  PR: https://github.com/bshaffer/oauth2-server-php/pull/643
+
+  * bug #594 - adds jti
+  * bug #598 - fixes lifetime configurations for JWTs
+  * bug #634 - fixes travis builds, upgrade to containers
+  * bug #586 - support for revoking tokens
+  * bug #636 - Adds FirebaseJWT bridge
+  * bug #639 - Mongo HHVM compatibility
+
+* 1.7.0 (2015-04-23)
+
+  PR: https://github.com/bshaffer/oauth2-server-php/pull/572
+
+  * bug #500 - PDO fetch mode changed from FETCH_BOTH to FETCH_ASSOC
+  * bug #508 - Case insensitive for Bearer token header name  ba716d4
+  * bug #512 - validateRedirectUri is now public
+  * bug #530 - Add PublicKeyInterface, UserClaimsInterface to Cassandra Storage
+  * bug #505 - DynamoDB storage fixes
+  * bug #556 - adds "code id_token" return type to openid connect
+  * bug #563 - Include "issuer" config key for JwtAccessToken
+  * bug #564 - Fixes JWT vulnerability
+  * bug #571 - Added unset_refresh_token_after_use option
+
+* 1.6 (2015-01-16)
+
+  PR: https://github.com/bshaffer/oauth2-server-php/pull/496
+
+  * bug 437 - renames CryptoToken to JwtAccessToken / use_crypto_tokens to use_jwt_access_tokens
+  * bug 447 - Adds a Couchbase storage implementation
+  * bug 460 - Rename JWT claims to match spec
+  * bug 470 - order does not matter for multi-valued response types
+  * bug 471 - Make validateAuthorizeRequest available for POST in addition to GET
+  * bug 475 - Adds JTI table definitiion
+  * bug 481 - better randomness for generating access tokens
+  * bug 480 - Use hash_equals() for signature verification (prevents remote timing attacks)
+  * bugs 489, 491, 498 - misc other fixes
+
+* 1.5 (2014-08-27)
+
+  PR: https://github.com/bshaffer/oauth2-server-php/pull/446
+
+  * bug #399 - Add DynamoDB Support
+  * bug #404 - renamed error name for malformed/expired tokens
+  * bug #412 - Openid connect: fixes for claims with more than one scope / Add support for the prompt parameter ('consent' and 'none')
+  * bug #411 - fixes xml output
+  * bug #413 - fixes invalid format error
+  * bug #401 - fixes code standards / whitespace
+  * bug #354 - bundles PDO SQL with the library
+  * [BC] bug #397 - refresh tokens should not be encrypted
+  * bug #423 - makes "scope" optional for refresh token storage
+
+* 1.4 (2014-06-12)
+
+  PR: https://github.com/bshaffer/oauth2-server-php/pull/392
+
+  * bug #189 Storage\PDO - allows DSN string in constructor
+  * bug #233 Bearer Tokens - allows token in request body for PUT requests
+  * bug #346 Fixes open_basedir warning
+  * bug #351 Adds OpenID Connect support
+  * bug #355 Adds php 5.6 and HHVM to travis.ci testing
+  * [BC] bug #358 Adds `getQueryStringIdentifier()` to the GrantType interface
+  * bug #363 Encryption\JWT - Allows for subclassing JWT Headers
+  * bug #349 Bearer Tokens - adds requestHasToken method for when access tokens are optional
+  * bug #301 Encryption\JWT - fixes urlSafeB64Encode(): ensures newlines are replaced as expected
+  * bug #323 ResourceController - client_id is no longer required to be returned when calling getAccessToken
+  * bug #367 Storage\PDO - adds Postgres support
+  * bug #368 Access Tokens - use mcrypt_create_iv or openssl_random_pseudo_bytes to create token string
+  * bug #376 Request - allows case insensitive headers
+  * bug #384 Storage\PDO - can pass in PDO options in constructor of PDO storage
+  * misc fixes #361, #292, #373, #374, #379, #396
+* 1.3 (2014-02-27)
+
+  PR: https://github.com/bshaffer/oauth2-server-php/pull/325
+
+  * bug #311 adds cassandra storage
+  * bug #298 fixes response code for user credentials grant type
+  * bug #318 adds 'use_crypto_tokens' config to Server class for better DX
+  * [BC] bug #320 pass client_id to getDefaultScope
+  * bug #324 better feedback when running tests
+  * bug #335 adds support for non-expiring refresh tokens
+  * bug #333 fixes Pdo storage for getClientKey
+  * bug #336 fixes Redis storage for expireAuthorizationCode
+
+* 1.3 (2014-02-27)
+
+  PR: https://github.com/bshaffer/oauth2-server-php/pull/325
+
+  * bug #311 adds cassandra storage
+  * bug #298 fixes response code for user credentials grant type
+  * bug #318 adds 'use_crypto_tokens' config to Server class for better DX
+  * bug #320 pass client_id to getDefaultScope
+  * bug #324 better feedback when running tests
+  * bug #335 adds support for non-expiring refresh tokens
+  * bug #333 fixes Pdo storage for getClientKey
+  * bug #336 fixes Redis storage for expireAuthorizationCode
+
+* 1.2 (2014-01-03)
+
+  PR: https://github.com/bshaffer/oauth2-server-php/pull/288
+
+  * bug #285 changed response header from 200 to 401 when empty token received
+  * bug #286 adds documentation and links to spec for not including error messages when no token is supplied
+  * bug #280 ensures PHP warnings do not get thrown as a result of an invalid argument to $jwt->decode()
+  * bug #279 predis wrong number of arguments
+  * bug #277 Securing JS WebApp client secret w/ password grant type
+
+* 1.1 (2013-12-17)
+
+  PR: https://github.com/bshaffer/oauth2-server-php/pull/276
+
+  * bug #278 adds refresh token configuration to Server class
+  * bug #274 Supplying a null client_id and client_secret grants API access
+  * bug #244 [MongoStorage] More detailed implementation info
+  * bug #268 Implement jti for JWT Bearer tokens to prevent replay attacks.
+  * bug #266 Removing unused argument to getAccessTokenData
+  * bug #247 Make Bearer token type consistent
+  * bug #253 Fixing CryptoToken refresh token lifetime
+  * bug #246 refactors public key logic to be more intuitive
+  * bug #245 adds support for JSON crypto tokens
+  * bug #230 Remove unused columns in oauth_clients
+  * bug #215 makes Redis Scope Storage obey the same paradigm as PDO
+  * bug #228 removes scope group
+  * bug #227 squelches open basedir restriction error
+  * bug #223 Updated docblocks for RefreshTokenInterface.php
+  * bug #224 Adds protected properties
+  * bug #217 Implement ScopeInterface for PDO, Redis
+
+* 1.0 (2013-08-12)
+
+  * bug #203 Add redirect\_status_code config param for AuthorizeController
+  * bug #205 ensures unnecessary ? is not set when  ** bug
+  * bug #204 Fixed call to LogicException
+  * bug #202 Add explode to checkRestrictedGrant in PDO Storage
+  * bug #197 adds support for 'false' default scope  ** bug
+  * bug #192 reference errors and adds tests
+  * bug #194 makes some appropriate properties  ** bug
+  * bug #191 passes config to HttpBasic
+  * bug #190 validates client credentials before  ** bug
+  * bug #171 Fix wrong redirect following authorization step
+  * bug #187 client_id is now passed to getDefaultScope().
+  * bug #176 Require refresh_token in getRefreshToken response
+  * bug #174 make user\_id not required for refresh_token grant
+  * bug #173 Duplication in JwtBearer Grant
+  * bug #168 user\_id not required for authorization_code grant
+  * bug #133 hardens default security for user object
+  * bug #163 allows redirect\_uri on authorization_code to be NULL in docs example
+  * bug #162 adds getToken on ResourceController for convenience
+  * bug #161 fixes fatal error
+  * bug #163 Invalid redirect_uri handling
+  * bug #156 user\_id in OAuth2\_Storage_AuthorizationCodeInterface::getAuthorizationCode() response
+  * bug #157 Fix for extending access and refresh tokens
+  * bug #154 ResponseInterface: getParameter method is used in the library but not defined in the interface
+  * bug #148 Add more detail to examples in Readme.md

+ 21 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/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.

+ 8 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/README.md

@@ -0,0 +1,8 @@
+oauth2-server-php
+=================
+
+[![Build Status](https://travis-ci.org/bshaffer/oauth2-server-php.svg?branch=master)](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](https://bshaffer.github.io/oauth2-server-php-docs/)

+ 36 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/composer.json

@@ -0,0 +1,36 @@
+{
+    "name": "bshaffer/oauth2-server-php",
+    "description":"OAuth2 Server for PHP",
+    "keywords":["oauth","oauth2","auth"],
+    "type":"library",
+    "license":"MIT",
+    "authors":[
+        {
+            "name":"Brent Shaffer",
+            "email": "bshafs@gmail.com",
+            "homepage":"http://brentertainment.com"
+        }
+    ],
+    "homepage": "http://github.com/bshaffer/oauth2-server-php",
+    "autoload": {
+        "psr-0": { "OAuth2": "src/" }
+    },
+    "require":{
+        "php":">=5.3.9"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^4.0",
+        "aws/aws-sdk-php": "~2.8",
+        "firebase/php-jwt": "~2.2",
+        "predis/predis": "dev-master",
+        "thobbs/phpcassa": "dev-master",
+        "mongodb/mongodb": "^1.1"
+    },
+    "suggest": {
+        "predis/predis": "Required to use Redis storage",
+        "thobbs/phpcassa": "Required to use Cassandra storage",
+        "aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage",
+        "firebase/php-jwt": "~2.2 is required to use JWT features",
+        "mongodb/mongodb": "^1.1 is required to use MongoDB storage"
+    }
+}

+ 54 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace OAuth2;
+
+/**
+ * Autoloads OAuth2 classes
+ *
+ * @author    Brent Shaffer <bshafs at gmail dot com>
+ * @license   MIT License
+ */
+class Autoloader
+{
+    /**
+     * @var string
+     */
+    private $dir;
+
+    /**
+     * @param string $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;
+        }
+    }
+}

+ 28 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace OAuth2\ClientAssertionType;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * Interface for all OAuth2 Client Assertion Types
+ */
+interface ClientAssertionTypeInterface
+{
+    /**
+     * Validate the OAuth request
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return mixed
+     */
+    public function validateRequest(RequestInterface $request, ResponseInterface $response);
+
+    /**
+     * Get the client id
+     *
+     * @return mixed
+     */
+    public function getClientId();
+}

+ 139 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/HttpBasic.php

@@ -0,0 +1,139 @@
+<?php
+
+namespace OAuth2\ClientAssertionType;
+
+use OAuth2\Storage\ClientCredentialsInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use LogicException;
+
+/**
+ * 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;
+
+    /**
+     * Config array $config should look as follows:
+     * @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
+     *     );
+     * @endcode
+     *
+     * @param ClientCredentialsInterface $storage Storage
+     * @param array                      $config  Configuration options for the server
+     */
+    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);
+    }
+
+    /**
+     * Validate the OAuth request
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return bool|mixed
+     * @throws LogicException
+     */
+    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;
+    }
+
+    /**
+     * Get the client id
+     *
+     * @return mixed
+     */
+    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.
+     *
+     * @param RequestInterface  $request
+     * @param ResponseInterface $response
+     * @return array|null 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;
+    }
+}

+ 480 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeController.php

@@ -0,0 +1,480 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\Storage\ClientInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use OAuth2\Scope;
+use InvalidArgumentException;
+
+/**
+ * @see AuthorizeControllerInterface
+ */
+class AuthorizeController implements AuthorizeControllerInterface
+{
+    /**
+     * @var string
+     */
+    private $scope;
+
+    /**
+     * @var int
+     */
+    private $state;
+
+    /**
+     * @var mixed
+     */
+    private $client_id;
+
+    /**
+     * @var string
+     */
+    private $redirect_uri;
+
+    /**
+     * The response type
+     *
+     * @var string
+     */
+    private $response_type;
+
+    /**
+     * @var ClientInterface
+     */
+    protected $clientStorage;
+
+    /**
+     * @var array
+     */
+    protected $responseTypes;
+
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * @var ScopeInterface
+     */
+    protected $scopeUtil;
+
+    /**
+     * Constructor
+     *
+     * @param 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:
+     * @param ScopeInterface  $scopeUtil     OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope
+     * @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
+     *     );
+     * @endcode
+     */
+    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;
+    }
+
+    /**
+     * Handle the authorization request
+     *
+     * @param RequestInterface  $request
+     * @param ResponseInterface $response
+     * @param boolean           $is_authorized
+     * @param mixed             $user_id
+     * @return mixed|void
+     * @throws InvalidArgumentException
+     */
+    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);
+    }
+
+    /**
+     * Set not authorized response
+     *
+     * @param RequestInterface  $request
+     * @param ResponseInterface $response
+     * @param string            $redirect_uri
+     * @param mixed             $user_id
+     */
+    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
+     *
+     * @TODO: add dependency injection for the parameters in this method
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @param mixed $user_id
+     * @return array
+     */
+    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;
+    }
+
+    /**
+     * Validate the OAuth request
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return bool
+     */
+    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 response type
+        $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 ((empty($clientScope) && !$this->scopeUtil->scopeExists($requestedScope))
+                || (!empty($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 string $uri    An absolute URI.
+     * @param array  $params Parameters to be append as GET.
+     *
+     * @return string
+     * 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 the uri 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
+     * @return bool
+     * @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 {
+                $registered_uri_length = strlen($registered_uri);
+                if ($registered_uri_length === 0) {
+                    return false;
+                }
+
+                // 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, $registered_uri_length), $registered_uri) === 0) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Convenience method to access the scope
+     *
+     * @return string
+     */
+    public function getScope()
+    {
+        return $this->scope;
+    }
+
+    /**
+     * Convenience method to access the state
+     *
+     * @return int
+     */
+    public function getState()
+    {
+        return $this->state;
+    }
+
+    /**
+     * Convenience method to access the client id
+     *
+     * @return mixed
+     */
+    public function getClientId()
+    {
+        return $this->client_id;
+    }
+
+    /**
+     * Convenience method to access the redirect url
+     *
+     * @return string
+     */
+    public function getRedirectUri()
+    {
+        return $this->redirect_uri;
+    }
+
+    /**
+     * Convenience method to access the response type
+     *
+     * @return string
+     */
+    public function getResponseType()
+    {
+        return $this->response_type;
+    }
+}

+ 58 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/AuthorizeControllerInterface.php

@@ -0,0 +1,58 @@
+<?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
+ *
+ *  @code
+ *      $user_id = $this->somehowDetermineUserId();
+ *      $is_authorized = $this->somehowDetermineUserAuthorization();
+ *      $response = new OAuth2\Response();
+ *      $authorizeController->handleAuthorizeRequest(
+ *          OAuth2\Request::createFromGlobals(),
+ *          $response,
+ *          $is_authorized,
+ *          $user_id
+ *      );
+ *      $response->send();
+ * @endcode
+ */
+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';
+
+    /**
+     * Handle the OAuth request
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @param $is_authorized
+     * @param null $user_id
+     * @return mixed
+     */
+    public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null);
+
+    /**
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return bool
+     */
+    public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response);
+}

+ 156 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceController.php

@@ -0,0 +1,156 @@
+<?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 ResourceControllerInterface
+ */
+class ResourceController implements ResourceControllerInterface
+{
+    /**
+     * @var array
+     */
+    private $token;
+
+    /**
+     * @var TokenTypeInterface
+     */
+    protected $tokenType;
+
+    /**
+     * @var AccessTokenInterface
+     */
+    protected $tokenStorage;
+
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * @var ScopeInterface
+     */
+    protected $scopeUtil;
+
+    /**
+     * Constructor
+     *
+     * @param TokenTypeInterface   $tokenType
+     * @param AccessTokenInterface $tokenStorage
+     * @param array                $config
+     * @param ScopeInterface       $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;
+    }
+
+    /**
+     * Verify the resource request
+     *
+     * @param RequestInterface  $request
+     * @param ResponseInterface $response
+     * @param null              $scope
+     * @return bool
+     */
+    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;
+    }
+
+    /**
+     * Get access token data.
+     *
+     * @param RequestInterface  $request
+     * @param ResponseInterface $response
+     * @return array|null
+     */
+    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, 'invalid_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.
+     *
+     * @return array
+     */
+    public function getToken()
+    {
+        return $this->token;
+    }
+}

+ 41 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/ResourceControllerInterface.php

@@ -0,0 +1,41 @@
+<?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.
+ *
+ * @code
+ *     if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) {
+ *         $response->send(); // authorization failed
+ *         die();
+ *     }
+ *     return json_encode($resource); // valid token!  Send the stuff!
+ * @endcode
+ */
+interface ResourceControllerInterface
+{
+    /**
+     * Verify the resource request
+     *
+     * @param RequestInterface  $request  - Request object
+     * @param ResponseInterface $response - Response object
+     * @param string            $scope
+     * @return mixed
+     */
+    public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null);
+
+    /**
+     * Get access token data.
+     *
+     * @param RequestInterface  $request  - Request object
+     * @param ResponseInterface $response - Response object
+     * @return mixed
+     */
+    public function getAccessTokenData(RequestInterface $request, ResponseInterface $response);
+}

+ 333 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenController.php

@@ -0,0 +1,333 @@
+<?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;
+use InvalidArgumentException;
+use LogicException;
+use RuntimeException;
+
+/**
+ * @see TokenControllerInterface
+ */
+class TokenController implements TokenControllerInterface
+{
+    /**
+     * @var AccessTokenInterface
+     */
+    protected $accessToken;
+
+    /**
+     * @var array<GrantTypeInterface>
+     */
+    protected $grantTypes;
+
+    /**
+     * @var ClientAssertionTypeInterface
+     */
+    protected $clientAssertionType;
+
+    /**
+     * @var ScopeInterface
+     */
+    protected $scopeUtil;
+
+    /**
+     * @var ClientInterface
+     */
+    protected $clientStorage;
+
+    /**
+     * Constructor
+     *
+     * @param AccessTokenInterface         $accessToken
+     * @param ClientInterface              $clientStorage
+     * @param array                        $grantTypes
+     * @param ClientAssertionTypeInterface $clientAssertionType
+     * @param ScopeInterface               $scopeUtil
+     * @throws InvalidArgumentException
+     */
+    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;
+    }
+
+    /**
+     * Handle the token request.
+     *
+     * @param RequestInterface  $request  - Request object to grant access token
+     * @param ResponseInterface $response - Response object
+     */
+    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',
+                'Content-Type' => 'application/json'
+            ));
+        }
+    }
+
+    /**
+     * 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 RequestInterface  $request  - Request object to grant access token
+     * @param ResponseInterface $response - Response object
+     *
+     * @return bool|null|array
+     *
+     * @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')) === 'options') {
+            $response->addHttpHeaders(array('Allow' => 'POST, OPTIONS'));
+
+            return null;
+        }
+
+        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, OPTIONS'));
+
+            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;
+        }
+
+        /** @var GrantTypeInterface $grantType */
+        $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);
+    }
+
+    /**
+     * Add grant type
+     *
+     * @param GrantTypeInterface $grantType  - the grant type to add for the specified identifier
+     * @param string|null        $identifier - 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;
+    }
+
+    /**
+     * @param RequestInterface  $request
+     * @param ResponseInterface $response
+     */
+    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
+     * @throws RuntimeException
+     * @return bool|null
+     */
+    public function revokeToken(RequestInterface $request, ResponseInterface $response)
+    {
+        if (strtolower($request->server('REQUEST_METHOD')) === 'options') {
+            $response->addHttpHeaders(array('Allow' => 'POST, OPTIONS'));
+
+            return null;
+        }
+
+        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, OPTIONS'));
+
+            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;
+    }
+}

+ 39 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Controller/TokenControllerInterface.php

@@ -0,0 +1,39 @@
+<?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
+ *
+ * @code
+ *     $tokenController->handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response());
+ *     $response->send();
+ * @endcode
+ */
+interface TokenControllerInterface
+{
+    /**
+     * Handle the token request
+     *
+     * @param RequestInterface $request   - The current http request
+     * @param ResponseInterface $response - An instance of OAuth2\ResponseInterface to contain the response data
+     */
+    public function handleTokenRequest(RequestInterface $request, ResponseInterface $response);
+
+    /**
+     * 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 RequestInterface  $request  - Request object to grant access token
+     * @param ResponseInterface $response - Response object
+     *
+     * @return mixed
+     */
+    public function grantAccessToken(RequestInterface $request, ResponseInterface $response);
+}

+ 34 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/EncryptionInterface.php

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

+ 47 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/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);
+    }
+}

+ 223 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Encryption/Jwt.php

@@ -0,0 +1,223 @@
+<?php
+
+namespace OAuth2\Encryption;
+
+use Exception;
+use InvalidArgumentException;
+
+/**
+ * @link https://github.com/F21/jwt
+ * @author F21
+ */
+class Jwt implements EncryptionInterface
+{
+    /**
+     * @param $payload
+     * @param $key
+     * @param string $algo
+     * @return string
+     */
+    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);
+    }
+
+    /**
+     * @param string      $jwt
+     * @param null        $key
+     * @param array|bool  $allowedAlgorithms
+     * @return bool|mixed
+     */
+    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;
+    }
+
+    /**
+     * @param $signature
+     * @param $input
+     * @param $key
+     * @param string $algo
+     * @return bool
+     * @throws InvalidArgumentException
+     */
+    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.");
+        }
+    }
+
+    /**
+     * @param $input
+     * @param $key
+     * @param string $algo
+     * @return string
+     * @throws Exception
+     */
+    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.");
+        }
+    }
+
+    /**
+     * @param $input
+     * @param $key
+     * @param string $algo
+     * @return mixed
+     * @throws Exception
+     */
+    private function generateRSASignature($input, $key, $algo)
+    {
+        if (!openssl_sign($input, $signature, $key, $algo)) {
+            throw new Exception("Unable to sign data.");
+        }
+
+        return $signature;
+    }
+
+    /**
+     * @param string $data
+     * @return string
+     */
+    public function urlSafeB64Encode($data)
+    {
+        $b64 = base64_encode($data);
+        $b64 = str_replace(array('+', '/', "\r", "\n", '='),
+                array('-', '_'),
+                $b64);
+
+        return $b64;
+    }
+
+    /**
+     * @param string $b64
+     * @return mixed|string
+     */
+    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,
+        );
+    }
+
+    /**
+     * @param string $a
+     * @param string $b
+     * @return bool
+     */
+    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;
+    }
+}

+ 142 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php

@@ -0,0 +1,142 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\AuthorizationCodeInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use Exception;
+
+/**
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode implements GrantTypeInterface
+{
+    /**
+     * @var AuthorizationCodeInterface
+     */
+    protected $storage;
+
+    /**
+     * @var array
+     */
+    protected $authCode;
+
+    /**
+     * @param AuthorizationCodeInterface $storage - REQUIRED Storage class for retrieving authorization code information
+     */
+    public function __construct(AuthorizationCodeInterface $storage)
+    {
+        $this->storage = $storage;
+    }
+
+    /**
+     * @return string
+     */
+    public function getQueryStringIdentifier()
+    {
+        return 'authorization_code';
+    }
+
+    /**
+     * Validate the OAuth request
+     *
+     * @param RequestInterface  $request
+     * @param ResponseInterface $response
+     * @return bool
+     * @throws Exception
+     */
+    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')) != urldecode($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;
+    }
+
+    /**
+     * Get the client id
+     *
+     * @return mixed
+     */
+    public function getClientId()
+    {
+        return $this->authCode['client_id'];
+    }
+
+    /**
+     * Get the scope
+     *
+     * @return string
+     */
+    public function getScope()
+    {
+        return isset($this->authCode['scope']) ? $this->authCode['scope'] : null;
+    }
+
+    /**
+     * Get the user id
+     *
+     * @return mixed
+     */
+    public function getUserId()
+    {
+        return isset($this->authCode['user_id']) ? $this->authCode['user_id'] : null;
+    }
+
+    /**
+     * Create access token
+     *
+     * @param AccessTokenInterface $accessToken
+     * @param mixed                $client_id   - client identifier related to the access token.
+     * @param mixed                $user_id     - user id associated with the access token
+     * @param string               $scope       - scopes to be stored in space-separated string.
+     * @return array
+     */
+    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;
+    }
+}

+ 98 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/ClientCredentials.php

@@ -0,0 +1,98 @@
+<?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 HttpBasic
+ */
+class ClientCredentials extends HttpBasic implements GrantTypeInterface
+{
+    /**
+     * @var array
+     */
+    private $clientData;
+
+    /**
+     * @param ClientCredentialsInterface $storage
+     * @param array $config
+     */
+    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);
+    }
+
+    /**
+     * Get query string identifier
+     *
+     * @return string
+     */
+    public function getQueryStringIdentifier()
+    {
+        return 'client_credentials';
+    }
+
+    /**
+     * Get scope
+     *
+     * @return string|null
+     */
+    public function getScope()
+    {
+        $this->loadClientData();
+
+        return isset($this->clientData['scope']) ? $this->clientData['scope'] : null;
+    }
+
+    /**
+     * Get user id
+     *
+     * @return mixed
+     */
+    public function getUserId()
+    {
+        $this->loadClientData();
+
+        return isset($this->clientData['user_id']) ? $this->clientData['user_id'] : null;
+    }
+
+    /**
+     * Create access token
+     *
+     * @param AccessTokenInterface $accessToken
+     * @param mixed                $client_id   - client identifier related to the access token.
+     * @param mixed                $user_id     - user id associated with the access token
+     * @param string               $scope       - scopes to be stored in space-separated string.
+     * @return array
+     */
+    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());
+        }
+    }
+}

+ 59 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/GrantTypeInterface.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * Interface for all OAuth2 Grant Types
+ */
+interface GrantTypeInterface
+{
+    /**
+     * Get query string identifier
+     *
+     * @return string
+     */
+    public function getQueryStringIdentifier();
+
+    /**
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return mixed
+     */
+    public function validateRequest(RequestInterface $request, ResponseInterface $response);
+
+    /**
+     * Get client id
+     *
+     * @return mixed
+     */
+    public function getClientId();
+
+    /**
+     * Get user id
+     *
+     * @return mixed
+     */
+    public function getUserId();
+
+    /**
+     * Get scope
+     *
+     * @return string|null
+     */
+    public function getScope();
+
+    /**
+     * Create access token
+     *
+     * @param AccessTokenInterface $accessToken
+     * @param mixed                $client_id   - client identifier related to the access token.
+     * @param mixed                $user_id     - user id associated with the access token
+     * @param string               $scope       - scopes to be stored in space-separated string.
+     * @return array
+     */
+    public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope);
+}

+ 247 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/JwtBearer.php

@@ -0,0 +1,247 @@
+<?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 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|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 string - The string identifier for grant_type.
+     *
+     * @see GrantTypeInterface::getQueryStringIdentifier()
+     */
+    public function getQueryStringIdentifier()
+    {
+        return 'urn:ietf:params:oauth:grant-type:jwt-bearer';
+    }
+
+    /**
+     * Validates the data from the decoded JWT.
+     *
+     * @param RequestInterface  $request
+     * @param ResponseInterface $response
+     * @return bool|mixed|null TRUE if the JWT request is valid and can be decoded. Otherwise, FALSE is returned.@see 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;
+    }
+
+    /**
+     * Get client id
+     *
+     * @return mixed
+     */
+    public function getClientId()
+    {
+        return $this->jwt['iss'];
+    }
+
+    /**
+     * Get user id
+     *
+     * @return mixed
+     */
+    public function getUserId()
+    {
+        return $this->jwt['sub'];
+    }
+
+    /**
+     * Get scope
+     *
+     * @return null
+     */
+    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 GrantTypeInterface::createAccessToken()
+     *
+     * @param AccessTokenInterface $accessToken
+     * @param mixed                $client_id   - client identifier related to the access token.
+     * @param mixed                $user_id     - user id associated with the access token
+     * @param string               $scope       - scopes to be stored in space-separated string.
+     * @return array
+     */
+    public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+    {
+        $includeRefreshToken = false;
+
+        return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken);
+    }
+}

+ 154 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/RefreshToken.php

@@ -0,0 +1,154 @@
+<?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
+{
+    /**
+     * @var array
+     */
+    private $refreshToken;
+
+    /**
+     * @var RefreshTokenInterface
+     */
+    protected $storage;
+
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * @param 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
+     *     );
+     * @endcode
+     */
+    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;
+    }
+
+    /**
+     * @return string
+     */
+    public function getQueryStringIdentifier()
+    {
+        return 'refresh_token';
+    }
+
+    /**
+     * Validate the OAuth request
+     *
+     * @param RequestInterface  $request
+     * @param ResponseInterface $response
+     * @return bool|mixed|null
+     */
+    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;
+    }
+
+    /**
+     * Get client id
+     *
+     * @return mixed
+     */
+    public function getClientId()
+    {
+        return $this->refreshToken['client_id'];
+    }
+
+    /**
+     * Get user id
+     *
+     * @return mixed|null
+     */
+    public function getUserId()
+    {
+        return isset($this->refreshToken['user_id']) ? $this->refreshToken['user_id'] : null;
+    }
+
+    /**
+     * Get scope
+     *
+     * @return null|string
+     */
+    public function getScope()
+    {
+        return isset($this->refreshToken['scope']) ? $this->refreshToken['scope'] : null;
+    }
+
+    /**
+     * Create access token
+     *
+     * @param AccessTokenInterface $accessToken
+     * @param mixed                $client_id   - client identifier related to the access token.
+     * @param mixed                $user_id     - user id associated with the access token
+     * @param string               $scope       - scopes to be stored in space-separated string.
+     * @return array
+     */
+    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;
+    }
+}

+ 123 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/GrantType/UserCredentials.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\UserCredentialsInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use LogicException;
+
+/**
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class UserCredentials implements GrantTypeInterface
+{
+    /**
+     * @var array
+     */
+    private $userInfo;
+
+    /**
+     * @var UserCredentialsInterface
+     */
+    protected $storage;
+
+    /**
+     * @param UserCredentialsInterface $storage - REQUIRED Storage class for retrieving user credentials information
+     */
+    public function __construct(UserCredentialsInterface $storage)
+    {
+        $this->storage = $storage;
+    }
+
+    /**
+     * @return string
+     */
+    public function getQueryStringIdentifier()
+    {
+        return 'password';
+    }
+
+    /**
+     * @param RequestInterface  $request
+     * @param ResponseInterface $response
+     * @return bool|mixed|null
+     *
+     * @throws LogicException
+     */
+    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;
+    }
+
+    /**
+     * Get client id
+     *
+     * @return mixed|null
+     */
+    public function getClientId()
+    {
+        return null;
+    }
+
+    /**
+     * Get user id
+     *
+     * @return mixed
+     */
+    public function getUserId()
+    {
+        return $this->userInfo['user_id'];
+    }
+
+    /**
+     * Get scope
+     *
+     * @return null|string
+     */
+    public function getScope()
+    {
+        return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null;
+    }
+
+    /**
+     * Create access token
+     *
+     * @param AccessTokenInterface $accessToken
+     * @param mixed                $client_id   - client identifier related to the access token.
+     * @param mixed                $user_id     - user id associated with the access token
+     * @param string               $scope       - scopes to be stored in space-separated string.
+     * @return array
+     */
+    public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+    {
+        return $accessToken->createAccessToken($client_id, $user_id, $scope);
+    }
+}

+ 135 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeController.php

@@ -0,0 +1,135 @@
+<?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
+{
+    /**
+     * @var mixed
+     */
+    private $nonce;
+
+    /**
+     * Set not authorized response
+     *
+     * @param RequestInterface  $request
+     * @param ResponseInterface $response
+     * @param string            $redirect_uri
+     * @param null              $user_id
+     */
+    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);
+    }
+
+    /**
+     * @TODO: add dependency injection for the parameters in this method
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @param mixed $user_id
+     * @return array
+     */
+    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;
+    }
+
+    /**
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return bool
+     */
+    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;
+    }
+
+    /**
+     * Array of valid response types
+     *
+     * @return array
+     */
+    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 string $request_scope - A space-separated string of scopes.
+     * @return boolean - 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);
+    }
+
+    /**
+     * @return mixed
+     */
+    public function getNonce()
+    {
+        return $this->nonce;
+    }
+}

+ 12 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php

@@ -0,0 +1,12 @@
+<?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';
+}

+ 62 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoController.php

@@ -0,0 +1,62 @@
+<?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
+{
+    /**
+     * @var UserClaimsInterface
+     */
+    protected $userClaimsStorage;
+
+    /**
+     * Constructor
+     *
+     * @param TokenTypeInterface   $tokenType
+     * @param AccessTokenInterface $tokenStorage
+     * @param UserClaimsInterface  $userClaimsStorage
+     * @param array                $config
+     * @param ScopeInterface       $scopeUtil
+     */
+    public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, UserClaimsInterface $userClaimsStorage, $config = array(), ScopeInterface $scopeUtil = null)
+    {
+        parent::__construct($tokenType, $tokenStorage, $config, $scopeUtil);
+
+        $this->userClaimsStorage = $userClaimsStorage;
+    }
+
+    /**
+     * Handle the user info request
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return void
+     */
+    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);
+    }
+}

+ 30 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php

@@ -0,0 +1,30 @@
+<?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.
+ *
+ * @code
+ *     $response = new OAuth2\Response();
+ *     $userInfoController->handleUserInfoRequest(
+ *         OAuth2\Request::createFromGlobals(),
+ *         $response
+ *     );
+ *     $response->send();
+ * @endcode
+ */
+interface UserInfoControllerInterface
+{
+    /**
+     * Handle user info request
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     */
+    public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response);
+}

+ 41 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/GrantType/AuthorizationCode.php

@@ -0,0 +1,41 @@
+<?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
+{
+    /**
+     * Create access token
+     *
+     * @param AccessTokenInterface $accessToken
+     * @param mixed                $client_id   - client identifier related to the access token.
+     * @param mixed                $user_id     - user id associated with the access token
+     * @param string               $scope       - scopes to be stored in space-separated string.
+     * @return array
+     */
+    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;
+    }
+}

+ 66 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php

@@ -0,0 +1,66 @@
+<?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
+{
+    /**
+     * Constructor
+     *
+     * @param AuthorizationCodeStorageInterface $storage
+     * @param array $config
+     */
+    public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array())
+    {
+        parent::__construct($storage, $config);
+    }
+
+    /**
+     * @param $params
+     * @param null $user_id
+     * @return array
+     */
+    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 mixed $client_id - Client identifier related to the authorization code
+     * @param mixed $user_id - User ID associated with the authorization code
+     * @param string $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 string $scope - OPTIONAL Scopes to be stored in space-separated string.
+     * @param string $id_token - OPTIONAL The OpenID Connect id_token.
+     *
+     * @return string
+     * @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
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/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 mixed  $client_id    - Client identifier related to the authorization code
+     * @param mixed  $user_id      - User ID associated with the authorization code
+     * @param string $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 string $scope        - OPTIONAL Scopes to be stored in space-separated string.
+     * @param string $id_token     - OPTIONAL The OpenID Connect id_token.
+     * @return string
+     *
+     * @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);
+}

+ 40 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdToken.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+class CodeIdToken implements CodeIdTokenInterface
+{
+    /**
+     * @var AuthorizationCodeInterface
+     */
+    protected $authCode;
+
+    /**
+     * @var IdTokenInterface
+     */
+    protected $idToken;
+
+    /**
+     * @param AuthorizationCodeInterface $authCode
+     * @param IdTokenInterface           $idToken
+     */
+    public function __construct(AuthorizationCodeInterface $authCode, IdTokenInterface $idToken)
+    {
+        $this->authCode = $authCode;
+        $this->idToken = $idToken;
+    }
+
+    /**
+     * @param array $params
+     * @param mixed $user_id
+     * @return mixed
+     */
+    public function getAuthorizeResponse($params, $user_id = null)
+    {
+        $result = $this->authCode->getAuthorizeResponse($params, $user_id);
+        $resultIdToken = $this->idToken->getAuthorizeResponse($params, $user_id);
+        $result[1]['query']['id_token'] = $resultIdToken[1]['fragment']['id_token'];
+
+        return $result;
+    }
+}

+ 9 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php

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

+ 178 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdToken.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\Encryption\EncryptionInterface;
+use OAuth2\Encryption\Jwt;
+use OAuth2\Storage\PublicKeyInterface;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use LogicException;
+
+class IdToken implements IdTokenInterface
+{
+    /**
+     * @var UserClaimsInterface
+     */
+    protected $userClaimsStorage;
+    /**
+     * @var PublicKeyInterface
+     */
+    protected $publicKeyStorage;
+
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * @var EncryptionInterface
+     */
+    protected $encryptionUtil;
+
+    /**
+     * Constructor
+     *
+     * @param UserClaimsInterface $userClaimsStorage
+     * @param PublicKeyInterface $publicKeyStorage
+     * @param array $config
+     * @param EncryptionInterface $encryptionUtil
+     * @throws LogicException
+     */
+    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);
+    }
+
+    /**
+     * @param array $params
+     * @param null $userInfo
+     * @return array|mixed
+     */
+    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);
+    }
+
+    /**
+     * Create id token
+     *
+     * @param string $client_id
+     * @param mixed  $userInfo
+     * @param mixed  $nonce
+     * @param mixed  $userClaims
+     * @param mixed  $access_token
+     * @return mixed|string
+     */
+    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);
+    }
+
+    /**
+     * @param $access_token
+     * @param null $client_id
+     * @return mixed|string
+     */
+    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, true);
+        $at_hash = substr($hash, 0, strlen($hash) / 2);
+
+        return $this->encryptionUtil->urlSafeB64Encode($at_hash);
+    }
+
+    /**
+     * @param array $token
+     * @param null $client_id
+     * @return mixed|string
+     */
+    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);
+    }
+
+    /**
+     * @param $userInfo
+     * @return array
+     * @throws LogicException
+     */
+    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);
+    }
+}

+ 30 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php

@@ -0,0 +1,30 @@
+<?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 mixed  $userInfo         - User info
+     * @param string $nonce            - OPTIONAL The nonce.
+     * @param string $userClaims       - OPTIONAL Claims about the user.
+     * @param string $access_token     - OPTIONAL The access token, if known.
+
+     * @internal param string $user_id - The user id.
+     * @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);
+}

+ 45 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenToken.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\AccessTokenInterface;
+
+class IdTokenToken implements IdTokenTokenInterface
+{
+    /**
+     * @var AccessTokenInterface
+     */
+    protected $accessToken;
+
+    /**
+     * @var IdTokenInterface
+     */
+    protected $idToken;
+
+    /**
+     * Constructor
+     *
+     * @param AccessTokenInterface $accessToken
+     * @param IdTokenInterface $idToken
+     */
+    public function __construct(AccessTokenInterface $accessToken, IdTokenInterface $idToken)
+    {
+        $this->accessToken = $accessToken;
+        $this->idToken = $idToken;
+    }
+
+    /**
+     * @param array $params
+     * @param mixed $user_id
+     * @return mixed
+     */
+    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
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php

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

+ 37 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/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 string $code         - authorization code to be stored.
+     * @param mixed $client_id     - client identifier to be stored.
+     * @param mixed $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);
+}

+ 35 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/OpenID/Storage/UserClaimsInterface.php

@@ -0,0 +1,35 @@
+<?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 mixed  $user_id - The id of the user for which claims should be returned.
+     * @param string $scope   - The requested scope.
+     * Scopes with matching claims: profile, email, address, phone.
+     *
+     * @return array - 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);
+}

+ 252 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Request.php

@@ -0,0 +1,252 @@
+<?php
+
+namespace OAuth2;
+
+use LogicException;
+
+/**
+ * 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
+     * @param array  $headers    - The headers
+     *
+     * @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
+     * @param array  $headers    - The headers
+     *
+     * @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;
+
+        if ($headers === null) {
+            $headers = array();
+        }
+
+        $this->headers = $headers + $this->getHeadersFromServer($this->server);
+    }
+
+    /**
+     * @param string $name
+     * @param mixed  $default
+     * @return mixed
+     */
+    public function query($name, $default = null)
+    {
+        return isset($this->query[$name]) ? $this->query[$name] : $default;
+    }
+
+    /**
+     * @param string $name
+     * @param mixed  $default
+     * @return mixed
+     */
+    public function request($name, $default = null)
+    {
+        return isset($this->request[$name]) ? $this->request[$name] : $default;
+    }
+
+    /**
+     * @param string $name
+     * @param mixed  $default
+     * @return mixed
+     */
+    public function server($name, $default = null)
+    {
+        return isset($this->server[$name]) ? $this->server[$name] : $default;
+    }
+
+    /**
+     * @param string $name
+     * @param mixed  $default
+     * @return mixed
+     */
+    public function headers($name, $default = null)
+    {
+        $headers = array_change_key_case($this->headers);
+        $name = strtolower($name);
+
+        return isset($headers[$name]) ? $headers[$name] : $default;
+    }
+
+    /**
+     * @return array
+     */
+    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.
+     *
+     * @throws LogicException
+     */
+    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;
+    }
+
+    /**
+     * @param array $server
+     * @return array
+     */
+    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();
+
+        /** @var Request $request */
+        $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;
+    }
+}

+ 39 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/RequestInterface.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace OAuth2;
+
+interface RequestInterface
+{
+    /**
+     * @param string $name
+     * @param mixed  $default
+     * @return mixed
+     */
+    public function query($name, $default = null);
+
+    /**
+     * @param string $name
+     * @param mixed  $default
+     * @return mixed
+     */
+    public function request($name, $default = null);
+
+    /**
+     * @param string $name
+     * @param mixed  $default
+     * @return mixed
+     */
+    public function server($name, $default = null);
+
+    /**
+     * @param string $name
+     * @param mixed  $default
+     * @return mixed
+     */
+    public function headers($name, $default = null);
+
+    /**
+     * @return mixed
+     */
+    public function getAllQueryParameters();
+}

+ 487 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Response.php

@@ -0,0 +1,487 @@
+<?php
+
+namespace OAuth2;
+
+use InvalidArgumentException;
+
+/**
+ * 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
+{
+    /**
+     * @var string
+     */
+    public $version;
+
+    /**
+     * @var int
+     */
+    protected $statusCode = 200;
+
+    /**
+     * @var string
+     */
+    protected $statusText;
+
+    /**
+     * @var array
+     */
+    protected $parameters = array();
+
+    /**
+     * @var array
+     */
+    protected $httpHeaders = array();
+
+    /**
+     * @var 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',
+    );
+
+    /**
+     * @param array $parameters
+     * @param int   $statusCode
+     * @param array $headers
+     */
+    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);
+    }
+
+    /**
+     * @return int
+     */
+    public function getStatusCode()
+    {
+        return $this->statusCode;
+    }
+
+    /**
+     * @param int $statusCode
+     * @param string $text
+     * @throws InvalidArgumentException
+     */
+    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);
+    }
+
+    /**
+     * @return string
+     */
+    public function getStatusText()
+    {
+        return $this->statusText;
+    }
+
+    /**
+     * @return array
+     */
+    public function getParameters()
+    {
+        return $this->parameters;
+    }
+
+    /**
+     * @param array $parameters
+     */
+    public function setParameters(array $parameters)
+    {
+        $this->parameters = $parameters;
+    }
+
+    /**
+     * @param array $parameters
+     */
+    public function addParameters(array $parameters)
+    {
+        $this->parameters = array_merge($this->parameters, $parameters);
+    }
+
+    /**
+     * @param string $name
+     * @param mixed  $default
+     * @return mixed
+     */
+    public function getParameter($name, $default = null)
+    {
+        return isset($this->parameters[$name]) ? $this->parameters[$name] : $default;
+    }
+
+    /**
+     * @param string $name
+     * @param mixed  $value
+     */
+    public function setParameter($name, $value)
+    {
+        $this->parameters[$name] = $value;
+    }
+
+    /**
+     * @param array $httpHeaders
+     */
+    public function setHttpHeaders(array $httpHeaders)
+    {
+        $this->httpHeaders = $httpHeaders;
+    }
+
+    /**
+     * @param string $name
+     * @param mixed $value
+     */
+    public function setHttpHeader($name, $value)
+    {
+        $this->httpHeaders[$name] = $value;
+    }
+
+    /**
+     * @param array $httpHeaders
+     */
+    public function addHttpHeaders(array $httpHeaders)
+    {
+        $this->httpHeaders = array_merge($this->httpHeaders, $httpHeaders);
+    }
+
+    /**
+     * @return array
+     */
+    public function getHttpHeaders()
+    {
+        return $this->httpHeaders;
+    }
+
+    /**
+     * @param string $name
+     * @param mixed  $default
+     * @return mixed
+     */
+    public function getHttpHeader($name, $default = null)
+    {
+        return isset($this->httpHeaders[$name]) ? $this->httpHeaders[$name] : $default;
+    }
+
+    /**
+     * @param string $format
+     * @return mixed
+     * @throws InvalidArgumentException
+     */
+    public function getResponseBody($format = 'json')
+    {
+        switch ($format) {
+            case 'json':
+                return $this->parameters ? 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));
+
+    }
+
+    /**
+     * @param string $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);
+    }
+
+    /**
+     * @param int $statusCode
+     * @param string $error
+     * @param string $errorDescription
+     * @param string $errorUri
+     * @return mixed
+     * @throws InvalidArgumentException
+     */
+    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));
+        }
+    }
+
+    /**
+     * @param int $statusCode
+     * @param string $url
+     * @param string $state
+     * @param string $error
+     * @param string $errorDescription
+     * @param string $errorUri
+     * @return mixed
+     * @throws InvalidArgumentException
+     */
+    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']) && !empty($parts['query']) ? '&' : '?';
+            $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));
+        }
+    }
+
+    /**
+     * @return Boolean
+     *
+     * @api
+     *
+     * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+     */
+    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;
+    }
+
+    /**
+     * Function from Symfony2 HttpFoundation - output pretty header
+     *
+     * @param array $headers
+     * @return string
+     */
+    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;
+    }
+
+    /**
+     * Function from Symfony2 HttpFoundation - output pretty header
+     *
+     * @param string $name
+     * @return mixed
+     */
+    private function beautifyHeaderName($name)
+    {
+        return preg_replace_callback('/\-(.)/', array($this, 'beautifyCallback'), ucfirst($name));
+    }
+
+    /**
+     * Function from Symfony2 HttpFoundation - output pretty header
+     *
+     * @param array $match
+     * @return string
+     */
+    private function beautifyCallback($match)
+    {
+        return '-'.strtoupper($match[1]);
+    }
+}

+ 53 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseInterface.php

@@ -0,0 +1,53 @@
+<?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
+{
+    /**
+     * @param array $parameters
+     */
+    public function addParameters(array $parameters);
+
+    /**
+     * @param array $httpHeaders
+     */
+    public function addHttpHeaders(array $httpHeaders);
+
+    /**
+     * @param int $statusCode
+     */
+    public function setStatusCode($statusCode);
+
+    /**
+     * @param int    $statusCode
+     * @param string $name
+     * @param string $description
+     * @param string $uri
+     * @return mixed
+     */
+    public function setError($statusCode, $name, $description = null, $uri = null);
+
+    /**
+     * @param int    $statusCode
+     * @param string $url
+     * @param string $state
+     * @param string $error
+     * @param string $errorDescription
+     * @param string $errorUri
+     * @return mixed
+     */
+    public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null);
+
+    /**
+     * @param string $name
+     * @return mixed
+     */
+    public function getParameter($name);
+}

+ 218 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessToken.php

@@ -0,0 +1,218 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+use OAuth2\Storage\AccessTokenInterface as AccessTokenStorageInterface;
+use OAuth2\Storage\RefreshTokenInterface;
+use RuntimeException;
+
+/**
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AccessToken implements AccessTokenInterface
+{
+    /**
+     * @var AccessTokenInterface
+     */
+    protected $tokenStorage;
+
+    /**
+     * @var RefreshTokenInterface
+     */
+    protected $refreshStorage;
+
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * @param AccessTokenStorageInterface $tokenStorage   - REQUIRED Storage class for saving access token information
+     * @param 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);
+    }
+
+    /**
+     * Get authorize response
+     *
+     * @param array $params
+     * @param mixed $user_id
+     * @return array
+     */
+    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 mixed  $client_id           - client identifier related to the access token.
+     * @param mixed  $user_id             - user ID associated with the access token
+     * @param string $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
+     * @return array
+     *
+     * @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 string - A unique access token.
+     *
+     * @ingroup oauth2_section_4
+     */
+    protected function generateAccessToken()
+    {
+        if (function_exists('random_bytes')) {
+            $randomData = random_bytes(20);
+            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 (function_exists('mcrypt_create_iv')) {
+            $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM);
+            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 string - A unique refresh token.
+     *
+     * @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
+     * @throws RuntimeException
+     * @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;
+    }
+}

+ 33 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AccessTokenInterface.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+/**
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface AccessTokenInterface extends ResponseTypeInterface
+{
+    /**
+     * Handle the creation of access token, also issue refresh token if supported / desirable.
+     *
+     * @param mixed  $client_id           - client identifier related to the access token.
+     * @param mixed  $user_id             - user ID associated with the access token
+     * @param string $scope               - OPTONAL 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);
+
+    /**
+     * Handle the revoking of refresh tokens, and access tokens if supported / desirable
+     *
+     * @param $token
+     * @param $tokenTypeHint
+     * @return mixed
+     *
+     * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x
+     */
+    //public function revokeToken($token, $tokenTypeHint);
+}

+ 101 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCode.php

@@ -0,0 +1,101 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+use OAuth2\Storage\AuthorizationCodeInterface as AuthorizationCodeStorageInterface;
+
+/**
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode implements AuthorizationCodeInterface
+{
+    protected $storage;
+    protected $config;
+
+    public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array())
+    {
+        $this->storage = $storage;
+        $this->config = array_merge(array(
+            'enforce_redirect' => false,
+            'auth_code_lifetime' => 30,
+        ), $config);
+    }
+
+    public function getAuthorizeResponse($params, $user_id = null)
+    {
+        // build the URL to redirect to
+        $result = array('query' => array());
+
+        $params += array('scope' => null, 'state' => null);
+
+        $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope']);
+
+        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.
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4
+     * @ingroup oauth2_section_4
+     */
+    public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null)
+    {
+        $code = $this->generateAuthorizationCode();
+        $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope);
+
+        return $code;
+    }
+
+    /**
+     * @return
+     * TRUE if the grant type requires a redirect_uri, FALSE if not
+     */
+    public function enforceRedirect()
+    {
+        return $this->config['enforce_redirect'];
+    }
+
+    /**
+     * Generates an unique auth code.
+     *
+     * Implementing classes may want to override this function to implement
+     * other auth code generation schemes.
+     *
+     * @return
+     * An unique auth code.
+     *
+     * @ingroup oauth2_section_4
+     */
+    protected function generateAuthorizationCode()
+    {
+        $tokenLen = 40;
+        if (function_exists('random_bytes')) {
+            $randomData = random_bytes(100);
+        } elseif (function_exists('openssl_random_pseudo_bytes')) {
+            $randomData = openssl_random_pseudo_bytes(100);
+        } elseif (function_exists('mcrypt_create_iv')) {
+            $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM);
+        } elseif (@file_exists('/dev/urandom')) { // Get 100 bytes of random data
+            $randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true);
+        } else {
+            $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true);
+        }
+
+        return substr(hash('sha512', $randomData), 0, $tokenLen);
+    }
+}

+ 30 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/AuthorizationCodeInterface.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+/**
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface AuthorizationCodeInterface extends ResponseTypeInterface
+{
+    /**
+     * @return
+     * TRUE if the grant type requires a redirect_uri, FALSE if not
+     */
+    public function enforceRedirect();
+
+    /**
+     * Handle the creation of the authorization code.
+     *
+     * @param mixed  $client_id    - Client identifier related to the authorization code
+     * @param mixed  $user_id      - User ID associated with the authorization code
+     * @param string $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 string $scope        - OPTIONAL Scopes to be stored in space-separated string.
+     * @return string
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4
+     * @ingroup oauth2_section_4
+     */
+    public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null);
+}

+ 159 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/JwtAccessToken.php

@@ -0,0 +1,159 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+use OAuth2\Encryption\EncryptionInterface;
+use OAuth2\Encryption\Jwt;
+use OAuth2\Storage\AccessTokenInterface as AccessTokenStorageInterface;
+use OAuth2\Storage\RefreshTokenInterface;
+use OAuth2\Storage\PublicKeyInterface;
+use OAuth2\Storage\Memory;
+
+/**
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class JwtAccessToken extends AccessToken
+{
+    protected $publicKeyStorage;
+    protected $encryptionUtil;
+
+    /**
+     * @param PublicKeyInterface          $publicKeyStorage -
+     * @param AccessTokenStorageInterface $tokenStorage     -
+     * @param RefreshTokenInterface       $refreshStorage   -
+     * @param array                       $config           - array with key store_encrypted_token_string (bool true)
+     *                                                        whether the entire encrypted string is stored,
+     *                                                        or just the token ID is stored
+     * @param EncryptionInterface         $encryptionUtil   -
+     */
+    public function __construct(PublicKeyInterface $publicKeyStorage = null, AccessTokenStorageInterface $tokenStorage = null, RefreshTokenInterface $refreshStorage = null, array $config = array(), EncryptionInterface $encryptionUtil = null)
+    {
+        $this->publicKeyStorage = $publicKeyStorage;
+        $config = array_merge(array(
+            'store_encrypted_token_string' => true,
+            'issuer' => ''
+        ), $config);
+        if (is_null($tokenStorage)) {
+            // a pass-thru, so we can call the parent constructor
+            $tokenStorage = new Memory();
+        }
+        if (is_null($encryptionUtil)) {
+            $encryptionUtil = new Jwt();
+        }
+        $this->encryptionUtil = $encryptionUtil;
+        parent::__construct($tokenStorage, $refreshStorage, $config);
+    }
+
+    /**
+     * Handle the creation of access token, also issue refresh token if supported / desirable.
+     *
+     * @param mixed  $client_id           - Client identifier related to the access token.
+     * @param mixed  $user_id             - User ID associated with the access token
+     * @param string $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
+     * @return array                      - The access token
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-5
+     * @ingroup oauth2_section_5
+     */
+    public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true)
+    {
+        // payload to encrypt
+        $payload = $this->createPayload($client_id, $user_id, $scope);
+
+        /*
+         * Encode the payload data into a single JWT access_token string
+         */
+        $access_token = $this->encodeToken($payload, $client_id);
+
+        /*
+         * Save the token to a secondary storage.  This is implemented on the
+         * OAuth2\Storage\JwtAccessToken side, and will not actually store anything,
+         * if no secondary storage has been supplied
+         */
+        $token_to_store = $this->config['store_encrypted_token_string'] ? $access_token : $payload['id'];
+        $this->tokenStorage->setAccessToken($token_to_store, $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope);
+
+        // token to return to the client
+        $token = array(
+            'access_token' => $access_token,
+            'expires_in' => $this->config['access_lifetime'],
+            'token_type' => $this->config['token_type'],
+            'scope' => $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) {
+            $refresh_token = $this->generateRefreshToken();
+            $expires = 0;
+            if ($this->config['refresh_token_lifetime'] > 0) {
+                $expires = time() + $this->config['refresh_token_lifetime'];
+            }
+            $this->refreshStorage->setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope);
+            $token['refresh_token'] = $refresh_token;
+        }
+
+        return $token;
+    }
+
+    /**
+     * @param array $token
+     * @param mixed $client_id
+     * @return mixed
+     */
+    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);
+    }
+
+    /**
+     * This function can be used to create custom JWT payloads
+     *
+     * @param mixed  $client_id           - Client identifier related to the access token.
+     * @param mixed  $user_id             - User ID associated with the access token
+     * @param string $scope               - (optional) Scopes to be stored in space-separated string.
+     * @return array                      - The access token
+     */
+    protected function createPayload($client_id, $user_id, $scope = null)
+    {
+        // token to encrypt
+        $expires = time() + $this->config['access_lifetime'];
+        $id = $this->generateAccessToken();
+
+        $payload = array(
+            'id'         => $id, // for BC (see #591)
+            'jti'        => $id,
+            'iss'        => $this->config['issuer'],
+            'aud'        => $client_id,
+            'sub'        => $user_id,
+            'exp'        => $expires,
+            'iat'        => time(),
+            'token_type' => $this->config['token_type'],
+            'scope'      => $scope
+        );
+        
+        if (isset($this->config['jwt_extra_payload_callable'])) {
+            if (!is_callable($this->config['jwt_extra_payload_callable'])) {
+                throw new \InvalidArgumentException('jwt_extra_payload_callable is not callable');
+            }
+            
+            $extra = call_user_func($this->config['jwt_extra_payload_callable'], $client_id, $user_id, $scope);
+            
+            if (!is_array($extra)) {
+                throw new \InvalidArgumentException('jwt_extra_payload_callable must return array');
+            }
+            
+            $payload = array_merge($extra, $payload);
+        }
+        
+        return $payload;
+    }
+}

+ 13 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ResponseType/ResponseTypeInterface.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+interface ResponseTypeInterface
+{
+    /**
+     * @param array $params
+     * @param mixed $user_id
+     * @return mixed
+     */
+    public function getAuthorizeResponse($params, $user_id = null);
+}

+ 109 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Scope.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace OAuth2;
+
+use InvalidArgumentException;
+use OAuth2\Storage\Memory;
+use OAuth2\Storage\ScopeInterface as ScopeStorageInterface;
+
+/**
+* @see ScopeInterface
+*/
+class Scope implements ScopeInterface
+{
+    protected $storage;
+
+    /**
+     * Constructor
+     *
+     * @param mixed $storage - Either an array of supported scopes, or an instance of OAuth2\Storage\ScopeInterface
+     *
+     * @throws InvalidArgumentException
+     */
+    public function __construct($storage = null)
+    {
+        if (is_null($storage) || is_array($storage)) {
+            $storage = new Memory((array) $storage);
+        }
+
+        if (!$storage instanceof ScopeStorageInterface) {
+            throw new InvalidArgumentException("Argument 1 to OAuth2\Scope must be null, an array, or instance of OAuth2\Storage\ScopeInterface");
+        }
+
+        $this->storage = $storage;
+    }
+
+    /**
+     * Check if everything in required scope is contained in available scope.
+     *
+     * @param string $required_scope  - A space-separated string of scopes.
+     * @param string $available_scope - A space-separated string of scopes.
+     * @return bool                   - TRUE if everything in required scope is contained in available scope and FALSE
+     *                                  if it isn't.
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-7
+     *
+     * @ingroup oauth2_section_7
+     */
+    public function checkScope($required_scope, $available_scope)
+    {
+        $required_scope = explode(' ', trim($required_scope));
+        $available_scope = explode(' ', trim($available_scope));
+
+        return (count(array_diff($required_scope, $available_scope)) == 0);
+    }
+
+    /**
+     * Check if the provided scope exists in storage.
+     *
+     * @param string $scope - A space-separated string of scopes.
+     * @return bool         - TRUE if it exists, FALSE otherwise.
+     */
+    public function scopeExists($scope)
+    {
+        // Check reserved scopes first.
+        $scope = explode(' ', trim($scope));
+        $reservedScope = $this->getReservedScopes();
+        $nonReservedScopes = array_diff($scope, $reservedScope);
+        if (count($nonReservedScopes) == 0) {
+            return true;
+        } else {
+            // Check the storage for non-reserved scopes.
+            $nonReservedScopes = implode(' ', $nonReservedScopes);
+
+            return $this->storage->scopeExists($nonReservedScopes);
+        }
+    }
+
+    /**
+     * @param RequestInterface $request
+     * @return string
+     */
+    public function getScopeFromRequest(RequestInterface $request)
+    {
+        // "scope" is valid if passed in either POST or QUERY
+        return $request->request('scope', $request->query('scope'));
+    }
+
+    /**
+     * @param null $client_id
+     * @return mixed
+     */
+    public function getDefaultScope($client_id = null)
+    {
+        return $this->storage->getDefaultScope($client_id);
+    }
+
+    /**
+     * Get reserved scopes needed by the server.
+     *
+     * In case OpenID Connect is used, these scopes must include:
+     * 'openid', offline_access'.
+     *
+     * @return array - An array of reserved scopes.
+     */
+    public function getReservedScopes()
+    {
+        return array('openid', 'offline_access');
+    }
+}

+ 35 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/ScopeInterface.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace OAuth2;
+
+use OAuth2\Storage\ScopeInterface as ScopeStorageInterface;
+
+/**
+ * Class to handle scope implementation logic
+ *
+ * @see \OAuth2\Storage\ScopeInterface
+ */
+interface ScopeInterface extends ScopeStorageInterface
+{
+    /**
+     * Check if everything in required scope is contained in available scope.
+     *
+     * @param string $required_scope  - A space-separated string of scopes.
+     * @param string $available_scope - A space-separated string of scopes.
+     * @return boolean                - TRUE if everything in required scope is contained in available scope and FALSE
+     *                                  if it isn't.
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-7
+     *
+     * @ingroup oauth2_section_7
+     */
+    public function checkScope($required_scope, $available_scope);
+
+    /**
+     * Return scope info from request
+     *
+     * @param RequestInterface $request - Request object to check
+     * @return string                   - representation of requested scope
+     */
+    public function getScopeFromRequest(RequestInterface $request);
+}

+ 1019 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Server.php

@@ -0,0 +1,1019 @@
+<?php
+
+namespace OAuth2;
+
+use OAuth2\Controller\ResourceControllerInterface;
+use OAuth2\Controller\ResourceController;
+use OAuth2\OpenID\Controller\UserInfoControllerInterface;
+use OAuth2\OpenID\Controller\UserInfoController;
+use OAuth2\OpenID\Controller\AuthorizeController as OpenIDAuthorizeController;
+use OAuth2\OpenID\ResponseType\AuthorizationCode as OpenIDAuthorizationCodeResponseType;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+use OAuth2\OpenID\GrantType\AuthorizationCode as OpenIDAuthorizationCodeGrantType;
+use OAuth2\Controller\AuthorizeControllerInterface;
+use OAuth2\Controller\AuthorizeController;
+use OAuth2\Controller\TokenControllerInterface;
+use OAuth2\Controller\TokenController;
+use OAuth2\ClientAssertionType\ClientAssertionTypeInterface;
+use OAuth2\ClientAssertionType\HttpBasic;
+use OAuth2\ResponseType\ResponseTypeInterface;
+use OAuth2\ResponseType\AuthorizationCode as AuthorizationCodeResponseType;
+use OAuth2\ResponseType\AccessToken;
+use OAuth2\ResponseType\JwtAccessToken;
+use OAuth2\OpenID\ResponseType\CodeIdToken;
+use OAuth2\OpenID\ResponseType\IdToken;
+use OAuth2\OpenID\ResponseType\IdTokenToken;
+use OAuth2\TokenType\TokenTypeInterface;
+use OAuth2\TokenType\Bearer;
+use OAuth2\GrantType\GrantTypeInterface;
+use OAuth2\GrantType\UserCredentials;
+use OAuth2\GrantType\ClientCredentials;
+use OAuth2\GrantType\RefreshToken;
+use OAuth2\GrantType\AuthorizationCode;
+use OAuth2\Storage\ClientCredentialsInterface;
+use OAuth2\Storage\ClientInterface;
+use OAuth2\Storage\JwtAccessToken as JwtAccessTokenStorage;
+use OAuth2\Storage\JwtAccessTokenInterface;
+use InvalidArgumentException;
+use LogicException;
+
+/**
+* Server class for OAuth2
+* This class serves as a convience class which wraps the other Controller classes
+*
+* @see \OAuth2\Controller\ResourceController
+* @see \OAuth2\Controller\AuthorizeController
+* @see \OAuth2\Controller\TokenController
+*/
+class Server implements ResourceControllerInterface,
+    AuthorizeControllerInterface,
+    TokenControllerInterface,
+    UserInfoControllerInterface
+{
+    /**
+     * @var ResponseInterface
+     */
+    protected $response;
+
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * @var array
+     */
+    protected $storages;
+
+    /**
+     * @var AuthorizeControllerInterface
+     */
+    protected $authorizeController;
+
+    /**
+     * @var TokenControllerInterface
+     */
+    protected $tokenController;
+
+    /**
+     * @var ResourceControllerInterface
+     */
+    protected $resourceController;
+
+    /**
+     * @var UserInfoControllerInterface
+     */
+    protected $userInfoController;
+
+    /**
+     * @var array
+     */
+    protected $grantTypes = array();
+
+    /**
+     * @var array
+     */
+    protected $responseTypes = array();
+
+    /**
+     * @var TokenTypeInterface
+     */
+    protected $tokenType;
+
+    /**
+     * @var ScopeInterface
+     */
+    protected $scopeUtil;
+
+    /**
+     * @var ClientAssertionTypeInterface
+     */
+    protected $clientAssertionType;
+
+    /**
+     * @var array
+     */
+    protected $storageMap = array(
+        'access_token' => 'OAuth2\Storage\AccessTokenInterface',
+        'authorization_code' => 'OAuth2\Storage\AuthorizationCodeInterface',
+        'client_credentials' => 'OAuth2\Storage\ClientCredentialsInterface',
+        'client' => 'OAuth2\Storage\ClientInterface',
+        'refresh_token' => 'OAuth2\Storage\RefreshTokenInterface',
+        'user_credentials' => 'OAuth2\Storage\UserCredentialsInterface',
+        'user_claims' => 'OAuth2\OpenID\Storage\UserClaimsInterface',
+        'public_key' => 'OAuth2\Storage\PublicKeyInterface',
+        'jwt_bearer' => 'OAuth2\Storage\JWTBearerInterface',
+        'scope' => 'OAuth2\Storage\ScopeInterface',
+    );
+
+    /**
+     * @var array
+     */
+    protected $responseTypeMap = array(
+        'token' => 'OAuth2\ResponseType\AccessTokenInterface',
+        'code' => 'OAuth2\ResponseType\AuthorizationCodeInterface',
+        'id_token' => 'OAuth2\OpenID\ResponseType\IdTokenInterface',
+        'id_token token' => 'OAuth2\OpenID\ResponseType\IdTokenTokenInterface',
+        'code id_token' => 'OAuth2\OpenID\ResponseType\CodeIdTokenInterface',
+    );
+
+    /**
+     * @param mixed                        $storage             (array or OAuth2\Storage) - single object or array of objects implementing the
+     *                                                          required storage types (ClientCredentialsInterface and AccessTokenInterface as a minimum)
+     * @param array                        $config              specify a different token lifetime, token header name, etc
+     * @param array                        $grantTypes          An array of OAuth2\GrantType\GrantTypeInterface to use for granting access tokens
+     * @param array                        $responseTypes       Response types to use. array keys should be "code" and "token" for
+     *                                                          Access Token and Authorization Code response types
+     * @param TokenTypeInterface           $tokenType           The token type object to use. Valid token types are "bearer" and "mac"
+     * @param ScopeInterface               $scopeUtil           The scope utility class to use to validate scope
+     * @param ClientAssertionTypeInterface $clientAssertionType The method in which to verify the client identity.  Default is HttpBasic
+     *
+     * @ingroup oauth2_section_7
+     */
+    public function __construct($storage = array(), array $config = array(), array $grantTypes = array(), array $responseTypes = array(), TokenTypeInterface $tokenType = null, ScopeInterface $scopeUtil = null, ClientAssertionTypeInterface $clientAssertionType = null)
+    {
+        $storage = is_array($storage) ? $storage : array($storage);
+        $this->storages = array();
+        foreach ($storage as $key => $service) {
+            $this->addStorage($service, $key);
+        }
+
+        // merge all config values.  These get passed to our controller objects
+        $this->config = array_merge(array(
+            'use_jwt_access_tokens'        => false,
+            'jwt_extra_payload_callable' => null,
+            'store_encrypted_token_string' => true,
+            'use_openid_connect'       => false,
+            'id_lifetime'              => 3600,
+            'access_lifetime'          => 3600,
+            'www_realm'                => 'Service',
+            'token_param_name'         => 'access_token',
+            'token_bearer_header_name' => 'Bearer',
+            'enforce_state'            => true,
+            'require_exact_redirect_uri' => true,
+            'allow_implicit'           => false,
+            'allow_credentials_in_request_body' => true,
+            'allow_public_clients'     => true,
+            'always_issue_new_refresh_token' => false,
+            'unset_refresh_token_after_use' => true,
+        ), $config);
+
+        foreach ($grantTypes as $key => $grantType) {
+            $this->addGrantType($grantType, $key);
+        }
+
+        foreach ($responseTypes as $key => $responseType) {
+            $this->addResponseType($responseType, $key);
+        }
+
+        $this->tokenType = $tokenType;
+        $this->scopeUtil = $scopeUtil;
+        $this->clientAssertionType = $clientAssertionType;
+
+        if ($this->config['use_openid_connect']) {
+            $this->validateOpenIdConnect();
+        }
+    }
+
+    /**
+     * @return AuthorizeControllerInterface
+     */
+    public function getAuthorizeController()
+    {
+        if (is_null($this->authorizeController)) {
+            $this->authorizeController = $this->createDefaultAuthorizeController();
+        }
+
+        return $this->authorizeController;
+    }
+
+    /**
+     * @return TokenController
+     */
+    public function getTokenController()
+    {
+        if (is_null($this->tokenController)) {
+            $this->tokenController = $this->createDefaultTokenController();
+        }
+
+        return $this->tokenController;
+    }
+
+    /**
+     * @return ResourceControllerInterface
+     */
+    public function getResourceController()
+    {
+        if (is_null($this->resourceController)) {
+            $this->resourceController = $this->createDefaultResourceController();
+        }
+
+        return $this->resourceController;
+    }
+
+    /**
+     * @return UserInfoControllerInterface
+     */
+    public function getUserInfoController()
+    {
+        if (is_null($this->userInfoController)) {
+            $this->userInfoController = $this->createDefaultUserInfoController();
+        }
+
+        return $this->userInfoController;
+    }
+
+    /**
+     * @param AuthorizeControllerInterface $authorizeController
+     */
+    public function setAuthorizeController(AuthorizeControllerInterface $authorizeController)
+    {
+        $this->authorizeController = $authorizeController;
+    }
+
+    /**
+     * @param TokenControllerInterface $tokenController
+     */
+    public function setTokenController(TokenControllerInterface $tokenController)
+    {
+        $this->tokenController = $tokenController;
+    }
+
+    /**
+     * @param ResourceControllerInterface $resourceController
+     */
+    public function setResourceController(ResourceControllerInterface $resourceController)
+    {
+        $this->resourceController = $resourceController;
+    }
+
+    /**
+     * @param UserInfoControllerInterface $userInfoController
+     */
+    public function setUserInfoController(UserInfoControllerInterface $userInfoController)
+    {
+        $this->userInfoController = $userInfoController;
+    }
+
+    /**
+     * Return claims about the authenticated end-user.
+     * This would be called from the "/UserInfo" endpoint as defined in the spec.
+     *
+     * @param RequestInterface  $request  - Request object to grant access token
+     * @param ResponseInterface $response - Response object containing error messages (failure) or user claims (success)
+     * @return ResponseInterface
+     *
+     * @throws \InvalidArgumentException
+     * @throws \LogicException
+     *
+     * @see http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
+     */
+    public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $this->getUserInfoController()->handleUserInfoRequest($request, $this->response);
+
+        return $this->response;
+    }
+
+    /**
+     * Grant or deny a requested access token.
+     * This would be called from the "/token" endpoint as defined in the spec.
+     * Obviously, you can call your endpoint whatever you want.
+     *
+     * @param RequestInterface $request   - Request object to grant access token
+     * @param ResponseInterface $response - Response object containing error messages (failure) or access token (success)
+     * @return ResponseInterface
+     *
+     * @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 handleTokenRequest(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $this->getTokenController()->handleTokenRequest($request, $this->response);
+
+        return $this->response;
+    }
+
+    /**
+     * @param RequestInterface  $request  - Request object to grant access token
+     * @param ResponseInterface $response - Response object
+     * @return mixed
+     */
+    public function grantAccessToken(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $value = $this->getTokenController()->grantAccessToken($request, $this->response);
+
+        return $value;
+    }
+
+    /**
+     * Handle a revoke token request
+     * This would be called from the "/revoke" endpoint as defined in the draft Token Revocation spec
+     *
+     * @see https://tools.ietf.org/html/rfc7009#section-2
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return Response|ResponseInterface
+     */
+    public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $this->getTokenController()->handleRevokeRequest($request, $this->response);
+
+        return $this->response;
+    }
+
+    /**
+     * Redirect the user appropriately after approval.
+     *
+     * After the user has approved or denied the resource request the
+     * authorization server should call this function to redirect the user
+     * appropriately.
+     *
+     * @param RequestInterface  $request - The request should have the follow parameters set in the querystring:
+     * - response_type: The requested response: an access token, an authorization code, or both.
+     * - client_id: The client identifier as described in Section 2.
+     * - redirect_uri: An absolute URI to which the authorization server will redirect the user-agent to when the
+     *   end-user authorization step is completed.
+     * - scope: (optional) The scope of the resource request expressed as a list of space-delimited strings.
+     * - state: (optional) An opaque value used by the client to maintain state between the request and callback.
+     *
+     * @param ResponseInterface $response      - Response object
+     * @param bool              $is_authorized - TRUE or FALSE depending on whether the user authorized the access.
+     * @param mixed             $user_id       - Identifier of user who authorized the client
+     * @return ResponseInterface
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null)
+    {
+        $this->response = $response;
+        $this->getAuthorizeController()->handleAuthorizeRequest($request, $this->response, $is_authorized, $user_id);
+
+        return $this->response;
+    }
+
+    /**
+     * Pull the authorization request data out of the HTTP request.
+     * - The redirect_uri is OPTIONAL as per draft 20. But your implementation can enforce it
+     *   by setting $config['enforce_redirect'] to true.
+     * - The state is OPTIONAL but recommended to enforce CSRF. Draft 21 states, however, that
+     *   CSRF protection is MANDATORY. You can enforce this by setting the $config['enforce_state'] to true.
+     *
+     * The draft specifies that the parameters should be retrieved from GET, override the Response
+     * object to change this
+     *
+     * @param RequestInterface  $request  - Request object
+     * @param ResponseInterface $response - Response object
+     * @return bool
+     *
+     * The authorization parameters so the authorization server can prompt
+     * the user for approval if valid.
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4.1.1
+     * @see http://tools.ietf.org/html/rfc6749#section-10.12
+     *
+     * @ingroup oauth2_section_3
+     */
+    public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $value = $this->getAuthorizeController()->validateAuthorizeRequest($request, $this->response);
+
+        return $value;
+    }
+
+    /**
+     * @param RequestInterface  $request  - Request object
+     * @param ResponseInterface $response - Response object
+     * @param string            $scope    - Scope
+     * @return mixed
+     */
+    public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response = null, $scope = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $value = $this->getResourceController()->verifyResourceRequest($request, $this->response, $scope);
+
+        return $value;
+    }
+
+    /**
+     * @param RequestInterface  $request  - Request object
+     * @param ResponseInterface $response - Response object
+     * @return mixed
+     */
+    public function getAccessTokenData(RequestInterface $request, ResponseInterface $response = null)
+    {
+        $this->response = is_null($response) ? new Response() : $response;
+        $value = $this->getResourceController()->getAccessTokenData($request, $this->response);
+
+        return $value;
+    }
+
+    /**
+     * @param GrantTypeInterface $grantType
+     * @param mixed              $identifier
+     */
+    public function addGrantType(GrantTypeInterface $grantType, $identifier = null)
+    {
+        if (!is_string($identifier)) {
+            $identifier = $grantType->getQueryStringIdentifier();
+        }
+
+        $this->grantTypes[$identifier] = $grantType;
+
+        // persist added grant type down to TokenController
+        if (!is_null($this->tokenController)) {
+            $this->getTokenController()->addGrantType($grantType, $identifier);
+        }
+    }
+
+    /**
+     * Set a storage object for the server
+     *
+     * @param object $storage - An object implementing one of the Storage interfaces
+     * @param mixed $key - If null, the storage is set to the key of each storage interface it implements
+     *
+     * @throws InvalidArgumentException
+     * @see storageMap
+     */
+    public function addStorage($storage, $key = null)
+    {
+        // if explicitly set to a valid key, do not "magically" set below
+        if (isset($this->storageMap[$key])) {
+            if (!is_null($storage) && !$storage instanceof $this->storageMap[$key]) {
+                throw new \InvalidArgumentException(sprintf('storage of type "%s" must implement interface "%s"', $key, $this->storageMap[$key]));
+            }
+            $this->storages[$key] = $storage;
+
+            // special logic to handle "client" and "client_credentials" strangeness
+            if ($key === 'client' && !isset($this->storages['client_credentials'])) {
+                if ($storage instanceof ClientCredentialsInterface) {
+                    $this->storages['client_credentials'] = $storage;
+                }
+            } elseif ($key === 'client_credentials' && !isset($this->storages['client'])) {
+                if ($storage instanceof ClientInterface) {
+                    $this->storages['client'] = $storage;
+                }
+            }
+        } elseif (!is_null($key) && !is_numeric($key)) {
+            throw new \InvalidArgumentException(sprintf('unknown storage key "%s", must be one of [%s]', $key, implode(', ', array_keys($this->storageMap))));
+        } else {
+            $set = false;
+            foreach ($this->storageMap as $type => $interface) {
+                if ($storage instanceof $interface) {
+                    $this->storages[$type] = $storage;
+                    $set = true;
+                }
+            }
+
+            if (!$set) {
+                throw new \InvalidArgumentException(sprintf('storage of class "%s" must implement one of [%s]', get_class($storage), implode(', ', $this->storageMap)));
+            }
+        }
+    }
+
+    /**
+     * @param ResponseTypeInterface $responseType
+     * @param mixed                 $key
+     *
+     * @throws InvalidArgumentException
+     */
+    public function addResponseType(ResponseTypeInterface $responseType, $key = null)
+    {
+        $key = $this->normalizeResponseType($key);
+
+        if (isset($this->responseTypeMap[$key])) {
+            if (!$responseType instanceof $this->responseTypeMap[$key]) {
+                throw new \InvalidArgumentException(sprintf('responseType of type "%s" must implement interface "%s"', $key, $this->responseTypeMap[$key]));
+            }
+            $this->responseTypes[$key] = $responseType;
+        } elseif (!is_null($key) && !is_numeric($key)) {
+            throw new \InvalidArgumentException(sprintf('unknown responseType key "%s", must be one of [%s]', $key, implode(', ', array_keys($this->responseTypeMap))));
+        } else {
+            $set = false;
+            foreach ($this->responseTypeMap as $type => $interface) {
+                if ($responseType instanceof $interface) {
+                    $this->responseTypes[$type] = $responseType;
+                    $set = true;
+                }
+            }
+
+            if (!$set) {
+                throw new \InvalidArgumentException(sprintf('Unknown response type %s.  Please implement one of [%s]', get_class($responseType), implode(', ', $this->responseTypeMap)));
+            }
+        }
+    }
+
+    /**
+     * @return ScopeInterface
+     */
+    public function getScopeUtil()
+    {
+        if (!$this->scopeUtil) {
+            $storage = isset($this->storages['scope']) ? $this->storages['scope'] : null;
+            $this->scopeUtil = new Scope($storage);
+        }
+
+        return $this->scopeUtil;
+    }
+
+    /**
+     * @param ScopeInterface $scopeUtil
+     */
+    public function setScopeUtil($scopeUtil)
+    {
+        $this->scopeUtil = $scopeUtil;
+    }
+
+    /**
+     * @return AuthorizeControllerInterface
+     * @throws LogicException
+     */
+    protected function createDefaultAuthorizeController()
+    {
+        if (!isset($this->storages['client'])) {
+            throw new \LogicException('You must supply a storage object implementing \OAuth2\Storage\ClientInterface to use the authorize server');
+        }
+        if (0 == count($this->responseTypes)) {
+            $this->responseTypes = $this->getDefaultResponseTypes();
+        }
+        if ($this->config['use_openid_connect'] && !isset($this->responseTypes['id_token'])) {
+            $this->responseTypes['id_token'] = $this->createDefaultIdTokenResponseType();
+            if ($this->config['allow_implicit']) {
+                $this->responseTypes['id_token token'] = $this->createDefaultIdTokenTokenResponseType();
+            }
+        }
+
+        $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri')));
+
+        if ($this->config['use_openid_connect']) {
+            return new OpenIDAuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil());
+        }
+
+        return new AuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil());
+    }
+
+    /**
+     * @return TokenControllerInterface
+     * @throws LogicException
+     */
+    protected function createDefaultTokenController()
+    {
+        if (0 == count($this->grantTypes)) {
+            $this->grantTypes = $this->getDefaultGrantTypes();
+        }
+
+        if (is_null($this->clientAssertionType)) {
+            // see if HttpBasic assertion type is requred.  If so, then create it from storage classes.
+            foreach ($this->grantTypes as $grantType) {
+                if (!$grantType instanceof ClientAssertionTypeInterface) {
+                    if (!isset($this->storages['client_credentials'])) {
+                        throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\ClientCredentialsInterface to use the token server');
+                    }
+                    $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_credentials_in_request_body allow_public_clients')));
+                    $this->clientAssertionType = new HttpBasic($this->storages['client_credentials'], $config);
+                    break;
+                }
+            }
+        }
+
+        if (!isset($this->storages['client'])) {
+            throw new LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server");
+        }
+
+        $accessTokenResponseType = $this->getAccessTokenResponseType();
+
+        return new TokenController($accessTokenResponseType, $this->storages['client'], $this->grantTypes, $this->clientAssertionType, $this->getScopeUtil());
+    }
+
+    /**
+     * @return ResourceControllerInterface
+     * @throws LogicException
+     */
+    protected function createDefaultResourceController()
+    {
+        if ($this->config['use_jwt_access_tokens']) {
+            // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set
+            if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) {
+                $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage();
+            }
+        } elseif (!isset($this->storages['access_token'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the resource server');
+        }
+
+        if (!$this->tokenType) {
+            $this->tokenType = $this->getDefaultTokenType();
+        }
+
+        $config = array_intersect_key($this->config, array('www_realm' => ''));
+
+        return new ResourceController($this->tokenType, $this->storages['access_token'], $config, $this->getScopeUtil());
+    }
+
+    /**
+     * @return UserInfoControllerInterface
+     * @throws LogicException
+     */
+    protected function createDefaultUserInfoController()
+    {
+        if ($this->config['use_jwt_access_tokens']) {
+            // overwrites access token storage with crypto token storage if "use_jwt_access_tokens" is set
+            if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof JwtAccessTokenInterface) {
+                $this->storages['access_token'] = $this->createDefaultJwtAccessTokenStorage();
+            }
+        } elseif (!isset($this->storages['access_token'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use JwtAccessTokens to use the UserInfo server');
+        }
+
+        if (!isset($this->storages['user_claims'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use the UserInfo server');
+        }
+
+        if (!$this->tokenType) {
+            $this->tokenType = $this->getDefaultTokenType();
+        }
+
+        $config = array_intersect_key($this->config, array('www_realm' => ''));
+
+        return new UserInfoController($this->tokenType, $this->storages['access_token'], $this->storages['user_claims'], $config, $this->getScopeUtil());
+    }
+
+    /**
+     * @return Bearer
+     */
+    protected function getDefaultTokenType()
+    {
+        $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_param_name token_bearer_header_name')));
+
+        return new Bearer($config);
+    }
+
+    /**
+     * @return array
+     * @throws LogicException
+     */
+    protected function getDefaultResponseTypes()
+    {
+        $responseTypes = array();
+
+        if ($this->config['allow_implicit']) {
+            $responseTypes['token'] = $this->getAccessTokenResponseType();
+        }
+
+        if ($this->config['use_openid_connect']) {
+            $responseTypes['id_token'] = $this->getIdTokenResponseType();
+            if ($this->config['allow_implicit']) {
+                $responseTypes['id_token token'] = $this->getIdTokenTokenResponseType();
+            }
+        }
+
+        if (isset($this->storages['authorization_code'])) {
+            $config = array_intersect_key($this->config, array_flip(explode(' ', 'enforce_redirect auth_code_lifetime')));
+            if ($this->config['use_openid_connect']) {
+                if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) {
+                    throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true');
+                }
+                $responseTypes['code'] = new OpenIDAuthorizationCodeResponseType($this->storages['authorization_code'], $config);
+                $responseTypes['code id_token'] = new CodeIdToken($responseTypes['code'], $responseTypes['id_token']);
+            } else {
+                $responseTypes['code'] = new AuthorizationCodeResponseType($this->storages['authorization_code'], $config);
+            }
+        }
+
+        if (count($responseTypes) == 0) {
+            throw new \LogicException('You must supply an array of response_types in the constructor or implement a OAuth2\Storage\AuthorizationCodeInterface storage object or set "allow_implicit" to true and implement a OAuth2\Storage\AccessTokenInterface storage object');
+        }
+
+        return $responseTypes;
+    }
+
+    /**
+     * @return array
+     * @throws LogicException
+     */
+    protected function getDefaultGrantTypes()
+    {
+        $grantTypes = array();
+
+        if (isset($this->storages['user_credentials'])) {
+            $grantTypes['password'] = new UserCredentials($this->storages['user_credentials']);
+        }
+
+        if (isset($this->storages['client_credentials'])) {
+            $config = array_intersect_key($this->config, array('allow_credentials_in_request_body' => ''));
+            $grantTypes['client_credentials'] = new ClientCredentials($this->storages['client_credentials'], $config);
+        }
+
+        if (isset($this->storages['refresh_token'])) {
+            $config = array_intersect_key($this->config, array_flip(explode(' ', 'always_issue_new_refresh_token unset_refresh_token_after_use')));
+            $grantTypes['refresh_token'] = new RefreshToken($this->storages['refresh_token'], $config);
+        }
+
+        if (isset($this->storages['authorization_code'])) {
+            if ($this->config['use_openid_connect']) {
+                if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) {
+                    throw new \LogicException('Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when "use_openid_connect" is true');
+                }
+                $grantTypes['authorization_code'] = new OpenIDAuthorizationCodeGrantType($this->storages['authorization_code']);
+            } else {
+                $grantTypes['authorization_code'] = new AuthorizationCode($this->storages['authorization_code']);
+            }
+        }
+
+        if (count($grantTypes) == 0) {
+            throw new \LogicException('Unable to build default grant types - You must supply an array of grant_types in the constructor');
+        }
+
+        return $grantTypes;
+    }
+
+    /**
+     * @return AccessToken
+     */
+    protected function getAccessTokenResponseType()
+    {
+        if (isset($this->responseTypes['token'])) {
+            return $this->responseTypes['token'];
+        }
+
+        if ($this->config['use_jwt_access_tokens']) {
+            return $this->createDefaultJwtAccessTokenResponseType();
+        }
+
+        return $this->createDefaultAccessTokenResponseType();
+    }
+
+    /**
+     * @return IdToken
+     */
+    protected function getIdTokenResponseType()
+    {
+        if (isset($this->responseTypes['id_token'])) {
+            return $this->responseTypes['id_token'];
+        }
+
+        return $this->createDefaultIdTokenResponseType();
+    }
+
+    /**
+     * @return IdTokenToken
+     */
+    protected function getIdTokenTokenResponseType()
+    {
+        if (isset($this->responseTypes['id_token token'])) {
+            return $this->responseTypes['id_token token'];
+        }
+
+        return $this->createDefaultIdTokenTokenResponseType();
+    }
+
+    /**
+     * For Resource Controller
+     *
+     * @return JwtAccessTokenStorage
+     * @throws LogicException
+     */
+    protected function createDefaultJwtAccessTokenStorage()
+    {
+        if (!isset($this->storages['public_key'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens');
+        }
+        $tokenStorage = null;
+        if (!empty($this->config['store_encrypted_token_string']) && isset($this->storages['access_token'])) {
+            $tokenStorage = $this->storages['access_token'];
+        }
+        // wrap the access token storage as required.
+        return new JwtAccessTokenStorage($this->storages['public_key'], $tokenStorage);
+    }
+
+    /**
+     * For Authorize and Token Controllers
+     *
+     * @return JwtAccessToken
+     * @throws LogicException
+     */
+    protected function createDefaultJwtAccessTokenResponseType()
+    {
+        if (!isset($this->storages['public_key'])) {
+            throw new \LogicException('You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use crypto tokens');
+        }
+
+        $tokenStorage = null;
+        if (isset($this->storages['access_token'])) {
+            $tokenStorage = $this->storages['access_token'];
+        }
+
+        $refreshStorage = null;
+        if (isset($this->storages['refresh_token'])) {
+            $refreshStorage = $this->storages['refresh_token'];
+        }
+
+        $config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string issuer access_lifetime refresh_token_lifetime jwt_extra_payload_callable')));
+
+        return new JwtAccessToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config);
+    }
+
+    /**
+     * @return AccessToken
+     * @throws LogicException
+     */
+    protected function createDefaultAccessTokenResponseType()
+    {
+        if (!isset($this->storages['access_token'])) {
+            throw new LogicException("You must supply a response type implementing OAuth2\ResponseType\AccessTokenInterface, or a storage object implementing OAuth2\Storage\AccessTokenInterface to use the token server");
+        }
+
+        $refreshStorage = null;
+        if (isset($this->storages['refresh_token'])) {
+            $refreshStorage = $this->storages['refresh_token'];
+        }
+
+        $config = array_intersect_key($this->config, array_flip(explode(' ', 'access_lifetime refresh_token_lifetime')));
+        $config['token_type'] = $this->tokenType ? $this->tokenType->getTokenType() :  $this->getDefaultTokenType()->getTokenType();
+
+        return new AccessToken($this->storages['access_token'], $refreshStorage, $config);
+    }
+
+    /**
+     * @return IdToken
+     * @throws LogicException
+     */
+    protected function createDefaultIdTokenResponseType()
+    {
+        if (!isset($this->storages['user_claims'])) {
+            throw new LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use openid connect");
+        }
+        if (!isset($this->storages['public_key'])) {
+            throw new LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect");
+        }
+
+        $config = array_intersect_key($this->config, array_flip(explode(' ', 'issuer id_lifetime')));
+
+        return new IdToken($this->storages['user_claims'], $this->storages['public_key'], $config);
+    }
+
+    /**
+     * @return IdTokenToken
+     */
+    protected function createDefaultIdTokenTokenResponseType()
+    {
+        return new IdTokenToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType());
+    }
+
+    /**
+     * @throws InvalidArgumentException
+     */
+    protected function validateOpenIdConnect()
+    {
+        $authCodeGrant = $this->getGrantType('authorization_code');
+        if (!empty($authCodeGrant) && !$authCodeGrant instanceof OpenIDAuthorizationCodeGrantType) {
+            throw new \InvalidArgumentException('You have enabled OpenID Connect, but supplied a grant type that does not support it.');
+        }
+    }
+
+    /**
+     * @param string $name
+     * @return string
+     */
+    protected function normalizeResponseType($name)
+    {
+        // for multiple-valued response types - make them alphabetical
+        if (!empty($name) && false !== strpos($name, ' ')) {
+            $types = explode(' ', $name);
+            sort($types);
+            $name = implode(' ', $types);
+        }
+
+        return $name;
+    }
+
+    /**
+     * @return mixed
+     */
+    public function getResponse()
+    {
+        return $this->response;
+    }
+
+    /**
+     * @return array
+     */
+    public function getStorages()
+    {
+        return $this->storages;
+    }
+
+    /**
+     * @param string $name
+     * @return object|null
+     */
+    public function getStorage($name)
+    {
+        return isset($this->storages[$name]) ? $this->storages[$name] : null;
+    }
+
+    /**
+     * @return array
+     */
+    public function getGrantTypes()
+    {
+        return $this->grantTypes;
+    }
+
+    /**
+     * @param string $name
+     * @return object|null
+     */
+    public function getGrantType($name)
+    {
+        return isset($this->grantTypes[$name]) ? $this->grantTypes[$name] : null;
+    }
+
+    /**
+     * @return array
+     */
+    public function getResponseTypes()
+    {
+        return $this->responseTypes;
+    }
+
+    /**
+     * @param string $name
+     * @return object|null
+     */
+    public function getResponseType($name)
+    {
+        // for multiple-valued response types - make them alphabetical
+        $name = $this->normalizeResponseType($name);
+
+        return isset($this->responseTypes[$name]) ? $this->responseTypes[$name] : null;
+    }
+
+    /**
+     * @return TokenTypeInterface
+     */
+    public function getTokenType()
+    {
+        return $this->tokenType;
+    }
+
+    /**
+     * @return ClientAssertionTypeInterface
+     */
+    public function getClientAssertionType()
+    {
+        return $this->clientAssertionType;
+    }
+
+    /**
+     * @param string $name
+     * @param mixed $value
+     */
+    public function setConfig($name, $value)
+    {
+        $this->config[$name] = $value;
+    }
+
+    /**
+     * @param string $name
+     * @param mixed $default
+     * @return mixed
+     */
+    public function getConfig($name, $default = null)
+    {
+        return isset($this->config[$name]) ? $this->config[$name] : $default;
+    }
+}

+ 65 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AccessTokenInterface.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should get/save access tokens
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface AccessTokenInterface
+{
+    /**
+     * Look up the supplied oauth_token from storage.
+     *
+     * We need to retrieve access token data as we create and verify tokens.
+     *
+     * @param string $oauth_token - oauth_token to be check with.
+     *
+     * @return array|null - An associative array as below, and return NULL if the supplied oauth_token is invalid:
+     * @code
+     *     array(
+     *         'expires'   => $expires,   // Stored expiration in unix timestamp.
+     *         'client_id' => $client_id, // (optional) Stored client identifier.
+     *         'user_id'   => $user_id,   // (optional) Stored user identifier.
+     *         'scope'     => $scope,     // (optional) Stored scope values in space-separated string.
+     *         'id_token'  => $id_token   // (optional) Stored id_token (if "use_openid_connect" is true).
+     *     );
+     * @endcode
+     *
+     * @ingroup oauth2_section_7
+     */
+    public function getAccessToken($oauth_token);
+
+    /**
+     * Store the supplied access token values to storage.
+     *
+     * We need to store access token data as we create and verify tokens.
+     *
+     * @param string $oauth_token - oauth_token to be stored.
+     * @param mixed  $client_id   - client identifier to be stored.
+     * @param mixed  $user_id     - user identifier to be stored.
+     * @param int    $expires     - expiration to be stored as a Unix timestamp.
+     * @param string $scope       - OPTIONAL Scopes to be stored in space-separated string.
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null);
+
+    /**
+     * Expire an access token.
+     *
+     * This is not explicitly required in the spec, but if defined in a draft RFC for token
+     * revoking (RFC 7009) https://tools.ietf.org/html/rfc7009
+     *
+     * @param $access_token
+     * Access token to be expired.
+     *
+     * @return BOOL true if an access token was unset, false if not
+     * @ingroup oauth2_section_6
+     *
+     * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x
+     */
+    //public function unsetAccessToken($access_token);
+}

+ 86 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/AuthorizationCodeInterface.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * 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
+{
+    /**
+     * The Authorization Code grant type supports a response type of "code".
+     *
+     * @var string
+     * @see http://tools.ietf.org/html/rfc6749#section-1.4.1
+     * @see http://tools.ietf.org/html/rfc6749#section-4.2
+     */
+    const RESPONSE_TYPE_CODE = "code";
+
+    /**
+     * Fetch authorization code data (probably the most common grant type).
+     *
+     * Retrieve the stored data for the given authorization code.
+     *
+     * Required for OAuth2::GRANT_TYPE_AUTH_CODE.
+     *
+     * @param $code
+     * Authorization code to be check with.
+     *
+     * @return
+     * An associative array as below, and NULL if the code is invalid
+     * @code
+     * return array(
+     *     "client_id"    => CLIENT_ID,      // REQUIRED Stored client identifier
+     *     "user_id"      => USER_ID,        // REQUIRED Stored user identifier
+     *     "expires"      => EXPIRES,        // REQUIRED Stored expiration in unix timestamp
+     *     "redirect_uri" => REDIRECT_URI,   // REQUIRED Stored redirect URI
+     *     "scope"        => SCOPE,          // OPTIONAL Stored scope values in space-separated string
+     * );
+     * @endcode
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4.1
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function getAuthorizationCode($code);
+
+    /**
+     * 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 string $code         - Authorization code to be stored.
+     * @param mixed  $client_id    - Client identifier to be stored.
+     * @param mixed  $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.
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null);
+
+    /**
+     * once an Authorization Code is used, it must be expired
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4.1.2
+     *
+     *    The client MUST NOT use the authorization code
+     *    more than once.  If an authorization code is used more than
+     *    once, the authorization server MUST deny the request and SHOULD
+     *    revoke (when possible) all tokens previously issued based on
+     *    that authorization code
+     *
+     */
+    public function expireAuthorizationCode($code);
+}

+ 660 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Cassandra.php

@@ -0,0 +1,660 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use phpcassa\ColumnFamily;
+use phpcassa\ColumnSlice;
+use phpcassa\Connection\ConnectionPool;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+use InvalidArgumentException;
+
+/**
+ * Cassandra storage for all storage types
+ *
+ * To use, install "thobbs/phpcassa" via composer:
+ * <code>
+ *     composer require thobbs/phpcassa:dev-master
+ * </code>
+ *
+ * Once this is done, instantiate the connection:
+ * <code>
+ *     $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160'));
+ * </code>
+ *
+ * Then, register the storage client:
+ * <code>
+ *     $storage = new OAuth2\Storage\Cassandra($cassandra);
+ *     $storage->setClientDetails($client_id, $client_secret, $redirect_uri);
+ * </code>
+ *
+ * @see test/lib/OAuth2/Storage/Bootstrap::getCassandraStorage
+ */
+class Cassandra implements AuthorizationCodeInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    UserCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    ScopeInterface,
+    PublicKeyInterface,
+    UserClaimsInterface,
+    OpenIDAuthorizationCodeInterface
+{
+
+    private $cache;
+
+    /**
+     * @var ConnectionPool
+     */
+    protected $cassandra;
+
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * Cassandra Storage! uses phpCassa
+     *
+     * @param ConnectionPool|array $connection
+     * @param array                $config
+     *
+     * @throws InvalidArgumentException
+     */
+    public function __construct($connection = array(), array $config = array())
+    {
+        if ($connection instanceof ConnectionPool) {
+            $this->cassandra = $connection;
+        } else {
+            if (!is_array($connection)) {
+                throw new InvalidArgumentException('First argument to OAuth2\Storage\Cassandra must be an instance of phpcassa\Connection\ConnectionPool or a configuration array');
+            }
+            $connection = array_merge(array(
+                'keyspace' => 'oauth2',
+                'servers'  => null,
+            ), $connection);
+
+            $this->cassandra = new ConnectionPool($connection['keyspace'], $connection['servers']);
+        }
+
+        $this->config = array_merge(array(
+            // cassandra config
+            'column_family' => 'auth',
+
+            // key names
+            'client_key' => 'oauth_clients:',
+            'access_token_key' => 'oauth_access_tokens:',
+            'refresh_token_key' => 'oauth_refresh_tokens:',
+            'code_key' => 'oauth_authorization_codes:',
+            'user_key' => 'oauth_users:',
+            'jwt_key' => 'oauth_jwt:',
+            'scope_key' => 'oauth_scopes:',
+            'public_key_key'  => 'oauth_public_keys:',
+        ), $config);
+    }
+
+    /**
+     * @param $key
+     * @return bool|mixed
+     */
+    protected function getValue($key)
+    {
+        if (isset($this->cache[$key])) {
+            return $this->cache[$key];
+        }
+        $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
+
+        try {
+            $value = $cf->get($key, new ColumnSlice("", ""));
+            $value = array_shift($value);
+        } catch (\cassandra\NotFoundException $e) {
+            return false;
+        }
+
+        return json_decode($value, true);
+    }
+
+    /**
+     * @param $key
+     * @param $value
+     * @param int $expire
+     * @return bool
+     */
+    protected function setValue($key, $value, $expire = 0)
+    {
+        $this->cache[$key] = $value;
+
+        $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
+
+        $str = json_encode($value);
+        if ($expire > 0) {
+            try {
+                $seconds = $expire - time();
+                // __data key set as C* requires a field, note: max TTL can only be 630720000 seconds
+                $cf->insert($key, array('__data' => $str), null, $seconds);
+            } catch (\Exception $e) {
+                return false;
+            }
+        } else {
+            try {
+                // __data key set as C* requires a field
+                $cf->insert($key, array('__data' => $str));
+            } catch (\Exception $e) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * @param $key
+     * @return bool
+     */
+    protected function expireValue($key)
+    {
+        unset($this->cache[$key]);
+
+        $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
+
+        if ($cf->get_count($key) > 0) {
+            try {
+                // __data key set as C* requires a field
+                $cf->remove($key, array('__data'));
+            } catch (\Exception $e) {
+                return false;
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @param string $code
+     * @return bool|mixed
+     */
+    public function getAuthorizationCode($code)
+    {
+        return $this->getValue($this->config['code_key'] . $code);
+    }
+
+    /**
+     * @param string $authorization_code
+     * @param mixed  $client_id
+     * @param mixed  $user_id
+     * @param string $redirect_uri
+     * @param int    $expires
+     * @param string $scope
+     * @param string $id_token
+     * @return bool
+     */
+    public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        return $this->setValue(
+            $this->config['code_key'] . $authorization_code,
+            compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'),
+            $expires
+        );
+    }
+
+    /**
+     * @param string $code
+     * @return bool
+     */
+    public function expireAuthorizationCode($code)
+    {
+        $key = $this->config['code_key'] . $code;
+        unset($this->cache[$key]);
+
+        return $this->expireValue($key);
+    }
+
+    /**
+     * @param string $username
+     * @param string $password
+     * @return bool
+     */
+    public function checkUserCredentials($username, $password)
+    {
+        if ($user = $this->getUser($username)) {
+            return $this->checkPassword($user, $password);
+        }
+
+        return false;
+    }
+
+    /**
+     * plaintext passwords are bad!  Override this for your application
+     *
+     * @param array  $user
+     * @param string $password
+     * @return bool
+     */
+    protected function checkPassword($user, $password)
+    {
+        return $user['password'] == $this->hashPassword($password);
+    }
+
+    // use a secure hashing algorithm when storing passwords. Override this for your application
+    protected function hashPassword($password)
+    {
+        return sha1($password);
+    }
+
+    /**
+     * @param string $username
+     * @return array|bool|false
+     */
+    public function getUserDetails($username)
+    {
+        return $this->getUser($username);
+    }
+
+    /**
+     * @param string $username
+     * @return array|bool
+     */
+    public function getUser($username)
+    {
+        if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) {
+            return false;
+        }
+
+        // the default behavior is to use "username" as the user_id
+        return array_merge(array(
+            'user_id' => $username,
+        ), $userInfo);
+    }
+
+    /**
+     * @param string $username
+     * @param string $password
+     * @param string $first_name
+     * @param string $last_name
+     * @return bool
+     */
+    public function setUser($username, $password, $first_name = null, $last_name = null)
+    {
+        $password = $this->hashPassword($password);
+
+        return $this->setValue(
+            $this->config['user_key'] . $username,
+            compact('username', 'password', 'first_name', 'last_name')
+        );
+    }
+
+    /**
+     * @param mixed  $client_id
+     * @param string $client_secret
+     * @return bool
+     */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        if (!$client = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        return isset($client['client_secret'])
+            && $client['client_secret'] == $client_secret;
+    }
+
+    /**
+     * @param $client_id
+     * @return bool
+     */
+    public function isPublicClient($client_id)
+    {
+        if (!$client = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        return empty($client['client_secret']);
+    }
+
+    /**
+     * @param $client_id
+     * @return array|bool|mixed
+     */
+    public function getClientDetails($client_id)
+    {
+        return $this->getValue($this->config['client_key'] . $client_id);
+    }
+
+    /**
+     * @param $client_id
+     * @param null $client_secret
+     * @param null $redirect_uri
+     * @param null $grant_types
+     * @param null $scope
+     * @param null $user_id
+     * @return bool
+     */
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        return $this->setValue(
+            $this->config['client_key'] . $client_id,
+            compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id')
+        );
+    }
+
+    /**
+     * @param $client_id
+     * @param $grant_type
+     * @return bool
+     */
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+
+            return in_array($grant_type, (array) $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /**
+     * @param $refresh_token
+     * @return bool|mixed
+     */
+    public function getRefreshToken($refresh_token)
+    {
+        return $this->getValue($this->config['refresh_token_key'] . $refresh_token);
+    }
+
+    /**
+     * @param $refresh_token
+     * @param $client_id
+     * @param $user_id
+     * @param $expires
+     * @param null $scope
+     * @return bool
+     */
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        return $this->setValue(
+            $this->config['refresh_token_key'] . $refresh_token,
+            compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'),
+            $expires
+        );
+    }
+
+    /**
+     * @param $refresh_token
+     * @return bool
+     */
+    public function unsetRefreshToken($refresh_token)
+    {
+        return $this->expireValue($this->config['refresh_token_key'] . $refresh_token);
+    }
+
+    /**
+     * @param string $access_token
+     * @return array|bool|mixed|null
+     */
+    public function getAccessToken($access_token)
+    {
+        return $this->getValue($this->config['access_token_key'].$access_token);
+    }
+
+    /**
+     * @param string $access_token
+     * @param mixed $client_id
+     * @param mixed $user_id
+     * @param int $expires
+     * @param null $scope
+     * @return bool
+     */
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        return $this->setValue(
+            $this->config['access_token_key'].$access_token,
+            compact('access_token', 'client_id', 'user_id', 'expires', 'scope'),
+            $expires
+        );
+    }
+
+    /**
+     * @param $access_token
+     * @return bool
+     */
+    public function unsetAccessToken($access_token)
+    {
+        return $this->expireValue($this->config['access_token_key'] . $access_token);
+    }
+
+    /**
+     * @param $scope
+     * @return bool
+     */
+    public function scopeExists($scope)
+    {
+        $scope = explode(' ', $scope);
+
+        $result = $this->getValue($this->config['scope_key'].'supported:global');
+
+        $supportedScope = explode(' ', (string) $result);
+
+        return (count(array_diff($scope, $supportedScope)) == 0);
+    }
+
+    /**
+     * @param null $client_id
+     * @return bool|mixed
+     */
+    public function getDefaultScope($client_id = null)
+    {
+        if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) {
+            $result = $this->getValue($this->config['scope_key'].'default:global');
+        }
+
+        return $result;
+    }
+
+    /**
+     * @param $scope
+     * @param null $client_id
+     * @param string $type
+     * @return bool
+     * @throws \InvalidArgumentException
+     */
+    public function setScope($scope, $client_id = null, $type = 'supported')
+    {
+        if (!in_array($type, array('default', 'supported'))) {
+            throw new \InvalidArgumentException('"$type" must be one of "default", "supported"');
+        }
+
+        if (is_null($client_id)) {
+            $key = $this->config['scope_key'].$type.':global';
+        } else {
+            $key = $this->config['scope_key'].$type.':'.$client_id;
+        }
+
+        return $this->setValue($key, $scope);
+    }
+
+    /**
+     * @param $client_id
+     * @param $subject
+     * @return bool|null
+     */
+    public function getClientKey($client_id, $subject)
+    {
+        if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) {
+            return false;
+        }
+
+        if (isset($jwt['subject']) && $jwt['subject'] == $subject ) {
+            return $jwt['key'];
+        }
+
+        return null;
+    }
+
+    /**
+     * @param $client_id
+     * @param $key
+     * @param null $subject
+     * @return bool
+     */
+    public function setClientKey($client_id, $key, $subject = null)
+    {
+        return $this->setValue($this->config['jwt_key'] . $client_id, array(
+            'key' => $key,
+            'subject' => $subject
+        ));
+    }
+
+    /**
+     * @param $client_id
+     * @return bool|null
+     */
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    /**
+     * @param $client_id
+     * @param $subject
+     * @param $audience
+     * @param $expiration
+     * @param $jti
+     * @throws \Exception
+     */
+    public function getJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs cassandra implementation.
+        throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.');
+    }
+
+    /**
+     * @param $client_id
+     * @param $subject
+     * @param $audience
+     * @param $expiration
+     * @param $jti
+     * @throws \Exception
+     */
+    public function setJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs cassandra implementation.
+        throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.');
+    }
+
+    /**
+     * @param string $client_id
+     * @return mixed
+     */
+    public function getPublicKey($client_id = '')
+    {
+        $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
+        if (is_array($public_key)) {
+            return $public_key['public_key'];
+        }
+        $public_key = $this->getValue($this->config['public_key_key']);
+        if (is_array($public_key)) {
+            return $public_key['public_key'];
+        }
+    }
+
+    /**
+     * @param string $client_id
+     * @return mixed
+     */
+    public function getPrivateKey($client_id = '')
+    {
+        $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
+        if (is_array($public_key)) {
+            return $public_key['private_key'];
+        }
+        $public_key = $this->getValue($this->config['public_key_key']);
+        if (is_array($public_key)) {
+            return $public_key['private_key'];
+        }
+    }
+
+    /**
+     * @param null $client_id
+     * @return mixed|string
+     */
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
+        if (is_array($public_key)) {
+            return $public_key['encryption_algorithm'];
+        }
+        $public_key = $this->getValue($this->config['public_key_key']);
+        if (is_array($public_key)) {
+            return $public_key['encryption_algorithm'];
+        }
+
+        return 'RS256';
+    }
+
+    /**
+     * @param mixed $user_id
+     * @param string $claims
+     * @return array|bool
+     */
+    public function getUserClaims($user_id, $claims)
+    {
+        $userDetails = $this->getUserDetails($user_id);
+        if (!is_array($userDetails)) {
+            return false;
+        }
+
+        $claims = explode(' ', trim($claims));
+        $userClaims = array();
+
+        // for each requested claim, if the user has the claim, set it in the response
+        $validClaims = explode(' ', self::VALID_CLAIMS);
+        foreach ($validClaims as $validClaim) {
+            if (in_array($validClaim, $claims)) {
+                if ($validClaim == 'address') {
+                    // address is an object with subfields
+                    $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
+                } else {
+                    $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
+                }
+            }
+        }
+
+        return $userClaims;
+    }
+
+    /**
+     * @param $claim
+     * @param $userDetails
+     * @return array
+     */
+    protected function getUserClaim($claim, $userDetails)
+    {
+        $userClaims = array();
+        $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
+        $claimValues = explode(' ', $claimValuesString);
+
+        foreach ($claimValues as $value) {
+            if ($value == 'email_verified') {
+                $userClaims[$value] = $userDetails[$value]=='true' ? true : false;
+            } else {
+                $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
+            }
+        }
+
+        return $userClaims;
+    }
+}

+ 49 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientCredentialsInterface.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * Implement this interface to specify how the OAuth2 Server
+ * should verify client credentials
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface ClientCredentialsInterface extends ClientInterface
+{
+
+    /**
+     * Make sure that the client credentials is valid.
+     *
+     * @param $client_id
+     * Client identifier to be check with.
+     * @param $client_secret
+     * (optional) If a secret is required, check that they've given the right one.
+     *
+     * @return
+     * TRUE if the client credentials are valid, and MUST return FALSE if it isn't.
+     * @endcode
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-3.1
+     *
+     * @ingroup oauth2_section_3
+     */
+    public function checkClientCredentials($client_id, $client_secret = null);
+
+    /**
+     * Determine if the client is a "public" client, and therefore
+     * does not require passing credentials for certain grant types
+     *
+     * @param $client_id
+     * Client identifier to be check with.
+     *
+     * @return
+     * TRUE if the client is public, and FALSE if it isn't.
+     * @endcode
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-2.3
+     * @see https://github.com/bshaffer/oauth2-server-php/issues/257
+     *
+     * @ingroup oauth2_section_2
+     */
+    public function isPublicClient($client_id);
+}

+ 66 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ClientInterface.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should retrieve client information
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface ClientInterface
+{
+    /**
+     * Get client details corresponding client_id.
+     *
+     * OAuth says we should store request URIs for each registered client.
+     * Implement this function to grab the stored URI for a given client id.
+     *
+     * @param $client_id
+     * Client identifier to be check with.
+     *
+     * @return array
+     *               Client details. The only mandatory key in the array is "redirect_uri".
+     *               This function MUST return FALSE if the given client does not exist or is
+     *               invalid. "redirect_uri" can be space-delimited to allow for multiple valid uris.
+     *               <code>
+     *               return array(
+     *               "redirect_uri" => REDIRECT_URI,      // REQUIRED redirect_uri registered for the client
+     *               "client_id"    => CLIENT_ID,         // OPTIONAL the client id
+     *               "grant_types"  => GRANT_TYPES,       // OPTIONAL an array of restricted grant types
+     *               "user_id"      => USER_ID,           // OPTIONAL the user identifier associated with this client
+     *               "scope"        => SCOPE,             // OPTIONAL the scopes allowed for this client
+     *               );
+     *               </code>
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function getClientDetails($client_id);
+
+    /**
+     * Get the scope associated with this client
+     *
+     * @return
+     * STRING the space-delineated scope list for the specified client_id
+     */
+    public function getClientScope($client_id);
+
+    /**
+     * Check restricted grant types of corresponding client identifier.
+     *
+     * If you want to restrict clients to certain grant types, override this
+     * function.
+     *
+     * @param $client_id
+     * Client identifier to be check with.
+     * @param $grant_type
+     * Grant type to be check with
+     *
+     * @return
+     * TRUE if the grant type is supported by this client identifier, and
+     * FALSE if it isn't.
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function checkRestrictedGrantType($client_id, $grant_type);
+}

+ 331 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/CouchbaseDB.php

@@ -0,0 +1,331 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * Simple Couchbase storage for all storage types
+ *
+ * This class should be extended or overridden as required
+ *
+ * NOTE: Passwords are stored in plaintext, which is never
+ * a good idea.  Be sure to override this for your application
+ *
+ * @author Tom Park <tom@raucter.com>
+ */
+class CouchbaseDB implements AuthorizationCodeInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    UserCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    OpenIDAuthorizationCodeInterface
+{
+    protected $db;
+    protected $config;
+
+    public function __construct($connection, $config = array())
+    {
+        if ($connection instanceof \Couchbase) {
+            $this->db = $connection;
+        } else {
+            if (!is_array($connection) || !is_array($connection['servers'])) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\CouchbaseDB must be an instance of Couchbase or a configuration array containing a server array');
+            }
+
+            $this->db = new \Couchbase($connection['servers'], (!isset($connection['username'])) ? '' : $connection['username'], (!isset($connection['password'])) ? '' : $connection['password'], $connection['bucket'], false);
+        }
+
+        $this->config = array_merge(array(
+            'client_table' => 'oauth_clients',
+            'access_token_table' => 'oauth_access_tokens',
+            'refresh_token_table' => 'oauth_refresh_tokens',
+            'code_table' => 'oauth_authorization_codes',
+            'user_table' => 'oauth_users',
+            'jwt_table' => 'oauth_jwt',
+        ), $config);
+    }
+
+    // Helper function to access couchbase item by type:
+    protected function getObjectByType($name,$id)
+    {
+        return json_decode($this->db->get($this->config[$name].'-'.$id),true);
+    }
+
+    // Helper function to set couchbase item by type:
+    protected function setObjectByType($name,$id,$array)
+    {
+        $array['type'] = $name;
+
+        return $this->db->set($this->config[$name].'-'.$id,json_encode($array));
+    }
+
+    // Helper function to delete couchbase item by type, wait for persist to at least 1 node
+    protected function deleteObjectByType($name,$id)
+    {
+        $this->db->delete($this->config[$name].'-'.$id,"",1);
+    }
+
+    /* ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        if ($result = $this->getObjectByType('client_table',$client_id)) {
+            return $result['client_secret'] == $client_secret;
+        }
+
+        return false;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        if (!$result = $this->getObjectByType('client_table',$client_id)) {
+            return false;
+        }
+
+        return empty($result['client_secret']);
+    }
+
+    /* ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        $result = $this->getObjectByType('client_table',$client_id);
+
+        return is_null($result) ? false : $result;
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        if ($this->getClientDetails($client_id)) {
+
+            $this->setObjectByType('client_table',$client_id, array(
+                'client_id'     => $client_id,
+                'client_secret' => $client_secret,
+                'redirect_uri'  => $redirect_uri,
+                'grant_types'   => $grant_types,
+                'scope'         => $scope,
+                'user_id'       => $user_id,
+            ));
+        } else {
+            $this->setObjectByType('client_table',$client_id, array(
+                'client_id'     => $client_id,
+                'client_secret' => $client_secret,
+                'redirect_uri'  => $redirect_uri,
+                'grant_types'   => $grant_types,
+                'scope'         => $scope,
+                'user_id'       => $user_id,
+            ));
+        }
+
+        return true;
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+
+            return in_array($grant_type, $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /* AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        $token = $this->getObjectByType('access_token_table',$access_token);
+
+        return is_null($token) ? false : $token;
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // if it exists, update it.
+        if ($this->getAccessToken($access_token)) {
+            $this->setObjectByType('access_token_table',$access_token, array(
+                'access_token' => $access_token,
+                'client_id' => $client_id,
+                'expires' => $expires,
+                'user_id' => $user_id,
+                'scope' => $scope
+            ));
+        } else {
+            $this->setObjectByType('access_token_table',$access_token,  array(
+                'access_token' => $access_token,
+                'client_id' => $client_id,
+                'expires' => $expires,
+                'user_id' => $user_id,
+                'scope' => $scope
+            ));
+        }
+
+        return true;
+    }
+
+    /* AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        $code = $this->getObjectByType('code_table',$code);
+
+        return is_null($code) ? false : $code;
+    }
+
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        // if it exists, update it.
+        if ($this->getAuthorizationCode($code)) {
+            $this->setObjectByType('code_table',$code, array(
+                'authorization_code' => $code,
+                'client_id' => $client_id,
+                'user_id' => $user_id,
+                'redirect_uri' => $redirect_uri,
+                'expires' => $expires,
+                'scope' => $scope,
+                'id_token' => $id_token,
+            ));
+        } else {
+            $this->setObjectByType('code_table',$code,array(
+                'authorization_code' => $code,
+                'client_id' => $client_id,
+                'user_id' => $user_id,
+                'redirect_uri' => $redirect_uri,
+                'expires' => $expires,
+                'scope' => $scope,
+                'id_token' => $id_token,
+            ));
+        }
+
+        return true;
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+        $this->deleteObjectByType('code_table',$code);
+
+        return true;
+    }
+
+    /* UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        if ($user = $this->getUser($username)) {
+            return $this->checkPassword($user, $password);
+        }
+
+        return false;
+    }
+
+    public function getUserDetails($username)
+    {
+        if ($user = $this->getUser($username)) {
+            $user['user_id'] = $user['username'];
+        }
+
+        return $user;
+    }
+
+    /* RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        $token = $this->getObjectByType('refresh_token_table',$refresh_token);
+
+        return is_null($token) ? false : $token;
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        $this->setObjectByType('refresh_token_table',$refresh_token, array(
+            'refresh_token' => $refresh_token,
+            'client_id' => $client_id,
+            'user_id' => $user_id,
+            'expires' => $expires,
+            'scope' => $scope
+        ));
+
+        return true;
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        $this->deleteObjectByType('refresh_token_table',$refresh_token);
+
+        return true;
+    }
+
+    // plaintext passwords are bad!  Override this for your application
+    protected function checkPassword($user, $password)
+    {
+        return $user['password'] == $password;
+    }
+
+    public function getUser($username)
+    {
+        $result = $this->getObjectByType('user_table',$username);
+
+        return is_null($result) ? false : $result;
+    }
+
+    public function setUser($username, $password, $firstName = null, $lastName = null)
+    {
+        if ($this->getUser($username)) {
+            $this->setObjectByType('user_table',$username, array(
+                'username' => $username,
+                'password' => $password,
+                'first_name' => $firstName,
+                'last_name' => $lastName
+            ));
+
+        } else {
+            $this->setObjectByType('user_table',$username, array(
+                'username' => $username,
+                'password' => $password,
+                'first_name' => $firstName,
+                'last_name' => $lastName
+            ));
+
+        }
+
+        return true;
+    }
+
+    public function getClientKey($client_id, $subject)
+    {
+        if (!$jwt = $this->getObjectByType('jwt_table',$client_id)) {
+            return false;
+        }
+
+        if (isset($jwt['subject']) && $jwt['subject'] == $subject) {
+            return $jwt['key'];
+        }
+
+        return false;
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs couchbase implementation.
+        throw new \Exception('getJti() for the Couchbase driver is currently unimplemented.');
+    }
+
+    public function setJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs couchbase implementation.
+        throw new \Exception('setJti() for the Couchbase driver is currently unimplemented.');
+    }
+}

+ 540 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/DynamoDB.php

@@ -0,0 +1,540 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use Aws\DynamoDb\DynamoDbClient;
+
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+/**
+ * DynamoDB storage for all storage types
+ *
+ * To use, install "aws/aws-sdk-php" via composer
+ * <code>
+ *  composer require aws/aws-sdk-php:dev-master
+ * </code>
+ *
+ * Once this is done, instantiate the DynamoDB client
+ * <code>
+ *  $storage = new OAuth2\Storage\Dynamodb(array("key" => "YOURKEY", "secret" => "YOURSECRET", "region" => "YOURREGION"));
+ * </code>
+ *
+ * Table :
+ *  - oauth_access_tokens (primary hash key : access_token)
+ *  - oauth_authorization_codes (primary hash key : authorization_code)
+ *  - oauth_clients (primary hash key : client_id)
+ *  - oauth_jwt (primary hash key : client_id, primary range key : subject)
+ *  - oauth_public_keys (primary hash key : client_id)
+ *  - oauth_refresh_tokens (primary hash key : refresh_token)
+ *  - oauth_scopes (primary hash key : scope, secondary index : is_default-index hash key is_default)
+ *  - oauth_users (primary hash key : username)
+ *
+ * @author Frederic AUGUSTE <frederic.auguste at gmail dot com>
+ */
+class DynamoDB implements
+    AuthorizationCodeInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    UserCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    ScopeInterface,
+    PublicKeyInterface,
+    UserClaimsInterface,
+    OpenIDAuthorizationCodeInterface
+{
+    protected $client;
+    protected $config;
+
+    public function __construct($connection, $config = array())
+    {
+        if (!($connection instanceof DynamoDbClient)) {
+            if (!is_array($connection)) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region');
+            }
+            if (!array_key_exists("key",$connection) || !array_key_exists("secret",$connection) || !array_key_exists("region",$connection) ) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region');
+            }
+            $this->client = DynamoDbClient::factory(array(
+                'key' => $connection["key"],
+                'secret' => $connection["secret"],
+                'region' =>$connection["region"]
+            ));
+        } else {
+            $this->client = $connection;
+        }
+
+        $this->config = array_merge(array(
+            'client_table' => 'oauth_clients',
+            'access_token_table' => 'oauth_access_tokens',
+            'refresh_token_table' => 'oauth_refresh_tokens',
+            'code_table' => 'oauth_authorization_codes',
+            'user_table' => 'oauth_users',
+            'jwt_table'  => 'oauth_jwt',
+            'scope_table'  => 'oauth_scopes',
+            'public_key_table'  => 'oauth_public_keys',
+        ), $config);
+    }
+
+    /* OAuth2\Storage\ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['client_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+
+        return  $result->count()==1 && $result["Item"]["client_secret"]["S"] == $client_secret;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['client_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+
+        if ($result->count()==0) {
+            return false ;
+        }
+
+        return empty($result["Item"]["client_secret"]);
+    }
+
+    /* OAuth2\Storage\ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['client_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $result = $this->dynamo2array($result);
+        foreach (array('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') as $key => $val) {
+            if (!array_key_exists ($val, $result)) {
+                $result[$val] = null;
+            }
+        }
+
+        return $result;
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        $clientData = compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id');
+        $clientData = array_filter($clientData, 'self::isNotEmpty');
+
+        $result = $this->client->putItem(array(
+            'TableName' =>  $this->config['client_table'],
+            'Item' => $this->client->formatAttributes($clientData)
+        ));
+
+        return true;
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+
+            return in_array($grant_type, (array) $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /* OAuth2\Storage\AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['access_token_table'],
+            "Key" => array('access_token'   => array('S' => $access_token))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+        if (array_key_exists ('expires', $token)) {
+            $token['expires'] = strtotime($token['expires']);
+        }
+
+        return $token;
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        $clientData = compact('access_token', 'client_id', 'user_id', 'expires', 'scope');
+        $clientData = array_filter($clientData, 'self::isNotEmpty');
+
+        $result = $this->client->putItem(array(
+            'TableName' =>  $this->config['access_token_table'],
+            'Item' => $this->client->formatAttributes($clientData)
+        ));
+
+        return true;
+
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        $result = $this->client->deleteItem(array(
+            'TableName' =>  $this->config['access_token_table'],
+            'Key' => $this->client->formatAttributes(array("access_token" => $access_token)),
+            'ReturnValues' => 'ALL_OLD',
+        ));
+
+        return null !== $result->get('Attributes');
+    }
+
+    /* OAuth2\Storage\AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['code_table'],
+            "Key" => array('authorization_code'   => array('S' => $code))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+        if (!array_key_exists("id_token", $token )) {
+            $token['id_token'] = null;
+        }
+        $token['expires'] = strtotime($token['expires']);
+
+        return $token;
+
+    }
+
+    public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope');
+        $clientData = array_filter($clientData, 'self::isNotEmpty');
+
+        $result = $this->client->putItem(array(
+            'TableName' =>  $this->config['code_table'],
+            'Item' => $this->client->formatAttributes($clientData)
+        ));
+
+        return true;
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+
+        $result = $this->client->deleteItem(array(
+            'TableName' =>  $this->config['code_table'],
+            'Key' => $this->client->formatAttributes(array("authorization_code" => $code))
+        ));
+
+        return true;
+    }
+
+    /* OAuth2\Storage\UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        if ($user = $this->getUser($username)) {
+            return $this->checkPassword($user, $password);
+        }
+
+        return false;
+    }
+
+    public function getUserDetails($username)
+    {
+        return $this->getUser($username);
+    }
+
+    /* UserClaimsInterface */
+    public function getUserClaims($user_id, $claims)
+    {
+        if (!$userDetails = $this->getUserDetails($user_id)) {
+            return false;
+        }
+
+        $claims = explode(' ', trim($claims));
+        $userClaims = array();
+
+        // for each requested claim, if the user has the claim, set it in the response
+        $validClaims = explode(' ', self::VALID_CLAIMS);
+        foreach ($validClaims as $validClaim) {
+            if (in_array($validClaim, $claims)) {
+                if ($validClaim == 'address') {
+                    // address is an object with subfields
+                    $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
+                } else {
+                    $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
+                }
+            }
+        }
+
+        return $userClaims;
+    }
+
+    protected function getUserClaim($claim, $userDetails)
+    {
+        $userClaims = array();
+        $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
+        $claimValues = explode(' ', $claimValuesString);
+
+        foreach ($claimValues as $value) {
+            if ($value == 'email_verified') {
+                $userClaims[$value] = $userDetails[$value]=='true' ? true : false;
+            } else {
+                $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
+            }
+        }
+
+        return $userClaims;
+    }
+
+    /* OAuth2\Storage\RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['refresh_token_table'],
+            "Key" => array('refresh_token'   => array('S' => $refresh_token))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+        $token['expires'] = strtotime($token['expires']);
+
+        return $token;
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        $clientData = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope');
+        $clientData = array_filter($clientData, 'self::isNotEmpty');
+
+        $result = $this->client->putItem(array(
+            'TableName' =>  $this->config['refresh_token_table'],
+            'Item' => $this->client->formatAttributes($clientData)
+        ));
+
+        return true;
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        $result = $this->client->deleteItem(array(
+            'TableName' =>  $this->config['refresh_token_table'],
+            'Key' => $this->client->formatAttributes(array("refresh_token" => $refresh_token))
+        ));
+
+        return true;
+    }
+
+    // plaintext passwords are bad!  Override this for your application
+    protected function checkPassword($user, $password)
+    {
+        return $user['password'] == $this->hashPassword($password);
+    }
+
+    // use a secure hashing algorithm when storing passwords. Override this for your application
+    protected function hashPassword($password)
+    {
+        return sha1($password);
+    }
+
+    public function getUser($username)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['user_table'],
+            "Key" => array('username'   => array('S' => $username))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+        $token['user_id'] = $username;
+
+        return $token;
+    }
+
+    public function setUser($username, $password, $first_name = null, $last_name = null)
+    {
+        // do not store in plaintext
+        $password = $this->hashPassword($password);
+
+        $clientData = compact('username', 'password', 'first_name', 'last_name');
+        $clientData = array_filter($clientData, 'self::isNotEmpty');
+
+        $result = $this->client->putItem(array(
+            'TableName' =>  $this->config['user_table'],
+            'Item' => $this->client->formatAttributes($clientData)
+        ));
+
+        return true;
+
+    }
+
+    /* ScopeInterface */
+    public function scopeExists($scope)
+    {
+        $scope = explode(' ', $scope);
+        $scope_query = array();
+        $count = 0;
+        foreach ($scope as $key => $val) {
+            $result = $this->client->query(array(
+                'TableName'     => $this->config['scope_table'],
+                'Select'        => 'COUNT',
+                'KeyConditions' => array(
+                    'scope' => array(
+                        'AttributeValueList' => array(array('S' => $val)),
+                        'ComparisonOperator' => 'EQ'
+                    )
+                )
+            ));
+            $count += $result['Count'];
+        }
+
+        return $count == count($scope);
+    }
+
+    public function getDefaultScope($client_id = null)
+    {
+
+        $result = $this->client->query(array(
+            'TableName' => $this->config['scope_table'],
+            'IndexName' => 'is_default-index',
+            'Select' => 'ALL_ATTRIBUTES',
+            'KeyConditions' => array(
+                'is_default' => array(
+                    'AttributeValueList' => array(array('S' => 'true')),
+                    'ComparisonOperator' => 'EQ',
+                ),
+            )
+        ));
+        $defaultScope = array();
+        if ($result->count() > 0) {
+            $array = $result->toArray();
+            foreach ($array["Items"] as $item) {
+                $defaultScope[]  = $item['scope']['S'];
+            }
+
+            return empty($defaultScope) ? null : implode(' ', $defaultScope);
+        }
+
+        return null;
+    }
+
+    /* JWTBearerInterface */
+    public function getClientKey($client_id, $subject)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['jwt_table'],
+            "Key" => array('client_id'   => array('S' => $client_id), 'subject' => array('S' => $subject))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+
+        return $token['public_key'];
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        //TODO not use.
+    }
+
+    public function setJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        //TODO not use.
+    }
+
+    /* PublicKeyInterface */
+    public function getPublicKey($client_id = '0')
+    {
+
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['public_key_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+
+        return $token['public_key'];
+
+    }
+
+    public function getPrivateKey($client_id = '0')
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['public_key_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+        if ($result->count()==0) {
+            return false ;
+        }
+        $token = $this->dynamo2array($result);
+
+        return $token['private_key'];
+    }
+
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        $result = $this->client->getItem(array(
+            "TableName"=> $this->config['public_key_table'],
+            "Key" => array('client_id'   => array('S' => $client_id))
+        ));
+        if ($result->count()==0) {
+            return 'RS256' ;
+        }
+        $token = $this->dynamo2array($result);
+
+        return $token['encryption_algorithm'];
+    }
+
+    /**
+     * Transform dynamodb resultset to an array.
+     * @param $dynamodbResult
+     * @return $array
+     */
+    private function dynamo2array($dynamodbResult)
+    {
+        $result = array();
+        foreach ($dynamodbResult["Item"] as $key => $val) {
+            $result[$key] = $val["S"];
+            $result[] = $val["S"];
+        }
+
+        return $result;
+    }
+
+    private static function isNotEmpty($value)
+    {
+        return null !== $value && '' !== $value;
+    }
+}

+ 87 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessToken.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\Encryption\EncryptionInterface;
+use OAuth2\Encryption\Jwt;
+
+/**
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class JwtAccessToken implements JwtAccessTokenInterface
+{
+    protected $publicKeyStorage;
+    protected $tokenStorage;
+    protected $encryptionUtil;
+
+    /**
+     * @param OAuth2\Encryption\PublicKeyInterface  $publicKeyStorage the public key encryption to use
+     * @param OAuth2\Storage\AccessTokenInterface   $tokenStorage     OPTIONAL persist the access token to another storage. This is useful if
+     *                                                                you want to retain access token grant information somewhere, but
+     *                                                                is not necessary when using this grant type.
+     * @param OAuth2\Encryption\EncryptionInterface $encryptionUtil   OPTIONAL class to use for "encode" and "decode" functions.
+     */
+    public function __construct(PublicKeyInterface $publicKeyStorage, AccessTokenInterface $tokenStorage = null, EncryptionInterface $encryptionUtil = null)
+    {
+        $this->publicKeyStorage = $publicKeyStorage;
+        $this->tokenStorage = $tokenStorage;
+        if (is_null($encryptionUtil)) {
+            $encryptionUtil = new Jwt;
+        }
+        $this->encryptionUtil = $encryptionUtil;
+    }
+
+    public function getAccessToken($oauth_token)
+    {
+        // just decode the token, don't verify
+        if (!$tokenData = $this->encryptionUtil->decode($oauth_token, null, false)) {
+            return false;
+        }
+
+        $client_id  = isset($tokenData['aud']) ? $tokenData['aud'] : null;
+        $public_key = $this->publicKeyStorage->getPublicKey($client_id);
+        $algorithm  = $this->publicKeyStorage->getEncryptionAlgorithm($client_id);
+
+        // now that we have the client_id, verify the token
+        if (false === $this->encryptionUtil->decode($oauth_token, $public_key, array($algorithm))) {
+            return false;
+        }
+
+        // normalize the JWT claims to the format expected by other components in this library
+        return $this->convertJwtToOAuth2($tokenData);
+    }
+
+    public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        if ($this->tokenStorage) {
+            return $this->tokenStorage->setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope);
+        }
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        if ($this->tokenStorage) {
+            return $this->tokenStorage->unsetAccessToken($access_token);
+        }
+    }
+
+
+    // converts a JWT access token into an OAuth2-friendly format
+    protected function convertJwtToOAuth2($tokenData)
+    {
+        $keyMapping = array(
+            'aud' => 'client_id',
+            'exp' => 'expires',
+            'sub' => 'user_id'
+        );
+
+        foreach ($keyMapping as $jwtKey => $oauth2Key) {
+            if (isset($tokenData[$jwtKey])) {
+                $tokenData[$oauth2Key] = $tokenData[$jwtKey];
+                unset($tokenData[$jwtKey]);
+            }
+        }
+
+        return $tokenData;
+    }
+}

+ 14 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtAccessTokenInterface.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * No specific methods, but allows the library to check "instanceof"
+ * against interface rather than class
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface JwtAccessTokenInterface extends AccessTokenInterface
+{
+
+}

+ 74 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/JwtBearerInterface.php

@@ -0,0 +1,74 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should get the JWT key for clients
+ *
+ * @TODO consider extending ClientInterface, as this will almost always
+ * be the same storage as retrieving clientData
+ *
+ * @author F21
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface JwtBearerInterface
+{
+    /**
+     * Get the public key associated with a client_id
+     *
+     * @param $client_id
+     * Client identifier to be checked with.
+     *
+     * @return
+     * STRING Return the public key for the client_id if it exists, and MUST return FALSE if it doesn't.
+     */
+    public function getClientKey($client_id, $subject);
+
+    /**
+     * Get a jti (JSON token identifier) by matching against the client_id, subject, audience and expiration.
+     *
+     * @param $client_id
+     * Client identifier to match.
+     *
+     * @param $subject
+     * The subject to match.
+     *
+     * @param $audience
+     * The audience to match.
+     *
+     * @param $expiration
+     * The expiration of the jti.
+     *
+     * @param $jti
+     * The jti to match.
+     *
+     * @return
+     * An associative array as below, and return NULL if the jti does not exist.
+     * - issuer: Stored client identifier.
+     * - subject: Stored subject.
+     * - audience: Stored audience.
+     * - expires: Stored expiration in unix timestamp.
+     * - jti: The stored jti.
+     */
+    public function getJti($client_id, $subject, $audience, $expiration, $jti);
+
+    /**
+     * Store a used jti so that we can check against it to prevent replay attacks.
+     * @param $client_id
+     * Client identifier to insert.
+     *
+     * @param $subject
+     * The subject to insert.
+     *
+     * @param $audience
+     * The audience to insert.
+     *
+     * @param $expiration
+     * The expiration of the jti.
+     *
+     * @param $jti
+     * The jti to insert.
+     */
+    public function setJti($client_id, $subject, $audience, $expiration, $jti);
+}

+ 381 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Memory.php

@@ -0,0 +1,381 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * Simple in-memory storage for all storage types
+ *
+ * NOTE: This class should never be used in production, and is
+ * a stub class for example use only
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class Memory implements AuthorizationCodeInterface,
+    UserCredentialsInterface,
+    UserClaimsInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    ScopeInterface,
+    PublicKeyInterface,
+    OpenIDAuthorizationCodeInterface
+{
+    public $authorizationCodes;
+    public $userCredentials;
+    public $clientCredentials;
+    public $refreshTokens;
+    public $accessTokens;
+    public $jwt;
+    public $jti;
+    public $supportedScopes;
+    public $defaultScope;
+    public $keys;
+
+    public function __construct($params = array())
+    {
+        $params = array_merge(array(
+            'authorization_codes' => array(),
+            'user_credentials' => array(),
+            'client_credentials' => array(),
+            'refresh_tokens' => array(),
+            'access_tokens' => array(),
+            'jwt' => array(),
+            'jti' => array(),
+            'default_scope' => null,
+            'supported_scopes' => array(),
+            'keys' => array(),
+        ), $params);
+
+        $this->authorizationCodes = $params['authorization_codes'];
+        $this->userCredentials = $params['user_credentials'];
+        $this->clientCredentials = $params['client_credentials'];
+        $this->refreshTokens = $params['refresh_tokens'];
+        $this->accessTokens = $params['access_tokens'];
+        $this->jwt = $params['jwt'];
+        $this->jti = $params['jti'];
+        $this->supportedScopes = $params['supported_scopes'];
+        $this->defaultScope = $params['default_scope'];
+        $this->keys = $params['keys'];
+    }
+
+    /* AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        if (!isset($this->authorizationCodes[$code])) {
+            return false;
+        }
+
+        return array_merge(array(
+            'authorization_code' => $code,
+        ), $this->authorizationCodes[$code]);
+    }
+
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token');
+
+        return true;
+    }
+
+    public function setAuthorizationCodes($authorization_codes)
+    {
+        $this->authorizationCodes = $authorization_codes;
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+        unset($this->authorizationCodes[$code]);
+    }
+
+    /* UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        $userDetails = $this->getUserDetails($username);
+
+        return $userDetails && $userDetails['password'] && $userDetails['password'] === $password;
+    }
+
+    public function setUser($username, $password, $firstName = null, $lastName = null)
+    {
+        $this->userCredentials[$username] = array(
+            'password'   => $password,
+            'first_name' => $firstName,
+            'last_name'  => $lastName,
+        );
+
+        return true;
+    }
+
+    public function getUserDetails($username)
+    {
+        if (!isset($this->userCredentials[$username])) {
+            return false;
+        }
+
+        return array_merge(array(
+            'user_id'    => $username,
+            'password'   => null,
+            'first_name' => null,
+            'last_name'  => null,
+        ), $this->userCredentials[$username]);
+    }
+
+    /* UserClaimsInterface */
+    public function getUserClaims($user_id, $claims)
+    {
+        if (!$userDetails = $this->getUserDetails($user_id)) {
+            return false;
+        }
+
+        $claims = explode(' ', trim($claims));
+        $userClaims = array();
+
+        // for each requested claim, if the user has the claim, set it in the response
+        $validClaims = explode(' ', self::VALID_CLAIMS);
+        foreach ($validClaims as $validClaim) {
+            if (in_array($validClaim, $claims)) {
+                if ($validClaim == 'address') {
+                    // address is an object with subfields
+                    $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
+                } else {
+                    $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
+                }
+            }
+        }
+
+        return $userClaims;
+    }
+
+    protected function getUserClaim($claim, $userDetails)
+    {
+        $userClaims = array();
+        $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
+        $claimValues = explode(' ', $claimValuesString);
+
+        foreach ($claimValues as $value) {
+            $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
+        }
+
+        return $userClaims;
+    }
+
+    /* ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        return isset($this->clientCredentials[$client_id]['client_secret']) && $this->clientCredentials[$client_id]['client_secret'] === $client_secret;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        if (!isset($this->clientCredentials[$client_id])) {
+            return false;
+        }
+
+        return empty($this->clientCredentials[$client_id]['client_secret']);
+    }
+
+    /* ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        if (!isset($this->clientCredentials[$client_id])) {
+            return false;
+        }
+
+        $clientDetails = array_merge(array(
+            'client_id'     => $client_id,
+            'client_secret' => null,
+            'redirect_uri'  => null,
+            'scope'         => null,
+        ), $this->clientCredentials[$client_id]);
+
+        return $clientDetails;
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        if (isset($this->clientCredentials[$client_id]['grant_types'])) {
+            $grant_types = explode(' ', $this->clientCredentials[$client_id]['grant_types']);
+
+            return in_array($grant_type, $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        $this->clientCredentials[$client_id] = array(
+            'client_id'     => $client_id,
+            'client_secret' => $client_secret,
+            'redirect_uri'  => $redirect_uri,
+            'grant_types'   => $grant_types,
+            'scope'         => $scope,
+            'user_id'       => $user_id,
+        );
+
+        return true;
+    }
+
+    /* RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        return isset($this->refreshTokens[$refresh_token]) ? $this->refreshTokens[$refresh_token] : false;
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        $this->refreshTokens[$refresh_token] = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope');
+
+        return true;
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        if (isset($this->refreshTokens[$refresh_token])) {
+            unset($this->refreshTokens[$refresh_token]);
+
+            return true;
+        }
+
+        return false;
+    }
+
+    public function setRefreshTokens($refresh_tokens)
+    {
+        $this->refreshTokens = $refresh_tokens;
+    }
+
+    /* AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        return isset($this->accessTokens[$access_token]) ? $this->accessTokens[$access_token] : false;
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null, $id_token = null)
+    {
+        $this->accessTokens[$access_token] = compact('access_token', 'client_id', 'user_id', 'expires', 'scope', 'id_token');
+
+        return true;
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        if (isset($this->accessTokens[$access_token])) {
+            unset($this->accessTokens[$access_token]);
+
+            return true;
+        }
+
+        return false;
+    }
+
+    public function scopeExists($scope)
+    {
+        $scope = explode(' ', trim($scope));
+
+        return (count(array_diff($scope, $this->supportedScopes)) == 0);
+    }
+
+    public function getDefaultScope($client_id = null)
+    {
+        return $this->defaultScope;
+    }
+
+    /*JWTBearerInterface */
+    public function getClientKey($client_id, $subject)
+    {
+        if (isset($this->jwt[$client_id])) {
+            $jwt = $this->jwt[$client_id];
+            if ($jwt) {
+                if ($jwt["subject"] == $subject) {
+                    return $jwt["key"];
+                }
+            }
+        }
+
+        return false;
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        foreach ($this->jti as $storedJti) {
+            if ($storedJti['issuer'] == $client_id && $storedJti['subject'] == $subject && $storedJti['audience'] == $audience && $storedJti['expires'] == $expires && $storedJti['jti'] == $jti) {
+                return array(
+                    'issuer' => $storedJti['issuer'],
+                    'subject' => $storedJti['subject'],
+                    'audience' => $storedJti['audience'],
+                    'expires' => $storedJti['expires'],
+                    'jti' => $storedJti['jti']
+                );
+            }
+        }
+
+        return null;
+    }
+
+    public function setJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        $this->jti[] = array('issuer' => $client_id, 'subject' => $subject, 'audience' => $audience, 'expires' => $expires, 'jti' => $jti);
+    }
+
+    /*PublicKeyInterface */
+    public function getPublicKey($client_id = null)
+    {
+        if (isset($this->keys[$client_id])) {
+            return $this->keys[$client_id]['public_key'];
+        }
+
+        // use a global encryption pair
+        if (isset($this->keys['public_key'])) {
+            return $this->keys['public_key'];
+        }
+
+        return false;
+    }
+
+    public function getPrivateKey($client_id = null)
+    {
+        if (isset($this->keys[$client_id])) {
+            return $this->keys[$client_id]['private_key'];
+        }
+
+        // use a global encryption pair
+        if (isset($this->keys['private_key'])) {
+            return $this->keys['private_key'];
+        }
+
+        return false;
+    }
+
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        if (isset($this->keys[$client_id]['encryption_algorithm'])) {
+            return $this->keys[$client_id]['encryption_algorithm'];
+        }
+
+        // use a global encryption algorithm
+        if (isset($this->keys['encryption_algorithm'])) {
+            return $this->keys['encryption_algorithm'];
+        }
+
+        return 'RS256';
+    }
+}

+ 392 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Mongo.php

@@ -0,0 +1,392 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * Simple MongoDB storage for all storage types
+ *
+ * NOTE: This class is meant to get users started
+ * quickly. If your application requires further
+ * customization, extend this class or create your own.
+ *
+ * NOTE: Passwords are stored in plaintext, which is never
+ * a good idea.  Be sure to override this for your application
+ *
+ * @author Julien Chaumond <chaumond@gmail.com>
+ */
+class Mongo implements AuthorizationCodeInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    UserCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    PublicKeyInterface,
+    OpenIDAuthorizationCodeInterface
+{
+    protected $db;
+    protected $config;
+
+    public function __construct($connection, $config = array())
+    {
+        if ($connection instanceof \MongoDB) {
+            $this->db = $connection;
+        } else {
+            if (!is_array($connection)) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB or a configuration array');
+            }
+            $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']);
+            $m = new \MongoClient($server);
+            $this->db = $m->{$connection['database']};
+        }
+
+        $this->config = array_merge(array(
+            'client_table' => 'oauth_clients',
+            'access_token_table' => 'oauth_access_tokens',
+            'refresh_token_table' => 'oauth_refresh_tokens',
+            'code_table' => 'oauth_authorization_codes',
+            'user_table' => 'oauth_users',
+            'key_table' => 'oauth_keys',
+            'jwt_table' => 'oauth_jwt',
+        ), $config);
+    }
+
+    // Helper function to access a MongoDB collection by `type`:
+    protected function collection($name)
+    {
+        return $this->db->{$this->config[$name]};
+    }
+
+    /* ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) {
+            return $result['client_secret'] == $client_secret;
+        }
+
+        return false;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) {
+            return false;
+        }
+
+        return empty($result['client_secret']);
+    }
+
+    /* ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        $result = $this->collection('client_table')->findOne(array('client_id' => $client_id));
+
+        return is_null($result) ? false : $result;
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        if ($this->getClientDetails($client_id)) {
+            $this->collection('client_table')->update(
+                array('client_id' => $client_id),
+                array('$set' => array(
+                    'client_secret' => $client_secret,
+                    'redirect_uri'  => $redirect_uri,
+                    'grant_types'   => $grant_types,
+                    'scope'         => $scope,
+                    'user_id'       => $user_id,
+                ))
+            );
+        } else {
+            $client = array(
+                'client_id'     => $client_id,
+                'client_secret' => $client_secret,
+                'redirect_uri'  => $redirect_uri,
+                'grant_types'   => $grant_types,
+                'scope'         => $scope,
+                'user_id'       => $user_id,
+            );
+            $this->collection('client_table')->insert($client);
+        }
+
+        return true;
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+
+            return in_array($grant_type, $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /* AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token));
+
+        return is_null($token) ? false : $token;
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // if it exists, update it.
+        if ($this->getAccessToken($access_token)) {
+            $this->collection('access_token_table')->update(
+                array('access_token' => $access_token),
+                array('$set' => array(
+                    'client_id' => $client_id,
+                    'expires' => $expires,
+                    'user_id' => $user_id,
+                    'scope' => $scope
+                ))
+            );
+        } else {
+            $token = array(
+                'access_token' => $access_token,
+                'client_id' => $client_id,
+                'expires' => $expires,
+                'user_id' => $user_id,
+                'scope' => $scope
+            );
+            $this->collection('access_token_table')->insert($token);
+        }
+
+        return true;
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        $result = $this->collection('access_token_table')->remove(array(
+            'access_token' => $access_token
+        ), array('w' => 1));
+
+        return $result['n'] > 0;
+    }
+
+
+    /* AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        $code = $this->collection('code_table')->findOne(array('authorization_code' => $code));
+
+        return is_null($code) ? false : $code;
+    }
+
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        // if it exists, update it.
+        if ($this->getAuthorizationCode($code)) {
+            $this->collection('code_table')->update(
+                array('authorization_code' => $code),
+                array('$set' => array(
+                    'client_id' => $client_id,
+                    'user_id' => $user_id,
+                    'redirect_uri' => $redirect_uri,
+                    'expires' => $expires,
+                    'scope' => $scope,
+                    'id_token' => $id_token,
+                ))
+            );
+        } else {
+            $token = array(
+                'authorization_code' => $code,
+                'client_id' => $client_id,
+                'user_id' => $user_id,
+                'redirect_uri' => $redirect_uri,
+                'expires' => $expires,
+                'scope' => $scope,
+                'id_token' => $id_token,
+            );
+            $this->collection('code_table')->insert($token);
+        }
+
+        return true;
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+        $this->collection('code_table')->remove(array('authorization_code' => $code));
+
+        return true;
+    }
+
+    /* UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        if ($user = $this->getUser($username)) {
+            return $this->checkPassword($user, $password);
+        }
+
+        return false;
+    }
+
+    public function getUserDetails($username)
+    {
+        if ($user = $this->getUser($username)) {
+            $user['user_id'] = $user['username'];
+        }
+
+        return $user;
+    }
+
+    /* RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        $token = $this->collection('refresh_token_table')->findOne(array('refresh_token' => $refresh_token));
+
+        return is_null($token) ? false : $token;
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        $token = array(
+            'refresh_token' => $refresh_token,
+            'client_id' => $client_id,
+            'user_id' => $user_id,
+            'expires' => $expires,
+            'scope' => $scope
+        );
+        $this->collection('refresh_token_table')->insert($token);
+
+        return true;
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        $result = $this->collection('refresh_token_table')->remove(array(
+            'refresh_token' => $refresh_token
+        ), array('w' => 1));
+
+        return $result['n'] > 0;
+    }
+
+    // plaintext passwords are bad!  Override this for your application
+    protected function checkPassword($user, $password)
+    {
+        return $user['password'] == $password;
+    }
+
+    public function getUser($username)
+    {
+        $result = $this->collection('user_table')->findOne(array('username' => $username));
+
+        return is_null($result) ? false : $result;
+    }
+
+    public function setUser($username, $password, $firstName = null, $lastName = null)
+    {
+        if ($this->getUser($username)) {
+            $this->collection('user_table')->update(
+                array('username' => $username),
+                array('$set' => array(
+                    'password' => $password,
+                    'first_name' => $firstName,
+                    'last_name' => $lastName
+                ))
+            );
+        } else {
+            $user = array(
+                'username' => $username,
+                'password' => $password,
+                'first_name' => $firstName,
+                'last_name' => $lastName
+            );
+            $this->collection('user_table')->insert($user);
+        }
+
+        return true;
+    }
+
+    public function getClientKey($client_id, $subject)
+    {
+        $result = $this->collection('jwt_table')->findOne(array(
+            'client_id' => $client_id,
+            'subject' => $subject
+        ));
+
+        return is_null($result) ? false : $result['key'];
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs mongodb implementation.
+        throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.');
+    }
+
+    public function setJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs mongodb implementation.
+        throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.');
+    }
+
+    public function getPublicKey($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['public_key'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? false : $result['public_key'];
+    }
+
+    public function getPrivateKey($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['private_key'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? false : $result['private_key'];
+    }
+
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['encryption_algorithm'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? 'RS256' : $result['encryption_algorithm'];
+    }
+}

+ 380 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/MongoDB.php

@@ -0,0 +1,380 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use MongoDB\Client;
+use MongoDB\Database;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * Simple MongoDB storage for all storage types
+ *
+ * NOTE: This class is meant to get users started
+ * quickly. If your application requires further
+ * customization, extend this class or create your own.
+ *
+ * NOTE: Passwords are stored in plaintext, which is never
+ * a good idea.  Be sure to override this for your application
+ *
+ * @author Julien Chaumond <chaumond@gmail.com>
+ */
+class MongoDB implements AuthorizationCodeInterface,
+    UserCredentialsInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    PublicKeyInterface,
+    OpenIDAuthorizationCodeInterface
+{
+    protected $db;
+    protected $config;
+
+    public function __construct($connection, $config = array())
+    {
+        if ($connection instanceof Database) {
+            $this->db = $connection;
+        } else {
+            if (!is_array($connection)) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB\Database or a configuration array');
+            }
+            $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']);
+            $m = new Client($server);
+            $this->db = $m->selectDatabase($connection['database']);
+        }
+        $this->config = array_merge(array(
+            'client_table' => 'oauth_clients',
+            'access_token_table' => 'oauth_access_tokens',
+            'refresh_token_table' => 'oauth_refresh_tokens',
+            'code_table' => 'oauth_authorization_codes',
+            'user_table' => 'oauth_users',
+            'jwt_table' => 'oauth_jwt',
+            'jti_table' => 'oauth_jti',
+            'scope_table'  => 'oauth_scopes',
+            'key_table'  => 'oauth_keys',
+        ), $config);
+    }
+
+    /* ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) {
+            return $result['client_secret'] == $client_secret;
+        }
+        return false;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) {
+            return false;
+        }
+        return empty($result['client_secret']);
+    }
+
+    /* ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        $result = $this->collection('client_table')->findOne(array('client_id' => $client_id));
+        return is_null($result) ? false : $result;
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        if ($this->getClientDetails($client_id)) {
+            $result = $this->collection('client_table')->updateOne(
+                array('client_id' => $client_id),
+                array('$set' => array(
+                    'client_secret' => $client_secret,
+                    'redirect_uri'  => $redirect_uri,
+                    'grant_types'   => $grant_types,
+                    'scope'         => $scope,
+                    'user_id'       => $user_id,
+                ))
+            );
+            return $result->getMatchedCount() > 0;
+        }
+        $client = array(
+            'client_id'     => $client_id,
+            'client_secret' => $client_secret,
+            'redirect_uri'  => $redirect_uri,
+            'grant_types'   => $grant_types,
+            'scope'         => $scope,
+            'user_id'       => $user_id,
+        );
+        $result = $this->collection('client_table')->insertOne($client);
+        return $result->getInsertedCount() > 0;
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+            return in_array($grant_type, $grant_types);
+        }
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /* AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token));
+        return is_null($token) ? false : $token;
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // if it exists, update it.
+        if ($this->getAccessToken($access_token)) {
+            $result = $this->collection('access_token_table')->updateOne(
+                array('access_token' => $access_token),
+                array('$set' => array(
+                    'client_id' => $client_id,
+                    'expires' => $expires,
+                    'user_id' => $user_id,
+                    'scope' => $scope
+                ))
+            );
+            return $result->getMatchedCount() > 0;
+        }
+        $token = array(
+            'access_token' => $access_token,
+            'client_id' => $client_id,
+            'expires' => $expires,
+            'user_id' => $user_id,
+            'scope' => $scope
+        );
+        $result = $this->collection('access_token_table')->insertOne($token);
+        return $result->getInsertedCount() > 0;
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        $result = $this->collection('access_token_table')->deleteOne(array(
+            'access_token' => $access_token
+        ));
+        return $result->getDeletedCount() > 0;
+    }
+
+    /* AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        $code = $this->collection('code_table')->findOne(array(
+            'authorization_code' => $code
+        ));
+        return is_null($code) ? false : $code;
+    }
+
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        // if it exists, update it.
+        if ($this->getAuthorizationCode($code)) {
+            $result = $this->collection('code_table')->updateOne(
+                array('authorization_code' => $code),
+                array('$set' => array(
+                    'client_id' => $client_id,
+                    'user_id' => $user_id,
+                    'redirect_uri' => $redirect_uri,
+                    'expires' => $expires,
+                    'scope' => $scope,
+                    'id_token' => $id_token,
+                ))
+            );
+            return $result->getMatchedCount() > 0;
+        }
+        $token = array(
+            'authorization_code' => $code,
+            'client_id' => $client_id,
+            'user_id' => $user_id,
+            'redirect_uri' => $redirect_uri,
+            'expires' => $expires,
+            'scope' => $scope,
+            'id_token' => $id_token,
+        );
+        $result = $this->collection('code_table')->insertOne($token);
+        return $result->getInsertedCount() > 0;
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+        $result = $this->collection('code_table')->deleteOne(array(
+            'authorization_code' => $code
+        ));
+        return $result->getDeletedCount() > 0;
+    }
+
+    /* UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        if ($user = $this->getUser($username)) {
+            return $this->checkPassword($user, $password);
+        }
+        return false;
+    }
+
+    public function getUserDetails($username)
+    {
+        if ($user = $this->getUser($username)) {
+            $user['user_id'] = $user['username'];
+        }
+        return $user;
+    }
+
+    /* RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        $token = $this->collection('refresh_token_table')->findOne(array(
+            'refresh_token' => $refresh_token
+        ));
+        return is_null($token) ? false : $token;
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        $token = array(
+            'refresh_token' => $refresh_token,
+            'client_id' => $client_id,
+            'user_id' => $user_id,
+            'expires' => $expires,
+            'scope' => $scope
+        );
+        $result = $this->collection('refresh_token_table')->insertOne($token);
+        return $result->getInsertedCount() > 0;
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        $result = $this->collection('refresh_token_table')->deleteOne(array(
+            'refresh_token' => $refresh_token
+        ));
+        return $result->getDeletedCount() > 0;
+    }
+
+    // plaintext passwords are bad!  Override this for your application
+    protected function checkPassword($user, $password)
+    {
+        return $user['password'] == $password;
+    }
+
+    public function getUser($username)
+    {
+        $result = $this->collection('user_table')->findOne(array('username' => $username));
+        return is_null($result) ? false : $result;
+    }
+
+    public function setUser($username, $password, $firstName = null, $lastName = null)
+    {
+        if ($this->getUser($username)) {
+            $result = $this->collection('user_table')->updateOne(
+                array('username' => $username),
+                array('$set' => array(
+                    'password' => $password,
+                    'first_name' => $firstName,
+                    'last_name' => $lastName
+                ))
+            );
+
+            return $result->getMatchedCount() > 0;
+        }
+
+        $user = array(
+            'username' => $username,
+            'password' => $password,
+            'first_name' => $firstName,
+            'last_name' => $lastName
+        );
+        $result = $this->collection('user_table')->insertOne($user);
+        return $result->getInsertedCount() > 0;
+    }
+
+    public function getClientKey($client_id, $subject)
+    {
+        $result = $this->collection('jwt_table')->findOne(array(
+            'client_id' => $client_id,
+            'subject' => $subject
+        ));
+        return is_null($result) ? false : $result['key'];
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        //TODO: Needs mongodb implementation.
+        throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.');
+    }
+
+    public function setJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        //TODO: Needs mongodb implementation.
+        throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.');
+    }
+
+    public function getPublicKey($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['public_key'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? false : $result['public_key'];
+    }
+
+    public function getPrivateKey($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['private_key'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? false : $result['private_key'];
+    }
+
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        if ($client_id) {
+            $result = $this->collection('key_table')->findOne(array(
+                'client_id' => $client_id
+            ));
+            if ($result) {
+                return $result['encryption_algorithm'];
+            }
+        }
+
+        $result = $this->collection('key_table')->findOne(array(
+            'client_id' => null
+        ));
+        return is_null($result) ? 'RS256' : $result['encryption_algorithm'];
+    }
+
+    // Helper function to access a MongoDB collection by `type`:
+    protected function collection($name)
+    {
+        return $this->db->{$this->config[$name]};
+    }
+}

+ 731 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Pdo.php

@@ -0,0 +1,731 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+use InvalidArgumentException;
+
+/**
+ * Simple PDO storage for all storage types
+ *
+ * NOTE: This class is meant to get users started
+ * quickly. If your application requires further
+ * customization, extend this class or create your own.
+ *
+ * NOTE: Passwords are stored in plaintext, which is never
+ * a good idea.  Be sure to override this for your application
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class Pdo implements
+    AuthorizationCodeInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    UserCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    ScopeInterface,
+    PublicKeyInterface,
+    UserClaimsInterface,
+    OpenIDAuthorizationCodeInterface
+{
+    /**
+     * @var \PDO
+     */
+    protected $db;
+
+    /**
+     * @var array
+     */
+    protected $config;
+
+    /**
+     * @param mixed $connection
+     * @param array $config
+     *
+     * @throws InvalidArgumentException
+     */
+    public function __construct($connection, $config = array())
+    {
+        if (!$connection instanceof \PDO) {
+            if (is_string($connection)) {
+                $connection = array('dsn' => $connection);
+            }
+            if (!is_array($connection)) {
+                throw new \InvalidArgumentException('First argument to OAuth2\Storage\Pdo must be an instance of PDO, a DSN string, or a configuration array');
+            }
+            if (!isset($connection['dsn'])) {
+                throw new \InvalidArgumentException('configuration array must contain "dsn"');
+            }
+            // merge optional parameters
+            $connection = array_merge(array(
+                'username' => null,
+                'password' => null,
+                'options' => array(),
+            ), $connection);
+            $connection = new \PDO($connection['dsn'], $connection['username'], $connection['password'], $connection['options']);
+        }
+        $this->db = $connection;
+
+        // debugging
+        $connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+
+        $this->config = array_merge(array(
+            'client_table' => 'oauth_clients',
+            'access_token_table' => 'oauth_access_tokens',
+            'refresh_token_table' => 'oauth_refresh_tokens',
+            'code_table' => 'oauth_authorization_codes',
+            'user_table' => 'oauth_users',
+            'jwt_table'  => 'oauth_jwt',
+            'jti_table'  => 'oauth_jti',
+            'scope_table'  => 'oauth_scopes',
+            'public_key_table'  => 'oauth_public_keys',
+        ), $config);
+    }
+
+    /**
+     * @param string $client_id
+     * @param null|string $client_secret
+     * @return bool
+     */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table']));
+        $stmt->execute(compact('client_id'));
+        $result = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+        // make this extensible
+        return $result && $result['client_secret'] == $client_secret;
+    }
+
+    /**
+     * @param string $client_id
+     * @return bool
+     */
+    public function isPublicClient($client_id)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table']));
+        $stmt->execute(compact('client_id'));
+
+        if (!$result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return false;
+        }
+
+        return empty($result['client_secret']);
+    }
+
+    /**
+     * @param string $client_id
+     * @return array|mixed
+     */
+    public function getClientDetails($client_id)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table']));
+        $stmt->execute(compact('client_id'));
+
+        return $stmt->fetch(\PDO::FETCH_ASSOC);
+    }
+
+    /**
+     * @param string $client_id
+     * @param null|string $client_secret
+     * @param null|string $redirect_uri
+     * @param null|array  $grant_types
+     * @param null|string $scope
+     * @param null|string $user_id
+     * @return bool
+     */
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        // if it exists, update it.
+        if ($this->getClientDetails($client_id)) {
+            $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_secret=:client_secret, redirect_uri=:redirect_uri, grant_types=:grant_types, scope=:scope, user_id=:user_id where client_id=:client_id', $this->config['client_table']));
+        } else {
+            $stmt = $this->db->prepare(sprintf('INSERT INTO %s (client_id, client_secret, redirect_uri, grant_types, scope, user_id) VALUES (:client_id, :client_secret, :redirect_uri, :grant_types, :scope, :user_id)', $this->config['client_table']));
+        }
+
+        return $stmt->execute(compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id'));
+    }
+
+    /**
+     * @param $client_id
+     * @param $grant_type
+     * @return bool
+     */
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+
+            return in_array($grant_type, (array) $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /**
+     * @param string $access_token
+     * @return array|bool|mixed|null
+     */
+    public function getAccessToken($access_token)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * from %s where access_token = :access_token', $this->config['access_token_table']));
+
+        $token = $stmt->execute(compact('access_token'));
+        if ($token = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            // convert date string back to timestamp
+            $token['expires'] = strtotime($token['expires']);
+        }
+
+        return $token;
+    }
+
+    /**
+     * @param string $access_token
+     * @param mixed  $client_id
+     * @param mixed  $user_id
+     * @param int    $expires
+     * @param string $scope
+     * @return bool
+     */
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        // if it exists, update it.
+        if ($this->getAccessToken($access_token)) {
+            $stmt = $this->db->prepare(sprintf('UPDATE %s SET client_id=:client_id, expires=:expires, user_id=:user_id, scope=:scope where access_token=:access_token', $this->config['access_token_table']));
+        } else {
+            $stmt = $this->db->prepare(sprintf('INSERT INTO %s (access_token, client_id, expires, user_id, scope) VALUES (:access_token, :client_id, :expires, :user_id, :scope)', $this->config['access_token_table']));
+        }
+
+        return $stmt->execute(compact('access_token', 'client_id', 'user_id', 'expires', 'scope'));
+    }
+
+    /**
+     * @param $access_token
+     * @return bool
+     */
+    public function unsetAccessToken($access_token)
+    {
+        $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE access_token = :access_token', $this->config['access_token_table']));
+
+        $stmt->execute(compact('access_token'));
+
+        return $stmt->rowCount() > 0;
+    }
+
+    /* OAuth2\Storage\AuthorizationCodeInterface */
+    /**
+     * @param string $code
+     * @return mixed
+     */
+    public function getAuthorizationCode($code)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * from %s where authorization_code = :code', $this->config['code_table']));
+        $stmt->execute(compact('code'));
+
+        if ($code = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            // convert date string back to timestamp
+            $code['expires'] = strtotime($code['expires']);
+        }
+
+        return $code;
+    }
+
+    /**
+     * @param string $code
+     * @param mixed  $client_id
+     * @param mixed  $user_id
+     * @param string $redirect_uri
+     * @param int    $expires
+     * @param string $scope
+     * @param string $id_token
+     * @return bool|mixed
+     */
+    public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        if (func_num_args() > 6) {
+            // we are calling with an id token
+            return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args());
+        }
+
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        // if it exists, update it.
+        if ($this->getAuthorizationCode($code)) {
+            $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope where authorization_code=:code', $this->config['code_table']));
+        } else {
+            $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope)', $this->config['code_table']));
+        }
+
+        return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope'));
+    }
+
+    /**
+     * @param string $code
+     * @param mixed  $client_id
+     * @param mixed  $user_id
+     * @param string $redirect_uri
+     * @param string $expires
+     * @param string $scope
+     * @param string $id_token
+     * @return bool
+     */
+    private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        // if it exists, update it.
+        if ($this->getAuthorizationCode($code)) {
+            $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, id_token =:id_token where authorization_code=:code', $this->config['code_table']));
+        } else {
+            $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, id_token) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :id_token)', $this->config['code_table']));
+        }
+
+        return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'));
+    }
+
+    /**
+     * @param string $code
+     * @return bool
+     */
+    public function expireAuthorizationCode($code)
+    {
+        $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE authorization_code = :code', $this->config['code_table']));
+
+        return $stmt->execute(compact('code'));
+    }
+
+    /**
+     * @param string $username
+     * @param string $password
+     * @return bool
+     */
+    public function checkUserCredentials($username, $password)
+    {
+        if ($user = $this->getUser($username)) {
+            return $this->checkPassword($user, $password);
+        }
+
+        return false;
+    }
+
+    /**
+     * @param string $username
+     * @return array|bool
+     */
+    public function getUserDetails($username)
+    {
+        return $this->getUser($username);
+    }
+
+    /**
+     * @param mixed  $user_id
+     * @param string $claims
+     * @return array|bool
+     */
+    public function getUserClaims($user_id, $claims)
+    {
+        if (!$userDetails = $this->getUserDetails($user_id)) {
+            return false;
+        }
+
+        $claims = explode(' ', trim($claims));
+        $userClaims = array();
+
+        // for each requested claim, if the user has the claim, set it in the response
+        $validClaims = explode(' ', self::VALID_CLAIMS);
+        foreach ($validClaims as $validClaim) {
+            if (in_array($validClaim, $claims)) {
+                if ($validClaim == 'address') {
+                    // address is an object with subfields
+                    $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
+                } else {
+                    $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
+                }
+            }
+        }
+
+        return $userClaims;
+    }
+
+    /**
+     * @param string $claim
+     * @param array  $userDetails
+     * @return array
+     */
+    protected function getUserClaim($claim, $userDetails)
+    {
+        $userClaims = array();
+        $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
+        $claimValues = explode(' ', $claimValuesString);
+
+        foreach ($claimValues as $value) {
+            $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
+        }
+
+        return $userClaims;
+    }
+
+    /**
+     * @param string $refresh_token
+     * @return bool|mixed
+     */
+    public function getRefreshToken($refresh_token)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT * FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table']));
+
+        $token = $stmt->execute(compact('refresh_token'));
+        if ($token = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            // convert expires to epoch time
+            $token['expires'] = strtotime($token['expires']);
+        }
+
+        return $token;
+    }
+
+    /**
+     * @param string $refresh_token
+     * @param mixed  $client_id
+     * @param mixed  $user_id
+     * @param string $expires
+     * @param string $scope
+     * @return bool
+     */
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        // convert expires to datestring
+        $expires = date('Y-m-d H:i:s', $expires);
+
+        $stmt = $this->db->prepare(sprintf('INSERT INTO %s (refresh_token, client_id, user_id, expires, scope) VALUES (:refresh_token, :client_id, :user_id, :expires, :scope)', $this->config['refresh_token_table']));
+
+        return $stmt->execute(compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'));
+    }
+
+    /**
+     * @param string $refresh_token
+     * @return bool
+     */
+    public function unsetRefreshToken($refresh_token)
+    {
+        $stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table']));
+
+        $stmt->execute(compact('refresh_token'));
+
+        return $stmt->rowCount() > 0;
+    }
+
+    /**
+     * plaintext passwords are bad!  Override this for your application
+     *
+     * @param array $user
+     * @param string $password
+     * @return bool
+     */
+    protected function checkPassword($user, $password)
+    {
+        return $user['password'] == $this->hashPassword($password);
+    }
+
+    // use a secure hashing algorithm when storing passwords. Override this for your application
+    protected function hashPassword($password)
+    {
+        return sha1($password);
+    }
+
+    /**
+     * @param string $username
+     * @return array|bool
+     */
+    public function getUser($username)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT * from %s where username=:username', $this->config['user_table']));
+        $stmt->execute(array('username' => $username));
+
+        if (!$userInfo = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return false;
+        }
+
+        // the default behavior is to use "username" as the user_id
+        return array_merge(array(
+            'user_id' => $username
+        ), $userInfo);
+    }
+
+    /**
+     * plaintext passwords are bad!  Override this for your application
+     *
+     * @param string $username
+     * @param string $password
+     * @param string $firstName
+     * @param string $lastName
+     * @return bool
+     */
+    public function setUser($username, $password, $firstName = null, $lastName = null)
+    {
+        // do not store in plaintext
+        $password = $this->hashPassword($password);
+
+        // if it exists, update it.
+        if ($this->getUser($username)) {
+            $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET password=:password, first_name=:firstName, last_name=:lastName where username=:username', $this->config['user_table']));
+        } else {
+            $stmt = $this->db->prepare(sprintf('INSERT INTO %s (username, password, first_name, last_name) VALUES (:username, :password, :firstName, :lastName)', $this->config['user_table']));
+        }
+
+        return $stmt->execute(compact('username', 'password', 'firstName', 'lastName'));
+    }
+
+    /**
+     * @param string $scope
+     * @return bool
+     */
+    public function scopeExists($scope)
+    {
+        $scope = explode(' ', $scope);
+        $whereIn = implode(',', array_fill(0, count($scope), '?'));
+        $stmt = $this->db->prepare(sprintf('SELECT count(scope) as count FROM %s WHERE scope IN (%s)', $this->config['scope_table'], $whereIn));
+        $stmt->execute($scope);
+
+        if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return $result['count'] == count($scope);
+        }
+
+        return false;
+    }
+
+    /**
+     * @param mixed $client_id
+     * @return null|string
+     */
+    public function getDefaultScope($client_id = null)
+    {
+        $stmt = $this->db->prepare(sprintf('SELECT scope FROM %s WHERE is_default=:is_default', $this->config['scope_table']));
+        $stmt->execute(array('is_default' => true));
+
+        if ($result = $stmt->fetchAll(\PDO::FETCH_ASSOC)) {
+            $defaultScope = array_map(function ($row) {
+                return $row['scope'];
+            }, $result);
+
+            return implode(' ', $defaultScope);
+        }
+
+        return null;
+    }
+
+    /**
+     * @param mixed $client_id
+     * @param $subject
+     * @return string
+     */
+    public function getClientKey($client_id, $subject)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT public_key from %s where client_id=:client_id AND subject=:subject', $this->config['jwt_table']));
+
+        $stmt->execute(array('client_id' => $client_id, 'subject' => $subject));
+
+        return $stmt->fetchColumn();
+    }
+
+    /**
+     * @param mixed $client_id
+     * @return bool|null
+     */
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    /**
+     * @param mixed $client_id
+     * @param $subject
+     * @param $audience
+     * @param $expires
+     * @param $jti
+     * @return array|null
+     */
+    public function getJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT * FROM %s WHERE issuer=:client_id AND subject=:subject AND audience=:audience AND expires=:expires AND jti=:jti', $this->config['jti_table']));
+
+        $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti'));
+
+        if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return array(
+                'issuer' => $result['issuer'],
+                'subject' => $result['subject'],
+                'audience' => $result['audience'],
+                'expires' => $result['expires'],
+                'jti' => $result['jti'],
+            );
+        }
+
+        return null;
+    }
+
+    /**
+     * @param mixed $client_id
+     * @param $subject
+     * @param $audience
+     * @param $expires
+     * @param $jti
+     * @return bool
+     */
+    public function setJti($client_id, $subject, $audience, $expires, $jti)
+    {
+        $stmt = $this->db->prepare(sprintf('INSERT INTO %s (issuer, subject, audience, expires, jti) VALUES (:client_id, :subject, :audience, :expires, :jti)', $this->config['jti_table']));
+
+        return $stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti'));
+    }
+
+    /**
+     * @param mixed $client_id
+     * @return mixed
+     */
+    public function getPublicKey($client_id = null)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT public_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table']));
+
+        $stmt->execute(compact('client_id'));
+        if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return $result['public_key'];
+        }
+    }
+
+    /**
+     * @param mixed $client_id
+     * @return mixed
+     */
+    public function getPrivateKey($client_id = null)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT private_key FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table']));
+
+        $stmt->execute(compact('client_id'));
+        if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return $result['private_key'];
+        }
+    }
+
+    /**
+     * @param mixed $client_id
+     * @return string
+     */
+    public function getEncryptionAlgorithm($client_id = null)
+    {
+        $stmt = $this->db->prepare($sql = sprintf('SELECT encryption_algorithm FROM %s WHERE client_id=:client_id OR client_id IS NULL ORDER BY client_id IS NOT NULL DESC', $this->config['public_key_table']));
+
+        $stmt->execute(compact('client_id'));
+        if ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return $result['encryption_algorithm'];
+        }
+
+        return 'RS256';
+    }
+
+    /**
+     * DDL to create OAuth2 database and tables for PDO storage
+     *
+     * @see https://github.com/dsquier/oauth2-server-php-mysql
+     *
+     * @param string $dbName
+     * @return string
+     */
+    public function getBuildSql($dbName = 'oauth2_server_php')
+    {
+        $sql = "
+        CREATE TABLE {$this->config['client_table']} (
+          client_id             VARCHAR(80)   NOT NULL,
+          client_secret         VARCHAR(80),
+          redirect_uri          VARCHAR(2000),
+          grant_types           VARCHAR(80),
+          scope                 VARCHAR(4000),
+          user_id               VARCHAR(80),
+          PRIMARY KEY (client_id)
+        );
+
+            CREATE TABLE {$this->config['access_token_table']} (
+              access_token         VARCHAR(40)    NOT NULL,
+              client_id            VARCHAR(80)    NOT NULL,
+              user_id              VARCHAR(80),
+              expires              TIMESTAMP      NOT NULL,
+              scope                VARCHAR(4000),
+              PRIMARY KEY (access_token)
+            );
+
+            CREATE TABLE {$this->config['code_table']} (
+              authorization_code  VARCHAR(40)    NOT NULL,
+              client_id           VARCHAR(80)    NOT NULL,
+              user_id             VARCHAR(80),
+              redirect_uri        VARCHAR(2000),
+              expires             TIMESTAMP      NOT NULL,
+              scope               VARCHAR(4000),
+              id_token            VARCHAR(1000),
+              PRIMARY KEY (authorization_code)
+            );
+
+            CREATE TABLE {$this->config['refresh_token_table']} (
+              refresh_token       VARCHAR(40)    NOT NULL,
+              client_id           VARCHAR(80)    NOT NULL,
+              user_id             VARCHAR(80),
+              expires             TIMESTAMP      NOT NULL,
+              scope               VARCHAR(4000),
+              PRIMARY KEY (refresh_token)
+            );
+
+            CREATE TABLE {$this->config['user_table']} (
+              username            VARCHAR(80),
+              password            VARCHAR(80),
+              first_name          VARCHAR(80),
+              last_name           VARCHAR(80),
+              email               VARCHAR(80),
+              email_verified      BOOLEAN,
+              scope               VARCHAR(4000)
+            );
+
+            CREATE TABLE {$this->config['scope_table']} (
+              scope               VARCHAR(80)  NOT NULL,
+              is_default          BOOLEAN,
+              PRIMARY KEY (scope)
+            );
+
+            CREATE TABLE {$this->config['jwt_table']} (
+              client_id           VARCHAR(80)   NOT NULL,
+              subject             VARCHAR(80),
+              public_key          VARCHAR(2000) NOT NULL
+            );
+
+            CREATE TABLE {$this->config['jti_table']} (
+              issuer              VARCHAR(80)   NOT NULL,
+              subject             VARCHAR(80),
+              audiance            VARCHAR(80),
+              expires             TIMESTAMP     NOT NULL,
+              jti                 VARCHAR(2000) NOT NULL
+            );
+
+            CREATE TABLE {$this->config['public_key_table']} (
+              client_id            VARCHAR(80),
+              public_key           VARCHAR(2000),
+              private_key          VARCHAR(2000),
+              encryption_algorithm VARCHAR(100) DEFAULT 'RS256'
+            )
+        ";
+
+        return $sql;
+    }
+}

+ 30 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/PublicKeyInterface.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should get public/private key information
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface PublicKeyInterface
+{
+    /**
+     * @param mixed $client_id
+     * @return mixed
+     */
+    public function getPublicKey($client_id = null);
+
+    /**
+     * @param mixed $client_id
+     * @return mixed
+     */
+    public function getPrivateKey($client_id = null);
+
+    /**
+     * @param mixed $client_id
+     * @return mixed
+     */
+    public function getEncryptionAlgorithm($client_id = null);
+}

+ 321 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/Redis.php

@@ -0,0 +1,321 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * redis storage for all storage types
+ *
+ * To use, install "predis/predis" via composer
+ *
+ * Register client:
+ * <code>
+ *  $storage = new OAuth2\Storage\Redis($redis);
+ *  $storage->setClientDetails($client_id, $client_secret, $redirect_uri);
+ * </code>
+ */
+class Redis implements AuthorizationCodeInterface,
+    AccessTokenInterface,
+    ClientCredentialsInterface,
+    UserCredentialsInterface,
+    RefreshTokenInterface,
+    JwtBearerInterface,
+    ScopeInterface,
+    OpenIDAuthorizationCodeInterface
+{
+
+    private $cache;
+
+    /* The redis client */
+    protected $redis;
+
+    /* Configuration array */
+    protected $config;
+
+    /**
+     * Redis Storage!
+     *
+     * @param \Predis\Client $redis
+     * @param array          $config
+     */
+    public function __construct($redis, $config=array())
+    {
+        $this->redis = $redis;
+        $this->config = array_merge(array(
+            'client_key' => 'oauth_clients:',
+            'access_token_key' => 'oauth_access_tokens:',
+            'refresh_token_key' => 'oauth_refresh_tokens:',
+            'code_key' => 'oauth_authorization_codes:',
+            'user_key' => 'oauth_users:',
+            'jwt_key' => 'oauth_jwt:',
+            'scope_key' => 'oauth_scopes:',
+        ), $config);
+    }
+
+    protected function getValue($key)
+    {
+        if ( isset($this->cache[$key]) ) {
+            return $this->cache[$key];
+        }
+        $value = $this->redis->get($key);
+        if ( isset($value) ) {
+            return json_decode($value, true);
+        } else {
+            return false;
+        }
+    }
+
+    protected function setValue($key, $value, $expire=0)
+    {
+        $this->cache[$key] = $value;
+        $str = json_encode($value);
+        if ($expire > 0) {
+            $seconds = $expire - time();
+            $ret = $this->redis->setex($key, $seconds, $str);
+        } else {
+            $ret = $this->redis->set($key, $str);
+        }
+
+        // check that the key was set properly
+        // if this fails, an exception will usually thrown, so this step isn't strictly necessary
+        return is_bool($ret) ? $ret : $ret->getPayload() == 'OK';
+    }
+
+    protected function expireValue($key)
+    {
+        unset($this->cache[$key]);
+
+        return $this->redis->del($key);
+    }
+
+    /* AuthorizationCodeInterface */
+    public function getAuthorizationCode($code)
+    {
+        return $this->getValue($this->config['code_key'] . $code);
+    }
+
+    public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+    {
+        return $this->setValue(
+            $this->config['code_key'] . $authorization_code,
+            compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'),
+            $expires
+        );
+    }
+
+    public function expireAuthorizationCode($code)
+    {
+        $key = $this->config['code_key'] . $code;
+        unset($this->cache[$key]);
+
+        return $this->expireValue($key);
+    }
+
+    /* UserCredentialsInterface */
+    public function checkUserCredentials($username, $password)
+    {
+        $user = $this->getUserDetails($username);
+
+        return $user && $user['password'] === $password;
+    }
+
+    public function getUserDetails($username)
+    {
+        return $this->getUser($username);
+    }
+
+    public function getUser($username)
+    {
+        if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) {
+            return false;
+        }
+
+        // the default behavior is to use "username" as the user_id
+        return array_merge(array(
+            'user_id' => $username,
+        ), $userInfo);
+    }
+
+    public function setUser($username, $password, $first_name = null, $last_name = null)
+    {
+        return $this->setValue(
+            $this->config['user_key'] . $username,
+            compact('username', 'password', 'first_name', 'last_name')
+        );
+    }
+
+    /* ClientCredentialsInterface */
+    public function checkClientCredentials($client_id, $client_secret = null)
+    {
+        if (!$client = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        return isset($client['client_secret'])
+            && $client['client_secret'] == $client_secret;
+    }
+
+    public function isPublicClient($client_id)
+    {
+        if (!$client = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        return empty($client['client_secret']);
+    }
+
+    /* ClientInterface */
+    public function getClientDetails($client_id)
+    {
+        return $this->getValue($this->config['client_key'] . $client_id);
+    }
+
+    public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
+    {
+        return $this->setValue(
+            $this->config['client_key'] . $client_id,
+            compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id')
+        );
+    }
+
+    public function checkRestrictedGrantType($client_id, $grant_type)
+    {
+        $details = $this->getClientDetails($client_id);
+        if (isset($details['grant_types'])) {
+            $grant_types = explode(' ', $details['grant_types']);
+
+            return in_array($grant_type, (array) $grant_types);
+        }
+
+        // if grant_types are not defined, then none are restricted
+        return true;
+    }
+
+    /* RefreshTokenInterface */
+    public function getRefreshToken($refresh_token)
+    {
+        return $this->getValue($this->config['refresh_token_key'] . $refresh_token);
+    }
+
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        return $this->setValue(
+            $this->config['refresh_token_key'] . $refresh_token,
+            compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'),
+            $expires
+        );
+    }
+
+    public function unsetRefreshToken($refresh_token)
+    {
+        $result = $this->expireValue($this->config['refresh_token_key'] . $refresh_token);
+
+        return $result > 0;
+    }
+
+    /* AccessTokenInterface */
+    public function getAccessToken($access_token)
+    {
+        return $this->getValue($this->config['access_token_key'].$access_token);
+    }
+
+    public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+    {
+        return $this->setValue(
+            $this->config['access_token_key'].$access_token,
+            compact('access_token', 'client_id', 'user_id', 'expires', 'scope'),
+            $expires
+        );
+    }
+
+    public function unsetAccessToken($access_token)
+    {
+        $result = $this->expireValue($this->config['access_token_key'] . $access_token);
+
+        return $result > 0;
+    }
+
+    /* ScopeInterface */
+    public function scopeExists($scope)
+    {
+        $scope = explode(' ', $scope);
+
+        $result = $this->getValue($this->config['scope_key'].'supported:global');
+
+        $supportedScope = explode(' ', (string) $result);
+
+        return (count(array_diff($scope, $supportedScope)) == 0);
+    }
+
+    public function getDefaultScope($client_id = null)
+    {
+        if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) {
+            $result = $this->getValue($this->config['scope_key'].'default:global');
+        }
+
+        return $result;
+    }
+
+    public function setScope($scope, $client_id = null, $type = 'supported')
+    {
+        if (!in_array($type, array('default', 'supported'))) {
+            throw new \InvalidArgumentException('"$type" must be one of "default", "supported"');
+        }
+
+        if (is_null($client_id)) {
+            $key = $this->config['scope_key'].$type.':global';
+        } else {
+            $key = $this->config['scope_key'].$type.':'.$client_id;
+        }
+
+        return $this->setValue($key, $scope);
+    }
+
+    /*JWTBearerInterface */
+    public function getClientKey($client_id, $subject)
+    {
+        if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) {
+            return false;
+        }
+
+        if (isset($jwt['subject']) && $jwt['subject'] == $subject) {
+            return $jwt['key'];
+        }
+
+        return null;
+    }
+
+    public function setClientKey($client_id, $key, $subject = null)
+    {
+        return $this->setValue($this->config['jwt_key'] . $client_id, array(
+            'key' => $key,
+            'subject' => $subject
+        ));
+    }
+
+    public function getClientScope($client_id)
+    {
+        if (!$clientDetails = $this->getClientDetails($client_id)) {
+            return false;
+        }
+
+        if (isset($clientDetails['scope'])) {
+            return $clientDetails['scope'];
+        }
+
+        return null;
+    }
+
+    public function getJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs redis implementation.
+        throw new \Exception('getJti() for the Redis driver is currently unimplemented.');
+    }
+
+    public function setJti($client_id, $subject, $audience, $expiration, $jti)
+    {
+        //TODO: Needs redis implementation.
+        throw new \Exception('setJti() for the Redis driver is currently unimplemented.');
+    }
+}

+ 82 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/RefreshTokenInterface.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should get/save refresh tokens for the "Refresh Token"
+ * grant type
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface RefreshTokenInterface
+{
+    /**
+     * Grant refresh access tokens.
+     *
+     * Retrieve the stored data for the given refresh token.
+     *
+     * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN.
+     *
+     * @param $refresh_token
+     * Refresh token to be check with.
+     *
+     * @return
+     * An associative array as below, and NULL if the refresh_token is
+     * invalid:
+     * - refresh_token: Refresh token identifier.
+     * - client_id: Client identifier.
+     * - user_id: User identifier.
+     * - expires: Expiration unix timestamp, or 0 if the token doesn't expire.
+     * - scope: (optional) Scope values in space-separated string.
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-6
+     *
+     * @ingroup oauth2_section_6
+     */
+    public function getRefreshToken($refresh_token);
+
+    /**
+     * Take the provided refresh token values and store them somewhere.
+     *
+     * This function should be the storage counterpart to getRefreshToken().
+     *
+     * If storage fails for some reason, we're not currently checking for
+     * any sort of success/failure, so you should bail out of the script
+     * and provide a descriptive fail message.
+     *
+     * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN.
+     *
+     * @param $refresh_token
+     * Refresh token to be stored.
+     * @param $client_id
+     * Client identifier to be stored.
+     * @param $user_id
+     * User identifier to be stored.
+     * @param $expires
+     * Expiration timestamp to be stored. 0 if the token doesn't expire.
+     * @param $scope
+     * (optional) Scopes to be stored in space-separated string.
+     *
+     * @ingroup oauth2_section_6
+     */
+    public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null);
+
+    /**
+     * Expire a used refresh token.
+     *
+     * This is not explicitly required in the spec, but is almost implied.
+     * After granting a new refresh token, the old one is no longer useful and
+     * so should be forcibly expired in the data store so it can't be used again.
+     *
+     * If storage fails for some reason, we're not currently checking for
+     * any sort of success/failure, so you should bail out of the script
+     * and provide a descriptive fail message.
+     *
+     * @param $refresh_token
+     * Refresh token to be expired.
+     *
+     * @ingroup oauth2_section_6
+     */
+    public function unsetRefreshToken($refresh_token);
+}

+ 46 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/ScopeInterface.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should retrieve data involving the relevent scopes associated
+ * with this implementation.
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface ScopeInterface
+{
+    /**
+     * Check if the provided scope exists.
+     *
+     * @param $scope
+     * A space-separated string of scopes.
+     *
+     * @return
+     * TRUE if it exists, FALSE otherwise.
+     */
+    public function scopeExists($scope);
+
+    /**
+     * The default scope to use in the event the client
+     * does not request one. By returning "false", a
+     * request_error is returned by the server to force a
+     * scope request by the client. By returning "null",
+     * opt out of requiring scopes
+     *
+     * @param $client_id
+     * An optional client id that can be used to return customized default scopes.
+     *
+     * @return
+     * string representation of default scope, null if
+     * scopes are not defined, or false to force scope
+     * request by the client
+     *
+     * ex:
+     *     'default'
+     * ex:
+     *     null
+     */
+    public function getDefaultScope($client_id = null);
+}

+ 52 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/Storage/UserCredentialsInterface.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should retrieve user credentials for the
+ * "Resource Owner Password Credentials" grant type
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface UserCredentialsInterface
+{
+    /**
+     * Grant access tokens for basic user credentials.
+     *
+     * Check the supplied username and password for validity.
+     *
+     * You can also use the $client_id param to do any checks required based
+     * on a client, if you need that.
+     *
+     * Required for OAuth2::GRANT_TYPE_USER_CREDENTIALS.
+     *
+     * @param $username
+     * Username to be check with.
+     * @param $password
+     * Password to be check with.
+     *
+     * @return
+     * TRUE if the username and password are valid, and FALSE if it isn't.
+     * Moreover, if the username and password are valid, and you want to
+     *
+     * @see http://tools.ietf.org/html/rfc6749#section-4.3
+     *
+     * @ingroup oauth2_section_4
+     */
+    public function checkUserCredentials($username, $password);
+
+    /**
+     * @param string $username - username to get details for
+     * @return array|false     - the associated "user_id" and optional "scope" values
+     *                           This function MUST return FALSE if the requested user does not exist or is
+     *                           invalid. "scope" is a space-separated list of restricted scopes.
+     * @code
+     *     return array(
+     *         "user_id"  => USER_ID,    // REQUIRED user_id to be stored with the authorization code or access token
+     *         "scope"    => SCOPE       // OPTIONAL space-separated list of restricted scopes
+     *     );
+     * @endcode
+     */
+    public function getUserDetails($username);
+}

+ 130 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Bearer.php

@@ -0,0 +1,130 @@
+<?php
+
+namespace OAuth2\TokenType;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+*
+*/
+class Bearer implements TokenTypeInterface
+{
+    private $config;
+
+    public function __construct(array $config = array())
+    {
+        $this->config = array_merge(array(
+            'token_param_name'         => 'access_token',
+            'token_bearer_header_name' => 'Bearer',
+        ), $config);
+    }
+
+    public function getTokenType()
+    {
+        return 'Bearer';
+    }
+
+    /**
+     * Check if the request has supplied token
+     *
+     * @see https://github.com/bshaffer/oauth2-server-php/issues/349#issuecomment-37993588
+     */
+    public function requestHasToken(RequestInterface $request)
+    {
+        $headers = $request->headers('AUTHORIZATION');
+
+        // check the header, then the querystring, then the request body
+        return !empty($headers) || (bool) ($request->request($this->config['token_param_name'])) || (bool) ($request->query($this->config['token_param_name']));
+    }
+
+    /**
+     * This is a convenience function that can be used to get the token, which can then
+     * be passed to getAccessTokenData(). The constraints specified by the draft are
+     * attempted to be adheared to in this method.
+     *
+     * As per the Bearer spec (draft 8, section 2) - there are three ways for a client
+     * to specify the bearer token, in order of preference: Authorization Header,
+     * POST and GET.
+     *
+     * NB: Resource servers MUST accept tokens via the Authorization scheme
+     * (http://tools.ietf.org/html/rfc6750#section-2).
+     *
+     * @todo Should we enforce TLS/SSL in this function?
+     *
+     * @see http://tools.ietf.org/html/rfc6750#section-2.1
+     * @see http://tools.ietf.org/html/rfc6750#section-2.2
+     * @see http://tools.ietf.org/html/rfc6750#section-2.3
+     *
+     * Old Android version bug (at least with version 2.2)
+     * @see http://code.google.com/p/android/issues/detail?id=6684
+     *
+     */
+    public function getAccessTokenParameter(RequestInterface $request, ResponseInterface $response)
+    {
+        $headers = $request->headers('AUTHORIZATION');
+
+        /**
+         * Ensure more than one method is not used for including an
+         * access token
+         *
+         * @see http://tools.ietf.org/html/rfc6750#section-3.1
+         */
+        $methodsUsed = !empty($headers) + (bool) ($request->query($this->config['token_param_name'])) + (bool) ($request->request($this->config['token_param_name']));
+        if ($methodsUsed > 1) {
+            $response->setError(400, 'invalid_request', 'Only one method may be used to authenticate at a time (Auth header, GET or POST)');
+
+            return null;
+        }
+
+        /**
+         * If no authentication is provided, set the status code
+         * to 401 and return no other error information
+         *
+         * @see http://tools.ietf.org/html/rfc6750#section-3.1
+         */
+        if ($methodsUsed == 0) {
+            $response->setStatusCode(401);
+
+            return null;
+        }
+
+        // HEADER: Get the access token from the header
+        if (!empty($headers)) {
+            if (!preg_match('/' . $this->config['token_bearer_header_name'] . '\s(\S+)/i', $headers, $matches)) {
+                $response->setError(400, 'invalid_request', 'Malformed auth header');
+
+                return null;
+            }
+
+            return $matches[1];
+        }
+
+        if ($request->request($this->config['token_param_name'])) {
+            // // POST: Get the token from POST data
+            if (!in_array(strtolower($request->server('REQUEST_METHOD')), array('post', 'put'))) {
+                $response->setError(400, 'invalid_request', 'When putting the token in the body, the method must be POST or PUT', '#section-2.2');
+
+                return null;
+            }
+
+            $contentType = $request->server('CONTENT_TYPE');
+            if (false !== $pos = strpos($contentType, ';')) {
+                $contentType = substr($contentType, 0, $pos);
+            }
+
+            if ($contentType !== null && $contentType != 'application/x-www-form-urlencoded') {
+                // IETF specifies content-type. NB: Not all webservers populate this _SERVER variable
+                // @see http://tools.ietf.org/html/rfc6750#section-2.2
+                $response->setError(400, 'invalid_request', 'The content type for POST requests must be "application/x-www-form-urlencoded"');
+
+                return null;
+            }
+
+            return $request->request($this->config['token_param_name']);
+        }
+
+        // GET method
+        return $request->query($this->config['token_param_name']);
+    }
+}

+ 22 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/Mac.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace OAuth2\TokenType;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+* This is not yet supported!
+*/
+class Mac implements TokenTypeInterface
+{
+    public function getTokenType()
+    {
+        return 'mac';
+    }
+
+    public function getAccessTokenParameter(RequestInterface $request, ResponseInterface $response)
+    {
+        throw new \LogicException("Not supported");
+    }
+}

+ 21 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/src/OAuth2/TokenType/TokenTypeInterface.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace OAuth2\TokenType;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+interface TokenTypeInterface
+{
+    /**
+     * Token type identification string
+     *
+     * ex: "bearer" or "mac"
+     */
+    public function getTokenType();
+
+    /**
+     * Retrieves the token string from the request object
+     */
+    public function getAccessTokenParameter(RequestInterface $request, ResponseInterface $response);
+}

+ 18 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/AutoloadTest.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace OAuth2;
+
+use PHPUnit\Framework\TestCase;
+
+class AutoloadTest extends TestCase
+{
+    public function testClassesExist()
+    {
+        // autoloader is called in test/bootstrap.php
+        $this->assertTrue(class_exists('OAuth2\Server'));
+        $this->assertTrue(class_exists('OAuth2\Request'));
+        $this->assertTrue(class_exists('OAuth2\Response'));
+        $this->assertTrue(class_exists('OAuth2\GrantType\UserCredentials'));
+        $this->assertTrue(interface_exists('OAuth2\Storage\AccessTokenInterface'));
+    }
+}

+ 493 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/AuthorizeControllerTest.php

@@ -0,0 +1,493 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\Storage\Memory;
+use OAuth2\Scope;
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\GrantType\AuthorizationCode;
+use OAuth2\Request;
+use OAuth2\Response;
+use OAuth2\Request\TestRequest;
+use PHPUnit\Framework\TestCase;
+
+class AuthorizeControllerTest extends TestCase
+{
+    public function testNoClientIdResponse()
+    {
+        $server = $this->getTestServer();
+        $request = new Request();
+        $server->handleAuthorizeRequest($request, $response = new Response(), false);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_client');
+        $this->assertEquals($response->getParameter('error_description'), 'No client id supplied');
+    }
+
+    public function testInvalidClientIdResponse()
+    {
+        $server = $this->getTestServer();
+        $request = new Request(array(
+            'client_id' => 'Fake Client ID', // invalid client id
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), false);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_client');
+        $this->assertEquals($response->getParameter('error_description'), 'The client id supplied is invalid');
+    }
+
+    public function testNoRedirectUriSuppliedOrStoredResponse()
+    {
+        $server = $this->getTestServer();
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), false);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_uri');
+        $this->assertEquals($response->getParameter('error_description'), 'No redirect URI was supplied or stored');
+    }
+
+    public function testNoResponseTypeResponse()
+    {
+        $server = $this->getTestServer();
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com', // valid redirect URI
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), false);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        parse_str($parts['query'], $query);
+
+        $this->assertEquals($query['error'], 'invalid_request');
+        $this->assertEquals($query['error_description'], 'Invalid or missing response type');
+    }
+
+    public function testInvalidResponseTypeResponse()
+    {
+        $server = $this->getTestServer();
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'invalid', // invalid response type
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), false);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        parse_str($parts['query'], $query);
+
+        $this->assertEquals($query['error'], 'invalid_request');
+        $this->assertEquals($query['error_description'], 'Invalid or missing response type');
+    }
+
+    public function testRedirectUriFragmentResponse()
+    {
+        $server = $this->getTestServer();
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com#fragment', // valid redirect URI
+            'response_type' => 'code', // invalid response type
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_uri');
+        $this->assertEquals($response->getParameter('error_description'), 'The redirect URI must not contain a fragment');
+    }
+
+    public function testEnforceState()
+    {
+        $server = $this->getTestServer(array('enforce_state' => true));
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'code',
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        parse_str($parts['query'], $query);
+
+        $this->assertEquals($query['error'], 'invalid_request');
+        $this->assertEquals($query['error_description'], 'The state parameter is required');
+    }
+
+    public function testDoNotEnforceState()
+    {
+        $server = $this->getTestServer(array('enforce_state' => false));
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'code',
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $this->assertNotContains('error', $response->getHttpHeader('Location'));
+    }
+
+    public function testEnforceScope()
+    {
+        $server = $this->getTestServer();
+        $scopeStorage = new Memory(array('default_scope' => false, 'supported_scopes' => array('testscope')));
+        $server->setScopeUtil(new Scope($scopeStorage));
+
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'code',
+            'state' => 'xyz',
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $parts = parse_url($response->getHttpHeader('Location'));
+        parse_str($parts['query'], $query);
+
+        $this->assertEquals($query['error'], 'invalid_client');
+        $this->assertEquals($query['error_description'], 'This application requires you specify a scope parameter');
+
+        $request->query['scope'] = 'testscope';
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $this->assertNotContains('error', $response->getHttpHeader('Location'));
+    }
+
+    public function testInvalidRedirectUri()
+    {
+        $server = $this->getTestServer();
+        $request = new Request(array(
+            'client_id' => 'Test Client ID with Redirect Uri', // valid client id
+            'redirect_uri' => 'http://adobe.com', // invalid redirect URI
+            'response_type' => 'code',
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'redirect_uri_mismatch');
+        $this->assertEquals($response->getParameter('error_description'), 'The redirect URI provided is missing or does not match');
+    }
+
+    public function testInvalidRedirectUriApprovedByBuggyRegisteredUri()
+    {
+        $server = $this->getTestServer();
+        $server->setConfig('require_exact_redirect_uri', false);
+        $request = new Request(array(
+            'client_id' => 'Test Client ID with Buggy Redirect Uri', // valid client id
+            'redirect_uri' => 'http://adobe.com', // invalid redirect URI
+            'response_type' => 'code',
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'redirect_uri_mismatch');
+        $this->assertEquals($response->getParameter('error_description'), 'The redirect URI provided is missing or does not match');
+    }
+
+    public function testNoRedirectUriWithMultipleRedirectUris()
+    {
+        $server = $this->getTestServer();
+
+        // create a request with no "redirect_uri" in querystring
+        $request = new Request(array(
+            'client_id' => 'Test Client ID with Multiple Redirect Uris', // valid client id
+            'response_type' => 'code',
+        ));
+
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_uri');
+        $this->assertEquals($response->getParameter('error_description'), 'A redirect URI must be supplied when multiple redirect URIs are registered');
+    }
+
+    public function testRedirectUriWithValidRedirectUri()
+    {
+        $server = $this->getTestServer();
+
+        // create a request with no "redirect_uri" in querystring
+        $request = new Request(array(
+            'client_id'     => 'Test Client ID with Redirect Uri Parts', // valid client id
+            'response_type' => 'code',
+            'redirect_uri'  => 'http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true',
+            'state'         => 'xyz',
+        ));
+
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $this->assertContains('code', $response->getHttpHeader('Location'));
+    }
+
+    public function testRedirectUriWithDifferentQueryAndExactMatchRequired()
+    {
+        $server = $this->getTestServer(array('require_exact_redirect_uri' => true));
+
+        // create a request with no "redirect_uri" in querystring
+        $request = new Request(array(
+            'client_id' => 'Test Client ID with Redirect Uri Parts', // valid client id
+            'response_type' => 'code',
+            'redirect_uri' => 'http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true&hereisa=querystring',
+        ));
+
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'redirect_uri_mismatch');
+        $this->assertEquals($response->getParameter('error_description'), 'The redirect URI provided is missing or does not match');
+    }
+
+    public function testRedirectUriWithDifferentQueryAndExactMatchNotRequired()
+    {
+        $server = $this->getTestServer(array('require_exact_redirect_uri' => false));
+
+        // create a request with no "redirect_uri" in querystring
+        $request = new Request(array(
+            'client_id'     => 'Test Client ID with Redirect Uri Parts', // valid client id
+            'response_type' => 'code',
+            'redirect_uri'  => 'http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true&hereisa=querystring',
+            'state'         => 'xyz',
+        ));
+
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $this->assertContains('code', $response->getHttpHeader('Location'));
+    }
+
+    public function testMultipleRedirectUris()
+    {
+        $server = $this->getTestServer();
+        $request = new Request(array(
+            'client_id'     => 'Test Client ID with Multiple Redirect Uris', // valid client id
+            'redirect_uri'  => 'http://brentertainment.com', // valid redirect URI
+            'response_type' => 'code',
+            'state'         => 'xyz'
+        ));
+
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+        $this->assertEquals($response->getStatusCode(), 302);
+        $this->assertContains('code', $response->getHttpHeader('Location'));
+
+        // call again with different (but still valid) redirect URI
+        $request->query['redirect_uri'] = 'http://morehazards.com';
+
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+        $this->assertEquals($response->getStatusCode(), 302);
+        $this->assertContains('code', $response->getHttpHeader('Location'));
+    }
+
+    /**
+     * @see http://tools.ietf.org/html/rfc6749#section-4.1.3
+     * @see https://github.com/bshaffer/oauth2-server-php/issues/163
+     */
+    public function testNoRedirectUriSuppliedDoesNotRequireTokenRedirectUri()
+    {
+        $server = $this->getTestServer();
+        $request = new Request(array(
+            'client_id'     => 'Test Client ID with Redirect Uri', // valid client id
+            'response_type' => 'code',
+            'state'         => 'xyz',
+        ));
+
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+        $this->assertEquals($response->getStatusCode(), 302);
+        $this->assertContains('state', $response->getHttpHeader('Location'));
+        $this->assertStringStartsWith('http://brentertainment.com?code=', $response->getHttpHeader('Location'));
+
+        $parts = parse_url($response->getHttpHeader('Location'));
+        parse_str($parts['query'], $query);
+
+        // call token endpoint with no redirect_uri supplied
+        $request = TestRequest::createPost(array(
+            'client_id'     => 'Test Client ID with Redirect Uri', // valid client id
+            'client_secret' => 'TestSecret2',
+            'grant_type'    => 'authorization_code',
+            'code'          => $query['code'],
+        ));
+
+        $server->handleTokenRequest($request, $response = new Response(), true);
+        $this->assertEquals($response->getStatusCode(), 200);
+        $this->assertNotNull($response->getParameter('access_token'));
+    }
+
+    public function testUserDeniesAccessResponse()
+    {
+        $server = $this->getTestServer();
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'code',
+            'state' => 'xyz',
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), false);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        parse_str($parts['query'], $query);
+
+        $this->assertEquals($query['error'], 'access_denied');
+        $this->assertEquals($query['error_description'], 'The user denied access to your application');
+    }
+
+    public function testCodeQueryParamIsSet()
+    {
+        $server = $this->getTestServer();
+        $request = new Request(array(
+            'client_id'     => 'Test Client ID', // valid client id
+            'redirect_uri'  => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'code',
+            'state'         => 'xyz',
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        parse_str($parts['query'], $query);
+
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+
+        $this->assertEquals('http', $parts['scheme']); // same as passed in to redirect_uri
+        $this->assertEquals('adobe.com', $parts['host']); // same as passed in to redirect_uri
+        $this->assertArrayHasKey('query', $parts);
+        $this->assertFalse(isset($parts['fragment']));
+
+        // assert fragment is in "application/x-www-form-urlencoded" format
+        parse_str($parts['query'], $query);
+        $this->assertNotNull($query);
+        $this->assertArrayHasKey('code', $query);
+
+        // ensure no id_token was saved, since the openid scope wasn't requested
+        $storage = $server->getStorage('authorization_code');
+        $code = $storage->getAuthorizationCode($query['code']);
+        $this->assertTrue(empty($code['id_token']));
+
+        // ensure no error was returned
+        $this->assertFalse(isset($query['error']));
+        $this->assertFalse(isset($query['error_description']));
+    }
+
+    public function testSuccessfulRequestReturnsStateParameter()
+    {
+        $server = $this->getTestServer(array('allow_implicit' => true));
+        $request = new Request(array(
+            'client_id'     => 'Test Client ID', // valid client id
+            'redirect_uri'  => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'code',
+            'state'         => 'test', // valid state string (just needs to be passed back to us)
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        $this->assertArrayHasKey('query', $parts);
+        parse_str($parts['query'], $query);
+
+        $this->assertArrayHasKey('state', $query);
+        $this->assertEquals($query['state'], 'test');
+
+        // ensure no error was returned
+        $this->assertFalse(isset($query['error']));
+        $this->assertFalse(isset($query['error_description']));
+    }
+
+    public function testSuccessfulRequestStripsExtraParameters()
+    {
+        $server = $this->getTestServer(array('allow_implicit' => true));
+        $request = new Request(array(
+            'client_id'     => 'Test Client ID', // valid client id
+            'redirect_uri'  => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'code',
+            'state'         => 'test',      // valid state string (just needs to be passed back to us)
+            'fake'          => 'something', // extra query param
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $this->assertNotContains('error', $location);
+
+        $parts = parse_url($location);
+        $this->assertFalse(isset($parts['fake']));
+        $this->assertArrayHasKey('query', $parts);
+        parse_str($parts['query'], $query);
+
+        $this->assertFalse(isset($parmas['fake']));
+        $this->assertArrayHasKey('state', $query);
+        $this->assertEquals($query['state'], 'test');
+    }
+
+    public function testSuccessfulOpenidConnectRequest()
+    {
+        $server = $this->getTestServer(array(
+            'use_openid_connect' => true,
+            'issuer' => 'bojanz',
+        ));
+
+        $request = new Request(array(
+            'client_id'     => 'Test Client ID',
+            'redirect_uri'  => 'http://adobe.com',
+            'response_type' => 'code',
+            'state'         => 'xyz',
+            'scope'         => 'openid',
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        parse_str($parts['query'], $query);
+
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        $this->assertArrayHasKey('query', $parts);
+        $this->assertFalse(isset($parts['fragment']));
+
+        // assert fragment is in "application/x-www-form-urlencoded" format
+        parse_str($parts['query'], $query);
+        $this->assertNotNull($query);
+        $this->assertArrayHasKey('code', $query);
+
+        // ensure no error was returned
+        $this->assertFalse(isset($query['error']));
+        $this->assertFalse(isset($query['error_description']));
+
+        // confirm that the id_token has been created.
+        $storage = $server->getStorage('authorization_code');
+        $code = $storage->getAuthorizationCode($query['code']);
+        $this->assertTrue(!empty($code['id_token']));
+    }
+
+    public function testCreateController()
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $controller = new AuthorizeController($storage);
+    }
+
+    private function getTestServer($config = array())
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage, $config);
+
+        // Add the two types supported for authorization grant
+        $server->addGrantType(new AuthorizationCode($storage));
+
+        return $server;
+    }
+}

+ 177 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/ResourceControllerTest.php

@@ -0,0 +1,177 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\GrantType\AuthorizationCode;
+use OAuth2\Request;
+use OAuth2\Response;
+use PHPUnit\Framework\TestCase;
+
+class ResourceControllerTest extends TestCase
+{
+    public function testNoAccessToken()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $allow = $server->verifyResourceRequest($request, $response = new Response());
+        $this->assertFalse($allow);
+
+        $this->assertEquals($response->getStatusCode(), 401);
+        $this->assertNull($response->getParameter('error'));
+        $this->assertNull($response->getParameter('error_description'));
+        $this->assertEquals('', $response->getResponseBody());
+    }
+
+    public function testMalformedHeader()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $request->headers['AUTHORIZATION'] = 'tH1s i5 B0gU5';
+        $allow = $server->verifyResourceRequest($request, $response = new Response());
+        $this->assertFalse($allow);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'Malformed auth header');
+    }
+
+    public function testMultipleTokensSubmitted()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $request->request['access_token'] = 'TEST';
+        $request->query['access_token'] = 'TEST';
+        $allow = $server->verifyResourceRequest($request, $response = new Response());
+        $this->assertFalse($allow);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'Only one method may be used to authenticate at a time (Auth header, GET or POST)');
+    }
+
+    public function testInvalidRequestMethod()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $request->server['REQUEST_METHOD'] = 'GET';
+        $request->request['access_token'] = 'TEST';
+        $allow = $server->verifyResourceRequest($request, $response = new Response());
+        $this->assertFalse($allow);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'When putting the token in the body, the method must be POST or PUT');
+    }
+
+    public function testInvalidContentType()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $request->server['REQUEST_METHOD'] = 'POST';
+        $request->server['CONTENT_TYPE'] = 'application/json';
+        $request->request['access_token'] = 'TEST';
+        $allow = $server->verifyResourceRequest($request, $response = new Response());
+        $this->assertFalse($allow);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'The content type for POST requests must be "application/x-www-form-urlencoded"');
+    }
+
+    public function testInvalidToken()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $request->headers['AUTHORIZATION'] = 'Bearer TESTTOKEN';
+        $allow = $server->verifyResourceRequest($request, $response = new Response());
+        $this->assertFalse($allow);
+
+        $this->assertEquals($response->getStatusCode(), 401);
+        $this->assertEquals($response->getParameter('error'), 'invalid_token');
+        $this->assertEquals($response->getParameter('error_description'), 'The access token provided is invalid');
+    }
+
+    public function testExpiredToken()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-expired';
+        $allow = $server->verifyResourceRequest($request, $response = new Response());
+        $this->assertFalse($allow);
+
+        $this->assertEquals($response->getStatusCode(), 401);
+        $this->assertEquals($response->getParameter('error'), 'invalid_token');
+        $this->assertEquals($response->getParameter('error_description'), 'The access token provided has expired');
+    }
+
+    public function testOutOfScopeToken()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-scope';
+        $scope = 'outofscope';
+        $allow = $server->verifyResourceRequest($request, $response = new Response(), $scope);
+        $this->assertFalse($allow);
+
+        $this->assertEquals($response->getStatusCode(), 403);
+        $this->assertEquals($response->getParameter('error'), 'insufficient_scope');
+        $this->assertEquals($response->getParameter('error_description'), 'The request requires higher privileges than provided by the access token');
+
+        // verify the "scope" has been set in the "WWW-Authenticate" header
+        preg_match('/scope="(.*?)"/', $response->getHttpHeader('WWW-Authenticate'), $matches);
+        $this->assertEquals(2, count($matches));
+        $this->assertEquals($matches[1], 'outofscope');
+    }
+
+    public function testMalformedToken()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-malformed';
+        $allow = $server->verifyResourceRequest($request, $response = new Response());
+        $this->assertFalse($allow);
+
+        $this->assertEquals($response->getStatusCode(), 401);
+        $this->assertEquals($response->getParameter('error'), 'malformed_token');
+        $this->assertEquals($response->getParameter('error_description'), 'Malformed token (missing "expires")');
+    }
+
+    public function testValidToken()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-scope';
+        $allow = $server->verifyResourceRequest($request, $response = new Response());
+        $this->assertTrue($allow);
+    }
+
+    public function testValidTokenWithScopeParam()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-scope';
+        $request->query['scope'] = 'testscope';
+        $allow = $server->verifyResourceRequest($request, $response = new Response());
+        $this->assertTrue($allow);
+    }
+
+    public function testCreateController()
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $tokenType = new \OAuth2\TokenType\Bearer();
+        $controller = new ResourceController($tokenType, $storage);
+    }
+
+    private function getTestServer($config = array())
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage, $config);
+
+        // Add the two types supported for authorization grant
+        $server->addGrantType(new AuthorizationCode($storage));
+
+        return $server;
+    }
+}

+ 332 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/Controller/TokenControllerTest.php

@@ -0,0 +1,332 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\GrantType\AuthorizationCode;
+use OAuth2\GrantType\ClientCredentials;
+use OAuth2\GrantType\UserCredentials;
+use OAuth2\Scope;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+use PHPUnit\Framework\TestCase;
+
+class TokenControllerTest extends TestCase
+{
+    public function testNoGrantType()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+        $server->handleTokenRequest(TestRequest::createPost(), $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'The grant type was not specified in the request');
+    }
+
+    public function testInvalidGrantType()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'invalid_grant_type', // invalid grant type
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'unsupported_grant_type');
+        $this->assertEquals($response->getParameter('error_description'), 'Grant type "invalid_grant_type" not supported');
+    }
+
+    public function testNoClientId()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'authorization_code', // valid grant type
+            'code'       => 'testcode',
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_client');
+        $this->assertEquals($response->getParameter('error_description'), 'Client credentials were not found in the headers or body');
+    }
+
+    public function testNoClientSecretWithConfidentialClient()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'authorization_code', // valid grant type
+            'code'       => 'testcode',
+            'client_id' => 'Test Client ID', // valid client id
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_client');
+        $this->assertEquals($response->getParameter('error_description'), 'This client is invalid or must authenticate using a client secret');
+    }
+
+    public function testNoClientSecretWithEmptySecret()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'authorization_code', // valid grant type
+            'code'       => 'testcode-empty-secret',
+            'client_id' => 'Test Client ID Empty Secret', // valid client id
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 200);
+    }
+
+    public function testInvalidClientId()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'authorization_code', // valid grant type
+            'code'       => 'testcode',
+            'client_id'  => 'Fake Client ID', // invalid client id
+            'client_secret' => 'TestSecret', // valid client secret
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_client');
+        $this->assertEquals($response->getParameter('error_description'), 'The client credentials are invalid');
+    }
+
+    public function testInvalidClientSecret()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'authorization_code', // valid grant type
+            'code'       => 'testcode',
+            'client_id'  => 'Test Client ID', // valid client id
+            'client_secret' => 'Fake Client Secret', // invalid client secret
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_client');
+        $this->assertEquals($response->getParameter('error_description'), 'The client credentials are invalid');
+    }
+
+    public function testValidTokenResponse()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'authorization_code', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code' => 'testcode', // valid authorization code
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertTrue($response instanceof Response);
+        $this->assertEquals($response->getStatusCode(), 200);
+        $this->assertNull($response->getParameter('error'));
+        $this->assertNull($response->getParameter('error_description'));
+        $this->assertNotNull($response->getParameter('access_token'));
+        $this->assertNotNull($response->getParameter('expires_in'));
+        $this->assertNotNull($response->getParameter('token_type'));
+    }
+
+    public function testValidClientIdScope()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'authorization_code', // valid grant type
+            'code'       => 'testcode',
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'scope' => 'clientscope1 clientscope2'
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 200);
+        $this->assertNull($response->getParameter('error'));
+        $this->assertNull($response->getParameter('error_description'));
+        $this->assertEquals('clientscope1 clientscope2', $response->getParameter('scope'));
+    }
+
+    public function testInvalidClientIdScope()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'authorization_code', // valid grant type
+            'code'       => 'testcode-with-scope',
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'scope' => 'clientscope3'
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_scope');
+        $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request');
+    }
+
+    public function testEnforceScope()
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage);
+        $server->addGrantType(new ClientCredentials($storage));
+
+        $scope = new Scope(array(
+            'default_scope' => false,
+            'supported_scopes' => array('testscope')
+        ));
+        $server->setScopeUtil($scope);
+
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'client_credentials', // valid grant type
+            'client_id'  => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+        ));
+        $response = $server->handleTokenRequest($request);
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_scope');
+        $this->assertEquals($response->getParameter('error_description'), 'This application requires you specify a scope parameter');
+    }
+
+    public function testCanReceiveAccessTokenUsingPasswordGrantTypeWithoutClientSecret()
+    {
+        // add the test parameters in memory
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage);
+        $server->addGrantType(new UserCredentials($storage));
+
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'password',                          // valid grant type
+            'client_id'  => 'Test Client ID For Password Grant', // valid client id
+            'username'   => 'johndoe',                           // valid username
+            'password'   => 'password',                          // valid password for username
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertTrue($response instanceof Response);
+        $this->assertEquals(200, $response->getStatusCode(), var_export($response, 1));
+        $this->assertNull($response->getParameter('error'));
+        $this->assertNull($response->getParameter('error_description'));
+        $this->assertNotNull($response->getParameter('access_token'));
+        $this->assertNotNull($response->getParameter('expires_in'));
+        $this->assertNotNull($response->getParameter('token_type'));
+    }
+
+    public function testInvalidTokenTypeHintForRevoke()
+    {
+        $server = $this->getTestServer();
+
+        $request = TestRequest::createPost(array(
+            'token_type_hint' => 'foo',
+            'token' => 'sometoken'
+        ));
+
+        $server->handleRevokeRequest($request, $response = new Response());
+
+        $this->assertTrue($response instanceof Response);
+        $this->assertEquals(400, $response->getStatusCode(), var_export($response, 1));
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'Token type hint must be either \'access_token\' or \'refresh_token\'');
+    }
+
+    public function testMissingTokenForRevoke()
+    {
+        $server = $this->getTestServer();
+
+        $request = TestRequest::createPost(array(
+            'token_type_hint' => 'access_token'
+        ));
+
+        $server->handleRevokeRequest($request, $response = new Response());
+        $this->assertTrue($response instanceof Response);
+        $this->assertEquals(400, $response->getStatusCode(), var_export($response, 1));
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'Missing token parameter to revoke');
+    }
+
+    public function testInvalidRequestMethodForRevoke()
+    {
+        $server = $this->getTestServer();
+
+        $request = new TestRequest();
+        $request->setQuery(array(
+            'token_type_hint' => 'access_token'
+        ));
+
+        $server->handleRevokeRequest($request, $response = new Response());
+        $this->assertTrue($response instanceof Response);
+        $this->assertEquals(405, $response->getStatusCode(), var_export($response, 1));
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'The request method must be POST when revoking an access token');
+    }
+
+    public function testCanUseCrossOriginRequestForRevoke()
+    {
+        $server = $this->getTestServer();
+
+        $request = new TestRequest();
+        $request->setMethod('OPTIONS');
+
+        $server->handleRevokeRequest($request, $response = new Response());
+        $this->assertTrue($response instanceof Response);
+        $this->assertEquals(200, $response->getStatusCode(), var_export($response, 1));
+        $this->assertEquals($response->getHttpHeader('Allow'), 'POST, OPTIONS');
+    }
+
+    public function testInvalidRequestMethodForAccessToken()
+    {
+        $server = $this->getTestServer();
+
+        $request = new TestRequest();
+        $request->setQuery(array(
+            'token_type_hint' => 'access_token'
+        ));
+
+        $server->handleTokenRequest($request, $response = new Response());
+        $this->assertTrue($response instanceof Response);
+        $this->assertEquals(405, $response->getStatusCode(), var_export($response, 1));
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'The request method must be POST when requesting an access token');
+    }
+
+    public function testCanUseCrossOriginRequestForAccessToken()
+    {
+        $server = $this->getTestServer();
+
+        $request = new TestRequest();
+        $request->setMethod('OPTIONS');
+
+        $server->handleTokenRequest($request, $response = new Response());
+        $this->assertTrue($response instanceof Response);
+        $this->assertEquals(200, $response->getStatusCode(), var_export($response, 1));
+        $this->assertEquals($response->getHttpHeader('Allow'), 'POST, OPTIONS');
+    }
+
+    public function testCreateController()
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $accessToken = new \OAuth2\ResponseType\AccessToken($storage);
+        $controller = new TokenController($accessToken, $storage);
+    }
+
+    private function getTestServer()
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage);
+        $server->addGrantType(new AuthorizationCode($storage));
+
+        return $server;
+    }
+}

+ 103 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/FirebaseJwtTest.php

@@ -0,0 +1,103 @@
+<?php
+
+namespace OAuth2\Encryption;
+
+use OAuth2\Storage\Bootstrap;
+use PHPUnit\Framework\TestCase;
+
+class FirebaseJwtTest extends TestCase
+{
+    private $privateKey;
+
+    public function setUp()
+    {
+        $this->privateKey = <<<EOD
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQC5/SxVlE8gnpFqCxgl2wjhzY7ucEi00s0kUg3xp7lVEvgLgYcA
+nHiWp+gtSjOFfH2zsvpiWm6Lz5f743j/FEzHIO1owR0p4d9pOaJK07d01+RzoQLO
+IQAgXrr4T1CCWUesncwwPBVCyy2Mw3Nmhmr9MrF8UlvdRKBxriRnlP3qJQIDAQAB
+AoGAVgJJVU4fhYMu1e5JfYAcTGfF+Gf+h3iQm4JCpoUcxMXf5VpB9ztk3K7LRN5y
+kwFuFALpnUAarRcUPs0D8FoP4qBluKksbAtgHkO7bMSH9emN+mH4le4qpFlR7+P1
+3fLE2Y19IBwPwEfClC+TpJvuog6xqUYGPlg6XLq/MxQUB4ECQQDgovP1v+ONSeGS
+R+NgJTR47noTkQT3M2izlce/OG7a+O0yw6BOZjNXqH2wx3DshqMcPUFrTjibIClP
+l/tEQ3ShAkEA0/TdBYDtXpNNjqg0R9GVH2pw7Kh68ne6mZTuj0kCgFYpUF6L6iMm
+zXamIJ51rTDsTyKTAZ1JuAhAsK/M2BbDBQJAKQ5fXEkIA+i+64dsDUR/hKLBeRYG
+PFAPENONQGvGBwt7/s02XV3cgGbxIgAxqWkqIp0neb9AJUoJgtyaNe3GQQJANoL4
+QQ0af0NVJAZgg8QEHTNL3aGrFSbzx8IE5Lb7PLRsJa5bP5lQxnDoYuU+EI/Phr62
+niisp/b/ZDGidkTMXQJBALeRsH1I+LmICAvWXpLKa9Gv0zGCwkuIJLiUbV9c6CVh
+suocCAteQwL5iW2gA4AnYr5OGeHFsEl7NCQcwfPZpJ0=
+-----END RSA PRIVATE KEY-----
+EOD;
+    }
+
+    /** @dataProvider provideClientCredentials */
+    public function testJwtUtil($client_id, $client_key)
+    {
+        $jwtUtil = new FirebaseJwt();
+
+        $params = array(
+            'iss' => $client_id,
+            'exp' => time() + 1000,
+            'iat' => time(),
+            'sub' => 'testuser@ourdomain.com',
+            'aud' => 'http://myapp.com/oauth/auth',
+            'scope' => null,
+        );
+
+        $encoded = $jwtUtil->encode($params, $this->privateKey, 'RS256');
+
+        // test BC behaviour of trusting the algorithm in the header
+        $payload = $jwtUtil->decode($encoded, $client_key, array('RS256'));
+        $this->assertEquals($params, $payload);
+
+        // test BC behaviour of not verifying by passing false
+        $payload = $jwtUtil->decode($encoded, $client_key, false);
+        $this->assertEquals($params, $payload);
+
+        // test the new restricted algorithms header
+        $payload = $jwtUtil->decode($encoded, $client_key, array('RS256'));
+        $this->assertEquals($params, $payload);
+    }
+
+    public function testInvalidJwt()
+    {
+        $jwtUtil = new FirebaseJwt();
+
+        $this->assertFalse($jwtUtil->decode('goob'));
+        $this->assertFalse($jwtUtil->decode('go.o.b'));
+    }
+
+    /** @dataProvider provideClientCredentials */
+    public function testInvalidJwtHeader($client_id, $client_key)
+    {
+        $jwtUtil = new FirebaseJwt();
+
+        $params = array(
+            'iss' => $client_id,
+            'exp' => time() + 1000,
+            'iat' => time(),
+            'sub' => 'testuser@ourdomain.com',
+            'aud' => 'http://myapp.com/oauth/auth',
+            'scope' => null,
+        );
+
+        // testing for algorithm tampering when only RSA256 signing is allowed
+        // @see https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/
+        $tampered = $jwtUtil->encode($params, $client_key, 'HS256');
+
+        $payload = $jwtUtil->decode($tampered, $client_key, array('RS256'));
+
+        $this->assertFalse($payload);
+    }
+
+    public function provideClientCredentials()
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $client_id  = 'Test Client ID';
+        $client_key = $storage->getClientKey($client_id, "testuser@ourdomain.com");
+
+        return array(
+            array($client_id, $client_key),
+        );
+    }
+}

+ 103 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/Encryption/JwtTest.php

@@ -0,0 +1,103 @@
+<?php
+
+namespace OAuth2\Encryption;
+
+use OAuth2\Storage\Bootstrap;
+use PHPUnit\Framework\TestCase;
+
+class JwtTest extends TestCase
+{
+    private $privateKey;
+
+    public function setUp()
+    {
+        $this->privateKey = <<<EOD
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQC5/SxVlE8gnpFqCxgl2wjhzY7ucEi00s0kUg3xp7lVEvgLgYcA
+nHiWp+gtSjOFfH2zsvpiWm6Lz5f743j/FEzHIO1owR0p4d9pOaJK07d01+RzoQLO
+IQAgXrr4T1CCWUesncwwPBVCyy2Mw3Nmhmr9MrF8UlvdRKBxriRnlP3qJQIDAQAB
+AoGAVgJJVU4fhYMu1e5JfYAcTGfF+Gf+h3iQm4JCpoUcxMXf5VpB9ztk3K7LRN5y
+kwFuFALpnUAarRcUPs0D8FoP4qBluKksbAtgHkO7bMSH9emN+mH4le4qpFlR7+P1
+3fLE2Y19IBwPwEfClC+TpJvuog6xqUYGPlg6XLq/MxQUB4ECQQDgovP1v+ONSeGS
+R+NgJTR47noTkQT3M2izlce/OG7a+O0yw6BOZjNXqH2wx3DshqMcPUFrTjibIClP
+l/tEQ3ShAkEA0/TdBYDtXpNNjqg0R9GVH2pw7Kh68ne6mZTuj0kCgFYpUF6L6iMm
+zXamIJ51rTDsTyKTAZ1JuAhAsK/M2BbDBQJAKQ5fXEkIA+i+64dsDUR/hKLBeRYG
+PFAPENONQGvGBwt7/s02XV3cgGbxIgAxqWkqIp0neb9AJUoJgtyaNe3GQQJANoL4
+QQ0af0NVJAZgg8QEHTNL3aGrFSbzx8IE5Lb7PLRsJa5bP5lQxnDoYuU+EI/Phr62
+niisp/b/ZDGidkTMXQJBALeRsH1I+LmICAvWXpLKa9Gv0zGCwkuIJLiUbV9c6CVh
+suocCAteQwL5iW2gA4AnYr5OGeHFsEl7NCQcwfPZpJ0=
+-----END RSA PRIVATE KEY-----
+EOD;
+    }
+
+    /** @dataProvider provideClientCredentials */
+    public function testJwtUtil($client_id, $client_key)
+    {
+        $jwtUtil = new Jwt();
+
+        $params = array(
+            'iss' => $client_id,
+            'exp' => time() + 1000,
+            'iat' => time(),
+            'sub' => 'testuser@ourdomain.com',
+            'aud' => 'http://myapp.com/oauth/auth',
+            'scope' => null,
+        );
+
+        $encoded = $jwtUtil->encode($params, $this->privateKey, 'RS256');
+
+        // test BC behaviour of trusting the algorithm in the header
+        $payload = $jwtUtil->decode($encoded, $client_key);
+        $this->assertEquals($params, $payload);
+
+        // test BC behaviour of not verifying by passing false
+        $payload = $jwtUtil->decode($encoded, $client_key, false);
+        $this->assertEquals($params, $payload);
+
+        // test the new restricted algorithms header
+        $payload = $jwtUtil->decode($encoded, $client_key, array('RS256'));
+        $this->assertEquals($params, $payload);
+    }
+
+    public function testInvalidJwt()
+    {
+        $jwtUtil = new Jwt();
+
+        $this->assertFalse($jwtUtil->decode('goob'));
+        $this->assertFalse($jwtUtil->decode('go.o.b'));
+    }
+
+    /** @dataProvider provideClientCredentials */
+    public function testInvalidJwtHeader($client_id, $client_key)
+    {
+        $jwtUtil = new Jwt();
+
+        $params = array(
+            'iss' => $client_id,
+            'exp' => time() + 1000,
+            'iat' => time(),
+            'sub' => 'testuser@ourdomain.com',
+            'aud' => 'http://myapp.com/oauth/auth',
+            'scope' => null,
+        );
+
+        // testing for algorithm tampering when only RSA256 signing is allowed
+        // @see https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/
+        $tampered = $jwtUtil->encode($params, $client_key, 'HS256');
+
+        $payload = $jwtUtil->decode($tampered, $client_key, array('RS256'));
+
+        $this->assertFalse($payload);
+    }
+
+    public function provideClientCredentials()
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $client_id  = 'Test Client ID';
+        $client_key = $storage->getClientKey($client_id, "testuser@ourdomain.com");
+
+        return array(
+            array($client_id, $client_key),
+        );
+    }
+}

+ 224 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/AuthorizationCodeTest.php

@@ -0,0 +1,224 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+use PHPUnit\Framework\TestCase;
+
+class AuthorizationCodeTest extends TestCase
+{
+    public function testNoCode()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'authorization_code', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'Missing parameter: "code" is required');
+    }
+
+    public function testInvalidCode()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'InvalidCode', // invalid authorization code
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'Authorization code doesn\'t exist or is invalid for the client');
+    }
+
+    public function testCodeCannotBeUsedTwice()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'testcode', // valid code
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 200);
+        $this->assertNotNull($response->getParameter('access_token'));
+
+        // try to use the same code again
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'Authorization code doesn\'t exist or is invalid for the client');
+    }
+
+    public function testExpiredCode()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'testcode-expired', // expired authorization code
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'The authorization code has expired');
+    }
+
+    public function testValidCode()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'testcode', // valid code
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+    }
+
+    public function testValidRedirectUri()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'redirect_uri'  => 'http://brentertainment.com/voil%C3%A0', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'testcode-redirect-uri', // valid code
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+    }
+
+    public function testValidCodeNoScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'testcode-with-scope', // valid code
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertArrayHasKey('scope', $token);
+        $this->assertEquals($token['scope'], 'scope1 scope2');
+    }
+
+    public function testValidCodeSameScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'testcode-with-scope', // valid code
+            'scope'         => 'scope2 scope1',
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertArrayHasKey('scope', $token);
+        $this->assertEquals($token['scope'], 'scope2 scope1');
+    }
+
+    public function testValidCodeLessScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'testcode-with-scope', // valid code
+            'scope'         => 'scope1',
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertArrayHasKey('scope', $token);
+        $this->assertEquals($token['scope'], 'scope1');
+    }
+
+    public function testValidCodeDifferentScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'testcode-with-scope', // valid code
+            'scope'         => 'scope3',
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_scope');
+        $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request');
+    }
+
+    public function testValidCodeInvalidScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'testcode-with-scope', // valid code
+            'scope'         => 'invalid-scope',
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_scope');
+        $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request');
+    }
+
+    public function testValidClientDifferentCode()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Some Other Client', // valid client id
+            'client_secret' => 'TestSecret3', // valid client secret
+            'code'          => 'testcode', // valid code
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'authorization_code doesn\'t exist or is invalid for the client');
+    }
+
+    private function getTestServer()
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage);
+        $server->addGrantType(new AuthorizationCode($storage));
+
+        return $server;
+    }
+}

+ 160 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ClientCredentialsTest.php

@@ -0,0 +1,160 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Request;
+use OAuth2\Response;
+use PHPUnit\Framework\TestCase;
+
+class ClientCredentialsTest extends TestCase
+{
+    public function testInvalidCredentials()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'client_credentials', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'FakeSecret', // valid client secret
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_client');
+        $this->assertEquals($response->getParameter('error_description'), 'The client credentials are invalid');
+    }
+
+    public function testValidCredentials()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'client_credentials', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('scope', $token);
+        $this->assertNull($token['scope']);
+    }
+
+    public function testValidCredentialsWithScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'client_credentials', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'scope' => 'scope1',
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertArrayHasKey('scope', $token);
+        $this->assertEquals($token['scope'], 'scope1');
+    }
+
+    public function testValidCredentialsInvalidScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'client_credentials', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'scope' => 'invalid-scope',
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_scope');
+        $this->assertEquals($response->getParameter('error_description'), 'An unsupported scope was requested');
+    }
+
+    public function testValidCredentialsInHeader()
+    {
+        // create with HTTP_AUTHORIZATION in header
+        $server = $this->getTestServer();
+        $headers = array('HTTP_AUTHORIZATION' => 'Basic '.base64_encode('Test Client ID:TestSecret'), 'REQUEST_METHOD' => 'POST');
+        $params  = array('grant_type' => 'client_credentials');
+        $request = new Request(array(), $params, array(), array(), array(), $headers);
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertNotNull($token['access_token']);
+
+        // create using PHP Authorization Globals
+        $headers = array('PHP_AUTH_USER' => 'Test Client ID', 'PHP_AUTH_PW' => 'TestSecret', 'REQUEST_METHOD' => 'POST');
+        $params  = array('grant_type' => 'client_credentials');
+        $request = new Request(array(), $params, array(), array(), array(), $headers);
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertNotNull($token['access_token']);
+    }
+
+    public function testValidCredentialsInRequest()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'client_credentials', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertNotNull($token['access_token']);
+    }
+
+    public function testValidCredentialsInQuerystring()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'client_credentials', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertNotNull($token['access_token']);
+    }
+
+    public function testClientUserIdIsSetInAccessToken()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'client_credentials', // valid grant type
+            'client_id'     => 'Client ID With User ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+
+        // verify the user_id was associated with the token
+        $storage = $server->getStorage('client');
+        $token = $storage->getAccessToken($token['access_token']);
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('user_id', $token);
+        $this->assertEquals($token['user_id'], 'brent@brentertainment.com');
+    }
+
+    private function getTestServer()
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage);
+        $server->addGrantType(new ClientCredentials($storage));
+
+        return $server;
+    }
+}

+ 144 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/ImplicitTest.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+use PHPUnit\Framework\TestCase;
+
+class ImplicitTest extends TestCase
+{
+    public function testImplicitNotAllowedResponse()
+    {
+        $server = $this->getTestServer();
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'token', // invalid response type
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), false);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        parse_str($parts['query'], $query);
+
+        $this->assertEquals($query['error'], 'unsupported_response_type');
+        $this->assertEquals($query['error_description'], 'implicit grant type not supported');
+    }
+
+    public function testUserDeniesAccessResponse()
+    {
+        $server = $this->getTestServer(array('allow_implicit' => true));
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'token', // valid response type
+            'state' => 'xyz',
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), false);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        parse_str($parts['query'], $query);
+
+        $this->assertEquals($query['error'], 'access_denied');
+        $this->assertEquals($query['error_description'], 'The user denied access to your application');
+    }
+
+    public function testSuccessfulRequestFragmentParameter()
+    {
+        $server = $this->getTestServer(array('allow_implicit' => true));
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'token', // valid response type
+            'state' => 'xyz',
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $this->assertNull($response->getParameter('error'));
+        $this->assertNull($response->getParameter('error_description'));
+
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+
+        $this->assertEquals('http', $parts['scheme']); // same as passed in to redirect_uri
+        $this->assertEquals('adobe.com', $parts['host']); // same as passed in to redirect_uri
+        $this->assertArrayHasKey('fragment', $parts);
+        $this->assertFalse(isset($parts['query']));
+
+        // assert fragment is in "application/x-www-form-urlencoded" format
+        parse_str($parts['fragment'], $params);
+        $this->assertNotNull($params);
+        $this->assertArrayHasKey('access_token', $params);
+        $this->assertArrayHasKey('expires_in', $params);
+        $this->assertArrayHasKey('token_type', $params);
+    }
+
+    public function testSuccessfulRequestReturnsStateParameter()
+    {
+        $server = $this->getTestServer(array('allow_implicit' => true));
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'token', // valid response type
+            'state' => 'test', // valid state string (just needs to be passed back to us)
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $this->assertNull($response->getParameter('error'));
+        $this->assertNull($response->getParameter('error_description'));
+
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        $this->assertArrayHasKey('fragment', $parts);
+        parse_str($parts['fragment'], $params);
+
+        $this->assertArrayHasKey('state', $params);
+        $this->assertEquals($params['state'], 'test');
+    }
+
+    public function testSuccessfulRequestStripsExtraParameters()
+    {
+        $server = $this->getTestServer(array('allow_implicit' => true));
+        $request = new Request(array(
+            'client_id' => 'Test Client ID', // valid client id
+            'redirect_uri' => 'http://adobe.com?fake=something', // valid redirect URI
+            'response_type' => 'token', // valid response type
+            'state' => 'test', // valid state string (just needs to be passed back to us)
+            'fake' => 'something', // add extra param to querystring
+        ));
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $this->assertNull($response->getParameter('error'));
+        $this->assertNull($response->getParameter('error_description'));
+
+        $location = $response->getHttpHeader('Location');
+        $parts = parse_url($location);
+        $this->assertFalse(isset($parts['fake']));
+        $this->assertArrayHasKey('fragment', $parts);
+        parse_str($parts['fragment'], $params);
+
+        $this->assertFalse(isset($params['fake']));
+        $this->assertArrayHasKey('state', $params);
+        $this->assertEquals($params['state'], 'test');
+    }
+
+    private function getTestServer($config = array())
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage, $config);
+
+        // Add the two types supported for authorization grant
+        $server->addGrantType(new AuthorizationCode($storage));
+
+        return $server;
+    }
+}

+ 361 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/JwtBearerTest.php

@@ -0,0 +1,361 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+use OAuth2\Encryption\Jwt;
+use PHPUnit\Framework\TestCase;
+
+class JwtBearerTest extends TestCase
+{
+    private $privateKey;
+
+    public function setUp()
+    {
+        $this->privateKey = <<<EOD
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQC5/SxVlE8gnpFqCxgl2wjhzY7ucEi00s0kUg3xp7lVEvgLgYcA
+nHiWp+gtSjOFfH2zsvpiWm6Lz5f743j/FEzHIO1owR0p4d9pOaJK07d01+RzoQLO
+IQAgXrr4T1CCWUesncwwPBVCyy2Mw3Nmhmr9MrF8UlvdRKBxriRnlP3qJQIDAQAB
+AoGAVgJJVU4fhYMu1e5JfYAcTGfF+Gf+h3iQm4JCpoUcxMXf5VpB9ztk3K7LRN5y
+kwFuFALpnUAarRcUPs0D8FoP4qBluKksbAtgHkO7bMSH9emN+mH4le4qpFlR7+P1
+3fLE2Y19IBwPwEfClC+TpJvuog6xqUYGPlg6XLq/MxQUB4ECQQDgovP1v+ONSeGS
+R+NgJTR47noTkQT3M2izlce/OG7a+O0yw6BOZjNXqH2wx3DshqMcPUFrTjibIClP
+l/tEQ3ShAkEA0/TdBYDtXpNNjqg0R9GVH2pw7Kh68ne6mZTuj0kCgFYpUF6L6iMm
+zXamIJ51rTDsTyKTAZ1JuAhAsK/M2BbDBQJAKQ5fXEkIA+i+64dsDUR/hKLBeRYG
+PFAPENONQGvGBwt7/s02XV3cgGbxIgAxqWkqIp0neb9AJUoJgtyaNe3GQQJANoL4
+QQ0af0NVJAZgg8QEHTNL3aGrFSbzx8IE5Lb7PLRsJa5bP5lQxnDoYuU+EI/Phr62
+niisp/b/ZDGidkTMXQJBALeRsH1I+LmICAvWXpLKa9Gv0zGCwkuIJLiUbV9c6CVh
+suocCAteQwL5iW2gA4AnYr5OGeHFsEl7NCQcwfPZpJ0=
+-----END RSA PRIVATE KEY-----
+EOD;
+    }
+
+    public function testMalformedJWT()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
+        ));
+
+        //Get the jwt and break it
+        $jwt = $this->getJWT();
+        $jwt = substr_replace($jwt, 'broken', 3, 6);
+
+        $request->request['assertion'] = $jwt;
+
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'JWT is malformed');
+    }
+
+    public function testBrokenSignature()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
+        ));
+
+        //Get the jwt and break signature
+        $jwt = $this->getJWT() . 'notSupposeToBeHere';
+        $request->request['assertion'] = $jwt;
+
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'JWT failed signature verification');
+    }
+
+    public function testExpiredJWT()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
+        ));
+
+        //Get an expired JWT
+        $jwt = $this->getJWT(1234);
+        $request->request['assertion'] = $jwt;
+
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'JWT has expired');
+    }
+
+    public function testBadExp()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
+        ));
+
+        //Get an expired JWT
+        $jwt = $this->getJWT('badtimestamp');
+        $request->request['assertion'] = $jwt;
+
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'Expiration (exp) time must be a unix time stamp');
+    }
+
+    public function testNoAssert()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
+        ));
+
+        //Do not pass the assert (JWT)
+
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'Missing parameters: "assertion" required');
+    }
+
+    public function testNotBefore()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
+        ));
+
+        //Get a future NBF
+        $jwt = $this->getJWT(null, time() + 10000);
+        $request->request['assertion'] = $jwt;
+
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'JWT cannot be used before the Not Before (nbf) time');
+    }
+
+    public function testBadNotBefore()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
+        ));
+
+        //Get a non timestamp nbf
+        $jwt = $this->getJWT(null, 'notatimestamp');
+        $request->request['assertion'] = $jwt;
+
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'Not Before (nbf) time must be a unix time stamp');
+    }
+
+    public function testNonMatchingAudience()
+    {
+        $server = $this->getTestServer('http://google.com/oauth/o/auth');
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', // valid grant type
+            'assertion' => $this->getJWT(),
+        ));
+
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'Invalid audience (aud)');
+    }
+
+    public function testBadClientID()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',  // valid grant type
+            'assertion' => $this->getJWT(null, null, null, 'bad_client_id'),
+        ));
+
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided');
+    }
+
+    public function testBadSubject()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',  // valid grant type
+            'assertion' => $this->getJWT(null, null, 'anotheruser@ourdomain,com'),
+        ));
+
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided');
+    }
+
+    public function testMissingKey()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',  // valid grant type
+            'assertion' => $this->getJWT(null, null, null, 'Missing Key Cli,nt'),
+        ));
+
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'Invalid issuer (iss) or subject (sub) provided');
+    }
+
+    public function testValidJwt()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',  // valid grant type
+            'assertion' => $this->getJWT(), // valid assertion
+        ));
+
+        $token = $server->grantAccessToken($request, new Response());
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+    }
+
+    public function testValidJwtWithScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',  // valid grant type
+            'assertion'  => $this->getJWT(null, null, null, 'Test Client ID'), // valid assertion
+            'scope'      => 'scope1', // valid scope
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertArrayHasKey('scope', $token);
+        $this->assertEquals($token['scope'], 'scope1');
+    }
+
+    public function testValidJwtInvalidScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',  // valid grant type
+            'assertion'  => $this->getJWT(null, null, null, 'Test Client ID'), // valid assertion
+            'scope'      => 'invalid-scope', // invalid scope
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_scope');
+        $this->assertEquals($response->getParameter('error_description'), 'An unsupported scope was requested');
+    }
+
+    public function testValidJti()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+                'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',  // valid grant type
+                'assertion' => $this->getJWT(null, null, 'testuser@ourdomain.com', 'Test Client ID', 'unused_jti'), // valid assertion with invalid scope
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+    }
+
+    public function testInvalidJti()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+                'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',  // valid grant type
+                'assertion' => $this->getJWT(99999999900, null, 'testuser@ourdomain.com', 'Test Client ID', 'used_jti'), // valid assertion with invalid scope
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'JSON Token Identifier (jti) has already been used');
+    }
+
+    public function testJtiReplayAttack()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+                'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',  // valid grant type
+                'assertion' => $this->getJWT(99999999900, null, 'testuser@ourdomain.com', 'Test Client ID', 'totally_new_jti'), // valid assertion with invalid scope
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+
+        //Replay the same request
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'JSON Token Identifier (jti) has already been used');
+    }
+
+    /**
+     * Generates a JWT
+     * @param $exp The expiration date. If the current time is greater than the exp, the JWT is invalid.
+     * @param $nbf The "not before" time. If the current time is less than the nbf, the JWT is invalid.
+     * @param $sub The subject we are acting on behalf of. This could be the email address of the user in the system.
+     * @param $iss The issuer, usually the client_id.
+     * @return string
+     */
+    private function getJWT($exp = null, $nbf = null, $sub = null, $iss = 'Test Client ID', $jti = null)
+    {
+        if (!$exp) {
+            $exp = time() + 1000;
+        }
+
+        if (!$sub) {
+            $sub = "testuser@ourdomain.com";
+        }
+
+        $params = array(
+            'iss' => $iss,
+            'exp' => $exp,
+            'iat' => time(),
+            'sub' => $sub,
+            'aud' => 'http://myapp.com/oauth/auth',
+        );
+
+        if ($nbf) {
+            $params['nbf'] = $nbf;
+        }
+
+        if ($jti) {
+            $params['jti'] = $jti;
+        }
+
+        $jwtUtil = new Jwt();
+
+        return $jwtUtil->encode($params, $this->privateKey, 'RS256');
+    }
+
+    private function getTestServer($audience = 'http://myapp.com/oauth/auth')
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage);
+        $server->addGrantType(new JwtBearer($storage, $audience, new Jwt()));
+
+        return $server;
+    }
+}

+ 205 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/RefreshTokenTest.php

@@ -0,0 +1,205 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+use PHPUnit\Framework\TestCase;
+
+class RefreshTokenTest extends TestCase
+{
+    private $storage;
+
+    public function testNoRefreshToken()
+    {
+        $server = $this->getTestServer();
+        $server->addGrantType(new RefreshToken($this->storage));
+
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'refresh_token',  // valid grant type
+            'client_id'  => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret',  // valid client secret
+        ));
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'Missing parameter: "refresh_token" is required');
+    }
+
+    public function testInvalidRefreshToken()
+    {
+        $server = $this->getTestServer();
+        $server->addGrantType(new RefreshToken($this->storage));
+
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'refresh_token', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'refresh_token' => 'fake-token', // invalid refresh token
+        ));
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'Invalid refresh token');
+    }
+
+    public function testValidRefreshTokenWithNewRefreshTokenInResponse()
+    {
+        $server = $this->getTestServer();
+        $server->addGrantType(new RefreshToken($this->storage, array('always_issue_new_refresh_token' => true)));
+
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'refresh_token', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'refresh_token' => 'test-refreshtoken', // valid refresh token
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+        $this->assertTrue(isset($token['refresh_token']), 'refresh token should always refresh');
+
+        $refresh_token = $this->storage->getRefreshToken($token['refresh_token']);
+        $this->assertNotNull($refresh_token);
+        $this->assertEquals($refresh_token['refresh_token'], $token['refresh_token']);
+        $this->assertEquals($refresh_token['client_id'], $request->request('client_id'));
+        $this->assertTrue($token['refresh_token'] != 'test-refreshtoken', 'the refresh token returned is not the one used');
+        $used_token = $this->storage->getRefreshToken('test-refreshtoken');
+        $this->assertFalse($used_token, 'the refresh token used is no longer valid');
+    }
+
+    public function testValidRefreshTokenDoesNotUnsetToken()
+    {
+        $server = $this->getTestServer();
+        $server->addGrantType(new RefreshToken($this->storage, array(
+            'always_issue_new_refresh_token' => true,
+            'unset_refresh_token_after_use'  => false,
+        )));
+
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'refresh_token', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'refresh_token' => 'test-refreshtoken', // valid refresh token
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+        $this->assertTrue(isset($token['refresh_token']), 'refresh token should always refresh');
+
+        $used_token = $this->storage->getRefreshToken('test-refreshtoken');
+        $this->assertNotNull($used_token, 'the refresh token used is still valid');
+    }
+
+    public function testValidRefreshTokenWithNoRefreshTokenInResponse()
+    {
+        $server = $this->getTestServer();
+        $server->addGrantType(new RefreshToken($this->storage, array('always_issue_new_refresh_token' => false)));
+
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'refresh_token', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'refresh_token' => 'test-refreshtoken', // valid refresh token
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+        $this->assertFalse(isset($token['refresh_token']), 'refresh token should not be returned');
+
+        $used_token = $this->storage->getRefreshToken('test-refreshtoken');
+        $this->assertNotNull($used_token, 'the refresh token used is still valid');
+    }
+
+    public function testValidRefreshTokenSameScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'refresh_token', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope)
+            'scope'         => 'scope2 scope1',
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertArrayHasKey('scope', $token);
+        $this->assertEquals($token['scope'], 'scope2 scope1');
+    }
+
+    public function testValidRefreshTokenLessScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'refresh_token', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope)
+            'scope'         => 'scope1',
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertArrayHasKey('scope', $token);
+        $this->assertEquals($token['scope'], 'scope1');
+    }
+
+    public function testValidRefreshTokenDifferentScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'refresh_token', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope)
+            'scope'         => 'scope3',
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_scope');
+        $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request');
+    }
+
+    public function testValidRefreshTokenInvalidScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'refresh_token', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'refresh_token' => 'test-refreshtoken-with-scope', // valid refresh token (with scope)
+            'scope'         => 'invalid-scope',
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_scope');
+        $this->assertEquals($response->getParameter('error_description'), 'The scope requested is invalid for this request');
+    }
+
+    public function testValidClientDifferentRefreshToken()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'refresh_token', // valid grant type
+            'client_id'     => 'Test Some Other Client', // valid client id
+            'client_secret' => 'TestSecret3', // valid client secret
+            'refresh_token' => 'test-refreshtoken', // valid refresh token
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'refresh_token doesn\'t exist or is invalid for the client');
+    }
+
+    private function getTestServer()
+    {
+        $this->storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($this->storage);
+
+        return $server;
+    }
+}

+ 173 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/GrantType/UserCredentialsTest.php

@@ -0,0 +1,173 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+use PHPUnit\Framework\TestCase;
+
+class UserCredentialsTest extends TestCase
+{
+    public function testNoUsername()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'password', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'password' => 'testpass', // valid password
+        ));
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'Missing parameters: "username" and "password" required');
+    }
+
+    public function testNoPassword()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'password', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'username' => 'test-username', // valid username
+        ));
+        $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_request');
+        $this->assertEquals($response->getParameter('error_description'), 'Missing parameters: "username" and "password" required');
+    }
+
+    public function testInvalidUsername()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'password', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'username' => 'fake-username', // valid username
+            'password' => 'testpass', // valid password
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 401);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'Invalid username and password combination');
+    }
+
+    public function testInvalidPassword()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'password', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'username' => 'test-username', // valid username
+            'password' => 'fakepass', // invalid password
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 401);
+        $this->assertEquals($response->getParameter('error'), 'invalid_grant');
+        $this->assertEquals($response->getParameter('error_description'), 'Invalid username and password combination');
+    }
+
+    public function testValidCredentials()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'password', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'username' => 'test-username', // valid username
+            'password' => 'testpass', // valid password
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+    }
+
+    public function testValidCredentialsWithScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'password', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'username' => 'test-username', // valid username
+            'password' => 'testpass', // valid password
+            'scope'    => 'scope1', // valid scope
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+        $this->assertArrayHasKey('scope', $token);
+        $this->assertEquals($token['scope'], 'scope1');
+    }
+
+    public function testValidCredentialsInvalidScope()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'password', // valid grant type
+            'client_id' => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'username' => 'test-username', // valid username
+            'password' => 'testpass', // valid password
+            'scope'         => 'invalid-scope',
+        ));
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_scope');
+        $this->assertEquals($response->getParameter('error_description'), 'An unsupported scope was requested');
+    }
+
+    public function testNoSecretWithPublicClient()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'password', // valid grant type
+            'client_id' => 'Test Client ID Empty Secret', // valid public client
+            'username' => 'test-username', // valid username
+            'password' => 'testpass', // valid password
+        ));
+
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('access_token', $token);
+    }
+
+    public function testNoSecretWithConfidentialClient()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type' => 'password', // valid grant type
+            'client_id' => 'Test Client ID', // valid public client
+            'username' => 'test-username', // valid username
+            'password' => 'testpass', // valid password
+        ));
+
+        $token = $server->grantAccessToken($request, $response = new Response());
+
+        $this->assertEquals($response->getStatusCode(), 400);
+        $this->assertEquals($response->getParameter('error'), 'invalid_client');
+        $this->assertEquals($response->getParameter('error_description'), 'This client is invalid or must authenticate using a client secret');
+    }
+
+    private function getTestServer()
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage);
+        $server->addGrantType(new UserCredentials($storage));
+
+        return $server;
+    }
+}

+ 183 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php

@@ -0,0 +1,183 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+use PHPUnit\Framework\TestCase;
+
+class AuthorizeControllerTest extends TestCase
+{
+    public function testValidateAuthorizeRequest()
+    {
+        $server = $this->getTestServer();
+
+        $response = new Response();
+        $request = new Request(array(
+            'client_id'     => 'Test Client ID', // valid client id
+            'redirect_uri'  => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'id_token',
+            'state'         => 'af0ifjsldkj',
+            'nonce'         => 'n-0S6_WzA2Mj',
+        ));
+
+        // Test valid id_token request
+        $server->handleAuthorizeRequest($request, $response, true);
+
+        $parts = parse_url($response->getHttpHeader('Location'));
+        parse_str($parts['fragment'], $query);
+
+        $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce());
+        $this->assertEquals($query['state'], 'af0ifjsldkj');
+
+        $this->assertArrayHasKey('id_token', $query);
+        $this->assertArrayHasKey('state', $query);
+        $this->assertArrayNotHasKey('access_token', $query);
+        $this->assertArrayNotHasKey('expires_in', $query);
+        $this->assertArrayNotHasKey('token_type', $query);
+
+        // Test valid token id_token request
+        $request->query['response_type'] = 'id_token token';
+        $server->handleAuthorizeRequest($request, $response, true);
+
+        $parts = parse_url($response->getHttpHeader('Location'));
+        parse_str($parts['fragment'], $query);
+
+        $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce());
+        $this->assertEquals($query['state'], 'af0ifjsldkj');
+
+        $this->assertArrayHasKey('access_token', $query);
+        $this->assertArrayHasKey('expires_in', $query);
+        $this->assertArrayHasKey('token_type', $query);
+        $this->assertArrayHasKey('state', $query);
+        $this->assertArrayHasKey('id_token', $query);
+
+        // assert that with multiple-valued response types, order does not matter
+        $request->query['response_type'] = 'token id_token';
+        $server->handleAuthorizeRequest($request, $response, true);
+
+        $parts = parse_url($response->getHttpHeader('Location'));
+        parse_str($parts['fragment'], $query);
+
+        $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce());
+        $this->assertEquals($query['state'], 'af0ifjsldkj');
+
+        $this->assertArrayHasKey('access_token', $query);
+        $this->assertArrayHasKey('expires_in', $query);
+        $this->assertArrayHasKey('token_type', $query);
+        $this->assertArrayHasKey('state', $query);
+        $this->assertArrayHasKey('id_token', $query);
+
+        // assert that with multiple-valued response types with extra spaces do not matter
+        $request->query['response_type'] = ' token  id_token ';
+        $server->handleAuthorizeRequest($request, $response, true);
+
+        $parts = parse_url($response->getHttpHeader('Location'));
+        parse_str($parts['fragment'], $query);
+
+        $this->assertEquals('n-0S6_WzA2Mj', $server->getAuthorizeController()->getNonce());
+        $this->assertEquals($query['state'], 'af0ifjsldkj');
+
+        $this->assertArrayHasKey('access_token', $query);
+        $this->assertArrayHasKey('expires_in', $query);
+        $this->assertArrayHasKey('token_type', $query);
+        $this->assertArrayHasKey('state', $query);
+        $this->assertArrayHasKey('id_token', $query);
+    }
+
+    public function testMissingNonce()
+    {
+        $server    = $this->getTestServer();
+        $authorize = $server->getAuthorizeController();
+
+        $response = new Response();
+        $request  = new Request(array(
+            'client_id'     => 'Test Client ID', // valid client id
+            'redirect_uri'  => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'id_token',
+            'state'         => 'xyz',
+        ));
+
+        // Test missing nonce for 'id_token' response type
+        $server->handleAuthorizeRequest($request, $response, true);
+        $params = $response->getParameters();
+
+        $this->assertEquals($params['error'], 'invalid_nonce');
+        $this->assertEquals($params['error_description'], 'This application requires you specify a nonce parameter');
+
+        // Test missing nonce for 'id_token token' response type
+        $request->query['response_type'] = 'id_token token';
+        $server->handleAuthorizeRequest($request, $response, true);
+        $params = $response->getParameters();
+
+        $this->assertEquals($params['error'], 'invalid_nonce');
+        $this->assertEquals($params['error_description'], 'This application requires you specify a nonce parameter');
+    }
+
+    public function testNotGrantedApplication()
+    {
+        $server = $this->getTestServer();
+
+        $response = new Response();
+        $request  = new Request(array(
+            'client_id'     => 'Test Client ID', // valid client id
+            'redirect_uri'  => 'http://adobe.com', // valid redirect URI
+            'response_type' => 'id_token',
+            'state'         => 'af0ifjsldkj',
+            'nonce'         => 'n-0S6_WzA2Mj',
+        ));
+
+        // Test not approved application
+        $server->handleAuthorizeRequest($request, $response, false);
+
+        $params = $response->getParameters();
+
+        $this->assertEquals($params['error'], 'consent_required');
+        $this->assertEquals($params['error_description'], 'The user denied access to your application');
+
+        // Test not approved application with prompt parameter
+        $request->query['prompt'] = 'none';
+        $server->handleAuthorizeRequest($request, $response, false);
+
+        $params = $response->getParameters();
+
+        $this->assertEquals($params['error'], 'login_required');
+        $this->assertEquals($params['error_description'], 'The user must log in');
+
+        // Test not approved application with user_id set
+        $request->query['prompt'] = 'none';
+        $server->handleAuthorizeRequest($request, $response, false, 'some-user-id');
+
+        $params = $response->getParameters();
+
+        $this->assertEquals($params['error'], 'interaction_required');
+        $this->assertEquals($params['error_description'], 'The user must grant access to your application');
+    }
+
+    public function testNeedsIdToken()
+    {
+        $server = $this->getTestServer();
+        $authorize = $server->getAuthorizeController();
+
+        $this->assertTrue($authorize->needsIdToken('openid'));
+        $this->assertTrue($authorize->needsIdToken('openid profile'));
+        $this->assertFalse($authorize->needsIdToken(''));
+        $this->assertFalse($authorize->needsIdToken('some-scope'));
+    }
+
+    private function getTestServer($config = array())
+    {
+        $config += array(
+            'use_openid_connect' => true,
+            'issuer'             => 'phpunit',
+            'allow_implicit'     => true
+        );
+
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server  = new Server($storage, $config);
+
+        return $server;
+    }
+}

+ 45 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+use PHPUnit\Framework\TestCase;
+
+class UserInfoControllerTest extends TestCase
+{
+    public function testCreateController()
+    {
+        $tokenType = new \OAuth2\TokenType\Bearer();
+        $storage = new \OAuth2\Storage\Memory();
+        $controller = new UserInfoController($tokenType, $storage, $storage);
+
+        $response = new Response();
+        $controller->handleUserInfoRequest(new Request(), $response);
+        $this->assertEquals(401, $response->getStatusCode());
+    }
+
+    public function testValidToken()
+    {
+        $server = $this->getTestServer();
+        $request = Request::createFromGlobals();
+        $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-openid-connect';
+        $response = new Response();
+
+        $server->handleUserInfoRequest($request, $response);
+        $parameters = $response->getParameters();
+        $this->assertEquals($parameters['sub'], 'testuser');
+        $this->assertEquals($parameters['email'], 'testuser@test.com');
+        $this->assertEquals($parameters['email_verified'], true);
+    }
+
+    private function getTestServer($config = array())
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage, $config);
+
+        return $server;
+    }
+}

+ 58 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace OAuth2\OpenID\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+use PHPUnit\Framework\TestCase;
+
+class AuthorizationCodeTest extends TestCase
+{
+    public function testValidCode()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'testcode-openid', // valid code
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('id_token', $token);
+        $this->assertEquals('test_id_token', $token['id_token']);
+
+        // this is only true if "offline_access" was requested
+        $this->assertFalse(isset($token['refresh_token']));
+    }
+
+    public function testOfflineAccess()
+    {
+        $server = $this->getTestServer();
+        $request = TestRequest::createPost(array(
+            'grant_type'    => 'authorization_code', // valid grant type
+            'client_id'     => 'Test Client ID', // valid client id
+            'client_secret' => 'TestSecret', // valid client secret
+            'code'          => 'testcode-openid', // valid code
+            'scope'         => 'offline_access', // valid code
+        ));
+        $token = $server->grantAccessToken($request, new Response());
+
+        $this->assertNotNull($token);
+        $this->assertArrayHasKey('id_token', $token);
+        $this->assertEquals('test_id_token', $token['id_token']);
+        $this->assertTrue(isset($token['refresh_token']));
+    }
+
+    private function getTestServer()
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage, array('use_openid_connect' => true));
+        $server->addGrantType(new AuthorizationCode($storage));
+
+        return $server;
+    }
+}

+ 183 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php

@@ -0,0 +1,183 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+use OAuth2\Storage\Bootstrap;
+use OAuth2\GrantType\ClientCredentials;
+use PHPUnit\Framework\TestCase;
+
+class CodeIdTokenTest extends TestCase
+{
+    public function testHandleAuthorizeRequest()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+
+        $request = new Request(array(
+            'response_type' => 'code id_token',
+            'redirect_uri'  => 'http://adobe.com',
+            'client_id'     => 'Test Client ID',
+            'scope'         => 'openid',
+            'state'         => 'test',
+            'nonce'         => 'test',
+        ));
+
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $this->assertNotContains('error', $location);
+
+        $parts = parse_url($location);
+        $this->assertArrayHasKey('query', $parts);
+
+        // assert fragment is in "application/x-www-form-urlencoded" format
+        parse_str($parts['query'], $params);
+        $this->assertNotNull($params);
+        $this->assertArrayHasKey('id_token', $params);
+        $this->assertArrayHasKey('code', $params);
+
+        // validate ID Token
+        $parts = explode('.', $params['id_token']);
+        foreach ($parts as &$part) {
+            // Each part is a base64url encoded json string.
+            $part = str_replace(array('-', '_'), array('+', '/'), $part);
+            $part = base64_decode($part);
+            $part = json_decode($part, true);
+        }
+        list($header, $claims, $signature) = $parts;
+
+        $this->assertArrayHasKey('iss', $claims);
+        $this->assertArrayHasKey('sub', $claims);
+        $this->assertArrayHasKey('aud', $claims);
+        $this->assertArrayHasKey('iat', $claims);
+        $this->assertArrayHasKey('exp', $claims);
+        $this->assertArrayHasKey('auth_time', $claims);
+        $this->assertArrayHasKey('nonce', $claims);
+
+        // only exists if an access token was granted along with the id_token
+        $this->assertArrayNotHasKey('at_hash', $claims);
+
+        $this->assertEquals($claims['iss'], 'test');
+        $this->assertEquals($claims['aud'], 'Test Client ID');
+        $this->assertEquals($claims['nonce'], 'test');
+        $duration = $claims['exp'] - $claims['iat'];
+        $this->assertEquals($duration, 3600);
+    }
+
+    public function testUserClaimsWithUserId()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+
+        $request = new Request(array(
+            'response_type' => 'code id_token',
+            'redirect_uri'  => 'http://adobe.com',
+            'client_id'     => 'Test Client ID',
+            'scope'         => 'openid email',
+            'state'         => 'test',
+            'nonce'         => 'test',
+        ));
+
+        $userId = 'testuser';
+        $server->handleAuthorizeRequest($request, $response = new Response(), true, $userId);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $this->assertNotContains('error', $location);
+
+        $parts = parse_url($location);
+        $this->assertArrayHasKey('query', $parts);
+
+        // assert fragment is in "application/x-www-form-urlencoded" format
+        parse_str($parts['query'], $params);
+        $this->assertNotNull($params);
+        $this->assertArrayHasKey('id_token', $params);
+        $this->assertArrayHasKey('code', $params);
+
+        // validate ID Token
+        $parts = explode('.', $params['id_token']);
+        foreach ($parts as &$part) {
+            // Each part is a base64url encoded json string.
+            $part = str_replace(array('-', '_'), array('+', '/'), $part);
+            $part = base64_decode($part);
+            $part = json_decode($part, true);
+        }
+        list($header, $claims, $signature) = $parts;
+
+        $this->assertArrayHasKey('email', $claims);
+        $this->assertArrayHasKey('email_verified', $claims);
+        $this->assertNotNull($claims['email']);
+        $this->assertNotNull($claims['email_verified']);
+    }
+
+    public function testUserClaimsWithoutUserId()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer();
+
+        $request = new Request(array(
+            'response_type' => 'code id_token',
+            'redirect_uri'  => 'http://adobe.com',
+            'client_id'     => 'Test Client ID',
+            'scope'         => 'openid email',
+            'state'         => 'test',
+            'nonce'         => 'test',
+        ));
+
+        $userId = null;
+        $server->handleAuthorizeRequest($request, $response = new Response(), true, $userId);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $this->assertNotContains('error', $location);
+
+        $parts = parse_url($location);
+        $this->assertArrayHasKey('query', $parts);
+
+        // assert fragment is in "application/x-www-form-urlencoded" format
+        parse_str($parts['query'], $params);
+        $this->assertNotNull($params);
+        $this->assertArrayHasKey('id_token', $params);
+        $this->assertArrayHasKey('code', $params);
+
+        // validate ID Token
+        $parts = explode('.', $params['id_token']);
+        foreach ($parts as &$part) {
+            // Each part is a base64url encoded json string.
+            $part = str_replace(array('-', '_'), array('+', '/'), $part);
+            $part = base64_decode($part);
+            $part = json_decode($part, true);
+        }
+        list($header, $claims, $signature) = $parts;
+
+        $this->assertArrayNotHasKey('email', $claims);
+        $this->assertArrayNotHasKey('email_verified', $claims);
+    }
+
+    private function getTestServer($config = array())
+    {
+        $config += array(
+            'use_openid_connect' => true,
+            'issuer' => 'test',
+            'id_lifetime' => 3600,
+            'allow_implicit' => true,
+        );
+
+        $memoryStorage = Bootstrap::getInstance()->getMemoryStorage();
+        $memoryStorage->supportedScopes[] = 'email';
+        $responseTypes = array(
+            'code'     => $code    = new AuthorizationCode($memoryStorage),
+            'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $config),
+            'code id_token' => new CodeIdToken($code, $idToken),
+        );
+
+        $server = new Server($memoryStorage, $config, array(), $responseTypes);
+        $server->addGrantType(new ClientCredentials($memoryStorage));
+
+        return $server;
+    }
+}

+ 185 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTest.php

@@ -0,0 +1,185 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+use OAuth2\Storage\Bootstrap;
+use OAuth2\GrantType\ClientCredentials;
+use OAuth2\Encryption\Jwt;
+use PHPUnit\Framework\TestCase;
+
+class IdTokenTest extends TestCase
+{
+    public function testValidateAuthorizeRequest()
+    {
+        $query = array(
+            'response_type' => 'id_token',
+            'redirect_uri'  => 'http://adobe.com',
+            'client_id'     => 'Test Client ID',
+            'scope'         => 'openid',
+            'state'         => 'test',
+        );
+
+        // attempt to do the request without a nonce.
+        $server = $this->getTestServer(array('allow_implicit' => true));
+        $request = new Request($query);
+        $valid = $server->validateAuthorizeRequest($request, $response = new Response());
+
+        // Add a nonce and retry.
+        $query['nonce'] = 'test';
+        $request = new Request($query);
+        $valid = $server->validateAuthorizeRequest($request, $response = new Response());
+        $this->assertTrue($valid);
+    }
+
+    public function testHandleAuthorizeRequest()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer(array('allow_implicit' => true));
+        $request = new Request(array(
+            'response_type' => 'id_token',
+            'redirect_uri'  => 'http://adobe.com',
+            'client_id'     => 'Test Client ID',
+            'scope'         => 'openid email',
+            'state'         => 'test',
+            'nonce'         => 'test',
+        ));
+
+        $user_id = 'testuser';
+        $server->handleAuthorizeRequest($request, $response = new Response(), true, $user_id);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $this->assertNotContains('error', $location);
+
+        $parts = parse_url($location);
+        $this->assertArrayHasKey('fragment', $parts);
+        $this->assertFalse(isset($parts['query']));
+
+        // assert fragment is in "application/x-www-form-urlencoded" format
+        parse_str($parts['fragment'], $params);
+        $this->assertNotNull($params);
+        $this->assertArrayHasKey('id_token', $params);
+        $this->assertArrayNotHasKey('access_token', $params);
+        $this->validateIdToken($params['id_token']);
+    }
+
+    public function testPassInAuthTime()
+    {
+        $server = $this->getTestServer(array('allow_implicit' => true));
+        $request = new Request(array(
+            'response_type' => 'id_token',
+            'redirect_uri'  => 'http://adobe.com',
+            'client_id'     => 'Test Client ID',
+            'scope'         => 'openid email',
+            'state'         => 'test',
+            'nonce'         => 'test',
+        ));
+
+        // test with a scalar user id
+        $user_id = 'testuser123';
+        $server->handleAuthorizeRequest($request, $response = new Response(), true, $user_id);
+
+        list($header, $payload, $signature) = $this->extractTokenDataFromResponse($response);
+
+        $this->assertTrue(is_array($payload));
+        $this->assertArrayHasKey('sub', $payload);
+        $this->assertEquals($user_id, $payload['sub']);
+        $this->assertArrayHasKey('auth_time', $payload);
+
+        // test with an array of user info
+        $userInfo = array(
+            'user_id' => 'testuser1234',
+            'auth_time' => date('Y-m-d H:i:s', strtotime('20 minutes ago')
+        ));
+
+        $server->handleAuthorizeRequest($request, $response = new Response(), true, $userInfo);
+
+        list($header, $payload, $signature) = $this->extractTokenDataFromResponse($response);
+
+        $this->assertTrue(is_array($payload));
+        $this->assertArrayHasKey('sub', $payload);
+        $this->assertEquals($userInfo['user_id'], $payload['sub']);
+        $this->assertArrayHasKey('auth_time', $payload);
+        $this->assertEquals($userInfo['auth_time'], $payload['auth_time']);
+    }
+
+    private function extractTokenDataFromResponse(Response $response)
+    {
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $this->assertNotContains('error', $location);
+
+        $parts = parse_url($location);
+        $this->assertArrayHasKey('fragment', $parts);
+        $this->assertFalse(isset($parts['query']));
+
+        parse_str($parts['fragment'], $params);
+        $this->assertNotNull($params);
+        $this->assertArrayHasKey('id_token', $params);
+        $this->assertArrayNotHasKey('access_token', $params);
+
+        list($headb64, $payloadb64, $signature) = explode('.', $params['id_token']);
+
+        $jwt = new Jwt();
+        $header = json_decode($jwt->urlSafeB64Decode($headb64), true);
+        $payload = json_decode($jwt->urlSafeB64Decode($payloadb64), true);
+
+        return array($header, $payload, $signature);
+    }
+
+    private function validateIdToken($id_token)
+    {
+        $parts = explode('.', $id_token);
+        foreach ($parts as &$part) {
+            // Each part is a base64url encoded json string.
+            $part = str_replace(array('-', '_'), array('+', '/'), $part);
+            $part = base64_decode($part);
+            $part = json_decode($part, true);
+        }
+        list($header, $claims, $signature) = $parts;
+
+        $this->assertArrayHasKey('iss', $claims);
+        $this->assertArrayHasKey('sub', $claims);
+        $this->assertArrayHasKey('aud', $claims);
+        $this->assertArrayHasKey('iat', $claims);
+        $this->assertArrayHasKey('exp', $claims);
+        $this->assertArrayHasKey('auth_time', $claims);
+        $this->assertArrayHasKey('nonce', $claims);
+        $this->assertArrayHasKey('email', $claims);
+        $this->assertArrayHasKey('email_verified', $claims);
+
+        $this->assertEquals($claims['iss'], 'test');
+        $this->assertEquals($claims['aud'], 'Test Client ID');
+        $this->assertEquals($claims['nonce'], 'test');
+        $this->assertEquals($claims['email'], 'testuser@test.com');
+        $duration = $claims['exp'] - $claims['iat'];
+        $this->assertEquals($duration, 3600);
+    }
+
+    private function getTestServer($config = array())
+    {
+        $config += array(
+            'use_openid_connect' => true,
+            'issuer' => 'test',
+            'id_lifetime' => 3600,
+        );
+
+        $memoryStorage = Bootstrap::getInstance()->getMemoryStorage();
+        $memoryStorage->supportedScopes[] = 'email';
+        $storage = array(
+            'client' => $memoryStorage,
+            'scope' => $memoryStorage,
+        );
+        $responseTypes = array(
+            'id_token' => new IdToken($memoryStorage, $memoryStorage, $config),
+        );
+
+        $server = new Server($storage, $config, array(), $responseTypes);
+        $server->addGrantType(new ClientCredentials($memoryStorage));
+
+        return $server;
+    }
+}

+ 92 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php

@@ -0,0 +1,92 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+use OAuth2\Storage\Bootstrap;
+use OAuth2\GrantType\ClientCredentials;
+use OAuth2\ResponseType\AccessToken;
+use PHPUnit\Framework\TestCase;
+
+class IdTokenTokenTest extends TestCase
+{
+
+    public function testHandleAuthorizeRequest()
+    {
+        // add the test parameters in memory
+        $server = $this->getTestServer(array('allow_implicit' => true));
+
+        $request = new Request(array(
+            'response_type' => 'id_token token',
+            'redirect_uri'  => 'http://adobe.com',
+            'client_id'     => 'Test Client ID',
+            'scope'         => 'openid',
+            'state'         => 'test',
+            'nonce'         => 'test',
+        ));
+
+        $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+        $this->assertEquals($response->getStatusCode(), 302);
+        $location = $response->getHttpHeader('Location');
+        $this->assertNotContains('error', $location);
+
+        $parts = parse_url($location);
+        $this->assertArrayHasKey('fragment', $parts);
+        $this->assertFalse(isset($parts['query']));
+
+        // assert fragment is in "application/x-www-form-urlencoded" format
+        parse_str($parts['fragment'], $params);
+        $this->assertNotNull($params);
+        $this->assertArrayHasKey('id_token', $params);
+        $this->assertArrayHasKey('access_token', $params);
+
+        // validate ID Token
+        $parts = explode('.', $params['id_token']);
+        foreach ($parts as &$part) {
+            // Each part is a base64url encoded json string.
+            $part = str_replace(array('-', '_'), array('+', '/'), $part);
+            $part = base64_decode($part);
+            $part = json_decode($part, true);
+        }
+        list($header, $claims, $signature) = $parts;
+
+        $this->assertArrayHasKey('iss', $claims);
+        $this->assertArrayHasKey('sub', $claims);
+        $this->assertArrayHasKey('aud', $claims);
+        $this->assertArrayHasKey('iat', $claims);
+        $this->assertArrayHasKey('exp', $claims);
+        $this->assertArrayHasKey('auth_time', $claims);
+        $this->assertArrayHasKey('nonce', $claims);
+        $this->assertArrayHasKey('at_hash', $claims);
+
+        $this->assertEquals($claims['iss'], 'test');
+        $this->assertEquals($claims['aud'], 'Test Client ID');
+        $this->assertEquals($claims['nonce'], 'test');
+        $duration = $claims['exp'] - $claims['iat'];
+        $this->assertEquals($duration, 3600);
+    }
+
+    private function getTestServer($config = array())
+    {
+        $config += array(
+            'use_openid_connect' => true,
+            'issuer' => 'test',
+            'id_lifetime' => 3600,
+        );
+
+        $memoryStorage = Bootstrap::getInstance()->getMemoryStorage();
+        $responseTypes = array(
+            'token'     => $token   = new AccessToken($memoryStorage, $memoryStorage),
+            'id_token'  => $idToken = new IdToken($memoryStorage, $memoryStorage, $config),
+            'id_token token' => new IdTokenToken($token, $idToken),
+        );
+
+        $server = new Server($memoryStorage, $config, array(), $responseTypes);
+        $server->addGrantType(new ClientCredentials($memoryStorage));
+
+        return $server;
+    }
+}

+ 95 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace OAuth2\OpenID\Storage;
+
+use OAuth2\Storage\BaseTest;
+use OAuth2\Storage\NullStorage;
+
+class AuthorizationCodeTest extends BaseTest
+{
+    /** @dataProvider provideStorage */
+    public function testCreateAuthorizationCode($storage)
+    {
+        if ($storage instanceof NullStorage) {
+            $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+            return;
+        }
+
+        if (!$storage instanceof AuthorizationCodeInterface) {
+            return;
+        }
+
+        // assert code we are about to add does not exist
+        $code = $storage->getAuthorizationCode('new-openid-code');
+        $this->assertFalse($code);
+
+        // add new code
+        $expires = time() + 20;
+        $scope = null;
+        $id_token = 'fake_id_token';
+        $success = $storage->setAuthorizationCode('new-openid-code', 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, $id_token);
+        $this->assertTrue($success);
+
+        $code = $storage->getAuthorizationCode('new-openid-code');
+        $this->assertNotNull($code);
+        $this->assertArrayHasKey('authorization_code', $code);
+        $this->assertArrayHasKey('client_id', $code);
+        $this->assertArrayHasKey('user_id', $code);
+        $this->assertArrayHasKey('redirect_uri', $code);
+        $this->assertArrayHasKey('expires', $code);
+        $this->assertEquals($code['authorization_code'], 'new-openid-code');
+        $this->assertEquals($code['client_id'], 'client ID');
+        $this->assertEquals($code['user_id'], 'SOMEUSERID');
+        $this->assertEquals($code['redirect_uri'], 'http://example.com');
+        $this->assertEquals($code['expires'], $expires);
+        $this->assertEquals($code['id_token'], $id_token);
+
+        // change existing code
+        $expires = time() + 42;
+        $new_id_token = 'fake_id_token-2';
+        $success = $storage->setAuthorizationCode('new-openid-code', 'client ID2', 'SOMEOTHERID', 'http://example.org', $expires, $scope, $new_id_token);
+        $this->assertTrue($success);
+
+        $code = $storage->getAuthorizationCode('new-openid-code');
+        $this->assertNotNull($code);
+        $this->assertArrayHasKey('authorization_code', $code);
+        $this->assertArrayHasKey('client_id', $code);
+        $this->assertArrayHasKey('user_id', $code);
+        $this->assertArrayHasKey('redirect_uri', $code);
+        $this->assertArrayHasKey('expires', $code);
+        $this->assertEquals($code['authorization_code'], 'new-openid-code');
+        $this->assertEquals($code['client_id'], 'client ID2');
+        $this->assertEquals($code['user_id'], 'SOMEOTHERID');
+        $this->assertEquals($code['redirect_uri'], 'http://example.org');
+        $this->assertEquals($code['expires'], $expires);
+        $this->assertEquals($code['id_token'], $new_id_token);
+    }
+
+        /** @dataProvider provideStorage */
+    public function testRemoveIdTokenFromAuthorizationCode($storage)
+    {
+        // add new code
+        $expires = time() + 20;
+        $scope = null;
+        $id_token = 'fake_id_token_to_remove';
+        $authcode = 'new-openid-code-'.rand();
+        $success = $storage->setAuthorizationCode($authcode, 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, $id_token);
+        $this->assertTrue($success);
+
+        // verify params were set
+        $code = $storage->getAuthorizationCode($authcode);
+        $this->assertNotNull($code);
+        $this->assertArrayHasKey('id_token', $code);
+        $this->assertEquals($code['id_token'], $id_token);
+
+        // remove the id_token
+        $success = $storage->setAuthorizationCode($authcode, 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, null);
+
+        // verify the "id_token" is now null
+        $code = $storage->getAuthorizationCode($authcode);
+        $this->assertNotNull($code);
+        $this->assertArrayHasKey('id_token', $code);
+        $this->assertEquals($code['id_token'], null);
+    }
+}

+ 41 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/OpenID/Storage/UserClaimsTest.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace OAuth2\OpenID\Storage;
+
+use OAuth2\Storage\BaseTest;
+use OAuth2\Storage\NullStorage;
+
+class UserClaimsTest extends BaseTest
+{
+    /** @dataProvider provideStorage */
+    public function testGetUserClaims($storage)
+    {
+        if ($storage instanceof NullStorage) {
+            $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+            return;
+        }
+
+        if (!$storage instanceof UserClaimsInterface) {
+            // incompatible storage
+            return;
+        }
+
+        // invalid user
+        $claims = $storage->getUserClaims('fake-user', '');
+        $this->assertFalse($claims);
+
+        // valid user (no scope)
+        $claims = $storage->getUserClaims('testuser', '');
+
+        /* assert the decoded token is the same */
+        $this->assertFalse(isset($claims['email']));
+
+        // valid user
+        $claims = $storage->getUserClaims('testuser', 'email');
+
+        /* assert the decoded token is the same */
+        $this->assertEquals($claims['email'], "testuser@test.com");
+        $this->assertEquals($claims['email_verified'], true);
+    }
+}

+ 117 - 0
data/web/inc/lib/vendor/bshaffer/oauth2-server-php/test/OAuth2/RequestTest.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace OAuth2;
+
+use OAuth2\Request\TestRequest;
+use OAuth2\Storage\Bootstrap;
+use OAuth2\GrantType\AuthorizationCode;
+use PHPUnit\Framework\TestCase;
+
+class RequestTest extends TestCase
+{
+    public function testRequestOverride()
+    {
+        $request = new TestRequest();
+        $server = $this->getTestServer();
+
+        // Smoke test for override request class
+        // $server->handleTokenRequest($request, $response = new Response());
+        // $this->assertInstanceOf('Response', $response);
+        // $server->handleAuthorizeRequest($request, $response = new Response(), true);
+        // $this->assertInstanceOf('Response', $response);
+        // $response = $server->verifyResourceRequest($request, $response = new Response());
+        // $this->assertTrue(is_bool($response));
+
+        /*** make some valid requests ***/
+
+        // Valid Token Request
+        $request->setPost(array(
+            'grant_type' => 'authorization_code',
+            'client_id'  => 'Test Client ID',
+            'client_secret' => 'TestSecret',
+            'code' => 'testcode',
+        ));
+        $server->handleTokenRequest($request, $response = new Response());
+        $this->assertEquals($response->getStatusCode(), 200);
+        $this->assertNull($response->getParameter('error'));
+        $this->assertNotNUll($response->getParameter('access_token'));
+    }
+
+    public function testHeadersReturnsValueByKey()
+    {
+        $request = new Request(
+            array(),
+            array(),
+            array(),
+            array(),
+            array(),
+            array(),
+            array(),
+            array('AUTHORIZATION' => 'Basic secret')
+        );
+
+        $this->assertEquals('Basic secret', $request->headers('AUTHORIZATION'));
+    }
+
+    public function testHeadersReturnsDefaultIfHeaderNotPresent()
+    {
+        $request = new Request();
+
+        $this->assertEquals('Bearer', $request->headers('AUTHORIZATION', 'Bearer'));
+    }
+
+    public function testHeadersIsCaseInsensitive()
+    {
+        $request = new Request(
+            array(),
+            array(),
+            array(),
+            array(),
+            array(),
+            array(),
+            array(),
+            array('AUTHORIZATION' => 'Basic secret')
+        );
+
+        $this->assertEquals('Basic secret', $request->headers('Authorization'));
+    }
+
+    public function testRequestReturnsPostParamIfNoQueryParamAvailable()
+    {
+        $request = new Request(
+            array(),
+            array('client_id' => 'correct')
+        );
+
+        $this->assertEquals('correct', $request->query('client_id', $request->request('client_id')));
+    }
+
+    public function testRequestHasHeadersAndServerHeaders()
+    {
+        $request = new Request(
+            array(),
+            array(),
+            array(),
+            array(),
+            array(),
+            array('CONTENT_TYPE' => 'text/xml', 'PHP_AUTH_USER' => 'client_id', 'PHP_AUTH_PW' => 'client_pass'),
+            null,
+            array('CONTENT_TYPE' => 'application/json')
+        );
+
+        $this->assertSame('client_id', $request->headers('PHP_AUTH_USER'));
+        $this->assertSame('client_pass', $request->headers('PHP_AUTH_PW'));
+        $this->assertSame('application/json', $request->headers('CONTENT_TYPE'));
+    }
+
+    private function getTestServer($config = array())
+    {
+        $storage = Bootstrap::getInstance()->getMemoryStorage();
+        $server = new Server($storage, $config);
+
+        // Add the two types supported for authorization grant
+        $server->addGrantType(new AuthorizationCode($storage));
+
+        return $server;
+    }
+}

Some files were not shown because too many files changed in this diff