aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--library/oauth2/.gitignore5
-rw-r--r--library/oauth2/.travis.yml30
-rw-r--r--library/oauth2/CHANGELOG.md152
-rw-r--r--library/oauth2/LICENSE21
-rw-r--r--library/oauth2/README.md8
-rw-r--r--library/oauth2/phpunit.xml25
-rw-r--r--library/oauth2/src/OAuth2/Autoloader.php48
-rw-r--r--library/oauth2/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php15
-rw-r--r--library/oauth2/src/OAuth2/ClientAssertionType/HttpBasic.php123
-rw-r--r--library/oauth2/src/OAuth2/Controller/AuthorizeController.php388
-rw-r--r--library/oauth2/src/OAuth2/Controller/AuthorizeControllerInterface.php43
-rw-r--r--library/oauth2/src/OAuth2/Controller/ResourceController.php111
-rw-r--r--library/oauth2/src/OAuth2/Controller/ResourceControllerInterface.php26
-rw-r--r--library/oauth2/src/OAuth2/Controller/TokenController.php278
-rw-r--r--library/oauth2/src/OAuth2/Controller/TokenControllerInterface.php32
-rw-r--r--library/oauth2/src/OAuth2/Encryption/EncryptionInterface.php11
-rw-r--r--library/oauth2/src/OAuth2/Encryption/FirebaseJwt.php47
-rw-r--r--library/oauth2/src/OAuth2/Encryption/Jwt.php173
-rw-r--r--library/oauth2/src/OAuth2/GrantType/AuthorizationCode.php100
-rw-r--r--library/oauth2/src/OAuth2/GrantType/ClientCredentials.php67
-rw-r--r--library/oauth2/src/OAuth2/GrantType/GrantTypeInterface.php20
-rw-r--r--library/oauth2/src/OAuth2/GrantType/JwtBearer.php226
-rw-r--r--library/oauth2/src/OAuth2/GrantType/RefreshToken.php111
-rw-r--r--library/oauth2/src/OAuth2/GrantType/UserCredentials.php83
-rw-r--r--library/oauth2/src/OAuth2/OpenID/Controller/AuthorizeController.php106
-rw-r--r--library/oauth2/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php10
-rw-r--r--library/oauth2/src/OAuth2/OpenID/Controller/UserInfoController.php58
-rw-r--r--library/oauth2/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php23
-rw-r--r--library/oauth2/src/OAuth2/OpenID/GrantType/AuthorizationCode.php33
-rw-r--r--library/oauth2/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php60
-rw-r--r--library/oauth2/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php27
-rw-r--r--library/oauth2/src/OAuth2/OpenID/ResponseType/CodeIdToken.php24
-rw-r--r--library/oauth2/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php9
-rw-r--r--library/oauth2/src/OAuth2/OpenID/ResponseType/IdToken.php124
-rw-r--r--library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php29
-rw-r--r--library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenToken.php27
-rw-r--r--library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php9
-rw-r--r--library/oauth2/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php37
-rw-r--r--library/oauth2/src/OAuth2/OpenID/Storage/UserClaimsInterface.php38
-rw-r--r--library/oauth2/src/OAuth2/Request.php213
-rw-r--r--library/oauth2/src/OAuth2/RequestInterface.php16
-rw-r--r--library/oauth2/src/OAuth2/Response.php369
-rw-r--r--library/oauth2/src/OAuth2/ResponseInterface.php24
-rw-r--r--library/oauth2/src/OAuth2/ResponseType/AccessToken.php194
-rw-r--r--library/oauth2/src/OAuth2/ResponseType/AccessTokenInterface.php34
-rw-r--r--library/oauth2/src/OAuth2/ResponseType/AuthorizationCode.php100
-rw-r--r--library/oauth2/src/OAuth2/ResponseType/AuthorizationCodeInterface.php30
-rw-r--r--library/oauth2/src/OAuth2/ResponseType/JwtAccessToken.php124
-rw-r--r--library/oauth2/src/OAuth2/ResponseType/ResponseTypeInterface.php8
-rw-r--r--library/oauth2/src/OAuth2/Scope.php103
-rw-r--r--library/oauth2/src/OAuth2/ScopeInterface.php40
-rw-r--r--library/oauth2/src/OAuth2/Server.php832
-rw-r--r--library/oauth2/src/OAuth2/Storage/AccessTokenInterface.php64
-rw-r--r--library/oauth2/src/OAuth2/Storage/AuthorizationCodeInterface.php86
-rw-r--r--library/oauth2/src/OAuth2/Storage/Cassandra.php480
-rw-r--r--library/oauth2/src/OAuth2/Storage/ClientCredentialsInterface.php49
-rw-r--r--library/oauth2/src/OAuth2/Storage/ClientInterface.php66
-rwxr-xr-xlibrary/oauth2/src/OAuth2/Storage/CouchbaseDB.php331
-rw-r--r--library/oauth2/src/OAuth2/Storage/DynamoDB.php540
-rw-r--r--library/oauth2/src/OAuth2/Storage/JwtAccessToken.php88
-rw-r--r--library/oauth2/src/OAuth2/Storage/JwtAccessTokenInterface.php14
-rw-r--r--library/oauth2/src/OAuth2/Storage/JwtBearerInterface.php74
-rw-r--r--library/oauth2/src/OAuth2/Storage/Memory.php381
-rw-r--r--library/oauth2/src/OAuth2/Storage/Mongo.php339
-rw-r--r--library/oauth2/src/OAuth2/Storage/Pdo.php553
-rw-r--r--library/oauth2/src/OAuth2/Storage/PublicKeyInterface.php16
-rw-r--r--library/oauth2/src/OAuth2/Storage/Redis.php321
-rw-r--r--library/oauth2/src/OAuth2/Storage/RefreshTokenInterface.php82
-rw-r--r--library/oauth2/src/OAuth2/Storage/ScopeInterface.php46
-rw-r--r--library/oauth2/src/OAuth2/Storage/UserCredentialsInterface.php52
-rw-r--r--library/oauth2/src/OAuth2/TokenType/Bearer.php130
-rw-r--r--library/oauth2/src/OAuth2/TokenType/Mac.php22
-rw-r--r--library/oauth2/src/OAuth2/TokenType/TokenTypeInterface.php21
-rw-r--r--library/oauth2/test/OAuth2/AutoloadTest.php16
-rw-r--r--library/oauth2/test/OAuth2/Controller/AuthorizeControllerTest.php492
-rw-r--r--library/oauth2/test/OAuth2/Controller/ResourceControllerTest.php175
-rw-r--r--library/oauth2/test/OAuth2/Controller/TokenControllerTest.php289
-rw-r--r--library/oauth2/test/OAuth2/Encryption/FirebaseJwtTest.php102
-rw-r--r--library/oauth2/test/OAuth2/Encryption/JwtTest.php102
-rw-r--r--library/oauth2/test/OAuth2/GrantType/AuthorizationCodeTest.php207
-rw-r--r--library/oauth2/test/OAuth2/GrantType/ClientCredentialsTest.php159
-rw-r--r--library/oauth2/test/OAuth2/GrantType/ImplicitTest.php143
-rw-r--r--library/oauth2/test/OAuth2/GrantType/JwtBearerTest.php360
-rw-r--r--library/oauth2/test/OAuth2/GrantType/RefreshTokenTest.php204
-rw-r--r--library/oauth2/test/OAuth2/GrantType/UserCredentialsTest.php172
-rw-r--r--library/oauth2/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php182
-rw-r--r--library/oauth2/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php44
-rw-r--r--library/oauth2/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php57
-rw-r--r--library/oauth2/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php182
-rw-r--r--library/oauth2/test/OAuth2/OpenID/ResponseType/IdTokenTest.php184
-rw-r--r--library/oauth2/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php91
-rw-r--r--library/oauth2/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php95
-rw-r--r--library/oauth2/test/OAuth2/OpenID/Storage/UserClaimsTest.php41
-rw-r--r--library/oauth2/test/OAuth2/RequestTest.php98
-rw-r--r--library/oauth2/test/OAuth2/ResponseTest.php17
-rw-r--r--library/oauth2/test/OAuth2/ResponseType/AccessTokenTest.php107
-rw-r--r--library/oauth2/test/OAuth2/ResponseType/JwtAccessTokenTest.php160
-rw-r--r--library/oauth2/test/OAuth2/ScopeTest.php42
-rw-r--r--library/oauth2/test/OAuth2/ServerTest.php684
-rw-r--r--library/oauth2/test/OAuth2/Storage/AccessTokenTest.php102
-rw-r--r--library/oauth2/test/OAuth2/Storage/AuthorizationCodeTest.php106
-rw-r--r--library/oauth2/test/OAuth2/Storage/ClientCredentialsTest.php28
-rw-r--r--library/oauth2/test/OAuth2/Storage/ClientTest.php110
-rw-r--r--library/oauth2/test/OAuth2/Storage/DynamoDBTest.php40
-rw-r--r--library/oauth2/test/OAuth2/Storage/JwtAccessTokenTest.php41
-rw-r--r--library/oauth2/test/OAuth2/Storage/JwtBearerTest.php25
-rw-r--r--library/oauth2/test/OAuth2/Storage/PdoTest.php39
-rw-r--r--library/oauth2/test/OAuth2/Storage/PublicKeyTest.php29
-rw-r--r--library/oauth2/test/OAuth2/Storage/RefreshTokenTest.php41
-rw-r--r--library/oauth2/test/OAuth2/Storage/ScopeTest.php53
-rw-r--r--library/oauth2/test/OAuth2/Storage/UserCredentialsTest.php40
-rw-r--r--library/oauth2/test/OAuth2/TokenType/BearerTest.php58
-rw-r--r--library/oauth2/test/bootstrap.php12
-rw-r--r--library/oauth2/test/cleanup.php15
-rw-r--r--library/oauth2/test/config/keys/id_rsa15
-rw-r--r--library/oauth2/test/config/keys/id_rsa.pub16
-rw-r--r--library/oauth2/test/config/storage.json181
-rw-r--r--library/oauth2/test/lib/OAuth2/Request/TestRequest.php61
-rwxr-xr-xlibrary/oauth2/test/lib/OAuth2/Storage/BaseTest.php34
-rwxr-xr-xlibrary/oauth2/test/lib/OAuth2/Storage/Bootstrap.php888
-rw-r--r--library/oauth2/test/lib/OAuth2/Storage/NullStorage.php32
121 files changed, 14970 insertions, 0 deletions
diff --git a/library/oauth2/.gitignore b/library/oauth2/.gitignore
new file mode 100644
index 000000000..c43a667d4
--- /dev/null
+++ b/library/oauth2/.gitignore
@@ -0,0 +1,5 @@
+# Test Files #
+test/config/test.sqlite
+vendor
+composer.lock
+.idea
diff --git a/library/oauth2/.travis.yml b/library/oauth2/.travis.yml
new file mode 100644
index 000000000..dd4aae4a6
--- /dev/null
+++ b/library/oauth2/.travis.yml
@@ -0,0 +1,30 @@
+language: php
+sudo: false
+cache:
+ directories:
+ - $HOME/.composer/cache
+ - vendor
+php:
+- 5.3
+- 5.4
+- 5.5
+- 5.6
+- 7
+- hhvm
+env:
+ global:
+ - SKIP_MONGO_TESTS=1
+ - secure: Bc5ZqvZ1YYpoPZNNuU2eCB8DS6vBYrAdfBtTenBs5NSxzb+Vjven4kWakbzaMvZjb/Ib7Uph7DGuOtJXpmxnvBXPLd707LZ89oFWN/yqQlZKCcm8iErvJCB5XL+/ONHj2iPdR242HJweMcat6bMCwbVWoNDidjtWMH0U2mYFy3M=
+ - secure: R3bXlymyFiY2k2jf7+fv/J8i34wtXTkmD4mCr5Ps/U+vn9axm2VtvR2Nj+r7LbRjn61gzFE/xIVjYft/wOyBOYwysrfriydrnRVS0owh6y+7EyOyQWbRX11vVQMf8o31QCQE5BY58V5AJZW3MjoOL0FVlTgySJiJvdw6Pv18v+E=
+services:
+- mongodb
+- redis-server
+- cassandra
+before_install:
+- phpenv config-rm xdebug.ini || return 0
+install:
+- composer install --no-interaction
+before_script:
+- psql -c 'create database oauth2_server_php;' -U postgres
+after_script:
+- php test/cleanup.php
diff --git a/library/oauth2/CHANGELOG.md b/library/oauth2/CHANGELOG.md
new file mode 100644
index 000000000..03d925e06
--- /dev/null
+++ b/library/oauth2/CHANGELOG.md
@@ -0,0 +1,152 @@
+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.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.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
diff --git a/library/oauth2/LICENSE b/library/oauth2/LICENSE
new file mode 100644
index 000000000..d7ece8467
--- /dev/null
+++ b/library/oauth2/LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2014 Brent Shaffer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/library/oauth2/README.md b/library/oauth2/README.md
new file mode 100644
index 000000000..4ceda6cf9
--- /dev/null
+++ b/library/oauth2/README.md
@@ -0,0 +1,8 @@
+oauth2-server-php
+=================
+
+[![Build Status](https://travis-ci.org/bshaffer/oauth2-server-php.svg?branch=develop)](https://travis-ci.org/bshaffer/oauth2-server-php)
+
+[![Total Downloads](https://poser.pugx.org/bshaffer/oauth2-server-php/downloads.png)](https://packagist.org/packages/bshaffer/oauth2-server-php)
+
+View the [complete documentation](http://bshaffer.github.io/oauth2-server-php-docs/) \ No newline at end of file
diff --git a/library/oauth2/phpunit.xml b/library/oauth2/phpunit.xml
new file mode 100644
index 000000000..e36403f0a
--- /dev/null
+++ b/library/oauth2/phpunit.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit backupGlobals="false"
+ backupStaticAttributes="false"
+ colors="true"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ processIsolation="false"
+ stopOnFailure="false"
+ syntaxCheck="false"
+ bootstrap="test/bootstrap.php"
+>
+ <testsuites>
+ <testsuite name="Oauth2 Test Suite">
+ <directory>./test/OAuth2/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory suffix=".php">./src/OAuth2/</directory>
+ </whitelist>
+ </filter>
+</phpunit>
diff --git a/library/oauth2/src/OAuth2/Autoloader.php b/library/oauth2/src/OAuth2/Autoloader.php
new file mode 100644
index 000000000..ecfb6ba75
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Autoloader.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace OAuth2;
+
+/**
+ * Autoloads OAuth2 classes
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ * @license MIT License
+ */
+class Autoloader
+{
+ private $dir;
+
+ public function __construct($dir = null)
+ {
+ if (is_null($dir)) {
+ $dir = dirname(__FILE__).'/..';
+ }
+ $this->dir = $dir;
+ }
+ /**
+ * Registers OAuth2\Autoloader as an SPL autoloader.
+ */
+ public static function register($dir = null)
+ {
+ ini_set('unserialize_callback_func', 'spl_autoload_call');
+ spl_autoload_register(array(new self($dir), 'autoload'));
+ }
+
+ /**
+ * Handles autoloading of classes.
+ *
+ * @param string $class A class name.
+ *
+ * @return boolean Returns true if the class has been loaded
+ */
+ public function autoload($class)
+ {
+ if (0 !== strpos($class, 'OAuth2')) {
+ return;
+ }
+
+ if (file_exists($file = $this->dir.'/'.str_replace('\\', '/', $class).'.php')) {
+ require $file;
+ }
+ }
+}
diff --git a/library/oauth2/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php b/library/oauth2/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php
new file mode 100644
index 000000000..29c7171b5
--- /dev/null
+++ b/library/oauth2/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace OAuth2\ClientAssertionType;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * Interface for all OAuth2 Client Assertion Types
+ */
+interface ClientAssertionTypeInterface
+{
+ public function validateRequest(RequestInterface $request, ResponseInterface $response);
+ public function getClientId();
+}
diff --git a/library/oauth2/src/OAuth2/ClientAssertionType/HttpBasic.php b/library/oauth2/src/OAuth2/ClientAssertionType/HttpBasic.php
new file mode 100644
index 000000000..0ecb7e18d
--- /dev/null
+++ b/library/oauth2/src/OAuth2/ClientAssertionType/HttpBasic.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace OAuth2\ClientAssertionType;
+
+use OAuth2\Storage\ClientCredentialsInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * Validate a client via Http Basic authentication
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class HttpBasic implements ClientAssertionTypeInterface
+{
+ private $clientData;
+
+ protected $storage;
+ protected $config;
+
+ /**
+ * @param OAuth2\Storage\ClientCredentialsInterface $clientStorage REQUIRED Storage class for retrieving client credentials information
+ * @param array $config OPTIONAL Configuration options for the server
+ * <code>
+ * $config = array(
+ * 'allow_credentials_in_request_body' => true, // whether to look for credentials in the POST body in addition to the Authorize HTTP Header
+ * 'allow_public_clients' => true // if true, "public clients" (clients without a secret) may be authenticated
+ * );
+ * </code>
+ */
+ public function __construct(ClientCredentialsInterface $storage, array $config = array())
+ {
+ $this->storage = $storage;
+ $this->config = array_merge(array(
+ 'allow_credentials_in_request_body' => true,
+ 'allow_public_clients' => true,
+ ), $config);
+ }
+
+ public function validateRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!$clientData = $this->getClientCredentials($request, $response)) {
+ return false;
+ }
+
+ if (!isset($clientData['client_id'])) {
+ throw new \LogicException('the clientData array must have "client_id" set');
+ }
+
+ if (!isset($clientData['client_secret']) || $clientData['client_secret'] == '') {
+ if (!$this->config['allow_public_clients']) {
+ $response->setError(400, 'invalid_client', 'client credentials are required');
+
+ return false;
+ }
+
+ if (!$this->storage->isPublicClient($clientData['client_id'])) {
+ $response->setError(400, 'invalid_client', 'This client is invalid or must authenticate using a client secret');
+
+ return false;
+ }
+ } elseif ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) {
+ $response->setError(400, 'invalid_client', 'The client credentials are invalid');
+
+ return false;
+ }
+
+ $this->clientData = $clientData;
+
+ return true;
+ }
+
+ public function getClientId()
+ {
+ return $this->clientData['client_id'];
+ }
+
+ /**
+ * Internal function used to get the client credentials from HTTP basic
+ * auth or POST data.
+ *
+ * According to the spec (draft 20), the client_id can be provided in
+ * the Basic Authorization header (recommended) or via GET/POST.
+ *
+ * @return
+ * A list containing the client identifier and password, for example
+ * @code
+ * return array(
+ * "client_id" => CLIENT_ID, // REQUIRED the client id
+ * "client_secret" => CLIENT_SECRET, // OPTIONAL the client secret (may be omitted for public clients)
+ * );
+ * @endcode
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-2.3.1
+ *
+ * @ingroup oauth2_section_2
+ */
+ public function getClientCredentials(RequestInterface $request, ResponseInterface $response = null)
+ {
+ if (!is_null($request->headers('PHP_AUTH_USER')) && !is_null($request->headers('PHP_AUTH_PW'))) {
+ return array('client_id' => $request->headers('PHP_AUTH_USER'), 'client_secret' => $request->headers('PHP_AUTH_PW'));
+ }
+
+ if ($this->config['allow_credentials_in_request_body']) {
+ // Using POST for HttpBasic authorization is not recommended, but is supported by specification
+ if (!is_null($request->request('client_id'))) {
+ /**
+ * client_secret can be null if the client's password is an empty string
+ * @see http://tools.ietf.org/html/rfc6749#section-2.3.1
+ */
+
+ return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret'));
+ }
+ }
+
+ if ($response) {
+ $message = $this->config['allow_credentials_in_request_body'] ? ' or body' : '';
+ $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers'.$message);
+ }
+
+ return null;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Controller/AuthorizeController.php b/library/oauth2/src/OAuth2/Controller/AuthorizeController.php
new file mode 100644
index 000000000..a9a722587
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/AuthorizeController.php
@@ -0,0 +1,388 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\Storage\ClientInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use OAuth2\Scope;
+
+/**
+ * @see OAuth2\Controller\AuthorizeControllerInterface
+ */
+class AuthorizeController implements AuthorizeControllerInterface
+{
+ private $scope;
+ private $state;
+ private $client_id;
+ private $redirect_uri;
+ private $response_type;
+
+ protected $clientStorage;
+ protected $responseTypes;
+ protected $config;
+ protected $scopeUtil;
+
+ /**
+ * @param OAuth2\Storage\ClientInterface $clientStorage REQUIRED Instance of OAuth2\Storage\ClientInterface to retrieve client information
+ * @param array $responseTypes OPTIONAL Array of OAuth2\ResponseType\ResponseTypeInterface objects. Valid array
+ * keys are "code" and "token"
+ * @param array $config OPTIONAL Configuration options for the server
+ * <code>
+ * $config = array(
+ * 'allow_implicit' => false, // if the controller should allow the "implicit" grant type
+ * 'enforce_state' => true // if the controller should require the "state" parameter
+ * 'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter
+ * 'redirect_status_code' => 302, // HTTP status code to use for redirect responses
+ * );
+ * </code>
+ * @param OAuth2\ScopeInterface $scopeUtil OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope
+ */
+ public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ScopeInterface $scopeUtil = null)
+ {
+ $this->clientStorage = $clientStorage;
+ $this->responseTypes = $responseTypes;
+ $this->config = array_merge(array(
+ 'allow_implicit' => false,
+ 'enforce_state' => true,
+ 'require_exact_redirect_uri' => true,
+ 'redirect_status_code' => 302,
+ ), $config);
+
+ if (is_null($scopeUtil)) {
+ $scopeUtil = new Scope();
+ }
+ $this->scopeUtil = $scopeUtil;
+ }
+
+ public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null)
+ {
+ if (!is_bool($is_authorized)) {
+ throw new \InvalidArgumentException('Argument "is_authorized" must be a boolean. This method must know if the user has granted access to the client.');
+ }
+
+ // We repeat this, because we need to re-validate. The request could be POSTed
+ // by a 3rd-party (because we are not internally enforcing NONCEs, etc)
+ if (!$this->validateAuthorizeRequest($request, $response)) {
+ return;
+ }
+
+ // If no redirect_uri is passed in the request, use client's registered one
+ if (empty($this->redirect_uri)) {
+ $clientData = $this->clientStorage->getClientDetails($this->client_id);
+ $registered_redirect_uri = $clientData['redirect_uri'];
+ }
+
+ // the user declined access to the client's application
+ if ($is_authorized === false) {
+ $redirect_uri = $this->redirect_uri ?: $registered_redirect_uri;
+ $this->setNotAuthorizedResponse($request, $response, $redirect_uri, $user_id);
+
+ return;
+ }
+
+ // build the parameters to set in the redirect URI
+ if (!$params = $this->buildAuthorizeParameters($request, $response, $user_id)) {
+ return;
+ }
+
+ $authResult = $this->responseTypes[$this->response_type]->getAuthorizeResponse($params, $user_id);
+
+ list($redirect_uri, $uri_params) = $authResult;
+
+ if (empty($redirect_uri) && !empty($registered_redirect_uri)) {
+ $redirect_uri = $registered_redirect_uri;
+ }
+
+ $uri = $this->buildUri($redirect_uri, $uri_params);
+
+ // return redirect response
+ $response->setRedirect($this->config['redirect_status_code'], $uri);
+ }
+
+ protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null)
+ {
+ $error = 'access_denied';
+ $error_message = 'The user denied access to your application';
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->state, $error, $error_message);
+ }
+
+ /*
+ * We have made this protected so this class can be extended to add/modify
+ * these parameters
+ */
+ protected function buildAuthorizeParameters($request, $response, $user_id)
+ {
+ // @TODO: we should be explicit with this in the future
+ $params = array(
+ 'scope' => $this->scope,
+ 'state' => $this->state,
+ 'client_id' => $this->client_id,
+ 'redirect_uri' => $this->redirect_uri,
+ 'response_type' => $this->response_type,
+ );
+
+ return $params;
+ }
+
+ public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ // Make sure a valid client id was supplied (we can not redirect because we were unable to verify the URI)
+ if (!$client_id = $request->query('client_id', $request->request('client_id'))) {
+ // We don't have a good URI to use
+ $response->setError(400, 'invalid_client', "No client id supplied");
+
+ return false;
+ }
+
+ // Get client details
+ if (!$clientData = $this->clientStorage->getClientDetails($client_id)) {
+ $response->setError(400, 'invalid_client', 'The client id supplied is invalid');
+
+ return false;
+ }
+
+ $registered_redirect_uri = isset($clientData['redirect_uri']) ? $clientData['redirect_uri'] : '';
+
+ // Make sure a valid redirect_uri was supplied. If specified, it must match the clientData URI.
+ // @see http://tools.ietf.org/html/rfc6749#section-3.1.2
+ // @see http://tools.ietf.org/html/rfc6749#section-4.1.2.1
+ // @see http://tools.ietf.org/html/rfc6749#section-4.2.2.1
+ if ($supplied_redirect_uri = $request->query('redirect_uri', $request->request('redirect_uri'))) {
+ // validate there is no fragment supplied
+ $parts = parse_url($supplied_redirect_uri);
+ if (isset($parts['fragment']) && $parts['fragment']) {
+ $response->setError(400, 'invalid_uri', 'The redirect URI must not contain a fragment');
+
+ return false;
+ }
+
+ // validate against the registered redirect uri(s) if available
+ if ($registered_redirect_uri && !$this->validateRedirectUri($supplied_redirect_uri, $registered_redirect_uri)) {
+ $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI provided is missing or does not match', '#section-3.1.2');
+
+ return false;
+ }
+ $redirect_uri = $supplied_redirect_uri;
+ } else {
+ // use the registered redirect_uri if none has been supplied, if possible
+ if (!$registered_redirect_uri) {
+ $response->setError(400, 'invalid_uri', 'No redirect URI was supplied or stored');
+
+ return false;
+ }
+
+ if (count(explode(' ', $registered_redirect_uri)) > 1) {
+ $response->setError(400, 'invalid_uri', 'A redirect URI must be supplied when multiple redirect URIs are registered', '#section-3.1.2.3');
+
+ return false;
+ }
+ $redirect_uri = $registered_redirect_uri;
+ }
+
+ // Select the redirect URI
+ $response_type = $request->query('response_type', $request->request('response_type'));
+
+ // for multiple-valued response types - make them alphabetical
+ if (false !== strpos($response_type, ' ')) {
+ $types = explode(' ', $response_type);
+ sort($types);
+ $response_type = ltrim(implode(' ', $types));
+ }
+
+ $state = $request->query('state', $request->request('state'));
+
+ // type and client_id are required
+ if (!$response_type || !in_array($response_type, $this->getValidResponseTypes())) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_request', 'Invalid or missing response type', null);
+
+ return false;
+ }
+
+ if ($response_type == self::RESPONSE_TYPE_AUTHORIZATION_CODE) {
+ if (!isset($this->responseTypes['code'])) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'authorization code grant type not supported', null);
+
+ return false;
+ }
+ if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'authorization_code')) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null);
+
+ return false;
+ }
+ if ($this->responseTypes['code']->enforceRedirect() && !$redirect_uri) {
+ $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI is mandatory and was not supplied');
+
+ return false;
+ }
+ } else {
+ if (!$this->config['allow_implicit']) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'implicit grant type not supported', null);
+
+ return false;
+ }
+ if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'implicit')) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null);
+
+ return false;
+ }
+ }
+
+ // validate requested scope if it exists
+ $requestedScope = $this->scopeUtil->getScopeFromRequest($request);
+
+ if ($requestedScope) {
+ // restrict scope by client specific scope if applicable,
+ // otherwise verify the scope exists
+ $clientScope = $this->clientStorage->getClientScope($client_id);
+ if ((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 $uri An absolute URI.
+ * @param $params Parameters to be append as GET.
+ *
+ * @return
+ * An absolute URI with supplied parameters.
+ *
+ * @ingroup oauth2_section_4
+ */
+ private function buildUri($uri, $params)
+ {
+ $parse_url = parse_url($uri);
+
+ // Add our params to the parsed uri
+ foreach ($params as $k => $v) {
+ if (isset($parse_url[$k])) {
+ $parse_url[$k] .= "&" . http_build_query($v, '', '&');
+ } else {
+ $parse_url[$k] = http_build_query($v, '', '&');
+ }
+ }
+
+ // Put humpty dumpty back together
+ return
+ ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "")
+ . ((isset($parse_url["user"])) ? $parse_url["user"]
+ . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "")
+ . ((isset($parse_url["host"])) ? $parse_url["host"] : "")
+ . ((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "")
+ . ((isset($parse_url["path"])) ? $parse_url["path"] : "")
+ . ((isset($parse_url["query"]) && !empty($parse_url['query'])) ? "?" . $parse_url["query"] : "")
+ . ((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : "")
+ ;
+ }
+
+ protected function getValidResponseTypes()
+ {
+ return array(
+ self::RESPONSE_TYPE_ACCESS_TOKEN,
+ self::RESPONSE_TYPE_AUTHORIZATION_CODE,
+ );
+ }
+
+ /**
+ * Internal method for validating redirect URI supplied
+ *
+ * @param string $inputUri The submitted URI to be validated
+ * @param string $registeredUriString The allowed URI(s) to validate against. Can be a space-delimited string of URIs to
+ * allow for multiple URIs
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-3.1.2
+ */
+ protected function validateRedirectUri($inputUri, $registeredUriString)
+ {
+ if (!$inputUri || !$registeredUriString) {
+ return false; // if either one is missing, assume INVALID
+ }
+
+ $registered_uris = explode(' ', $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 methods to access the parameters derived from the validated request
+ */
+
+ public function getScope()
+ {
+ return $this->scope;
+ }
+
+ public function getState()
+ {
+ return $this->state;
+ }
+
+ public function getClientId()
+ {
+ return $this->client_id;
+ }
+
+ public function getRedirectUri()
+ {
+ return $this->redirect_uri;
+ }
+
+ public function getResponseType()
+ {
+ return $this->response_type;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Controller/AuthorizeControllerInterface.php b/library/oauth2/src/OAuth2/Controller/AuthorizeControllerInterface.php
new file mode 100644
index 000000000..fa07ae8d2
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/AuthorizeControllerInterface.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * This controller is called when a user should be authorized
+ * by an authorization server. As OAuth2 does not handle
+ * authorization directly, this controller ensures the request is valid, but
+ * requires the application to determine the value of $is_authorized
+ *
+ * ex:
+ * > $user_id = $this->somehowDetermineUserId();
+ * > $is_authorized = $this->somehowDetermineUserAuthorization();
+ * > $response = new OAuth2\Response();
+ * > $authorizeController->handleAuthorizeRequest(
+ * > OAuth2\Request::createFromGlobals(),
+ * > $response,
+ * > $is_authorized,
+ * > $user_id);
+ * > $response->send();
+ *
+ */
+interface AuthorizeControllerInterface
+{
+ /**
+ * List of possible authentication response types.
+ * The "authorization_code" mechanism exclusively supports 'code'
+ * and the "implicit" mechanism exclusively supports 'token'.
+ *
+ * @var string
+ * @see http://tools.ietf.org/html/rfc6749#section-4.1.1
+ * @see http://tools.ietf.org/html/rfc6749#section-4.2.1
+ */
+ const RESPONSE_TYPE_AUTHORIZATION_CODE = 'code';
+ const RESPONSE_TYPE_ACCESS_TOKEN = 'token';
+
+ public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null);
+
+ public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response);
+}
diff --git a/library/oauth2/src/OAuth2/Controller/ResourceController.php b/library/oauth2/src/OAuth2/Controller/ResourceController.php
new file mode 100644
index 000000000..e8588188f
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/ResourceController.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\TokenType\TokenTypeInterface;
+use OAuth2\Storage\AccessTokenInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use OAuth2\Scope;
+
+/**
+ * @see OAuth2\Controller\ResourceControllerInterface
+ */
+class ResourceController implements ResourceControllerInterface
+{
+ private $token;
+
+ protected $tokenType;
+ protected $tokenStorage;
+ protected $config;
+ protected $scopeUtil;
+
+ public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, $config = array(), ScopeInterface $scopeUtil = null)
+ {
+ $this->tokenType = $tokenType;
+ $this->tokenStorage = $tokenStorage;
+
+ $this->config = array_merge(array(
+ 'www_realm' => 'Service',
+ ), $config);
+
+ if (is_null($scopeUtil)) {
+ $scopeUtil = new Scope();
+ }
+ $this->scopeUtil = $scopeUtil;
+ }
+
+ public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null)
+ {
+ $token = $this->getAccessTokenData($request, $response);
+
+ // Check if we have token data
+ if (is_null($token)) {
+ return false;
+ }
+
+ /**
+ * Check scope, if provided
+ * If token doesn't have a scope, it's null/empty, or it's insufficient, then throw 403
+ * @see http://tools.ietf.org/html/rfc6750#section-3.1
+ */
+ if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->scopeUtil->checkScope($scope, $token["scope"]))) {
+ $response->setError(403, 'insufficient_scope', 'The request requires higher privileges than provided by the access token');
+ $response->addHttpHeaders(array(
+ 'WWW-Authenticate' => sprintf('%s realm="%s", scope="%s", error="%s", error_description="%s"',
+ $this->tokenType->getTokenType(),
+ $this->config['www_realm'],
+ $scope,
+ $response->getParameter('error'),
+ $response->getParameter('error_description')
+ )
+ ));
+
+ return false;
+ }
+
+ // allow retrieval of the token
+ $this->token = $token;
+
+ return (bool) $token;
+ }
+
+ public function getAccessTokenData(RequestInterface $request, ResponseInterface $response)
+ {
+ // Get the token parameter
+ if ($token_param = $this->tokenType->getAccessTokenParameter($request, $response)) {
+ // Get the stored token data (from the implementing subclass)
+ // Check we have a well formed token
+ // Check token expiration (expires is a mandatory paramter)
+ if (!$token = $this->tokenStorage->getAccessToken($token_param)) {
+ $response->setError(401, 'invalid_token', 'The access token provided is invalid');
+ } elseif (!isset($token["expires"]) || !isset($token["client_id"])) {
+ $response->setError(401, 'malformed_token', 'Malformed token (missing "expires")');
+ } elseif (time() > $token["expires"]) {
+ $response->setError(401, 'expired_token', 'The access token provided has expired');
+ } else {
+ return $token;
+ }
+ }
+
+ $authHeader = sprintf('%s realm="%s"', $this->tokenType->getTokenType(), $this->config['www_realm']);
+
+ if ($error = $response->getParameter('error')) {
+ $authHeader = sprintf('%s, error="%s"', $authHeader, $error);
+ if ($error_description = $response->getParameter('error_description')) {
+ $authHeader = sprintf('%s, error_description="%s"', $authHeader, $error_description);
+ }
+ }
+
+ $response->addHttpHeaders(array('WWW-Authenticate' => $authHeader));
+
+ return null;
+ }
+
+ // convenience method to allow retrieval of the token
+ public function getToken()
+ {
+ return $this->token;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Controller/ResourceControllerInterface.php b/library/oauth2/src/OAuth2/Controller/ResourceControllerInterface.php
new file mode 100644
index 000000000..611421935
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/ResourceControllerInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * This controller is called when a "resource" is requested.
+ * call verifyResourceRequest in order to determine if the request
+ * contains a valid token.
+ *
+ * ex:
+ * > if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) {
+ * > $response->send(); // authorization failed
+ * > die();
+ * > }
+ * > return json_encode($resource); // valid token! Send the stuff!
+ *
+ */
+interface ResourceControllerInterface
+{
+ public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null);
+
+ public function getAccessTokenData(RequestInterface $request, ResponseInterface $response);
+}
diff --git a/library/oauth2/src/OAuth2/Controller/TokenController.php b/library/oauth2/src/OAuth2/Controller/TokenController.php
new file mode 100644
index 000000000..42dab892f
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/TokenController.php
@@ -0,0 +1,278 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\ClientAssertionType\ClientAssertionTypeInterface;
+use OAuth2\GrantType\GrantTypeInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\Scope;
+use OAuth2\Storage\ClientInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * @see OAuth2\Controller\TokenControllerInterface
+ */
+class TokenController implements TokenControllerInterface
+{
+ protected $accessToken;
+ protected $grantTypes;
+ protected $clientAssertionType;
+ protected $scopeUtil;
+ protected $clientStorage;
+
+ public function __construct(AccessTokenInterface $accessToken, ClientInterface $clientStorage, array $grantTypes = array(), ClientAssertionTypeInterface $clientAssertionType = null, ScopeInterface $scopeUtil = null)
+ {
+ if (is_null($clientAssertionType)) {
+ foreach ($grantTypes as $grantType) {
+ if (!$grantType instanceof ClientAssertionTypeInterface) {
+ throw new \InvalidArgumentException('You must supply an instance of OAuth2\ClientAssertionType\ClientAssertionTypeInterface or only use grant types which implement OAuth2\ClientAssertionType\ClientAssertionTypeInterface');
+ }
+ }
+ }
+ $this->clientAssertionType = $clientAssertionType;
+ $this->accessToken = $accessToken;
+ $this->clientStorage = $clientStorage;
+ foreach ($grantTypes as $grantType) {
+ $this->addGrantType($grantType);
+ }
+
+ if (is_null($scopeUtil)) {
+ $scopeUtil = new Scope();
+ }
+ $this->scopeUtil = $scopeUtil;
+ }
+
+ public function handleTokenRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if ($token = $this->grantAccessToken($request, $response)) {
+ // @see http://tools.ietf.org/html/rfc6749#section-5.1
+ // server MUST disable caching in headers when tokens are involved
+ $response->setStatusCode(200);
+ $response->addParameters($token);
+ $response->addHttpHeaders(array(
+ 'Cache-Control' => 'no-store',
+ 'Pragma' => 'no-cache',
+ '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 $request - RequestInterface
+ * Request object to grant access token
+ *
+ * @throws InvalidArgumentException
+ * @throws LogicException
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-4
+ * @see http://tools.ietf.org/html/rfc6749#section-10.6
+ * @see http://tools.ietf.org/html/rfc6749#section-4.1.3
+ *
+ * @ingroup oauth2_section_4
+ */
+ public function grantAccessToken(RequestInterface $request, ResponseInterface $response)
+ {
+ if (strtolower($request->server('REQUEST_METHOD')) != 'post') {
+ $response->setError(405, 'invalid_request', 'The request method must be POST when requesting an access token', '#section-3.2');
+ $response->addHttpHeaders(array('Allow' => 'POST'));
+
+ return null;
+ }
+
+ /**
+ * Determine grant type from request
+ * and validate the request for that grant type
+ */
+ if (!$grantTypeIdentifier = $request->request('grant_type')) {
+ $response->setError(400, 'invalid_request', 'The grant type was not specified in the request');
+
+ return null;
+ }
+
+ if (!isset($this->grantTypes[$grantTypeIdentifier])) {
+ /* TODO: If this is an OAuth2 supported grant type that we have chosen not to implement, throw a 501 Not Implemented instead */
+ $response->setError(400, 'unsupported_grant_type', sprintf('Grant type "%s" not supported', $grantTypeIdentifier));
+
+ return null;
+ }
+
+ $grantType = $this->grantTypes[$grantTypeIdentifier];
+
+ /**
+ * Retrieve the client information from the request
+ * ClientAssertionTypes allow for grant types which also assert the client data
+ * in which case ClientAssertion is handled in the validateRequest method
+ *
+ * @see OAuth2\GrantType\JWTBearer
+ * @see OAuth2\GrantType\ClientCredentials
+ */
+ if (!$grantType instanceof ClientAssertionTypeInterface) {
+ if (!$this->clientAssertionType->validateRequest($request, $response)) {
+ return null;
+ }
+ $clientId = $this->clientAssertionType->getClientId();
+ }
+
+ /**
+ * Retrieve the grant type information from the request
+ * The GrantTypeInterface object handles all validation
+ * If the object is an instance of ClientAssertionTypeInterface,
+ * That logic is handled here as well
+ */
+ if (!$grantType->validateRequest($request, $response)) {
+ return null;
+ }
+
+ if ($grantType instanceof ClientAssertionTypeInterface) {
+ $clientId = $grantType->getClientId();
+ } else {
+ // validate the Client ID (if applicable)
+ if (!is_null($storedClientId = $grantType->getClientId()) && $storedClientId != $clientId) {
+ $response->setError(400, 'invalid_grant', sprintf('%s doesn\'t exist or is invalid for the client', $grantTypeIdentifier));
+
+ return null;
+ }
+ }
+
+ /**
+ * Validate the client can use the requested grant type
+ */
+ if (!$this->clientStorage->checkRestrictedGrantType($clientId, $grantTypeIdentifier)) {
+ $response->setError(400, 'unauthorized_client', 'The grant type is unauthorized for this client_id');
+
+ return false;
+ }
+
+ /**
+ * Validate the scope of the token
+ *
+ * requestedScope - the scope specified in the token request
+ * availableScope - the scope associated with the grant type
+ * ex: in the case of the "Authorization Code" grant type,
+ * the scope is specified in the authorize request
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-3.3
+ */
+
+ $requestedScope = $this->scopeUtil->getScopeFromRequest($request);
+ $availableScope = $grantType->getScope();
+
+ if ($requestedScope) {
+ // validate the requested scope
+ if ($availableScope) {
+ if (!$this->scopeUtil->checkScope($requestedScope, $availableScope)) {
+ $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this request');
+
+ return null;
+ }
+ } else {
+ // validate the client has access to this scope
+ if ($clientScope = $this->clientStorage->getClientScope($clientId)) {
+ if (!$this->scopeUtil->checkScope($requestedScope, $clientScope)) {
+ $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this client');
+
+ return false;
+ }
+ } elseif (!$this->scopeUtil->scopeExists($requestedScope)) {
+ $response->setError(400, 'invalid_scope', 'An unsupported scope was requested');
+
+ return null;
+ }
+ }
+ } elseif ($availableScope) {
+ // use the scope associated with this grant type
+ $requestedScope = $availableScope;
+ } else {
+ // use a globally-defined default scope
+ $defaultScope = $this->scopeUtil->getDefaultScope($clientId);
+
+ // "false" means default scopes are not allowed
+ if (false === $defaultScope) {
+ $response->setError(400, 'invalid_scope', 'This application requires you specify a scope parameter');
+
+ return null;
+ }
+
+ $requestedScope = $defaultScope;
+ }
+
+ return $grantType->createAccessToken($this->accessToken, $clientId, $grantType->getUserId(), $requestedScope);
+ }
+
+ /**
+ * addGrantType
+ *
+ * @param grantType - OAuth2\GrantTypeInterface
+ * the grant type to add for the specified identifier
+ * @param identifier - string
+ * a string passed in as "grant_type" in the response that will call this grantType
+ */
+ public function addGrantType(GrantTypeInterface $grantType, $identifier = null)
+ {
+ if (is_null($identifier) || is_numeric($identifier)) {
+ $identifier = $grantType->getQuerystringIdentifier();
+ }
+
+ $this->grantTypes[$identifier] = $grantType;
+ }
+
+ public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if ($this->revokeToken($request, $response)) {
+ $response->setStatusCode(200);
+ $response->addParameters(array('revoked' => true));
+ }
+ }
+
+ /**
+ * Revoke a refresh or access token. Returns true on success and when tokens are invalid
+ *
+ * Note: invalid tokens do not cause an error response since the client
+ * cannot handle such an error in a reasonable way. Moreover, the
+ * purpose of the revocation request, invalidating the particular token,
+ * is already achieved.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool|null
+ */
+ public function revokeToken(RequestInterface $request, ResponseInterface $response)
+ {
+ if (strtolower($request->server('REQUEST_METHOD')) != 'post') {
+ $response->setError(405, 'invalid_request', 'The request method must be POST when revoking an access token', '#section-3.2');
+ $response->addHttpHeaders(array('Allow' => 'POST'));
+
+ return null;
+ }
+
+ $token_type_hint = $request->request('token_type_hint');
+ if (!in_array($token_type_hint, array(null, 'access_token', 'refresh_token'), true)) {
+ $response->setError(400, 'invalid_request', 'Token type hint must be either \'access_token\' or \'refresh_token\'');
+
+ return null;
+ }
+
+ $token = $request->request('token');
+ if ($token === null) {
+ $response->setError(400, 'invalid_request', 'Missing token parameter to revoke');
+
+ return null;
+ }
+
+ // @todo remove this check for v2.0
+ if (!method_exists($this->accessToken, 'revokeToken')) {
+ $class = get_class($this->accessToken);
+ throw new \RuntimeException("AccessToken {$class} does not implement required revokeToken method");
+ }
+
+ $this->accessToken->revokeToken($token, $token_type_hint);
+
+ return true;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Controller/TokenControllerInterface.php b/library/oauth2/src/OAuth2/Controller/TokenControllerInterface.php
new file mode 100644
index 000000000..72d72570f
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/TokenControllerInterface.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * This controller is called when a token is being requested.
+ * it is called to handle all grant types the application supports.
+ * It also validates the client's credentials
+ *
+ * ex:
+ * > $tokenController->handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response());
+ * > $response->send();
+ *
+ */
+interface TokenControllerInterface
+{
+ /**
+ * handleTokenRequest
+ *
+ * @param $request
+ * OAuth2\RequestInterface - The current http request
+ * @param $response
+ * OAuth2\ResponseInterface - An instance of OAuth2\ResponseInterface to contain the response data
+ *
+ */
+ public function handleTokenRequest(RequestInterface $request, ResponseInterface $response);
+
+ public function grantAccessToken(RequestInterface $request, ResponseInterface $response);
+}
diff --git a/library/oauth2/src/OAuth2/Encryption/EncryptionInterface.php b/library/oauth2/src/OAuth2/Encryption/EncryptionInterface.php
new file mode 100644
index 000000000..2d336c664
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Encryption/EncryptionInterface.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace OAuth2\Encryption;
+
+interface EncryptionInterface
+{
+ public function encode($payload, $key, $algorithm = null);
+ public function decode($payload, $key, $algorithm = null);
+ public function urlSafeB64Encode($data);
+ public function urlSafeB64Decode($b64);
+}
diff --git a/library/oauth2/src/OAuth2/Encryption/FirebaseJwt.php b/library/oauth2/src/OAuth2/Encryption/FirebaseJwt.php
new file mode 100644
index 000000000..1b527e0a0
--- /dev/null
+++ b/library/oauth2/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);
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Encryption/Jwt.php b/library/oauth2/src/OAuth2/Encryption/Jwt.php
new file mode 100644
index 000000000..ee576e643
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Encryption/Jwt.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace OAuth2\Encryption;
+
+/**
+ * @link https://github.com/F21/jwt
+ * @author F21
+ */
+class Jwt implements EncryptionInterface
+{
+ public function encode($payload, $key, $algo = 'HS256')
+ {
+ $header = $this->generateJwtHeader($payload, $algo);
+
+ $segments = array(
+ $this->urlSafeB64Encode(json_encode($header)),
+ $this->urlSafeB64Encode(json_encode($payload))
+ );
+
+ $signing_input = implode('.', $segments);
+
+ $signature = $this->sign($signing_input, $key, $algo);
+ $segments[] = $this->urlsafeB64Encode($signature);
+
+ return implode('.', $segments);
+ }
+
+ public function decode($jwt, $key = null, $allowedAlgorithms = true)
+ {
+ if (!strpos($jwt, '.')) {
+ return false;
+ }
+
+ $tks = explode('.', $jwt);
+
+ if (count($tks) != 3) {
+ return false;
+ }
+
+ list($headb64, $payloadb64, $cryptob64) = $tks;
+
+ if (null === ($header = json_decode($this->urlSafeB64Decode($headb64), true))) {
+ return false;
+ }
+
+ if (null === $payload = json_decode($this->urlSafeB64Decode($payloadb64), true)) {
+ return false;
+ }
+
+ $sig = $this->urlSafeB64Decode($cryptob64);
+
+ if ((bool) $allowedAlgorithms) {
+ if (!isset($header['alg'])) {
+ return false;
+ }
+
+ // check if bool arg supplied here to maintain BC
+ if (is_array($allowedAlgorithms) && !in_array($header['alg'], $allowedAlgorithms)) {
+ return false;
+ }
+
+ if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header['alg'])) {
+ return false;
+ }
+ }
+
+ return $payload;
+ }
+
+ private function verifySignature($signature, $input, $key, $algo = 'HS256')
+ {
+ // use constants when possible, for HipHop support
+ switch ($algo) {
+ case'HS256':
+ case'HS384':
+ case'HS512':
+ return $this->hash_equals(
+ $this->sign($input, $key, $algo),
+ $signature
+ );
+
+ case 'RS256':
+ return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256') === 1;
+
+ case 'RS384':
+ return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384') === 1;
+
+ case 'RS512':
+ return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512') === 1;
+
+ default:
+ throw new \InvalidArgumentException("Unsupported or invalid signing algorithm.");
+ }
+ }
+
+ private function sign($input, $key, $algo = 'HS256')
+ {
+ switch ($algo) {
+ case 'HS256':
+ return hash_hmac('sha256', $input, $key, true);
+
+ case 'HS384':
+ return hash_hmac('sha384', $input, $key, true);
+
+ case 'HS512':
+ return hash_hmac('sha512', $input, $key, true);
+
+ case 'RS256':
+ return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256');
+
+ case 'RS384':
+ return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384');
+
+ case 'RS512':
+ return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512');
+
+ default:
+ throw new \Exception("Unsupported or invalid signing algorithm.");
+ }
+ }
+
+ private function generateRSASignature($input, $key, $algo)
+ {
+ if (!openssl_sign($input, $signature, $key, $algo)) {
+ throw new \Exception("Unable to sign data.");
+ }
+
+ return $signature;
+ }
+
+ public function urlSafeB64Encode($data)
+ {
+ $b64 = base64_encode($data);
+ $b64 = str_replace(array('+', '/', "\r", "\n", '='),
+ array('-', '_'),
+ $b64);
+
+ return $b64;
+ }
+
+ public function urlSafeB64Decode($b64)
+ {
+ $b64 = str_replace(array('-', '_'),
+ array('+', '/'),
+ $b64);
+
+ return base64_decode($b64);
+ }
+
+ /**
+ * Override to create a custom header
+ */
+ protected function generateJwtHeader($payload, $algorithm)
+ {
+ return array(
+ 'typ' => 'JWT',
+ 'alg' => $algorithm,
+ );
+ }
+
+ protected function hash_equals($a, $b)
+ {
+ if (function_exists('hash_equals')) {
+ return hash_equals($a, $b);
+ }
+ $diff = strlen($a) ^ strlen($b);
+ for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) {
+ $diff |= ord($a[$i]) ^ ord($b[$i]);
+ }
+
+ return $diff === 0;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/GrantType/AuthorizationCode.php b/library/oauth2/src/OAuth2/GrantType/AuthorizationCode.php
new file mode 100644
index 000000000..e8995204c
--- /dev/null
+++ b/library/oauth2/src/OAuth2/GrantType/AuthorizationCode.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\AuthorizationCodeInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode implements GrantTypeInterface
+{
+ protected $storage;
+ protected $authCode;
+
+ /**
+ * @param OAuth2\Storage\AuthorizationCodeInterface $storage REQUIRED Storage class for retrieving authorization code information
+ */
+ public function __construct(AuthorizationCodeInterface $storage)
+ {
+ $this->storage = $storage;
+ }
+
+ public function getQuerystringIdentifier()
+ {
+ return 'authorization_code';
+ }
+
+ public function validateRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!$request->request('code')) {
+ $response->setError(400, 'invalid_request', 'Missing parameter: "code" is required');
+
+ return false;
+ }
+
+ $code = $request->request('code');
+ if (!$authCode = $this->storage->getAuthorizationCode($code)) {
+ $response->setError(400, 'invalid_grant', 'Authorization code doesn\'t exist or is invalid for the client');
+
+ return false;
+ }
+
+ /*
+ * 4.1.3 - ensure that the "redirect_uri" parameter is present if the "redirect_uri" parameter was included in the initial authorization request
+ * @uri - http://tools.ietf.org/html/rfc6749#section-4.1.3
+ */
+ if (isset($authCode['redirect_uri']) && $authCode['redirect_uri']) {
+ if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != $authCode['redirect_uri']) {
+ $response->setError(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3");
+
+ return false;
+ }
+ }
+
+ if (!isset($authCode['expires'])) {
+ throw new \Exception('Storage must return authcode with a value for "expires"');
+ }
+
+ if ($authCode["expires"] < time()) {
+ $response->setError(400, 'invalid_grant', "The authorization code has expired");
+
+ return false;
+ }
+
+ if (!isset($authCode['code'])) {
+ $authCode['code'] = $code; // used to expire the code after the access token is granted
+ }
+
+ $this->authCode = $authCode;
+
+ return true;
+ }
+
+ public function getClientId()
+ {
+ return $this->authCode['client_id'];
+ }
+
+ public function getScope()
+ {
+ return isset($this->authCode['scope']) ? $this->authCode['scope'] : null;
+ }
+
+ public function getUserId()
+ {
+ return isset($this->authCode['user_id']) ? $this->authCode['user_id'] : null;
+ }
+
+ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+ {
+ $token = $accessToken->createAccessToken($client_id, $user_id, $scope);
+ $this->storage->expireAuthorizationCode($this->authCode['code']);
+
+ return $token;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/GrantType/ClientCredentials.php b/library/oauth2/src/OAuth2/GrantType/ClientCredentials.php
new file mode 100644
index 000000000..f953e4e8d
--- /dev/null
+++ b/library/oauth2/src/OAuth2/GrantType/ClientCredentials.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\ClientAssertionType\HttpBasic;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\Storage\ClientCredentialsInterface;
+
+/**
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ *
+ * @see OAuth2\ClientAssertionType_HttpBasic
+ */
+class ClientCredentials extends HttpBasic implements GrantTypeInterface
+{
+ private $clientData;
+
+ public function __construct(ClientCredentialsInterface $storage, array $config = array())
+ {
+ /**
+ * The client credentials grant type MUST only be used by confidential clients
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-4.4
+ */
+ $config['allow_public_clients'] = false;
+
+ parent::__construct($storage, $config);
+ }
+
+ public function getQuerystringIdentifier()
+ {
+ return 'client_credentials';
+ }
+
+ public function getScope()
+ {
+ $this->loadClientData();
+
+ return isset($this->clientData['scope']) ? $this->clientData['scope'] : null;
+ }
+
+ public function getUserId()
+ {
+ $this->loadClientData();
+
+ return isset($this->clientData['user_id']) ? $this->clientData['user_id'] : null;
+ }
+
+ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+ {
+ /**
+ * Client Credentials Grant does NOT include a refresh token
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-4.4.3
+ */
+ $includeRefreshToken = false;
+
+ return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken);
+ }
+
+ private function loadClientData()
+ {
+ if (!$this->clientData) {
+ $this->clientData = $this->storage->getClientDetails($this->getClientId());
+ }
+ }
+}
diff --git a/library/oauth2/src/OAuth2/GrantType/GrantTypeInterface.php b/library/oauth2/src/OAuth2/GrantType/GrantTypeInterface.php
new file mode 100644
index 000000000..98489e9c1
--- /dev/null
+++ b/library/oauth2/src/OAuth2/GrantType/GrantTypeInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * Interface for all OAuth2 Grant Types
+ */
+interface GrantTypeInterface
+{
+ public function getQuerystringIdentifier();
+ public function validateRequest(RequestInterface $request, ResponseInterface $response);
+ public function getClientId();
+ public function getUserId();
+ public function getScope();
+ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope);
+}
diff --git a/library/oauth2/src/OAuth2/GrantType/JwtBearer.php b/library/oauth2/src/OAuth2/GrantType/JwtBearer.php
new file mode 100644
index 000000000..bb11a6954
--- /dev/null
+++ b/library/oauth2/src/OAuth2/GrantType/JwtBearer.php
@@ -0,0 +1,226 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\ClientAssertionType\ClientAssertionTypeInterface;
+use OAuth2\Storage\JwtBearerInterface;
+use OAuth2\Encryption\Jwt;
+use OAuth2\Encryption\EncryptionInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * The JWT bearer authorization grant implements JWT (JSON Web Tokens) as a grant type per the IETF draft.
+ *
+ * @see http://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-04#section-4
+ *
+ * @author F21
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface
+{
+ private $jwt;
+
+ protected $storage;
+ protected $audience;
+ protected $jwtUtil;
+ protected $allowedAlgorithms;
+
+ /**
+ * Creates an instance of the JWT bearer grant type.
+ *
+ * @param OAuth2\Storage\JWTBearerInterface|JwtBearerInterface $storage A valid storage interface that implements storage hooks for the JWT bearer grant type.
+ * @param string $audience The audience to validate the token against. This is usually the full URI of the OAuth token requests endpoint.
+ * @param EncryptionInterface|OAuth2\Encryption\JWT $jwtUtil OPTONAL The class used to decode, encode and verify JWTs.
+ * @param array $config
+ */
+ public function __construct(JwtBearerInterface $storage, $audience, EncryptionInterface $jwtUtil = null, array $config = array())
+ {
+ $this->storage = $storage;
+ $this->audience = $audience;
+
+ if (is_null($jwtUtil)) {
+ $jwtUtil = new Jwt();
+ }
+
+ $this->config = array_merge(array(
+ 'allowed_algorithms' => array('RS256', 'RS384', 'RS512')
+ ), $config);
+
+ $this->jwtUtil = $jwtUtil;
+
+ $this->allowedAlgorithms = $this->config['allowed_algorithms'];
+ }
+
+ /**
+ * Returns the grant_type get parameter to identify the grant type request as JWT bearer authorization grant.
+ *
+ * @return
+ * The string identifier for grant_type.
+ *
+ * @see OAuth2\GrantType\GrantTypeInterface::getQuerystringIdentifier()
+ */
+ public function getQuerystringIdentifier()
+ {
+ return 'urn:ietf:params:oauth:grant-type:jwt-bearer';
+ }
+
+ /**
+ * Validates the data from the decoded JWT.
+ *
+ * @return
+ * TRUE if the JWT request is valid and can be decoded. Otherwise, FALSE is returned.
+ *
+ * @see OAuth2\GrantType\GrantTypeInterface::getTokenData()
+ */
+ public function validateRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!$request->request("assertion")) {
+ $response->setError(400, 'invalid_request', 'Missing parameters: "assertion" required');
+
+ return null;
+ }
+
+ // Store the undecoded JWT for later use
+ $undecodedJWT = $request->request('assertion');
+
+ // Decode the JWT
+ $jwt = $this->jwtUtil->decode($request->request('assertion'), null, false);
+
+ if (!$jwt) {
+ $response->setError(400, 'invalid_request', "JWT is malformed");
+
+ return null;
+ }
+
+ // ensure these properties contain a value
+ // @todo: throw malformed error for missing properties
+ $jwt = array_merge(array(
+ 'scope' => null,
+ 'iss' => null,
+ 'sub' => null,
+ 'aud' => null,
+ 'exp' => null,
+ 'nbf' => null,
+ 'iat' => null,
+ 'jti' => null,
+ 'typ' => null,
+ ), $jwt);
+
+ if (!isset($jwt['iss'])) {
+ $response->setError(400, 'invalid_grant', "Invalid issuer (iss) provided");
+
+ return null;
+ }
+
+ if (!isset($jwt['sub'])) {
+ $response->setError(400, 'invalid_grant', "Invalid subject (sub) provided");
+
+ return null;
+ }
+
+ if (!isset($jwt['exp'])) {
+ $response->setError(400, 'invalid_grant', "Expiration (exp) time must be present");
+
+ return null;
+ }
+
+ // Check expiration
+ if (ctype_digit($jwt['exp'])) {
+ if ($jwt['exp'] <= time()) {
+ $response->setError(400, 'invalid_grant', "JWT has expired");
+
+ return null;
+ }
+ } else {
+ $response->setError(400, 'invalid_grant', "Expiration (exp) time must be a unix time stamp");
+
+ return null;
+ }
+
+ // Check the not before time
+ if ($notBefore = $jwt['nbf']) {
+ if (ctype_digit($notBefore)) {
+ if ($notBefore > time()) {
+ $response->setError(400, 'invalid_grant', "JWT cannot be used before the Not Before (nbf) time");
+
+ return null;
+ }
+ } else {
+ $response->setError(400, 'invalid_grant', "Not Before (nbf) time must be a unix time stamp");
+
+ return null;
+ }
+ }
+
+ // Check the audience if required to match
+ if (!isset($jwt['aud']) || ($jwt['aud'] != $this->audience)) {
+ $response->setError(400, 'invalid_grant', "Invalid audience (aud)");
+
+ return null;
+ }
+
+ // Check the jti (nonce)
+ // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-13#section-4.1.7
+ if (isset($jwt['jti'])) {
+ $jti = $this->storage->getJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']);
+
+ //Reject if jti is used and jwt is still valid (exp parameter has not expired).
+ if ($jti && $jti['expires'] > time()) {
+ $response->setError(400, 'invalid_grant', "JSON Token Identifier (jti) has already been used");
+
+ return null;
+ } else {
+ $this->storage->setJti($jwt['iss'], $jwt['sub'], $jwt['aud'], $jwt['exp'], $jwt['jti']);
+ }
+ }
+
+ // Get the iss's public key
+ // @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06#section-4.1.1
+ if (!$key = $this->storage->getClientKey($jwt['iss'], $jwt['sub'])) {
+ $response->setError(400, 'invalid_grant', "Invalid issuer (iss) or subject (sub) provided");
+
+ return null;
+ }
+
+ // Verify the JWT
+ if (!$this->jwtUtil->decode($undecodedJWT, $key, $this->allowedAlgorithms)) {
+ $response->setError(400, 'invalid_grant', "JWT failed signature verification");
+
+ return null;
+ }
+
+ $this->jwt = $jwt;
+
+ return true;
+ }
+
+ public function getClientId()
+ {
+ return $this->jwt['iss'];
+ }
+
+ public function getUserId()
+ {
+ return $this->jwt['sub'];
+ }
+
+ public function getScope()
+ {
+ return null;
+ }
+
+ /**
+ * Creates an access token that is NOT associated with a refresh token.
+ * If a subject (sub) the name of the user/account we are accessing data on behalf of.
+ *
+ * @see OAuth2\GrantType\GrantTypeInterface::createAccessToken()
+ */
+ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+ {
+ $includeRefreshToken = false;
+
+ return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken);
+ }
+}
diff --git a/library/oauth2/src/OAuth2/GrantType/RefreshToken.php b/library/oauth2/src/OAuth2/GrantType/RefreshToken.php
new file mode 100644
index 000000000..e55385222
--- /dev/null
+++ b/library/oauth2/src/OAuth2/GrantType/RefreshToken.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\RefreshTokenInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class RefreshToken implements GrantTypeInterface
+{
+ private $refreshToken;
+
+ protected $storage;
+ protected $config;
+
+ /**
+ * @param OAuth2\Storage\RefreshTokenInterface $storage REQUIRED Storage class for retrieving refresh token information
+ * @param array $config OPTIONAL Configuration options for the server
+ * <code>
+ * $config = array(
+ * 'always_issue_new_refresh_token' => true, // whether to issue a new refresh token upon successful token request
+ * 'unset_refresh_token_after_use' => true // whether to unset the refresh token after after using
+ * );
+ * </code>
+ */
+ public function __construct(RefreshTokenInterface $storage, $config = array())
+ {
+ $this->config = array_merge(array(
+ 'always_issue_new_refresh_token' => false,
+ 'unset_refresh_token_after_use' => true
+ ), $config);
+
+ // to preserve B.C. with v1.6
+ // @see https://github.com/bshaffer/oauth2-server-php/pull/580
+ // @todo - remove in v2.0
+ if (isset($config['always_issue_new_refresh_token']) && !isset($config['unset_refresh_token_after_use'])) {
+ $this->config['unset_refresh_token_after_use'] = $config['always_issue_new_refresh_token'];
+ }
+
+ $this->storage = $storage;
+ }
+
+ public function getQuerystringIdentifier()
+ {
+ return 'refresh_token';
+ }
+
+ public function validateRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!$request->request("refresh_token")) {
+ $response->setError(400, 'invalid_request', 'Missing parameter: "refresh_token" is required');
+
+ return null;
+ }
+
+ if (!$refreshToken = $this->storage->getRefreshToken($request->request("refresh_token"))) {
+ $response->setError(400, 'invalid_grant', 'Invalid refresh token');
+
+ return null;
+ }
+
+ if ($refreshToken['expires'] > 0 && $refreshToken["expires"] < time()) {
+ $response->setError(400, 'invalid_grant', 'Refresh token has expired');
+
+ return null;
+ }
+
+ // store the refresh token locally so we can delete it when a new refresh token is generated
+ $this->refreshToken = $refreshToken;
+
+ return true;
+ }
+
+ public function getClientId()
+ {
+ return $this->refreshToken['client_id'];
+ }
+
+ public function getUserId()
+ {
+ return isset($this->refreshToken['user_id']) ? $this->refreshToken['user_id'] : null;
+ }
+
+ public function getScope()
+ {
+ return isset($this->refreshToken['scope']) ? $this->refreshToken['scope'] : null;
+ }
+
+ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+ {
+ /*
+ * It is optional to force a new refresh token when a refresh token is used.
+ * However, if a new refresh token is issued, the old one MUST be expired
+ * @see http://tools.ietf.org/html/rfc6749#section-6
+ */
+ $issueNewRefreshToken = $this->config['always_issue_new_refresh_token'];
+ $unsetRefreshToken = $this->config['unset_refresh_token_after_use'];
+ $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $issueNewRefreshToken);
+
+ if ($unsetRefreshToken) {
+ $this->storage->unsetRefreshToken($this->refreshToken['refresh_token']);
+ }
+
+ return $token;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/GrantType/UserCredentials.php b/library/oauth2/src/OAuth2/GrantType/UserCredentials.php
new file mode 100644
index 000000000..f165538ba
--- /dev/null
+++ b/library/oauth2/src/OAuth2/GrantType/UserCredentials.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\UserCredentialsInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class UserCredentials implements GrantTypeInterface
+{
+ private $userInfo;
+
+ protected $storage;
+
+ /**
+ * @param OAuth2\Storage\UserCredentialsInterface $storage REQUIRED Storage class for retrieving user credentials information
+ */
+ public function __construct(UserCredentialsInterface $storage)
+ {
+ $this->storage = $storage;
+ }
+
+ public function getQuerystringIdentifier()
+ {
+ return 'password';
+ }
+
+ public function validateRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!$request->request("password") || !$request->request("username")) {
+ $response->setError(400, 'invalid_request', 'Missing parameters: "username" and "password" required');
+
+ return null;
+ }
+
+ if (!$this->storage->checkUserCredentials($request->request("username"), $request->request("password"))) {
+ $response->setError(401, 'invalid_grant', 'Invalid username and password combination');
+
+ return null;
+ }
+
+ $userInfo = $this->storage->getUserDetails($request->request("username"));
+
+ if (empty($userInfo)) {
+ $response->setError(400, 'invalid_grant', 'Unable to retrieve user information');
+
+ return null;
+ }
+
+ if (!isset($userInfo['user_id'])) {
+ throw new \LogicException("you must set the user_id on the array returned by getUserDetails");
+ }
+
+ $this->userInfo = $userInfo;
+
+ return true;
+ }
+
+ public function getClientId()
+ {
+ return null;
+ }
+
+ public function getUserId()
+ {
+ return $this->userInfo['user_id'];
+ }
+
+ public function getScope()
+ {
+ return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null;
+ }
+
+ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+ {
+ return $accessToken->createAccessToken($client_id, $user_id, $scope);
+ }
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/Controller/AuthorizeController.php b/library/oauth2/src/OAuth2/OpenID/Controller/AuthorizeController.php
new file mode 100644
index 000000000..c9b5c6af7
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/Controller/AuthorizeController.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\Controller\AuthorizeController as BaseAuthorizeController;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * @see OAuth2\Controller\AuthorizeControllerInterface
+ */
+class AuthorizeController extends BaseAuthorizeController implements AuthorizeControllerInterface
+{
+ private $nonce;
+
+ protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null)
+ {
+ $prompt = $request->query('prompt', 'consent');
+ if ($prompt == 'none') {
+ if (is_null($user_id)) {
+ $error = 'login_required';
+ $error_message = 'The user must log in';
+ } else {
+ $error = 'interaction_required';
+ $error_message = 'The user must grant access to your application';
+ }
+ } else {
+ $error = 'consent_required';
+ $error_message = 'The user denied access to your application';
+ }
+
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->getState(), $error, $error_message);
+ }
+
+ protected function buildAuthorizeParameters($request, $response, $user_id)
+ {
+ if (!$params = parent::buildAuthorizeParameters($request, $response, $user_id)) {
+ return;
+ }
+
+ // Generate an id token if needed.
+ if ($this->needsIdToken($this->getScope()) && $this->getResponseType() == self::RESPONSE_TYPE_AUTHORIZATION_CODE) {
+ $params['id_token'] = $this->responseTypes['id_token']->createIdToken($this->getClientId(), $user_id, $this->nonce);
+ }
+
+ // add the nonce to return with the redirect URI
+ $params['nonce'] = $this->nonce;
+
+ return $params;
+ }
+
+ public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!parent::validateAuthorizeRequest($request, $response)) {
+ return false;
+ }
+
+ $nonce = $request->query('nonce');
+
+ // Validate required nonce for "id_token" and "id_token token"
+ if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_ID_TOKEN_TOKEN))) {
+ $response->setError(400, 'invalid_nonce', 'This application requires you specify a nonce parameter');
+
+ return false;
+ }
+
+ $this->nonce = $nonce;
+
+ return true;
+ }
+
+ protected function getValidResponseTypes()
+ {
+ return array(
+ self::RESPONSE_TYPE_ACCESS_TOKEN,
+ self::RESPONSE_TYPE_AUTHORIZATION_CODE,
+ self::RESPONSE_TYPE_ID_TOKEN,
+ self::RESPONSE_TYPE_ID_TOKEN_TOKEN,
+ self::RESPONSE_TYPE_CODE_ID_TOKEN,
+ );
+ }
+
+ /**
+ * Returns whether the current request needs to generate an id token.
+ *
+ * ID Tokens are a part of the OpenID Connect specification, so this
+ * method checks whether OpenID Connect is enabled in the server settings
+ * and whether the openid scope was requested.
+ *
+ * @param $request_scope
+ * A space-separated string of scopes.
+ *
+ * @return
+ * TRUE if an id token is needed, FALSE otherwise.
+ */
+ public function needsIdToken($request_scope)
+ {
+ // see if the "openid" scope exists in the requested scope
+ return $this->scopeUtil->checkScope('openid', $request_scope);
+ }
+
+ public function getNonce()
+ {
+ return $this->nonce;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php b/library/oauth2/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php
new file mode 100644
index 000000000..1e231d844
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+interface AuthorizeControllerInterface
+{
+ const RESPONSE_TYPE_ID_TOKEN = 'id_token';
+ const RESPONSE_TYPE_ID_TOKEN_TOKEN = 'id_token token';
+ const RESPONSE_TYPE_CODE_ID_TOKEN = 'code id_token';
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/Controller/UserInfoController.php b/library/oauth2/src/OAuth2/OpenID/Controller/UserInfoController.php
new file mode 100644
index 000000000..30cb942d0
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/Controller/UserInfoController.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\Scope;
+use OAuth2\TokenType\TokenTypeInterface;
+use OAuth2\Storage\AccessTokenInterface;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\Controller\ResourceController;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * @see OAuth2\Controller\UserInfoControllerInterface
+ */
+class UserInfoController extends ResourceController implements UserInfoControllerInterface
+{
+ private $token;
+
+ protected $tokenType;
+ protected $tokenStorage;
+ protected $userClaimsStorage;
+ protected $config;
+ protected $scopeUtil;
+
+ public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, UserClaimsInterface $userClaimsStorage, $config = array(), ScopeInterface $scopeUtil = null)
+ {
+ $this->tokenType = $tokenType;
+ $this->tokenStorage = $tokenStorage;
+ $this->userClaimsStorage = $userClaimsStorage;
+
+ $this->config = array_merge(array(
+ 'www_realm' => 'Service',
+ ), $config);
+
+ if (is_null($scopeUtil)) {
+ $scopeUtil = new Scope();
+ }
+ $this->scopeUtil = $scopeUtil;
+ }
+
+ public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!$this->verifyResourceRequest($request, $response, 'openid')) {
+ return;
+ }
+
+ $token = $this->getToken();
+ $claims = $this->userClaimsStorage->getUserClaims($token['user_id'], $token['scope']);
+ // The sub Claim MUST always be returned in the UserInfo Response.
+ // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
+ $claims += array(
+ 'sub' => $token['user_id'],
+ );
+ $response->addParameters($claims);
+ }
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php b/library/oauth2/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php
new file mode 100644
index 000000000..a89049d49
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * This controller is called when the user claims for OpenID Connect's
+ * UserInfo endpoint should be returned.
+ *
+ * ex:
+ * > $response = new OAuth2\Response();
+ * > $userInfoController->handleUserInfoRequest(
+ * > OAuth2\Request::createFromGlobals(),
+ * > $response;
+ * > $response->send();
+ *
+ */
+interface UserInfoControllerInterface
+{
+ public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response);
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/GrantType/AuthorizationCode.php b/library/oauth2/src/OAuth2/OpenID/GrantType/AuthorizationCode.php
new file mode 100644
index 000000000..8ed1edc26
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/GrantType/AuthorizationCode.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace OAuth2\OpenID\GrantType;
+
+use OAuth2\GrantType\AuthorizationCode as BaseAuthorizationCode;
+use OAuth2\ResponseType\AccessTokenInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode extends BaseAuthorizationCode
+{
+ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+ {
+ $includeRefreshToken = true;
+ if (isset($this->authCode['id_token'])) {
+ // OpenID Connect requests include the refresh token only if the
+ // offline_access scope has been requested and granted.
+ $scopes = explode(' ', trim($scope));
+ $includeRefreshToken = in_array('offline_access', $scopes);
+ }
+
+ $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken);
+ if (isset($this->authCode['id_token'])) {
+ $token['id_token'] = $this->authCode['id_token'];
+ }
+
+ $this->storage->expireAuthorizationCode($this->authCode['code']);
+
+ return $token;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php b/library/oauth2/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php
new file mode 100644
index 000000000..8971954c5
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\AuthorizationCode as BaseAuthorizationCode;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as AuthorizationCodeStorageInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode extends BaseAuthorizationCode implements AuthorizationCodeInterface
+{
+ public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array())
+ {
+ parent::__construct($storage, $config);
+ }
+
+ public function getAuthorizeResponse($params, $user_id = null)
+ {
+ // build the URL to redirect to
+ $result = array('query' => array());
+
+ $params += array('scope' => null, 'state' => null, 'id_token' => null);
+
+ $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token']);
+
+ if (isset($params['state'])) {
+ $result['query']['state'] = $params['state'];
+ }
+
+ return array($params['redirect_uri'], $result);
+ }
+
+ /**
+ * Handle the creation of the authorization code.
+ *
+ * @param $client_id
+ * Client identifier related to the authorization code
+ * @param $user_id
+ * User ID associated with the authorization code
+ * @param $redirect_uri
+ * An absolute URI to which the authorization server will redirect the
+ * user-agent to when the end-user authorization step is completed.
+ * @param $scope
+ * (optional) Scopes to be stored in space-separated string.
+ * @param $id_token
+ * (optional) The OpenID Connect id_token.
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-4
+ * @ingroup oauth2_section_4
+ */
+ public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null)
+ {
+ $code = $this->generateAuthorizationCode();
+ $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token);
+
+ return $code;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php b/library/oauth2/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php
new file mode 100644
index 000000000..ea4779255
--- /dev/null
+++ b/library/oauth2/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 $client_id Client identifier related to the authorization code
+ * @param $user_id User ID associated with the authorization code
+ * @param $redirect_uri An absolute URI to which the authorization server will redirect the
+ * user-agent to when the end-user authorization step is completed.
+ * @param $scope OPTIONAL Scopes to be stored in space-separated string.
+ * @param $id_token OPTIONAL The OpenID Connect id_token.
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-4
+ * @ingroup oauth2_section_4
+ */
+ public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null);
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/ResponseType/CodeIdToken.php b/library/oauth2/src/OAuth2/OpenID/ResponseType/CodeIdToken.php
new file mode 100644
index 000000000..ac7764d6c
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/ResponseType/CodeIdToken.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+class CodeIdToken implements CodeIdTokenInterface
+{
+ protected $authCode;
+ protected $idToken;
+
+ public function __construct(AuthorizationCodeInterface $authCode, IdTokenInterface $idToken)
+ {
+ $this->authCode = $authCode;
+ $this->idToken = $idToken;
+ }
+
+ public function getAuthorizeResponse($params, $user_id = null)
+ {
+ $result = $this->authCode->getAuthorizeResponse($params, $user_id);
+ $resultIdToken = $this->idToken->getAuthorizeResponse($params, $user_id);
+ $result[1]['query']['id_token'] = $resultIdToken[1]['fragment']['id_token'];
+
+ return $result;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php b/library/oauth2/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php
new file mode 100644
index 000000000..629adcca8
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\ResponseTypeInterface;
+
+interface CodeIdTokenInterface extends ResponseTypeInterface
+{
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/ResponseType/IdToken.php b/library/oauth2/src/OAuth2/OpenID/ResponseType/IdToken.php
new file mode 100644
index 000000000..97777fbf2
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/ResponseType/IdToken.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\Encryption\EncryptionInterface;
+use OAuth2\Encryption\Jwt;
+use OAuth2\Storage\PublicKeyInterface;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+
+class IdToken implements IdTokenInterface
+{
+ protected $userClaimsStorage;
+ protected $publicKeyStorage;
+ protected $config;
+ protected $encryptionUtil;
+
+ public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInterface $publicKeyStorage, array $config = array(), EncryptionInterface $encryptionUtil = null)
+ {
+ $this->userClaimsStorage = $userClaimsStorage;
+ $this->publicKeyStorage = $publicKeyStorage;
+ if (is_null($encryptionUtil)) {
+ $encryptionUtil = new Jwt();
+ }
+ $this->encryptionUtil = $encryptionUtil;
+
+ if (!isset($config['issuer'])) {
+ throw new \LogicException('config parameter "issuer" must be set');
+ }
+ $this->config = array_merge(array(
+ 'id_lifetime' => 3600,
+ ), $config);
+ }
+
+ public function getAuthorizeResponse($params, $userInfo = null)
+ {
+ // build the URL to redirect to
+ $result = array('query' => array());
+ $params += array('scope' => null, 'state' => null, 'nonce' => null);
+
+ // create the id token.
+ list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo);
+ $userClaims = $this->userClaimsStorage->getUserClaims($user_id, $params['scope']);
+
+ $id_token = $this->createIdToken($params['client_id'], $userInfo, $params['nonce'], $userClaims, null);
+ $result["fragment"] = array('id_token' => $id_token);
+ if (isset($params['state'])) {
+ $result["fragment"]["state"] = $params['state'];
+ }
+
+ return array($params['redirect_uri'], $result);
+ }
+
+ public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null)
+ {
+ // pull auth_time from user info if supplied
+ list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo);
+
+ $token = array(
+ 'iss' => $this->config['issuer'],
+ 'sub' => $user_id,
+ 'aud' => $client_id,
+ 'iat' => time(),
+ 'exp' => time() + $this->config['id_lifetime'],
+ 'auth_time' => $auth_time,
+ );
+
+ if ($nonce) {
+ $token['nonce'] = $nonce;
+ }
+
+ if ($userClaims) {
+ $token += $userClaims;
+ }
+
+ if ($access_token) {
+ $token['at_hash'] = $this->createAtHash($access_token, $client_id);
+ }
+
+ return $this->encodeToken($token, $client_id);
+ }
+
+ protected function createAtHash($access_token, $client_id = null)
+ {
+ // maps HS256 and RS256 to sha256, etc.
+ $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id);
+ $hash_algorithm = 'sha' . substr($algorithm, 2);
+ $hash = hash($hash_algorithm, $access_token, true);
+ $at_hash = substr($hash, 0, strlen($hash) / 2);
+
+ return $this->encryptionUtil->urlSafeB64Encode($at_hash);
+ }
+
+ protected function encodeToken(array $token, $client_id = null)
+ {
+ $private_key = $this->publicKeyStorage->getPrivateKey($client_id);
+ $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id);
+
+ return $this->encryptionUtil->encode($token, $private_key, $algorithm);
+ }
+
+ private function getUserIdAndAuthTime($userInfo)
+ {
+ $auth_time = null;
+
+ // support an array for user_id / auth_time
+ if (is_array($userInfo)) {
+ if (!isset($userInfo['user_id'])) {
+ throw new \LogicException('if $user_id argument is an array, user_id index must be set');
+ }
+
+ $auth_time = isset($userInfo['auth_time']) ? $userInfo['auth_time'] : null;
+ $user_id = $userInfo['user_id'];
+ } else {
+ $user_id = $userInfo;
+ }
+
+ if (is_null($auth_time)) {
+ $auth_time = time();
+ }
+
+ // userInfo is a scalar, and so this is the $user_id. Auth Time is null
+ return array($user_id, $auth_time);
+ }
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php b/library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php
new file mode 100644
index 000000000..0bd2f8391
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\ResponseTypeInterface;
+
+interface IdTokenInterface extends ResponseTypeInterface
+{
+ /**
+ * Create the id token.
+ *
+ * If Authorization Code Flow is used, the id_token is generated when the
+ * authorization code is issued, and later returned from the token endpoint
+ * together with the access_token.
+ * If the Implicit Flow is used, the token and id_token are generated and
+ * returned together.
+ *
+ * @param string $client_id The client id.
+ * @param string $user_id The user id.
+ * @param string $nonce OPTIONAL The nonce.
+ * @param string $userClaims OPTIONAL Claims about the user.
+ * @param string $access_token OPTIONAL The access token, if known.
+ *
+ * @return string The ID Token represented as a JSON Web Token (JWT).
+ *
+ * @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken
+ */
+ public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null);
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenToken.php b/library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenToken.php
new file mode 100644
index 000000000..f0c59799b
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenToken.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\AccessTokenInterface;
+
+class IdTokenToken implements IdTokenTokenInterface
+{
+ protected $accessToken;
+ protected $idToken;
+
+ public function __construct(AccessTokenInterface $accessToken, IdTokenInterface $idToken)
+ {
+ $this->accessToken = $accessToken;
+ $this->idToken = $idToken;
+ }
+
+ public function getAuthorizeResponse($params, $user_id = null)
+ {
+ $result = $this->accessToken->getAuthorizeResponse($params, $user_id);
+ $access_token = $result[1]['fragment']['access_token'];
+ $id_token = $this->idToken->createIdToken($params['client_id'], $user_id, $params['nonce'], null, $access_token);
+ $result[1]['fragment']['id_token'] = $id_token;
+
+ return $result;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php b/library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php
new file mode 100644
index 000000000..ac13e2032
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\ResponseTypeInterface;
+
+interface IdTokenTokenInterface extends ResponseTypeInterface
+{
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php b/library/oauth2/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php
new file mode 100644
index 000000000..51dd867ec
--- /dev/null
+++ b/library/oauth2/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 $code authorization code to be stored.
+ * @param $client_id client identifier to be stored.
+ * @param $user_id user identifier to be stored.
+ * @param string $redirect_uri redirect URI(s) to be stored in a space-separated string.
+ * @param int $expires expiration to be stored as a Unix timestamp.
+ * @param string $scope OPTIONAL scopes to be stored in space-separated string.
+ * @param string $id_token OPTIONAL the OpenID Connect id_token.
+ *
+ * @ingroup oauth2_section_4
+ */
+ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null);
+}
diff --git a/library/oauth2/src/OAuth2/OpenID/Storage/UserClaimsInterface.php b/library/oauth2/src/OAuth2/OpenID/Storage/UserClaimsInterface.php
new file mode 100644
index 000000000..f230bef9e
--- /dev/null
+++ b/library/oauth2/src/OAuth2/OpenID/Storage/UserClaimsInterface.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace OAuth2\OpenID\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should retrieve user claims for the OpenID Connect id_token.
+ */
+interface UserClaimsInterface
+{
+ // valid scope values to pass into the user claims API call
+ const VALID_CLAIMS = 'profile email address phone';
+
+ // fields returned for the claims above
+ const PROFILE_CLAIM_VALUES = 'name family_name given_name middle_name nickname preferred_username profile picture website gender birthdate zoneinfo locale updated_at';
+ const EMAIL_CLAIM_VALUES = 'email email_verified';
+ const ADDRESS_CLAIM_VALUES = 'formatted street_address locality region postal_code country';
+ const PHONE_CLAIM_VALUES = 'phone_number phone_number_verified';
+
+ /**
+ * Return claims about the provided user id.
+ *
+ * Groups of claims are returned based on the requested scopes. No group
+ * is required, and no claim is required.
+ *
+ * @param $user_id
+ * The id of the user for which claims should be returned.
+ * @param $scope
+ * The requested scope.
+ * Scopes with matching claims: profile, email, address, phone.
+ *
+ * @return
+ * An array in the claim => value format.
+ *
+ * @see http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
+ */
+ public function getUserClaims($user_id, $scope);
+}
diff --git a/library/oauth2/src/OAuth2/Request.php b/library/oauth2/src/OAuth2/Request.php
new file mode 100644
index 000000000..c92cee821
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Request.php
@@ -0,0 +1,213 @@
+<?php
+
+namespace OAuth2;
+
+/**
+ * OAuth2\Request
+ * This class is taken from the Symfony2 Framework and is part of the Symfony package.
+ * See Symfony\Component\HttpFoundation\Request (https://github.com/symfony/symfony)
+ */
+class Request implements RequestInterface
+{
+ public $attributes;
+ public $request;
+ public $query;
+ public $server;
+ public $files;
+ public $cookies;
+ public $headers;
+ public $content;
+
+ /**
+ * Constructor.
+ *
+ * @param array $query The GET parameters
+ * @param array $request The POST parameters
+ * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
+ * @param array $cookies The COOKIE parameters
+ * @param array $files The FILES parameters
+ * @param array $server The SERVER parameters
+ * @param string $content The raw body data
+ *
+ * @api
+ */
+ public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null)
+ {
+ $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content, $headers);
+ }
+
+ /**
+ * Sets the parameters for this request.
+ *
+ * This method also re-initializes all properties.
+ *
+ * @param array $query The GET parameters
+ * @param array $request The POST parameters
+ * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
+ * @param array $cookies The COOKIE parameters
+ * @param array $files The FILES parameters
+ * @param array $server The SERVER parameters
+ * @param string $content The raw body data
+ *
+ * @api
+ */
+ public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null)
+ {
+ $this->request = $request;
+ $this->query = $query;
+ $this->attributes = $attributes;
+ $this->cookies = $cookies;
+ $this->files = $files;
+ $this->server = $server;
+ $this->content = $content;
+ $this->headers = is_null($headers) ? $this->getHeadersFromServer($this->server) : $headers;
+ }
+
+ public function query($name, $default = null)
+ {
+ return isset($this->query[$name]) ? $this->query[$name] : $default;
+ }
+
+ public function request($name, $default = null)
+ {
+ return isset($this->request[$name]) ? $this->request[$name] : $default;
+ }
+
+ public function server($name, $default = null)
+ {
+ return isset($this->server[$name]) ? $this->server[$name] : $default;
+ }
+
+ public function headers($name, $default = null)
+ {
+ $headers = array_change_key_case($this->headers);
+ $name = strtolower($name);
+
+ return isset($headers[$name]) ? $headers[$name] : $default;
+ }
+
+ public function getAllQueryParameters()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Returns the request body content.
+ *
+ * @param Boolean $asResource If true, a resource will be returned
+ *
+ * @return string|resource The request body content or a resource to read the body stream.
+ */
+ public function getContent($asResource = false)
+ {
+ if (false === $this->content || (true === $asResource && null !== $this->content)) {
+ throw new \LogicException('getContent() can only be called once when using the resource return type.');
+ }
+
+ if (true === $asResource) {
+ $this->content = false;
+
+ return fopen('php://input', 'rb');
+ }
+
+ if (null === $this->content) {
+ $this->content = file_get_contents('php://input');
+ }
+
+ return $this->content;
+ }
+
+ private function getHeadersFromServer($server)
+ {
+ $headers = array();
+ foreach ($server as $key => $value) {
+ if (0 === strpos($key, 'HTTP_')) {
+ $headers[substr($key, 5)] = $value;
+ }
+ // CONTENT_* are not prefixed with HTTP_
+ elseif (in_array($key, array('CONTENT_LENGTH', 'CONTENT_MD5', 'CONTENT_TYPE'))) {
+ $headers[$key] = $value;
+ }
+ }
+
+ if (isset($server['PHP_AUTH_USER'])) {
+ $headers['PHP_AUTH_USER'] = $server['PHP_AUTH_USER'];
+ $headers['PHP_AUTH_PW'] = isset($server['PHP_AUTH_PW']) ? $server['PHP_AUTH_PW'] : '';
+ } else {
+ /*
+ * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default
+ * For this workaround to work, add this line to your .htaccess file:
+ * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+ *
+ * A sample .htaccess file:
+ * RewriteEngine On
+ * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+ * RewriteCond %{REQUEST_FILENAME} !-f
+ * RewriteRule ^(.*)$ app.php [QSA,L]
+ */
+
+ $authorizationHeader = null;
+ if (isset($server['HTTP_AUTHORIZATION'])) {
+ $authorizationHeader = $server['HTTP_AUTHORIZATION'];
+ } elseif (isset($server['REDIRECT_HTTP_AUTHORIZATION'])) {
+ $authorizationHeader = $server['REDIRECT_HTTP_AUTHORIZATION'];
+ } elseif (function_exists('apache_request_headers')) {
+ $requestHeaders = (array) apache_request_headers();
+
+ // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization)
+ $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
+
+ if (isset($requestHeaders['Authorization'])) {
+ $authorizationHeader = trim($requestHeaders['Authorization']);
+ }
+ }
+
+ if (null !== $authorizationHeader) {
+ $headers['AUTHORIZATION'] = $authorizationHeader;
+ // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic
+ if (0 === stripos($authorizationHeader, 'basic')) {
+ $exploded = explode(':', base64_decode(substr($authorizationHeader, 6)));
+ if (count($exploded) == 2) {
+ list($headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']) = $exploded;
+ }
+ }
+ }
+ }
+
+ // PHP_AUTH_USER/PHP_AUTH_PW
+ if (isset($headers['PHP_AUTH_USER'])) {
+ $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.$headers['PHP_AUTH_PW']);
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Creates a new request with values from PHP's super globals.
+ *
+ * @return Request A new request
+ *
+ * @api
+ */
+ public static function createFromGlobals()
+ {
+ $class = get_called_class();
+ $request = new $class($_GET, $_POST, array(), $_COOKIE, $_FILES, $_SERVER);
+
+ $contentType = $request->server('CONTENT_TYPE', '');
+ $requestMethod = $request->server('REQUEST_METHOD', 'GET');
+ if (0 === strpos($contentType, 'application/x-www-form-urlencoded')
+ && in_array(strtoupper($requestMethod), array('PUT', 'DELETE'))
+ ) {
+ parse_str($request->getContent(), $data);
+ $request->request = $data;
+ } elseif (0 === strpos($contentType, 'application/json')
+ && in_array(strtoupper($requestMethod), array('POST', 'PUT', 'DELETE'))
+ ) {
+ $data = json_decode($request->getContent(), true);
+ $request->request = $data;
+ }
+
+ return $request;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/RequestInterface.php b/library/oauth2/src/OAuth2/RequestInterface.php
new file mode 100644
index 000000000..8a70d5fad
--- /dev/null
+++ b/library/oauth2/src/OAuth2/RequestInterface.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace OAuth2;
+
+interface RequestInterface
+{
+ public function query($name, $default = null);
+
+ public function request($name, $default = null);
+
+ public function server($name, $default = null);
+
+ public function headers($name, $default = null);
+
+ public function getAllQueryParameters();
+}
diff --git a/library/oauth2/src/OAuth2/Response.php b/library/oauth2/src/OAuth2/Response.php
new file mode 100644
index 000000000..d8eabe79e
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Response.php
@@ -0,0 +1,369 @@
+<?php
+
+namespace OAuth2;
+
+/**
+ * Class to handle OAuth2 Responses in a graceful way. Use this interface
+ * to output the proper OAuth2 responses.
+ *
+ * @see OAuth2\ResponseInterface
+ *
+ * This class borrows heavily from the Symfony2 Framework and is part of the symfony package
+ * @see Symfony\Component\HttpFoundation\Request (https://github.com/symfony/symfony)
+ */
+class Response implements ResponseInterface
+{
+ public $version;
+ protected $statusCode = 200;
+ protected $statusText;
+ protected $parameters = array();
+ protected $httpHeaders = array();
+
+ public static $statusTexts = array(
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 418 => 'I\'m a teapot',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ );
+
+ public function __construct($parameters = array(), $statusCode = 200, $headers = array())
+ {
+ $this->setParameters($parameters);
+ $this->setStatusCode($statusCode);
+ $this->setHttpHeaders($headers);
+ $this->version = '1.1';
+ }
+
+ /**
+ * Converts the response object to string containing all headers and the response content.
+ *
+ * @return string The response with headers and content
+ */
+ public function __toString()
+ {
+ $headers = array();
+ foreach ($this->httpHeaders as $name => $value) {
+ $headers[$name] = (array) $value;
+ }
+
+ return
+ sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n".
+ $this->getHttpHeadersAsString($headers)."\r\n".
+ $this->getResponseBody();
+ }
+
+ /**
+ * Returns the build header line.
+ *
+ * @param string $name The header name
+ * @param string $value The header value
+ *
+ * @return string The built header line
+ */
+ protected function buildHeader($name, $value)
+ {
+ return sprintf("%s: %s\n", $name, $value);
+ }
+
+ public function getStatusCode()
+ {
+ return $this->statusCode;
+ }
+
+ public function setStatusCode($statusCode, $text = null)
+ {
+ $this->statusCode = (int) $statusCode;
+ if ($this->isInvalid()) {
+ throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $statusCode));
+ }
+
+ $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text);
+ }
+
+ public function getStatusText()
+ {
+ return $this->statusText;
+ }
+
+ public function getParameters()
+ {
+ return $this->parameters;
+ }
+
+ public function setParameters(array $parameters)
+ {
+ $this->parameters = $parameters;
+ }
+
+ public function addParameters(array $parameters)
+ {
+ $this->parameters = array_merge($this->parameters, $parameters);
+ }
+
+ public function getParameter($name, $default = null)
+ {
+ return isset($this->parameters[$name]) ? $this->parameters[$name] : $default;
+ }
+
+ public function setParameter($name, $value)
+ {
+ $this->parameters[$name] = $value;
+ }
+
+ public function setHttpHeaders(array $httpHeaders)
+ {
+ $this->httpHeaders = $httpHeaders;
+ }
+
+ public function setHttpHeader($name, $value)
+ {
+ $this->httpHeaders[$name] = $value;
+ }
+
+ public function addHttpHeaders(array $httpHeaders)
+ {
+ $this->httpHeaders = array_merge($this->httpHeaders, $httpHeaders);
+ }
+
+ public function getHttpHeaders()
+ {
+ return $this->httpHeaders;
+ }
+
+ public function getHttpHeader($name, $default = null)
+ {
+ return isset($this->httpHeaders[$name]) ? $this->httpHeaders[$name] : $default;
+ }
+
+ public function getResponseBody($format = 'json')
+ {
+ switch ($format) {
+ case 'json':
+ return json_encode($this->parameters);
+ case 'xml':
+ // this only works for single-level arrays
+ $xml = new \SimpleXMLElement('<response/>');
+ foreach ($this->parameters as $key => $param) {
+ $xml->addChild($key, $param);
+ }
+
+ return $xml->asXML();
+ }
+
+ throw new \InvalidArgumentException(sprintf('The format %s is not supported', $format));
+
+ }
+
+ public function send($format = 'json')
+ {
+ // headers have already been sent by the developer
+ if (headers_sent()) {
+ return;
+ }
+
+ switch ($format) {
+ case 'json':
+ $this->setHttpHeader('Content-Type', 'application/json');
+ break;
+ case 'xml':
+ $this->setHttpHeader('Content-Type', 'text/xml');
+ break;
+ }
+ // status
+ header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText));
+
+ foreach ($this->getHttpHeaders() as $name => $header) {
+ header(sprintf('%s: %s', $name, $header));
+ }
+ echo $this->getResponseBody($format);
+ }
+
+ public function setError($statusCode, $error, $errorDescription = null, $errorUri = null)
+ {
+ $parameters = array(
+ 'error' => $error,
+ 'error_description' => $errorDescription,
+ );
+
+ if (!is_null($errorUri)) {
+ if (strlen($errorUri) > 0 && $errorUri[0] == '#') {
+ // we are referencing an oauth bookmark (for brevity)
+ $errorUri = 'http://tools.ietf.org/html/rfc6749' . $errorUri;
+ }
+ $parameters['error_uri'] = $errorUri;
+ }
+
+ $httpHeaders = array(
+ 'Cache-Control' => 'no-store'
+ );
+
+ $this->setStatusCode($statusCode);
+ $this->addParameters($parameters);
+ $this->addHttpHeaders($httpHeaders);
+
+ if (!$this->isClientError() && !$this->isServerError()) {
+ throw new \InvalidArgumentException(sprintf('The HTTP status code is not an error ("%s" given).', $statusCode));
+ }
+ }
+
+ public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null)
+ {
+ if (empty($url)) {
+ throw new \InvalidArgumentException('Cannot redirect to an empty URL.');
+ }
+
+ $parameters = array();
+
+ if (!is_null($state)) {
+ $parameters['state'] = $state;
+ }
+
+ if (!is_null($error)) {
+ $this->setError(400, $error, $errorDescription, $errorUri);
+ }
+ $this->setStatusCode($statusCode);
+ $this->addParameters($parameters);
+
+ if (count($this->parameters) > 0) {
+ // add parameters to URL redirection
+ $parts = parse_url($url);
+ $sep = isset($parts['query']) && count($parts['query']) > 0 ? '&' : '?';
+ $url .= $sep . http_build_query($this->parameters);
+ }
+
+ $this->addHttpHeaders(array('Location' => $url));
+
+ if (!$this->isRedirection()) {
+ throw new \InvalidArgumentException(sprintf('The HTTP status code is not a redirect ("%s" given).', $statusCode));
+ }
+ }
+
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+ /**
+ * @return Boolean
+ *
+ * @api
+ */
+ public function isInvalid()
+ {
+ return $this->statusCode < 100 || $this->statusCode >= 600;
+ }
+
+ /**
+ * @return Boolean
+ *
+ * @api
+ */
+ public function isInformational()
+ {
+ return $this->statusCode >= 100 && $this->statusCode < 200;
+ }
+
+ /**
+ * @return Boolean
+ *
+ * @api
+ */
+ public function isSuccessful()
+ {
+ return $this->statusCode >= 200 && $this->statusCode < 300;
+ }
+
+ /**
+ * @return Boolean
+ *
+ * @api
+ */
+ public function isRedirection()
+ {
+ return $this->statusCode >= 300 && $this->statusCode < 400;
+ }
+
+ /**
+ * @return Boolean
+ *
+ * @api
+ */
+ public function isClientError()
+ {
+ return $this->statusCode >= 400 && $this->statusCode < 500;
+ }
+
+ /**
+ * @return Boolean
+ *
+ * @api
+ */
+ public function isServerError()
+ {
+ return $this->statusCode >= 500 && $this->statusCode < 600;
+ }
+
+ /*
+ * Functions from Symfony2 HttpFoundation - output pretty header
+ */
+ private function getHttpHeadersAsString($headers)
+ {
+ if (count($headers) == 0) {
+ return '';
+ }
+
+ $max = max(array_map('strlen', array_keys($headers))) + 1;
+ $content = '';
+ ksort($headers);
+ foreach ($headers as $name => $values) {
+ foreach ($values as $value) {
+ $content .= sprintf("%-{$max}s %s\r\n", $this->beautifyHeaderName($name).':', $value);
+ }
+ }
+
+ return $content;
+ }
+
+ private function beautifyHeaderName($name)
+ {
+ return preg_replace_callback('/\-(.)/', array($this, 'beautifyCallback'), ucfirst($name));
+ }
+
+ private function beautifyCallback($match)
+ {
+ return '-'.strtoupper($match[1]);
+ }
+}
diff --git a/library/oauth2/src/OAuth2/ResponseInterface.php b/library/oauth2/src/OAuth2/ResponseInterface.php
new file mode 100644
index 000000000..c99b5f7d1
--- /dev/null
+++ b/library/oauth2/src/OAuth2/ResponseInterface.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OAuth2;
+
+/**
+ * Interface which represents an object response. Meant to handle and display the proper OAuth2 Responses
+ * for errors and successes
+ *
+ * @see OAuth2\Response
+ */
+interface ResponseInterface
+{
+ public function addParameters(array $parameters);
+
+ public function addHttpHeaders(array $httpHeaders);
+
+ public function setStatusCode($statusCode);
+
+ public function setError($statusCode, $name, $description = null, $uri = null);
+
+ public function setRedirect($statusCode, $url, $state = null, $error = null, $errorDescription = null, $errorUri = null);
+
+ public function getParameter($name);
+}
diff --git a/library/oauth2/src/OAuth2/ResponseType/AccessToken.php b/library/oauth2/src/OAuth2/ResponseType/AccessToken.php
new file mode 100644
index 000000000..b235ad0c5
--- /dev/null
+++ b/library/oauth2/src/OAuth2/ResponseType/AccessToken.php
@@ -0,0 +1,194 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+use OAuth2\Storage\AccessTokenInterface as AccessTokenStorageInterface;
+use OAuth2\Storage\RefreshTokenInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AccessToken implements AccessTokenInterface
+{
+ protected $tokenStorage;
+ protected $refreshStorage;
+ protected $config;
+
+ /**
+ * @param OAuth2\Storage\AccessTokenInterface $tokenStorage REQUIRED Storage class for saving access token information
+ * @param OAuth2\Storage\RefreshTokenInterface $refreshStorage OPTIONAL Storage class for saving refresh token information
+ * @param array $config OPTIONAL Configuration options for the server
+ * <code>
+ * $config = array(
+ * 'token_type' => 'bearer', // token type identifier
+ * 'access_lifetime' => 3600, // time before access token expires
+ * 'refresh_token_lifetime' => 1209600, // time before refresh token expires
+ * );
+ * </endcode>
+ */
+ public function __construct(AccessTokenStorageInterface $tokenStorage, RefreshTokenInterface $refreshStorage = null, array $config = array())
+ {
+ $this->tokenStorage = $tokenStorage;
+ $this->refreshStorage = $refreshStorage;
+
+ $this->config = array_merge(array(
+ 'token_type' => 'bearer',
+ 'access_lifetime' => 3600,
+ 'refresh_token_lifetime' => 1209600,
+ ), $config);
+ }
+
+ public function getAuthorizeResponse($params, $user_id = null)
+ {
+ // build the URL to redirect to
+ $result = array('query' => array());
+
+ $params += array('scope' => null, 'state' => null);
+
+ /*
+ * a refresh token MUST NOT be included in the fragment
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-4.2.2
+ */
+ $includeRefreshToken = false;
+ $result["fragment"] = $this->createAccessToken($params['client_id'], $user_id, $params['scope'], $includeRefreshToken);
+
+ if (isset($params['state'])) {
+ $result["fragment"]["state"] = $params['state'];
+ }
+
+ return array($params['redirect_uri'], $result);
+ }
+
+ /**
+ * Handle the creation of access token, also issue refresh token if supported / desirable.
+ *
+ * @param $client_id client identifier related to the access token.
+ * @param $user_id user ID associated with the access token
+ * @param $scope OPTIONAL scopes to be stored in space-separated string.
+ * @param bool $includeRefreshToken if true, a new refresh_token will be added to the response
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-5
+ * @ingroup oauth2_section_5
+ */
+ public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true)
+ {
+ $token = array(
+ "access_token" => $this->generateAccessToken(),
+ "expires_in" => $this->config['access_lifetime'],
+ "token_type" => $this->config['token_type'],
+ "scope" => $scope
+ );
+
+ $this->tokenStorage->setAccessToken($token["access_token"], $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope);
+
+ /*
+ * Issue a refresh token also, if we support them
+ *
+ * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface
+ * is supplied in the constructor
+ */
+ if ($includeRefreshToken && $this->refreshStorage) {
+ $token["refresh_token"] = $this->generateRefreshToken();
+ $expires = 0;
+ if ($this->config['refresh_token_lifetime'] > 0) {
+ $expires = time() + $this->config['refresh_token_lifetime'];
+ }
+ $this->refreshStorage->setRefreshToken($token['refresh_token'], $client_id, $user_id, $expires, $scope);
+ }
+
+ return $token;
+ }
+
+ /**
+ * Generates an unique access token.
+ *
+ * Implementing classes may want to override this function to implement
+ * other access token generation schemes.
+ *
+ * @return
+ * An unique access token.
+ *
+ * @ingroup oauth2_section_4
+ */
+ protected function generateAccessToken()
+ {
+ if (function_exists('mcrypt_create_iv')) {
+ $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM);
+ if ($randomData !== false && strlen($randomData) === 20) {
+ return bin2hex($randomData);
+ }
+ }
+ if (function_exists('openssl_random_pseudo_bytes')) {
+ $randomData = openssl_random_pseudo_bytes(20);
+ if ($randomData !== false && strlen($randomData) === 20) {
+ return bin2hex($randomData);
+ }
+ }
+ if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data
+ $randomData = file_get_contents('/dev/urandom', false, null, 0, 20);
+ if ($randomData !== false && strlen($randomData) === 20) {
+ return bin2hex($randomData);
+ }
+ }
+ // Last resort which you probably should just get rid of:
+ $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true);
+
+ return substr(hash('sha512', $randomData), 0, 40);
+ }
+
+ /**
+ * Generates an unique refresh token
+ *
+ * Implementing classes may want to override this function to implement
+ * other refresh token generation schemes.
+ *
+ * @return
+ * An unique refresh.
+ *
+ * @ingroup oauth2_section_4
+ * @see OAuth2::generateAccessToken()
+ */
+ protected function generateRefreshToken()
+ {
+ return $this->generateAccessToken(); // let's reuse the same scheme for token generation
+ }
+
+ /**
+ * Handle the revoking of refresh tokens, and access tokens if supported / desirable
+ * RFC7009 specifies that "If the server is unable to locate the token using
+ * the given hint, it MUST extend its search across all of its supported token types"
+ *
+ * @param $token
+ * @param null $tokenTypeHint
+ * @return boolean
+ */
+ public function revokeToken($token, $tokenTypeHint = null)
+ {
+ if ($tokenTypeHint == 'refresh_token') {
+ if ($this->refreshStorage && $revoked = $this->refreshStorage->unsetRefreshToken($token)) {
+ return true;
+ }
+ }
+
+ /** @TODO remove in v2 */
+ if (!method_exists($this->tokenStorage, 'unsetAccessToken')) {
+ throw new \RuntimeException(
+ sprintf('Token storage %s must implement unsetAccessToken method', get_class($this->tokenStorage)
+ ));
+ }
+
+ $revoked = $this->tokenStorage->unsetAccessToken($token);
+
+ // if a typehint is supplied and fails, try other storages
+ // @see https://tools.ietf.org/html/rfc7009#section-2.1
+ if (!$revoked && $tokenTypeHint != 'refresh_token') {
+ if ($this->refreshStorage) {
+ $revoked = $this->refreshStorage->unsetRefreshToken($token);
+ }
+ }
+
+ return $revoked;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/ResponseType/AccessTokenInterface.php b/library/oauth2/src/OAuth2/ResponseType/AccessTokenInterface.php
new file mode 100644
index 000000000..4bd3928d8
--- /dev/null
+++ b/library/oauth2/src/OAuth2/ResponseType/AccessTokenInterface.php
@@ -0,0 +1,34 @@
+<?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 $client_id client identifier related to the access token.
+ * @param $user_id user ID associated with the access token
+ * @param $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);
+}
diff --git a/library/oauth2/src/OAuth2/ResponseType/AuthorizationCode.php b/library/oauth2/src/OAuth2/ResponseType/AuthorizationCode.php
new file mode 100644
index 000000000..6a305fd75
--- /dev/null
+++ b/library/oauth2/src/OAuth2/ResponseType/AuthorizationCode.php
@@ -0,0 +1,100 @@
+<?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('mcrypt_create_iv')) {
+ $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM);
+ } elseif (function_exists('openssl_random_pseudo_bytes')) {
+ $randomData = openssl_random_pseudo_bytes(100);
+ } 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);
+ }
+}
diff --git a/library/oauth2/src/OAuth2/ResponseType/AuthorizationCodeInterface.php b/library/oauth2/src/OAuth2/ResponseType/AuthorizationCodeInterface.php
new file mode 100644
index 000000000..df777e221
--- /dev/null
+++ b/library/oauth2/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 $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);
+}
diff --git a/library/oauth2/src/OAuth2/ResponseType/JwtAccessToken.php b/library/oauth2/src/OAuth2/ResponseType/JwtAccessToken.php
new file mode 100644
index 000000000..3942fe41e
--- /dev/null
+++ b/library/oauth2/src/OAuth2/ResponseType/JwtAccessToken.php
@@ -0,0 +1,124 @@
+<?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 $config
+ * - store_encrypted_token_string (bool true)
+ * whether the entire encrypted string is stored,
+ * or just the token ID is stored
+ */
+ 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 $client_id
+ * Client identifier related to the access token.
+ * @param $user_id
+ * User ID associated with the access token
+ * @param $scope
+ * (optional) Scopes to be stored in space-separated string.
+ * @param bool $includeRefreshToken
+ * If true, a new refresh_token will be added to the response
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-5
+ * @ingroup oauth2_section_5
+ */
+ public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true)
+ {
+ // token to encrypt
+ $expires = time() + $this->config['access_lifetime'];
+ $id = $this->generateAccessToken();
+ $jwtAccessToken = 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
+ );
+
+ /*
+ * Encode the token data into a single access_token string
+ */
+ $access_token = $this->encodeToken($jwtAccessToken, $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 : $jwtAccessToken['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;
+ }
+
+ 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);
+ }
+}
diff --git a/library/oauth2/src/OAuth2/ResponseType/ResponseTypeInterface.php b/library/oauth2/src/OAuth2/ResponseType/ResponseTypeInterface.php
new file mode 100644
index 000000000..f8e26a5b0
--- /dev/null
+++ b/library/oauth2/src/OAuth2/ResponseType/ResponseTypeInterface.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+interface ResponseTypeInterface
+{
+ public function getAuthorizeResponse($params, $user_id = null);
+}
diff --git a/library/oauth2/src/OAuth2/Scope.php b/library/oauth2/src/OAuth2/Scope.php
new file mode 100644
index 000000000..c44350bfd
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Scope.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace OAuth2;
+
+use OAuth2\Storage\Memory;
+use OAuth2\Storage\ScopeInterface as ScopeStorageInterface;
+
+/**
+* @see OAuth2\ScopeInterface
+*/
+class Scope implements ScopeInterface
+{
+ protected $storage;
+
+ /**
+ * @param mixed @storage
+ * Either an array of supported scopes, or an instance of OAuth2\Storage\ScopeInterface
+ */
+ 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 $required_scope
+ * A space-separated string of scopes.
+ *
+ * @return
+ * 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 $scope
+ * A space-separated string of scopes.
+ *
+ * @return
+ * 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);
+ }
+ }
+
+ public function getScopeFromRequest(RequestInterface $request)
+ {
+ // "scope" is valid if passed in either POST or QUERY
+ return $request->request('scope', $request->query('scope'));
+ }
+
+ 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
+ * An array of reserved scopes.
+ */
+ public function getReservedScopes()
+ {
+ return array('openid', 'offline_access');
+ }
+}
diff --git a/library/oauth2/src/OAuth2/ScopeInterface.php b/library/oauth2/src/OAuth2/ScopeInterface.php
new file mode 100644
index 000000000..5b60f9aee
--- /dev/null
+++ b/library/oauth2/src/OAuth2/ScopeInterface.php
@@ -0,0 +1,40 @@
+<?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 $required_scope
+ * A space-separated string of scopes.
+ *
+ * @return
+ * 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 OAuth2\RequestInterface
+ * Request object to check
+ *
+ * @return
+ * string representation of requested scope
+ */
+ public function getScopeFromRequest(RequestInterface $request);
+}
diff --git a/library/oauth2/src/OAuth2/Server.php b/library/oauth2/src/OAuth2/Server.php
new file mode 100644
index 000000000..171a4f069
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Server.php
@@ -0,0 +1,832 @@
+<?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\JwtAccessToken as JwtAccessTokenStorage;
+use OAuth2\Storage\JwtAccessTokenInterface;
+
+/**
+* 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
+{
+ // misc properties
+ protected $response;
+ protected $config;
+ protected $storages;
+
+ // servers
+ protected $authorizeController;
+ protected $tokenController;
+ protected $resourceController;
+ protected $userInfoController;
+
+ // config classes
+ protected $grantTypes;
+ protected $responseTypes;
+ protected $tokenType;
+ protected $scopeUtil;
+ protected $clientAssertionType;
+
+ 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',
+ );
+
+ 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 and "token" for
+ * Access Token and Authorization Code response types
+ * @param OAuth2\TokenType\TokenTypeInterface $tokenType The token type object to use. Valid token types are "bearer" and "mac"
+ * @param OAuth2\ScopeInterface $scopeUtil The scope utility class to use to validate scope
+ * @param OAuth2\ClientAssertionType\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,
+ '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();
+ }
+ }
+
+ public function getAuthorizeController()
+ {
+ if (is_null($this->authorizeController)) {
+ $this->authorizeController = $this->createDefaultAuthorizeController();
+ }
+
+ return $this->authorizeController;
+ }
+
+ public function getTokenController()
+ {
+ if (is_null($this->tokenController)) {
+ $this->tokenController = $this->createDefaultTokenController();
+ }
+
+ return $this->tokenController;
+ }
+
+ public function getResourceController()
+ {
+ if (is_null($this->resourceController)) {
+ $this->resourceController = $this->createDefaultResourceController();
+ }
+
+ return $this->resourceController;
+ }
+
+ public function getUserInfoController()
+ {
+ if (is_null($this->userInfoController)) {
+ $this->userInfoController = $this->createDefaultUserInfoController();
+ }
+
+ return $this->userInfoController;
+ }
+
+ /**
+ * every getter deserves a setter
+ */
+ public function setAuthorizeController(AuthorizeControllerInterface $authorizeController)
+ {
+ $this->authorizeController = $authorizeController;
+ }
+
+ /**
+ * every getter deserves a setter
+ */
+ public function setTokenController(TokenControllerInterface $tokenController)
+ {
+ $this->tokenController = $tokenController;
+ }
+
+ /**
+ * every getter deserves a setter
+ */
+ public function setResourceController(ResourceControllerInterface $resourceController)
+ {
+ $this->resourceController = $resourceController;
+ }
+
+ /**
+ * every getter deserves a setter
+ */
+ 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 $request - OAuth2\RequestInterface
+ * Request object to grant access token
+ *
+ * @param $response - OAuth2\ResponseInterface
+ * Response object containing error messages (failure) or user claims (success)
+ *
+ * @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 $request - OAuth2\RequestInterface
+ * Request object to grant access token
+ *
+ * @param $response - OAuth2\ResponseInterface
+ * Response object containing error messages (failure) or access token (success)
+ *
+ * @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;
+ }
+
+ 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 $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 $is_authorized
+ * TRUE or FALSE depending on whether the user authorized the access.
+ * @param $user_id
+ * Identifier of user who authorized the client
+ *
+ * @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
+ *
+ * @return
+ * 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;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ 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 $storage
+ * An object implementing one of the Storage interfaces
+ * @param $key
+ * If null, the storage is set to the key of each storage interface it implements
+ *
+ * @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 \OAuth2\Storage\ClientCredentialsInterface) {
+ $this->storages['client_credentials'] = $storage;
+ }
+ } elseif ($key === 'client_credentials' && !isset($this->storages['client'])) {
+ if ($storage instanceof \OAuth2\Storage\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)));
+ }
+ }
+ }
+
+ 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)));
+ }
+ }
+ }
+
+ public function getScopeUtil()
+ {
+ if (!$this->scopeUtil) {
+ $storage = isset($this->storages['scope']) ? $this->storages['scope'] : null;
+ $this->scopeUtil = new Scope($storage);
+ }
+
+ return $this->scopeUtil;
+ }
+
+ /**
+ * every getter deserves a setter
+ */
+ public function setScopeUtil($scopeUtil)
+ {
+ $this->scopeUtil = $scopeUtil;
+ }
+
+ 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());
+ }
+
+ 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());
+ }
+
+ 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());
+ }
+
+ 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());
+ }
+
+ protected function getDefaultTokenType()
+ {
+ $config = array_intersect_key($this->config, array_flip(explode(' ', 'token_param_name token_bearer_header_name')));
+
+ return new Bearer($config);
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ 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();
+ }
+
+ protected function getIdTokenResponseType()
+ {
+ if (isset($this->responseTypes['id_token'])) {
+ return $this->responseTypes['id_token'];
+ }
+
+ return $this->createDefaultIdTokenResponseType();
+ }
+
+ protected function getIdTokenTokenResponseType()
+ {
+ if (isset($this->responseTypes['id_token token'])) {
+ return $this->responseTypes['id_token token'];
+ }
+
+ return $this->createDefaultIdTokenTokenResponseType();
+ }
+
+ /**
+ * For Resource Controller
+ */
+ 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
+ */
+ 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')));
+
+ return new JwtAccessToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config);
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+
+ protected function createDefaultIdTokenTokenResponseType()
+ {
+ return new IdTokenToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType());
+ }
+
+ 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.');
+ }
+ }
+
+ 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;
+ }
+
+ public function getResponse()
+ {
+ return $this->response;
+ }
+
+ public function getStorages()
+ {
+ return $this->storages;
+ }
+
+ public function getStorage($name)
+ {
+ return isset($this->storages[$name]) ? $this->storages[$name] : null;
+ }
+
+ public function getGrantTypes()
+ {
+ return $this->grantTypes;
+ }
+
+ public function getGrantType($name)
+ {
+ return isset($this->grantTypes[$name]) ? $this->grantTypes[$name] : null;
+ }
+
+ public function getResponseTypes()
+ {
+ return $this->responseTypes;
+ }
+
+ 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;
+ }
+
+ public function getTokenType()
+ {
+ return $this->tokenType;
+ }
+
+ public function getClientAssertionType()
+ {
+ return $this->clientAssertionType;
+ }
+
+ public function setConfig($name, $value)
+ {
+ $this->config[$name] = $value;
+ }
+
+ public function getConfig($name, $default = null)
+ {
+ return isset($this->config[$name]) ? $this->config[$name] : $default;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Storage/AccessTokenInterface.php b/library/oauth2/src/OAuth2/Storage/AccessTokenInterface.php
new file mode 100644
index 000000000..1819158af
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Storage/AccessTokenInterface.php
@@ -0,0 +1,64 @@
+<?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 $oauth_token
+ * oauth_token to be check with.
+ *
+ * @return
+ * An associative array as below, and return NULL if the supplied oauth_token
+ * is invalid:
+ * - expires: Stored expiration in unix timestamp.
+ * - client_id: (optional) Stored client identifier.
+ * - user_id: (optional) Stored user identifier.
+ * - scope: (optional) Stored scope values in space-separated string.
+ * - id_token: (optional) Stored id_token (if "use_openid_connect" is true).
+ *
+ * @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 $oauth_token oauth_token to be stored.
+ * @param $client_id client identifier to be stored.
+ * @param $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);
+}
diff --git a/library/oauth2/src/OAuth2/Storage/AuthorizationCodeInterface.php b/library/oauth2/src/OAuth2/Storage/AuthorizationCodeInterface.php
new file mode 100644
index 000000000..3beb0e437
--- /dev/null
+++ b/library/oauth2/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 exipired
+ *
+ * @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);
+}
diff --git a/library/oauth2/src/OAuth2/Storage/Cassandra.php b/library/oauth2/src/OAuth2/Storage/Cassandra.php
new file mode 100644
index 000000000..602e8a058
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Storage/Cassandra.php
@@ -0,0 +1,480 @@
+<?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;
+
+/**
+ * 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
+ * <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;
+
+ /* The cassandra client */
+ protected $cassandra;
+
+ /* Configuration array */
+ protected $config;
+
+ /**
+ * Cassandra Storage! uses phpCassa
+ *
+ * @param \phpcassa\ConnectionPool $cassandra
+ * @param array $config
+ */
+ 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);
+ }
+
+ 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);
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ /* 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)
+ {
+ if ($user = $this->getUser($username)) {
+ return $this->checkPassword($user, $password);
+ }
+
+ return false;
+ }
+
+ // 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 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)
+ {
+ $password = $this->hashPassword($password);
+
+ 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)
+ {
+ return $this->expireValue($this->config['refresh_token_key'] . $refresh_token);
+ }
+
+ /* 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)
+ {
+ return $this->expireValue($this->config['access_token_key'] . $access_token);
+ }
+
+ /* 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
+ ));
+ }
+
+ /*ScopeInterface */
+ 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 cassandra implementation.
+ throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.');
+ }
+
+ public function setJti($client_id, $subject, $audience, $expiration, $jti)
+ {
+ //TODO: Needs cassandra implementation.
+ throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.');
+ }
+
+ /* PublicKeyInterface */
+ 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'];
+ }
+ }
+
+ 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'];
+ }
+ }
+
+ 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';
+ }
+
+ /* UserClaimsInterface */
+ 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;
+ }
+
+ 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;
+ }
+
+}
diff --git a/library/oauth2/src/OAuth2/Storage/ClientCredentialsInterface.php b/library/oauth2/src/OAuth2/Storage/ClientCredentialsInterface.php
new file mode 100644
index 000000000..3318c6966
--- /dev/null
+++ b/library/oauth2/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);
+}
diff --git a/library/oauth2/src/OAuth2/Storage/ClientInterface.php b/library/oauth2/src/OAuth2/Storage/ClientInterface.php
new file mode 100644
index 000000000..09a5bffc1
--- /dev/null
+++ b/library/oauth2/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);
+}
diff --git a/library/oauth2/src/OAuth2/Storage/CouchbaseDB.php b/library/oauth2/src/OAuth2/Storage/CouchbaseDB.php
new file mode 100755
index 000000000..1eb55f027
--- /dev/null
+++ b/library/oauth2/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.');
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Storage/DynamoDB.php b/library/oauth2/src/OAuth2/Storage/DynamoDB.php
new file mode 100644
index 000000000..8347ab258
--- /dev/null
+++ b/library/oauth2/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;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Storage/JwtAccessToken.php b/library/oauth2/src/OAuth2/Storage/JwtAccessToken.php
new file mode 100644
index 000000000..75b49d301
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Storage/JwtAccessToken.php
@@ -0,0 +1,88 @@
+<?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;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Storage/JwtAccessTokenInterface.php b/library/oauth2/src/OAuth2/Storage/JwtAccessTokenInterface.php
new file mode 100644
index 000000000..3abb2aa2d
--- /dev/null
+++ b/library/oauth2/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
+{
+
+}
diff --git a/library/oauth2/src/OAuth2/Storage/JwtBearerInterface.php b/library/oauth2/src/OAuth2/Storage/JwtBearerInterface.php
new file mode 100644
index 000000000..c83aa72ea
--- /dev/null
+++ b/library/oauth2/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);
+}
diff --git a/library/oauth2/src/OAuth2/Storage/Memory.php b/library/oauth2/src/OAuth2/Storage/Memory.php
new file mode 100644
index 000000000..42d833ccb
--- /dev/null
+++ b/library/oauth2/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';
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Storage/Mongo.php b/library/oauth2/src/OAuth2/Storage/Mongo.php
new file mode 100644
index 000000000..cef35e5e9
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Storage/Mongo.php
@@ -0,0 +1,339 @@
+<?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,
+ 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',
+ '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.');
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Storage/Pdo.php b/library/oauth2/src/OAuth2/Storage/Pdo.php
new file mode 100644
index 000000000..ae5107e29
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Storage/Pdo.php
@@ -0,0 +1,553 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
+/**
+ * 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
+{
+ protected $db;
+ protected $config;
+
+ 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);
+ }
+
+ /* OAuth2\Storage\ClientCredentialsInterface */
+ 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;
+ }
+
+ 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']);
+ }
+
+ /* OAuth2\Storage\ClientInterface */
+ 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);
+ }
+
+ 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'));
+ }
+
+ 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)
+ {
+ $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;
+ }
+
+ 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'));
+ }
+
+ 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 */
+ 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;
+ }
+
+ 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'));
+ }
+
+ 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'));
+ }
+
+ 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'));
+ }
+
+ /* 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) {
+ $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
+ }
+
+ return $userClaims;
+ }
+
+ /* OAuth2\Storage\RefreshTokenInterface */
+ 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;
+ }
+
+ 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'));
+ }
+
+ 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
+ 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)
+ {
+ $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);
+ }
+
+ 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'));
+ }
+
+ /* ScopeInterface */
+ 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;
+ }
+
+ 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;
+ }
+
+ /* JWTBearerInterface */
+ 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();
+ }
+
+ 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)
+ {
+ $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;
+ }
+
+ 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'));
+ }
+
+ /* PublicKeyInterface */
+ 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'];
+ }
+ }
+
+ 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'];
+ }
+ }
+
+ 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
+ */
+ 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),
+ audience 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;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Storage/PublicKeyInterface.php b/library/oauth2/src/OAuth2/Storage/PublicKeyInterface.php
new file mode 100644
index 000000000..108418d3a
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Storage/PublicKeyInterface.php
@@ -0,0 +1,16 @@
+<?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
+{
+ public function getPublicKey($client_id = null);
+ public function getPrivateKey($client_id = null);
+ public function getEncryptionAlgorithm($client_id = null);
+}
diff --git a/library/oauth2/src/OAuth2/Storage/Redis.php b/library/oauth2/src/OAuth2/Storage/Redis.php
new file mode 100644
index 000000000..e6294e22d
--- /dev/null
+++ b/library/oauth2/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.');
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Storage/RefreshTokenInterface.php b/library/oauth2/src/OAuth2/Storage/RefreshTokenInterface.php
new file mode 100644
index 000000000..0273f2125
--- /dev/null
+++ b/library/oauth2/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 expirse.
+ *
+ * @ingroup oauth2_section_6
+ */
+ public function unsetRefreshToken($refresh_token);
+}
diff --git a/library/oauth2/src/OAuth2/Storage/ScopeInterface.php b/library/oauth2/src/OAuth2/Storage/ScopeInterface.php
new file mode 100644
index 000000000..a8292269b
--- /dev/null
+++ b/library/oauth2/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);
+}
diff --git a/library/oauth2/src/OAuth2/Storage/UserCredentialsInterface.php b/library/oauth2/src/OAuth2/Storage/UserCredentialsInterface.php
new file mode 100644
index 000000000..6e0fd7bad
--- /dev/null
+++ b/library/oauth2/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);
+
+ /**
+ * @return
+ * ARRAY 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);
+}
diff --git a/library/oauth2/src/OAuth2/TokenType/Bearer.php b/library/oauth2/src/OAuth2/TokenType/Bearer.php
new file mode 100644
index 000000000..8ac8596ac
--- /dev/null
+++ b/library/oauth2/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']);
+ }
+}
diff --git a/library/oauth2/src/OAuth2/TokenType/Mac.php b/library/oauth2/src/OAuth2/TokenType/Mac.php
new file mode 100644
index 000000000..fe6a86aa6
--- /dev/null
+++ b/library/oauth2/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");
+ }
+}
diff --git a/library/oauth2/src/OAuth2/TokenType/TokenTypeInterface.php b/library/oauth2/src/OAuth2/TokenType/TokenTypeInterface.php
new file mode 100644
index 000000000..ad77d4a25
--- /dev/null
+++ b/library/oauth2/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);
+}
diff --git a/library/oauth2/test/OAuth2/AutoloadTest.php b/library/oauth2/test/OAuth2/AutoloadTest.php
new file mode 100644
index 000000000..5901bdc42
--- /dev/null
+++ b/library/oauth2/test/OAuth2/AutoloadTest.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace OAuth2;
+
+class AutoloadTest extends \PHPUnit_Framework_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'));
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Controller/AuthorizeControllerTest.php b/library/oauth2/test/OAuth2/Controller/AuthorizeControllerTest.php
new file mode 100644
index 000000000..3bfc760e4
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Controller/AuthorizeControllerTest.php
@@ -0,0 +1,492 @@
+<?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;
+
+class AuthorizeControllerTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Controller/ResourceControllerTest.php b/library/oauth2/test/OAuth2/Controller/ResourceControllerTest.php
new file mode 100644
index 000000000..ee6d96ff8
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Controller/ResourceControllerTest.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\GrantType\AuthorizationCode;
+use OAuth2\Request;
+use OAuth2\Response;
+
+class ResourceControllerTest extends \PHPUnit_Framework_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'));
+ }
+
+ 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'), 'expired_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Controller/TokenControllerTest.php b/library/oauth2/test/OAuth2/Controller/TokenControllerTest.php
new file mode 100644
index 000000000..4a217bd55
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Controller/TokenControllerTest.php
@@ -0,0 +1,289 @@
+<?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;
+
+class TokenControllerTest extends \PHPUnit_Framework_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 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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Encryption/FirebaseJwtTest.php b/library/oauth2/test/OAuth2/Encryption/FirebaseJwtTest.php
new file mode 100644
index 000000000..d34136767
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Encryption/FirebaseJwtTest.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace OAuth2\Encryption;
+
+use OAuth2\Storage\Bootstrap;
+
+class FirebaseJwtTest extends \PHPUnit_Framework_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),
+ );
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Encryption/JwtTest.php b/library/oauth2/test/OAuth2/Encryption/JwtTest.php
new file mode 100644
index 000000000..214eebac8
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Encryption/JwtTest.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace OAuth2\Encryption;
+
+use OAuth2\Storage\Bootstrap;
+
+class JwtTest extends \PHPUnit_Framework_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),
+ );
+ }
+}
diff --git a/library/oauth2/test/OAuth2/GrantType/AuthorizationCodeTest.php b/library/oauth2/test/OAuth2/GrantType/AuthorizationCodeTest.php
new file mode 100644
index 000000000..740989635
--- /dev/null
+++ b/library/oauth2/test/OAuth2/GrantType/AuthorizationCodeTest.php
@@ -0,0 +1,207 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+
+class AuthorizationCodeTest extends \PHPUnit_Framework_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 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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/GrantType/ClientCredentialsTest.php b/library/oauth2/test/OAuth2/GrantType/ClientCredentialsTest.php
new file mode 100644
index 000000000..f0d46ccb3
--- /dev/null
+++ b/library/oauth2/test/OAuth2/GrantType/ClientCredentialsTest.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Request;
+use OAuth2\Response;
+
+class ClientCredentialsTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/GrantType/ImplicitTest.php b/library/oauth2/test/OAuth2/GrantType/ImplicitTest.php
new file mode 100644
index 000000000..a47aae3e8
--- /dev/null
+++ b/library/oauth2/test/OAuth2/GrantType/ImplicitTest.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+
+class ImplicitTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/GrantType/JwtBearerTest.php b/library/oauth2/test/OAuth2/GrantType/JwtBearerTest.php
new file mode 100644
index 000000000..0a6c4b827
--- /dev/null
+++ b/library/oauth2/test/OAuth2/GrantType/JwtBearerTest.php
@@ -0,0 +1,360 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+use OAuth2\Encryption\Jwt;
+
+class JwtBearerTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/GrantType/RefreshTokenTest.php b/library/oauth2/test/OAuth2/GrantType/RefreshTokenTest.php
new file mode 100644
index 000000000..a458aad8a
--- /dev/null
+++ b/library/oauth2/test/OAuth2/GrantType/RefreshTokenTest.php
@@ -0,0 +1,204 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+
+class RefreshTokenTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/GrantType/UserCredentialsTest.php b/library/oauth2/test/OAuth2/GrantType/UserCredentialsTest.php
new file mode 100644
index 000000000..18943d055
--- /dev/null
+++ b/library/oauth2/test/OAuth2/GrantType/UserCredentialsTest.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace OAuth2\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+
+class UserCredentialsTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php b/library/oauth2/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php
new file mode 100644
index 000000000..46de936d8
--- /dev/null
+++ b/library/oauth2/test/OAuth2/OpenID/Controller/AuthorizeControllerTest.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+
+class AuthorizeControllerTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php b/library/oauth2/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php
new file mode 100644
index 000000000..b1b687077
--- /dev/null
+++ b/library/oauth2/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+
+class UserInfoControllerTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php b/library/oauth2/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php
new file mode 100644
index 000000000..776002d1e
--- /dev/null
+++ b/library/oauth2/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace OAuth2\OpenID\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+
+class AuthorizationCodeTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php b/library/oauth2/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php
new file mode 100644
index 000000000..b0311434a
--- /dev/null
+++ b/library/oauth2/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+use OAuth2\Storage\Bootstrap;
+use OAuth2\GrantType\ClientCredentials;
+
+class CodeIdTokenTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/OpenID/ResponseType/IdTokenTest.php b/library/oauth2/test/OAuth2/OpenID/ResponseType/IdTokenTest.php
new file mode 100644
index 000000000..e772f6be4
--- /dev/null
+++ b/library/oauth2/test/OAuth2/OpenID/ResponseType/IdTokenTest.php
@@ -0,0 +1,184 @@
+<?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;
+
+class IdTokenTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php b/library/oauth2/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php
new file mode 100644
index 000000000..bc564d37b
--- /dev/null
+++ b/library/oauth2/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php
@@ -0,0 +1,91 @@
+<?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;
+
+class IdTokenTokenTest extends \PHPUnit_Framework_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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php b/library/oauth2/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php
new file mode 100644
index 000000000..bdfb085e3
--- /dev/null
+++ b/library/oauth2/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);
+ }
+}
diff --git a/library/oauth2/test/OAuth2/OpenID/Storage/UserClaimsTest.php b/library/oauth2/test/OAuth2/OpenID/Storage/UserClaimsTest.php
new file mode 100644
index 000000000..840f6c566
--- /dev/null
+++ b/library/oauth2/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);
+ }
+}
diff --git a/library/oauth2/test/OAuth2/RequestTest.php b/library/oauth2/test/OAuth2/RequestTest.php
new file mode 100644
index 000000000..10db3215c
--- /dev/null
+++ b/library/oauth2/test/OAuth2/RequestTest.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace OAuth2;
+
+use OAuth2\Request\TestRequest;
+use OAuth2\Storage\Bootstrap;
+use OAuth2\GrantType\AuthorizationCode;
+
+class RequestTest extends \PHPUnit_Framework_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')));
+ }
+
+ 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;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/ResponseTest.php b/library/oauth2/test/OAuth2/ResponseTest.php
new file mode 100644
index 000000000..b8149005d
--- /dev/null
+++ b/library/oauth2/test/OAuth2/ResponseTest.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace OAuth2;
+
+class ResponseTest extends \PHPUnit_Framework_TestCase
+{
+ public function testRenderAsXml()
+ {
+ $response = new Response(array(
+ 'foo' => 'bar',
+ 'halland' => 'oates',
+ ));
+
+ $string = $response->getResponseBody('xml');
+ $this->assertContains('<response><foo>bar</foo><halland>oates</halland></response>', $string);
+ }
+}
diff --git a/library/oauth2/test/OAuth2/ResponseType/AccessTokenTest.php b/library/oauth2/test/OAuth2/ResponseType/AccessTokenTest.php
new file mode 100644
index 000000000..0ed1c82fc
--- /dev/null
+++ b/library/oauth2/test/OAuth2/ResponseType/AccessTokenTest.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+use OAuth2\Server;
+use OAuth2\Storage\Memory;
+
+class AccessTokenTest extends \PHPUnit_Framework_TestCase
+{
+ public function testRevokeAccessTokenWithTypeHint()
+ {
+ $tokenStorage = new Memory(array(
+ 'access_tokens' => array(
+ 'revoke' => array('mytoken'),
+ ),
+ ));
+
+ $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke'));
+ $accessToken = new AccessToken($tokenStorage);
+ $accessToken->revokeToken('revoke', 'access_token');
+ $this->assertFalse($tokenStorage->getAccessToken('revoke'));
+ }
+
+ public function testRevokeAccessTokenWithoutTypeHint()
+ {
+ $tokenStorage = new Memory(array(
+ 'access_tokens' => array(
+ 'revoke' => array('mytoken'),
+ ),
+ ));
+
+ $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke'));
+ $accessToken = new AccessToken($tokenStorage);
+ $accessToken->revokeToken('revoke');
+ $this->assertFalse($tokenStorage->getAccessToken('revoke'));
+ }
+
+ public function testRevokeRefreshTokenWithTypeHint()
+ {
+ $tokenStorage = new Memory(array(
+ 'refresh_tokens' => array(
+ 'revoke' => array('mytoken'),
+ ),
+ ));
+
+ $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke'));
+ $accessToken = new AccessToken(new Memory, $tokenStorage);
+ $accessToken->revokeToken('revoke', 'refresh_token');
+ $this->assertFalse($tokenStorage->getRefreshToken('revoke'));
+ }
+
+ public function testRevokeRefreshTokenWithoutTypeHint()
+ {
+ $tokenStorage = new Memory(array(
+ 'refresh_tokens' => array(
+ 'revoke' => array('mytoken'),
+ ),
+ ));
+
+ $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke'));
+ $accessToken = new AccessToken(new Memory, $tokenStorage);
+ $accessToken->revokeToken('revoke');
+ $this->assertFalse($tokenStorage->getRefreshToken('revoke'));
+ }
+
+ public function testRevokeAccessTokenWithRefreshTokenTypeHint()
+ {
+ $tokenStorage = new Memory(array(
+ 'access_tokens' => array(
+ 'revoke' => array('mytoken'),
+ ),
+ ));
+
+ $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke'));
+ $accessToken = new AccessToken($tokenStorage);
+ $accessToken->revokeToken('revoke', 'refresh_token');
+ $this->assertFalse($tokenStorage->getAccessToken('revoke'));
+ }
+
+ public function testRevokeAccessTokenWithBogusTypeHint()
+ {
+ $tokenStorage = new Memory(array(
+ 'access_tokens' => array(
+ 'revoke' => array('mytoken'),
+ ),
+ ));
+
+ $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke'));
+ $accessToken = new AccessToken($tokenStorage);
+ $accessToken->revokeToken('revoke', 'foo');
+ $this->assertFalse($tokenStorage->getAccessToken('revoke'));
+ }
+
+ public function testRevokeRefreshTokenWithBogusTypeHint()
+ {
+ $tokenStorage = new Memory(array(
+ 'refresh_tokens' => array(
+ 'revoke' => array('mytoken'),
+ ),
+ ));
+
+ $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke'));
+ $accessToken = new AccessToken(new Memory, $tokenStorage);
+ $accessToken->revokeToken('revoke', 'foo');
+ $this->assertFalse($tokenStorage->getRefreshToken('revoke'));
+ }
+}
diff --git a/library/oauth2/test/OAuth2/ResponseType/JwtAccessTokenTest.php b/library/oauth2/test/OAuth2/ResponseType/JwtAccessTokenTest.php
new file mode 100644
index 000000000..51b01a927
--- /dev/null
+++ b/library/oauth2/test/OAuth2/ResponseType/JwtAccessTokenTest.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace OAuth2\ResponseType;
+
+use OAuth2\Server;
+use OAuth2\Response;
+use OAuth2\Request\TestRequest;
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Storage\JwtAccessToken as JwtAccessTokenStorage;
+use OAuth2\GrantType\ClientCredentials;
+use OAuth2\GrantType\UserCredentials;
+use OAuth2\GrantType\RefreshToken;
+use OAuth2\Encryption\Jwt;
+
+class JwtAccessTokenTest extends \PHPUnit_Framework_TestCase
+{
+ public function testCreateAccessToken()
+ {
+ $server = $this->getTestServer();
+ $jwtResponseType = $server->getResponseType('token');
+
+ $accessToken = $jwtResponseType->createAccessToken('Test Client ID', 123, 'test', false);
+ $jwt = new Jwt;
+ $decodedAccessToken = $jwt->decode($accessToken['access_token'], null, false);
+
+ $this->assertArrayHasKey('id', $decodedAccessToken);
+ $this->assertArrayHasKey('jti', $decodedAccessToken);
+ $this->assertArrayHasKey('iss', $decodedAccessToken);
+ $this->assertArrayHasKey('aud', $decodedAccessToken);
+ $this->assertArrayHasKey('exp', $decodedAccessToken);
+ $this->assertArrayHasKey('iat', $decodedAccessToken);
+ $this->assertArrayHasKey('token_type', $decodedAccessToken);
+ $this->assertArrayHasKey('scope', $decodedAccessToken);
+
+ $this->assertEquals('https://api.example.com', $decodedAccessToken['iss']);
+ $this->assertEquals('Test Client ID', $decodedAccessToken['aud']);
+ $this->assertEquals(123, $decodedAccessToken['sub']);
+ $delta = $decodedAccessToken['exp'] - $decodedAccessToken['iat'];
+ $this->assertEquals(3600, $delta);
+ $this->assertEquals($decodedAccessToken['id'], $decodedAccessToken['jti']);
+ }
+
+ public function testGrantJwtAccessToken()
+ {
+ // add the test parameters in memory
+ $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
+ ));
+ $server->handleTokenRequest($request, $response = new Response());
+
+ $this->assertNotNull($response->getParameter('access_token'));
+ $this->assertEquals(2, substr_count($response->getParameter('access_token'), '.'));
+ }
+
+ public function testAccessResourceWithJwtAccessToken()
+ {
+ // add the test parameters in memory
+ $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
+ ));
+ $server->handleTokenRequest($request, $response = new Response());
+ $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token'));
+
+ // make a call to the resource server using the crypto token
+ $request = TestRequest::createPost(array(
+ 'access_token' => $JwtAccessToken,
+ ));
+
+ $this->assertTrue($server->verifyResourceRequest($request));
+ }
+
+ public function testAccessResourceWithJwtAccessTokenUsingSecondaryStorage()
+ {
+ // add the test parameters in memory
+ $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
+ ));
+ $server->handleTokenRequest($request, $response = new Response());
+ $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token'));
+
+ // make a call to the resource server using the crypto token
+ $request = TestRequest::createPost(array(
+ 'access_token' => $JwtAccessToken,
+ ));
+
+ // create a resource server with the "memory" storage from the grant server
+ $resourceServer = new Server($server->getStorage('client_credentials'));
+
+ $this->assertTrue($resourceServer->verifyResourceRequest($request));
+ }
+
+ public function testJwtAccessTokenWithRefreshToken()
+ {
+ $server = $this->getTestServer();
+
+ // add "UserCredentials" grant type and "JwtAccessToken" response type
+ // and ensure "JwtAccessToken" response type has "RefreshToken" storage
+ $memoryStorage = Bootstrap::getInstance()->getMemoryStorage();
+ $server->addGrantType(new UserCredentials($memoryStorage));
+ $server->addGrantType(new RefreshToken($memoryStorage));
+ $server->addResponseType(new JwtAccessToken($memoryStorage, $memoryStorage, $memoryStorage), 'token');
+
+ $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
+ ));
+
+ // make the call to grant a crypto token
+ $server->handleTokenRequest($request, $response = new Response());
+ $this->assertNotNull($JwtAccessToken = $response->getParameter('access_token'));
+ $this->assertNotNull($refreshToken = $response->getParameter('refresh_token'));
+
+ // decode token and make sure refresh_token isn't set
+ list($header, $payload, $signature) = explode('.', $JwtAccessToken);
+ $decodedToken = json_decode(base64_decode($payload), true);
+ $this->assertFalse(array_key_exists('refresh_token', $decodedToken));
+
+ // use the refresh token to get another access token
+ $request = TestRequest::createPost(array(
+ 'grant_type' => 'refresh_token',
+ 'client_id' => 'Test Client ID', // valid client id
+ 'client_secret' => 'TestSecret', // valid client secret
+ 'refresh_token' => $refreshToken,
+ ));
+
+ $server->handleTokenRequest($request, $response = new Response());
+ $this->assertNotNull($response->getParameter('access_token'));
+ }
+
+ private function getTestServer()
+ {
+ $memoryStorage = Bootstrap::getInstance()->getMemoryStorage();
+
+ $storage = array(
+ 'access_token' => new JwtAccessTokenStorage($memoryStorage),
+ 'client' => $memoryStorage,
+ 'client_credentials' => $memoryStorage,
+ );
+ $server = new Server($storage);
+ $server->addGrantType(new ClientCredentials($memoryStorage));
+
+ // make the "token" response type a JwtAccessToken
+ $config = array('issuer' => 'https://api.example.com');
+ $server->addResponseType(new JwtAccessToken($memoryStorage, $memoryStorage, null, $config));
+
+ return $server;
+ }
+}
diff --git a/library/oauth2/test/OAuth2/ScopeTest.php b/library/oauth2/test/OAuth2/ScopeTest.php
new file mode 100644
index 000000000..99f9cf6eb
--- /dev/null
+++ b/library/oauth2/test/OAuth2/ScopeTest.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace OAuth2;
+
+use OAuth2\Storage\Memory;
+
+class ScopeTest extends \PHPUnit_Framework_TestCase
+{
+ public function testCheckScope()
+ {
+ $scopeUtil = new Scope();
+
+ $this->assertFalse($scopeUtil->checkScope('invalid', 'list of scopes'));
+ $this->assertTrue($scopeUtil->checkScope('valid', 'valid and-some other-scopes'));
+ $this->assertTrue($scopeUtil->checkScope('valid another-valid', 'valid another-valid and-some other-scopes'));
+ // all scopes must match
+ $this->assertFalse($scopeUtil->checkScope('valid invalid', 'valid and-some other-scopes'));
+ $this->assertFalse($scopeUtil->checkScope('valid valid2 invalid', 'valid valid2 and-some other-scopes'));
+ }
+
+ public function testScopeStorage()
+ {
+ $scopeUtil = new Scope();
+ $this->assertEquals($scopeUtil->getDefaultScope(), null);
+
+ $scopeUtil = new Scope(array(
+ 'default_scope' => 'default',
+ 'supported_scopes' => array('this', 'that', 'another'),
+ ));
+ $this->assertEquals($scopeUtil->getDefaultScope(), 'default');
+ $this->assertTrue($scopeUtil->scopeExists('this that another', 'client_id'));
+
+ $memoryStorage = new Memory(array(
+ 'default_scope' => 'base',
+ 'supported_scopes' => array('only-this-one'),
+ ));
+ $scopeUtil = new Scope($memoryStorage);
+
+ $this->assertEquals($scopeUtil->getDefaultScope(), 'base');
+ $this->assertTrue($scopeUtil->scopeExists('only-this-one', 'client_id'));
+ }
+}
diff --git a/library/oauth2/test/OAuth2/ServerTest.php b/library/oauth2/test/OAuth2/ServerTest.php
new file mode 100644
index 000000000..747e120f5
--- /dev/null
+++ b/library/oauth2/test/OAuth2/ServerTest.php
@@ -0,0 +1,684 @@
+<?php
+
+namespace OAuth2;
+
+use OAuth2\Request\TestRequest;
+use OAuth2\ResponseType\AuthorizationCode;
+use OAuth2\Storage\Bootstrap;
+
+class ServerTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @expectedException LogicException OAuth2\Storage\ClientInterface
+ **/
+ public function testGetAuthorizeControllerWithNoClientStorageThrowsException()
+ {
+ // must set Client Storage
+ $server = new Server();
+ $server->getAuthorizeController();
+ }
+
+ /**
+ * @expectedException LogicException OAuth2\Storage\AccessTokenInterface
+ **/
+ public function testGetAuthorizeControllerWithNoAccessTokenStorageThrowsException()
+ {
+ // must set AccessToken or AuthorizationCode
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface'));
+ $server->getAuthorizeController();
+ }
+
+ public function testGetAuthorizeControllerWithClientStorageAndAccessTokenResponseType()
+ {
+ // must set AccessToken or AuthorizationCode
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface'));
+ $server->addResponseType($this->getMock('OAuth2\ResponseType\AccessTokenInterface'));
+
+ $this->assertNotNull($server->getAuthorizeController());
+ }
+
+ public function testGetAuthorizeControllerWithClientStorageAndAuthorizationCodeResponseType()
+ {
+ // must set AccessToken or AuthorizationCode
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface'));
+ $server->addResponseType($this->getMock('OAuth2\ResponseType\AuthorizationCodeInterface'));
+
+ $this->assertNotNull($server->getAuthorizeController());
+ }
+
+ /**
+ * @expectedException LogicException allow_implicit
+ **/
+ public function testGetAuthorizeControllerWithClientStorageAndAccessTokenStorageThrowsException()
+ {
+ // must set AuthorizationCode or AccessToken / implicit
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface'));
+ $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'));
+
+ $this->assertNotNull($server->getAuthorizeController());
+ }
+
+ public function testGetAuthorizeControllerWithClientStorageAndAccessTokenStorage()
+ {
+ // must set AuthorizationCode or AccessToken / implicit
+ $server = new Server(array(), array('allow_implicit' => true));
+ $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface'));
+ $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'));
+
+ $this->assertNotNull($server->getAuthorizeController());
+ }
+
+ public function testGetAuthorizeControllerWithClientStorageAndAuthorizationCodeStorage()
+ {
+ // must set AccessToken or AuthorizationCode
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\ClientInterface'));
+ $server->addStorage($this->getMock('OAuth2\Storage\AuthorizationCodeInterface'));
+
+ $this->assertNotNull($server->getAuthorizeController());
+ }
+
+ /**
+ * @expectedException LogicException grant_types
+ **/
+ public function testGetTokenControllerWithGrantTypeStorageThrowsException()
+ {
+ $server = new Server();
+ $server->getTokenController();
+ }
+
+ /**
+ * @expectedException LogicException OAuth2\Storage\ClientCredentialsInterface
+ **/
+ public function testGetTokenControllerWithNoClientCredentialsStorageThrowsException()
+ {
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\UserCredentialsInterface'));
+ $server->getTokenController();
+ }
+
+ /**
+ * @expectedException LogicException OAuth2\Storage\AccessTokenInterface
+ **/
+ public function testGetTokenControllerWithNoAccessTokenStorageThrowsException()
+ {
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface'));
+ $server->getTokenController();
+ }
+
+ public function testGetTokenControllerWithAccessTokenAndClientCredentialsStorage()
+ {
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'));
+ $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface'));
+ $server->getTokenController();
+ }
+
+ public function testGetTokenControllerAccessTokenStorageAndClientCredentialsStorageAndGrantTypes()
+ {
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'));
+ $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface'));
+ $server->addGrantType($this->getMockBuilder('OAuth2\GrantType\AuthorizationCode')->disableOriginalConstructor()->getMock());
+ $server->getTokenController();
+ }
+
+ /**
+ * @expectedException LogicException OAuth2\Storage\AccessTokenInterface
+ **/
+ public function testGetResourceControllerWithNoAccessTokenStorageThrowsException()
+ {
+ $server = new Server();
+ $server->getResourceController();
+ }
+
+ public function testGetResourceControllerWithAccessTokenStorage()
+ {
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'));
+ $server->getResourceController();
+ }
+
+ /**
+ * @expectedException InvalidArgumentException OAuth2\Storage\AccessTokenInterface
+ **/
+ public function testAddingStorageWithInvalidClass()
+ {
+ $server = new Server();
+ $server->addStorage(new \StdClass());
+ }
+
+ /**
+ * @expectedException InvalidArgumentException access_token
+ **/
+ public function testAddingStorageWithInvalidKey()
+ {
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'), 'nonexistant_storage');
+ }
+
+ /**
+ * @expectedException InvalidArgumentException OAuth2\Storage\AuthorizationCodeInterface
+ **/
+ public function testAddingStorageWithInvalidKeyStorageCombination()
+ {
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'), 'authorization_code');
+ }
+
+ public function testAddingStorageWithValidKeyOnlySetsThatKey()
+ {
+ $server = new Server();
+ $server->addStorage($this->getMock('OAuth2\Storage\Memory'), 'access_token');
+
+ $reflection = new \ReflectionClass($server);
+ $prop = $reflection->getProperty('storages');
+ $prop->setAccessible(true);
+
+ $storages = $prop->getValue($server); // get the private "storages" property
+
+ $this->assertEquals(1, count($storages));
+ $this->assertTrue(isset($storages['access_token']));
+ $this->assertFalse(isset($storages['authorization_code']));
+ }
+
+ public function testAddingClientStorageSetsClientCredentialsStorageByDefault()
+ {
+ $server = new Server();
+ $memory = $this->getMock('OAuth2\Storage\Memory');
+ $server->addStorage($memory, 'client');
+
+ $client_credentials = $server->getStorage('client_credentials');
+
+ $this->assertNotNull($client_credentials);
+ $this->assertEquals($client_credentials, $memory);
+ }
+
+ public function testAddStorageWithNullValue()
+ {
+ $memory = $this->getMock('OAuth2\Storage\Memory');
+ $server = new Server($memory);
+ $server->addStorage(null, 'refresh_token');
+
+ $client_credentials = $server->getStorage('client_credentials');
+
+ $this->assertNotNull($client_credentials);
+ $this->assertEquals($client_credentials, $memory);
+
+ $refresh_token = $server->getStorage('refresh_token');
+
+ $this->assertNull($refresh_token);
+ }
+
+ public function testNewServerWithNullStorageValue()
+ {
+ $memory = $this->getMock('OAuth2\Storage\Memory');
+ $server = new Server(array(
+ 'client_credentials' => $memory,
+ 'refresh_token' => null,
+ ));
+
+ $client_credentials = $server->getStorage('client_credentials');
+
+ $this->assertNotNull($client_credentials);
+ $this->assertEquals($client_credentials, $memory);
+
+ $refresh_token = $server->getStorage('refresh_token');
+
+ $this->assertNull($refresh_token);
+ }
+
+ public function testAddingClientCredentialsStorageSetsClientStorageByDefault()
+ {
+ $server = new Server();
+ $memory = $this->getMock('OAuth2\Storage\Memory');
+ $server->addStorage($memory, 'client_credentials');
+
+ $client = $server->getStorage('client');
+
+ $this->assertNotNull($client);
+ $this->assertEquals($client, $memory);
+ }
+
+ public function testSettingClientStorageByDefaultDoesNotOverrideSetStorage()
+ {
+ $server = new Server();
+ $pdo = $this->getMockBuilder('OAuth2\Storage\Pdo')
+ ->disableOriginalConstructor()->getMock();
+
+ $memory = $this->getMock('OAuth2\Storage\Memory');
+
+ $server->addStorage($pdo, 'client');
+ $server->addStorage($memory, 'client_credentials');
+
+ $client = $server->getStorage('client');
+ $client_credentials = $server->getStorage('client_credentials');
+
+ $this->assertEquals($client, $pdo);
+ $this->assertEquals($client_credentials, $memory);
+ }
+
+ public function testAddingResponseType()
+ {
+ $storage = $this->getMock('OAuth2\Storage\Memory');
+ $storage
+ ->expects($this->any())
+ ->method('getClientDetails')
+ ->will($this->returnValue(array('client_id' => 'some_client')));
+ $storage
+ ->expects($this->any())
+ ->method('checkRestrictedGrantType')
+ ->will($this->returnValue(true));
+
+ // add with the "code" key explicitly set
+ $codeType = new AuthorizationCode($storage);
+ $server = new Server();
+ $server->addStorage($storage);
+ $server->addResponseType($codeType);
+ $request = new Request(array(
+ 'response_type' => 'code',
+ 'client_id' => 'some_client',
+ 'redirect_uri' => 'http://example.com',
+ 'state' => 'xyx',
+ ));
+ $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+ // the response is successful
+ $this->assertEquals($response->getStatusCode(), 302);
+ $parts = parse_url($response->getHttpHeader('Location'));
+ parse_str($parts['query'], $query);
+ $this->assertTrue(isset($query['code']));
+ $this->assertFalse(isset($query['error']));
+
+ // add with the "code" key not set
+ $codeType = new AuthorizationCode($storage);
+ $server = new Server(array($storage), array(), array(), array($codeType));
+ $request = new Request(array(
+ 'response_type' => 'code',
+ 'client_id' => 'some_client',
+ 'redirect_uri' => 'http://example.com',
+ 'state' => 'xyx',
+ ));
+ $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+ // the response is successful
+ $this->assertEquals($response->getStatusCode(), 302);
+ $parts = parse_url($response->getHttpHeader('Location'));
+ parse_str($parts['query'], $query);
+ $this->assertTrue(isset($query['code']));
+ $this->assertFalse(isset($query['error']));
+ }
+
+ public function testCustomClientAssertionType()
+ {
+ $request = TestRequest::createPost(array(
+ 'grant_type' => 'authorization_code',
+ 'client_id' =>'Test Client ID',
+ 'code' => 'testcode',
+ ));
+ // verify the mock clientAssertionType was called as expected
+ $clientAssertionType = $this->getMock('OAuth2\ClientAssertionType\ClientAssertionTypeInterface', array('validateRequest', 'getClientId'));
+ $clientAssertionType
+ ->expects($this->once())
+ ->method('validateRequest')
+ ->will($this->returnValue(true));
+ $clientAssertionType
+ ->expects($this->once())
+ ->method('getClientId')
+ ->will($this->returnValue('Test Client ID'));
+
+ // create mock storage
+ $storage = Bootstrap::getInstance()->getMemoryStorage();
+ $server = new Server(array($storage), array(), array(), array(), null, null, $clientAssertionType);
+ $server->handleTokenRequest($request, $response = new Response());
+ }
+
+ public function testHttpBasicConfig()
+ {
+ // create mock storage
+ $storage = Bootstrap::getInstance()->getMemoryStorage();
+ $server = new Server(array($storage), array(
+ 'allow_credentials_in_request_body' => false,
+ 'allow_public_clients' => false
+ ));
+ $server->getTokenController();
+ $httpBasic = $server->getClientAssertionType();
+
+ $reflection = new \ReflectionClass($httpBasic);
+ $prop = $reflection->getProperty('config');
+ $prop->setAccessible(true);
+
+ $config = $prop->getValue($httpBasic); // get the private "config" property
+
+ $this->assertEquals($config['allow_credentials_in_request_body'], false);
+ $this->assertEquals($config['allow_public_clients'], false);
+ }
+
+ public function testRefreshTokenConfig()
+ {
+ // create mock storage
+ $storage = Bootstrap::getInstance()->getMemoryStorage();
+ $server1 = new Server(array($storage));
+ $server2 = new Server(array($storage), array('always_issue_new_refresh_token' => true, 'unset_refresh_token_after_use' => false));
+
+ $server1->getTokenController();
+ $refreshToken1 = $server1->getGrantType('refresh_token');
+
+ $server2->getTokenController();
+ $refreshToken2 = $server2->getGrantType('refresh_token');
+
+ $reflection1 = new \ReflectionClass($refreshToken1);
+ $prop1 = $reflection1->getProperty('config');
+ $prop1->setAccessible(true);
+
+ $reflection2 = new \ReflectionClass($refreshToken2);
+ $prop2 = $reflection2->getProperty('config');
+ $prop2->setAccessible(true);
+
+ // get the private "config" property
+ $config1 = $prop1->getValue($refreshToken1);
+ $config2 = $prop2->getValue($refreshToken2);
+
+ $this->assertEquals($config1['always_issue_new_refresh_token'], false);
+ $this->assertEquals($config2['always_issue_new_refresh_token'], true);
+
+ $this->assertEquals($config1['unset_refresh_token_after_use'], true);
+ $this->assertEquals($config2['unset_refresh_token_after_use'], false);
+ }
+
+ /**
+ * Test setting "always_issue_new_refresh_token" on a server level
+ *
+ * @see test/OAuth2/GrantType/RefreshTokenTest::testValidRefreshTokenWithNewRefreshTokenInResponse
+ **/
+ public function testValidRefreshTokenWithNewRefreshTokenInResponse()
+ {
+ $storage = Bootstrap::getInstance()->getMemoryStorage();
+ $server = new Server($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 = $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 = $storage->getRefreshToken('test-refreshtoken');
+ $this->assertFalse($used_token, 'the refresh token used is no longer valid');
+ }
+
+ /**
+ * @expectedException InvalidArgumentException OAuth2\ResponseType\AuthorizationCodeInterface
+ **/
+ public function testAddingUnknownResponseTypeThrowsException()
+ {
+ $server = new Server();
+ $server->addResponseType($this->getMock('OAuth2\ResponseType\ResponseTypeInterface'));
+ }
+
+ /**
+ * @expectedException LogicException OAuth2\Storage\PublicKeyInterface
+ **/
+ public function testUsingJwtAccessTokensWithoutPublicKeyStorageThrowsException()
+ {
+ $server = new Server(array(), array('use_jwt_access_tokens' => true));
+ $server->addGrantType($this->getMock('OAuth2\GrantType\GrantTypeInterface'));
+ $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface'));
+ $server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface'));
+
+ $server->getTokenController();
+ }
+
+ public function testUsingJustJwtAccessTokenStorageWithResourceControllerIsOkay()
+ {
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true));
+
+ $this->assertNotNull($server->getResourceController());
+ $this->assertInstanceOf('OAuth2\Storage\PublicKeyInterface', $server->getStorage('public_key'));
+ }
+
+ /**
+ * @expectedException LogicException OAuth2\Storage\ClientInterface
+ **/
+ public function testUsingJustJwtAccessTokenStorageWithAuthorizeControllerThrowsException()
+ {
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true));
+ $this->assertNotNull($server->getAuthorizeController());
+ }
+
+ /**
+ * @expectedException LogicException grant_types
+ **/
+ public function testUsingJustJwtAccessTokenStorageWithTokenControllerThrowsException()
+ {
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true));
+ $server->getTokenController();
+ }
+
+ public function testUsingJwtAccessTokenAndClientStorageWithAuthorizeControllerIsOkay()
+ {
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $server = new Server(array($pubkey, $client), array('use_jwt_access_tokens' => true, 'allow_implicit' => true));
+ $this->assertNotNull($server->getAuthorizeController());
+
+ $this->assertInstanceOf('OAuth2\ResponseType\JwtAccessToken', $server->getResponseType('token'));
+ }
+
+ /**
+ * @expectedException LogicException UserClaims
+ **/
+ public function testUsingOpenIDConnectWithoutUserClaimsThrowsException()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $server = new Server($client, array('use_openid_connect' => true));
+
+ $server->getAuthorizeController();
+ }
+
+ /**
+ * @expectedException LogicException PublicKeyInterface
+ **/
+ public function testUsingOpenIDConnectWithoutPublicKeyThrowsException()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OPenID\Storage\UserClaimsInterface');
+ $server = new Server(array($client, $userclaims), array('use_openid_connect' => true));
+
+ $server->getAuthorizeController();
+ }
+
+ /**
+ * @expectedException LogicException issuer
+ **/
+ public function testUsingOpenIDConnectWithoutIssuerThrowsException()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $server = new Server(array($client, $userclaims, $pubkey), array('use_openid_connect' => true));
+
+ $server->getAuthorizeController();
+ }
+
+ public function testUsingOpenIDConnectWithIssuerPublicKeyAndUserClaimsIsOkay()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $server = new Server(array($client, $userclaims, $pubkey), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy',
+ ));
+
+ $server->getAuthorizeController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token'));
+ $this->assertNull($server->getResponseType('id_token token'));
+ }
+
+ /**
+ * @expectedException LogicException OAuth2\ResponseType\AccessTokenInterface
+ **/
+ public function testUsingOpenIDConnectWithAllowImplicitWithoutTokenStorageThrowsException()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $server = new Server(array($client, $userclaims, $pubkey), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy',
+ 'allow_implicit' => true,
+ ));
+
+ $server->getAuthorizeController();
+ }
+
+ public function testUsingOpenIDConnectWithAllowImplicitAndUseJwtAccessTokensIsOkay()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $server = new Server(array($client, $userclaims, $pubkey), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy',
+ 'allow_implicit' => true,
+ 'use_jwt_access_tokens' => true,
+ ));
+
+ $server->getAuthorizeController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token'));
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token'));
+ }
+
+ public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenStorageIsOkay()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $token = $this->getMock('OAuth2\Storage\AccessTokenInterface');
+ $server = new Server(array($client, $userclaims, $pubkey, $token), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy',
+ 'allow_implicit' => true,
+ ));
+
+ $server->getAuthorizeController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token'));
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token'));
+ }
+
+ public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenResponseTypeIsOkay()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ // $token = $this->getMock('OAuth2\Storage\AccessTokenInterface');
+ $server = new Server(array($client, $userclaims, $pubkey), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy',
+ 'allow_implicit' => true,
+ ));
+
+ $token = $this->getMock('OAuth2\ResponseType\AccessTokenInterface');
+ $server->addResponseType($token, 'token');
+
+ $server->getAuthorizeController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token'));
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token'));
+ }
+
+ /**
+ * @expectedException LogicException OAuth2\OpenID\Storage\AuthorizationCodeInterface
+ **/
+ public function testUsingOpenIDConnectWithAuthorizationCodeStorageThrowsException()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientCredentialsInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $token = $this->getMock('OAuth2\Storage\AccessTokenInterface');
+ $authcode = $this->getMock('OAuth2\Storage\AuthorizationCodeInterface');
+
+ $server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy'
+ ));
+
+ $server->getTokenController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\GrantType\AuthorizationCode', $server->getGrantType('authorization_code'));
+ }
+
+ public function testUsingOpenIDConnectWithOpenIDAuthorizationCodeStorageCreatesOpenIDAuthorizationCodeGrantType()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientCredentialsInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $token = $this->getMock('OAuth2\Storage\AccessTokenInterface');
+ $authcode = $this->getMock('OAuth2\OpenID\Storage\AuthorizationCodeInterface');
+
+ $server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy'
+ ));
+
+ $server->getTokenController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\GrantType\AuthorizationCode', $server->getGrantType('authorization_code'));
+ }
+
+ public function testMultipleValuedResponseTypeOrderDoesntMatter()
+ {
+ $responseType = $this->getMock('OAuth2\OpenID\ResponseType\IdTokenTokenInterface');
+ $server = new Server(array(), array(), array(), array(
+ 'token id_token' => $responseType,
+ ));
+
+ $this->assertEquals($responseType, $server->getResponseType('id_token token'));
+ }
+
+ public function testAddGrantTypeWithoutKey()
+ {
+ $server = new Server();
+ $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')));
+
+ $grantTypes = $server->getGrantTypes();
+ $this->assertEquals('authorization_code', key($grantTypes));
+ }
+
+ public function testAddGrantTypeWithKey()
+ {
+ $server = new Server();
+ $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')), 'ac');
+
+ $grantTypes = $server->getGrantTypes();
+ $this->assertEquals('ac', key($grantTypes));
+ }
+
+ public function testAddGrantTypeWithKeyNotString()
+ {
+ $server = new Server();
+ $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')), 42);
+
+ $grantTypes = $server->getGrantTypes();
+ $this->assertEquals('authorization_code', key($grantTypes));
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/AccessTokenTest.php b/library/oauth2/test/OAuth2/Storage/AccessTokenTest.php
new file mode 100644
index 000000000..b34e0bfc0
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/AccessTokenTest.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace OAuth2\Storage;
+
+class AccessTokenTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testSetAccessToken(AccessTokenInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // assert token we are about to add does not exist
+ $token = $storage->getAccessToken('newtoken');
+ $this->assertFalse($token);
+
+ // add new token
+ $expires = time() + 20;
+ $success = $storage->setAccessToken('newtoken', 'client ID', 'SOMEUSERID', $expires);
+ $this->assertTrue($success);
+
+ $token = $storage->getAccessToken('newtoken');
+ $this->assertNotNull($token);
+ $this->assertArrayHasKey('access_token', $token);
+ $this->assertArrayHasKey('client_id', $token);
+ $this->assertArrayHasKey('user_id', $token);
+ $this->assertArrayHasKey('expires', $token);
+ $this->assertEquals($token['access_token'], 'newtoken');
+ $this->assertEquals($token['client_id'], 'client ID');
+ $this->assertEquals($token['user_id'], 'SOMEUSERID');
+ $this->assertEquals($token['expires'], $expires);
+
+ // change existing token
+ $expires = time() + 42;
+ $success = $storage->setAccessToken('newtoken', 'client ID2', 'SOMEOTHERID', $expires);
+ $this->assertTrue($success);
+
+ $token = $storage->getAccessToken('newtoken');
+ $this->assertNotNull($token);
+ $this->assertArrayHasKey('access_token', $token);
+ $this->assertArrayHasKey('client_id', $token);
+ $this->assertArrayHasKey('user_id', $token);
+ $this->assertArrayHasKey('expires', $token);
+ $this->assertEquals($token['access_token'], 'newtoken');
+ $this->assertEquals($token['client_id'], 'client ID2');
+ $this->assertEquals($token['user_id'], 'SOMEOTHERID');
+ $this->assertEquals($token['expires'], $expires);
+
+ // add token with scope having an empty string value
+ $expires = time() + 42;
+ $success = $storage->setAccessToken('newtoken', 'client ID', 'SOMEOTHERID', $expires, '');
+ $this->assertTrue($success);
+ }
+
+ /** @dataProvider provideStorage */
+ public function testUnsetAccessToken(AccessTokenInterface $storage)
+ {
+ if ($storage instanceof NullStorage || !method_exists($storage, 'unsetAccessToken')) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // assert token we are about to unset does not exist
+ $token = $storage->getAccessToken('revokabletoken');
+ $this->assertFalse($token);
+
+ // add new token
+ $expires = time() + 20;
+ $success = $storage->setAccessToken('revokabletoken', 'client ID', 'SOMEUSERID', $expires);
+ $this->assertTrue($success);
+
+ // assert unsetAccessToken returns true
+ $result = $storage->unsetAccessToken('revokabletoken');
+ $this->assertTrue($result);
+
+ // assert token we unset does not exist
+ $token = $storage->getAccessToken('revokabletoken');
+ $this->assertFalse($token);
+ }
+
+ /** @dataProvider provideStorage */
+ public function testUnsetAccessTokenReturnsFalse(AccessTokenInterface $storage)
+ {
+ if ($storage instanceof NullStorage || !method_exists($storage, 'unsetAccessToken')) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // assert token we are about to unset does not exist
+ $token = $storage->getAccessToken('nonexistanttoken');
+ $this->assertFalse($token);
+
+ // assert unsetAccessToken returns false
+ $result = $storage->unsetAccessToken('nonexistanttoken');
+ $this->assertFalse($result);
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/AuthorizationCodeTest.php b/library/oauth2/test/OAuth2/Storage/AuthorizationCodeTest.php
new file mode 100644
index 000000000..2d901b501
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/AuthorizationCodeTest.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace OAuth2\Storage;
+
+class AuthorizationCodeTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testGetAuthorizationCode(AuthorizationCodeInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // nonexistant client_id
+ $details = $storage->getAuthorizationCode('faketoken');
+ $this->assertFalse($details);
+
+ // valid client_id
+ $details = $storage->getAuthorizationCode('testtoken');
+ $this->assertNotNull($details);
+ }
+
+ /** @dataProvider provideStorage */
+ public function testSetAuthorizationCode(AuthorizationCodeInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // assert code we are about to add does not exist
+ $code = $storage->getAuthorizationCode('newcode');
+ $this->assertFalse($code);
+
+ // add new code
+ $expires = time() + 20;
+ $success = $storage->setAuthorizationCode('newcode', 'client ID', 'SOMEUSERID', 'http://example.com', $expires);
+ $this->assertTrue($success);
+
+ $code = $storage->getAuthorizationCode('newcode');
+ $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'], 'newcode');
+ $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);
+
+ // change existing code
+ $expires = time() + 42;
+ $success = $storage->setAuthorizationCode('newcode', 'client ID2', 'SOMEOTHERID', 'http://example.org', $expires);
+ $this->assertTrue($success);
+
+ $code = $storage->getAuthorizationCode('newcode');
+ $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'], 'newcode');
+ $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);
+
+ // add new code with scope having an empty string value
+ $expires = time() + 20;
+ $success = $storage->setAuthorizationCode('newcode', 'client ID', 'SOMEUSERID', 'http://example.com', $expires, '');
+ $this->assertTrue($success);
+ }
+
+ /** @dataProvider provideStorage */
+ public function testExpireAccessToken(AccessTokenInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // create a valid code
+ $expires = time() + 20;
+ $success = $storage->setAuthorizationCode('code-to-expire', 'client ID', 'SOMEUSERID', 'http://example.com', time() + 20);
+ $this->assertTrue($success);
+
+ // verify the new code exists
+ $code = $storage->getAuthorizationCode('code-to-expire');
+ $this->assertNotNull($code);
+
+ $this->assertArrayHasKey('authorization_code', $code);
+ $this->assertEquals($code['authorization_code'], 'code-to-expire');
+
+ // now expire the code and ensure it's no longer available
+ $storage->expireAuthorizationCode('code-to-expire');
+ $code = $storage->getAuthorizationCode('code-to-expire');
+ $this->assertFalse($code);
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/ClientCredentialsTest.php b/library/oauth2/test/OAuth2/Storage/ClientCredentialsTest.php
new file mode 100644
index 000000000..15289af30
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/ClientCredentialsTest.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OAuth2\Storage;
+
+class ClientCredentialsTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testCheckClientCredentials(ClientCredentialsInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // nonexistant client_id
+ $pass = $storage->checkClientCredentials('fakeclient', 'testpass');
+ $this->assertFalse($pass);
+
+ // invalid password
+ $pass = $storage->checkClientCredentials('oauth_test_client', 'invalidcredentials');
+ $this->assertFalse($pass);
+
+ // valid credentials
+ $pass = $storage->checkClientCredentials('oauth_test_client', 'testpass');
+ $this->assertTrue($pass);
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/ClientTest.php b/library/oauth2/test/OAuth2/Storage/ClientTest.php
new file mode 100644
index 000000000..6a5cc0b49
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/ClientTest.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace OAuth2\Storage;
+
+class ClientTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testGetClientDetails(ClientInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // nonexistant client_id
+ $details = $storage->getClientDetails('fakeclient');
+ $this->assertFalse($details);
+
+ // valid client_id
+ $details = $storage->getClientDetails('oauth_test_client');
+ $this->assertNotNull($details);
+ $this->assertArrayHasKey('client_id', $details);
+ $this->assertArrayHasKey('client_secret', $details);
+ $this->assertArrayHasKey('redirect_uri', $details);
+ }
+
+ /** @dataProvider provideStorage */
+ public function testCheckRestrictedGrantType(ClientInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // Check invalid
+ $pass = $storage->checkRestrictedGrantType('oauth_test_client', 'authorization_code');
+ $this->assertFalse($pass);
+
+ // Check valid
+ $pass = $storage->checkRestrictedGrantType('oauth_test_client', 'implicit');
+ $this->assertTrue($pass);
+ }
+
+ /** @dataProvider provideStorage */
+ public function testGetAccessToken(ClientInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // nonexistant client_id
+ $details = $storage->getAccessToken('faketoken');
+ $this->assertFalse($details);
+
+ // valid client_id
+ $details = $storage->getAccessToken('testtoken');
+ $this->assertNotNull($details);
+ }
+
+ /** @dataProvider provideStorage */
+ public function testIsPublicClient(ClientInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ $publicClientId = 'public-client-'.rand();
+ $confidentialClientId = 'confidential-client-'.rand();
+
+ // create a new client
+ $success1 = $storage->setClientDetails($publicClientId, '');
+ $success2 = $storage->setClientDetails($confidentialClientId, 'some-secret');
+ $this->assertTrue($success1);
+ $this->assertTrue($success2);
+
+ // assert isPublicClient for both
+ $this->assertTrue($storage->isPublicClient($publicClientId));
+ $this->assertFalse($storage->isPublicClient($confidentialClientId));
+ }
+
+ /** @dataProvider provideStorage */
+ public function testSaveClient(ClientInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ $clientId = 'some-client-'.rand();
+
+ // create a new client
+ $success = $storage->setClientDetails($clientId, 'somesecret', 'http://test.com', 'client_credentials', 'clientscope1', 'brent@brentertainment.com');
+ $this->assertTrue($success);
+
+ // valid client_id
+ $details = $storage->getClientDetails($clientId);
+ $this->assertEquals($details['client_secret'], 'somesecret');
+ $this->assertEquals($details['redirect_uri'], 'http://test.com');
+ $this->assertEquals($details['grant_types'], 'client_credentials');
+ $this->assertEquals($details['scope'], 'clientscope1');
+ $this->assertEquals($details['user_id'], 'brent@brentertainment.com');
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/DynamoDBTest.php b/library/oauth2/test/OAuth2/Storage/DynamoDBTest.php
new file mode 100644
index 000000000..2147f0914
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/DynamoDBTest.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace OAuth2\Storage;
+
+class DynamoDBTest extends BaseTest
+{
+ public function testGetDefaultScope()
+ {
+ $client = $this->getMockBuilder('\Aws\DynamoDb\DynamoDbClient')
+ ->disableOriginalConstructor()
+ ->setMethods(array('query'))
+ ->getMock();
+
+ $return = $this->getMockBuilder('\Guzzle\Service\Resource\Model')
+ ->setMethods(array('count', 'toArray'))
+ ->getMock();
+
+ $data = array(
+ 'Items' => array(),
+ 'Count' => 0,
+ 'ScannedCount'=> 0
+ );
+
+ $return->expects($this->once())
+ ->method('count')
+ ->will($this->returnValue(count($data)));
+
+ $return->expects($this->once())
+ ->method('toArray')
+ ->will($this->returnValue($data));
+
+ // should return null default scope if none is set in database
+ $client->expects($this->once())
+ ->method('query')
+ ->will($this->returnValue($return));
+
+ $storage = new DynamoDB($client);
+ $this->assertNull($storage->getDefaultScope());
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/JwtAccessTokenTest.php b/library/oauth2/test/OAuth2/Storage/JwtAccessTokenTest.php
new file mode 100644
index 000000000..a6acbea1f
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/JwtAccessTokenTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\Encryption\Jwt;
+
+class JwtAccessTokenTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testSetAccessToken($storage)
+ {
+ if (!$storage instanceof PublicKey) {
+ // incompatible storage
+ return;
+ }
+
+ $crypto = new jwtAccessToken($storage);
+
+ $publicKeyStorage = Bootstrap::getInstance()->getMemoryStorage();
+ $encryptionUtil = new Jwt();
+
+ $jwtAccessToken = array(
+ 'access_token' => rand(),
+ 'expires' => time() + 100,
+ 'scope' => 'foo',
+ );
+
+ $token = $encryptionUtil->encode($jwtAccessToken, $storage->getPrivateKey(), $storage->getEncryptionAlgorithm());
+
+ $this->assertNotNull($token);
+
+ $tokenData = $crypto->getAccessToken($token);
+
+ $this->assertTrue(is_array($tokenData));
+
+ /* assert the decoded token is the same */
+ $this->assertEquals($tokenData['access_token'], $jwtAccessToken['access_token']);
+ $this->assertEquals($tokenData['expires'], $jwtAccessToken['expires']);
+ $this->assertEquals($tokenData['scope'], $jwtAccessToken['scope']);
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/JwtBearerTest.php b/library/oauth2/test/OAuth2/Storage/JwtBearerTest.php
new file mode 100644
index 000000000..d0ab9b899
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/JwtBearerTest.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace OAuth2\Storage;
+
+class JwtBearerTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testGetClientKey(JwtBearerInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // nonexistant client_id
+ $key = $storage->getClientKey('this-is-not-real', 'nor-is-this');
+ $this->assertFalse($key);
+
+ // valid client_id and subject
+ $key = $storage->getClientKey('oauth_test_client', 'test_subject');
+ $this->assertNotNull($key);
+ $this->assertEquals($key, Bootstrap::getInstance()->getTestPublicKey());
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/PdoTest.php b/library/oauth2/test/OAuth2/Storage/PdoTest.php
new file mode 100644
index 000000000..57eb39072
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/PdoTest.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace OAuth2\Storage;
+
+class PdoTest extends BaseTest
+{
+ public function testCreatePdoStorageUsingPdoClass()
+ {
+ $pdo = new \PDO(sprintf('sqlite://%s', Bootstrap::getInstance()->getSqliteDir()));
+ $storage = new Pdo($pdo);
+
+ $this->assertNotNull($storage->getClientDetails('oauth_test_client'));
+ }
+
+ public function testCreatePdoStorageUsingDSN()
+ {
+ $dsn = sprintf('sqlite://%s', Bootstrap::getInstance()->getSqliteDir());
+ $storage = new Pdo($dsn);
+
+ $this->assertNotNull($storage->getClientDetails('oauth_test_client'));
+ }
+
+ public function testCreatePdoStorageUsingConfig()
+ {
+ $config = array('dsn' => sprintf('sqlite://%s', Bootstrap::getInstance()->getSqliteDir()));
+ $storage = new Pdo($config);
+
+ $this->assertNotNull($storage->getClientDetails('oauth_test_client'));
+ }
+
+ /**
+ * @expectedException InvalidArgumentException dsn
+ */
+ public function testCreatePdoStorageWithoutDSNThrowsException()
+ {
+ $config = array('username' => 'brent', 'password' => 'brentisaballer');
+ $storage = new Pdo($config);
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/PublicKeyTest.php b/library/oauth2/test/OAuth2/Storage/PublicKeyTest.php
new file mode 100644
index 000000000..f85195870
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/PublicKeyTest.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace OAuth2\Storage;
+
+class PublicKeyTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testSetAccessToken($storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ if (!$storage instanceof PublicKeyInterface) {
+ // incompatible storage
+ return;
+ }
+
+ $configDir = Bootstrap::getInstance()->getConfigDir();
+ $globalPublicKey = file_get_contents($configDir.'/keys/id_rsa.pub');
+ $globalPrivateKey = file_get_contents($configDir.'/keys/id_rsa');
+
+ /* assert values from storage */
+ $this->assertEquals($storage->getPublicKey(), $globalPublicKey);
+ $this->assertEquals($storage->getPrivateKey(), $globalPrivateKey);
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/RefreshTokenTest.php b/library/oauth2/test/OAuth2/Storage/RefreshTokenTest.php
new file mode 100644
index 000000000..314c93195
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/RefreshTokenTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace OAuth2\Storage;
+
+class RefreshTokenTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testSetRefreshToken(RefreshTokenInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // assert token we are about to add does not exist
+ $token = $storage->getRefreshToken('refreshtoken');
+ $this->assertFalse($token);
+
+ // add new token
+ $expires = time() + 20;
+ $success = $storage->setRefreshToken('refreshtoken', 'client ID', 'SOMEUSERID', $expires);
+ $this->assertTrue($success);
+
+ $token = $storage->getRefreshToken('refreshtoken');
+ $this->assertNotNull($token);
+ $this->assertArrayHasKey('refresh_token', $token);
+ $this->assertArrayHasKey('client_id', $token);
+ $this->assertArrayHasKey('user_id', $token);
+ $this->assertArrayHasKey('expires', $token);
+ $this->assertEquals($token['refresh_token'], 'refreshtoken');
+ $this->assertEquals($token['client_id'], 'client ID');
+ $this->assertEquals($token['user_id'], 'SOMEUSERID');
+ $this->assertEquals($token['expires'], $expires);
+
+ // add token with scope having an empty string value
+ $expires = time() + 20;
+ $success = $storage->setRefreshToken('refreshtoken2', 'client ID', 'SOMEUSERID', $expires, '');
+ $this->assertTrue($success);
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/ScopeTest.php b/library/oauth2/test/OAuth2/Storage/ScopeTest.php
new file mode 100644
index 000000000..fd1edeb93
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/ScopeTest.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\Scope;
+
+class ScopeTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testScopeExists($storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ if (!$storage instanceof ScopeInterface) {
+ // incompatible storage
+ return;
+ }
+
+ //Test getting scopes
+ $scopeUtil = new Scope($storage);
+ $this->assertTrue($scopeUtil->scopeExists('supportedscope1'));
+ $this->assertTrue($scopeUtil->scopeExists('supportedscope1 supportedscope2 supportedscope3'));
+ $this->assertFalse($scopeUtil->scopeExists('fakescope'));
+ $this->assertFalse($scopeUtil->scopeExists('supportedscope1 supportedscope2 supportedscope3 fakescope'));
+ }
+
+ /** @dataProvider provideStorage */
+ public function testGetDefaultScope($storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ if (!$storage instanceof ScopeInterface) {
+ // incompatible storage
+ return;
+ }
+
+ // test getting default scope
+ $scopeUtil = new Scope($storage);
+ $expected = explode(' ', $scopeUtil->getDefaultScope());
+ $actual = explode(' ', 'defaultscope1 defaultscope2');
+ sort($expected);
+ sort($actual);
+ $this->assertEquals($expected, $actual);
+ }
+}
diff --git a/library/oauth2/test/OAuth2/Storage/UserCredentialsTest.php b/library/oauth2/test/OAuth2/Storage/UserCredentialsTest.php
new file mode 100644
index 000000000..65655a6b2
--- /dev/null
+++ b/library/oauth2/test/OAuth2/Storage/UserCredentialsTest.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace OAuth2\Storage;
+
+class UserCredentialsTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testCheckUserCredentials(UserCredentialsInterface $storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ // create a new user for testing
+ $success = $storage->setUser('testusername', 'testpass', 'Test', 'User');
+ $this->assertTrue($success);
+
+ // correct credentials
+ $this->assertTrue($storage->checkUserCredentials('testusername', 'testpass'));
+ // invalid password
+ $this->assertFalse($storage->checkUserCredentials('testusername', 'fakepass'));
+ // invalid username
+ $this->assertFalse($storage->checkUserCredentials('fakeusername', 'testpass'));
+
+ // invalid username
+ $this->assertFalse($storage->getUserDetails('fakeusername'));
+
+ // ensure all properties are set
+ $user = $storage->getUserDetails('testusername');
+ $this->assertTrue($user !== false);
+ $this->assertArrayHasKey('user_id', $user);
+ $this->assertArrayHasKey('first_name', $user);
+ $this->assertArrayHasKey('last_name', $user);
+ $this->assertEquals($user['user_id'], 'testusername');
+ $this->assertEquals($user['first_name'], 'Test');
+ $this->assertEquals($user['last_name'], 'User');
+ }
+}
diff --git a/library/oauth2/test/OAuth2/TokenType/BearerTest.php b/library/oauth2/test/OAuth2/TokenType/BearerTest.php
new file mode 100644
index 000000000..a2e000e22
--- /dev/null
+++ b/library/oauth2/test/OAuth2/TokenType/BearerTest.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace OAuth2\TokenType;
+
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+
+class BearerTest extends \PHPUnit_Framework_TestCase
+{
+ public function testValidContentTypeWithCharset()
+ {
+ $bearer = new Bearer();
+ $request = TestRequest::createPost(array(
+ 'access_token' => 'ThisIsMyAccessToken'
+ ));
+ $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8';
+
+ $param = $bearer->getAccessTokenParameter($request, $response = new Response());
+ $this->assertEquals($param, 'ThisIsMyAccessToken');
+ }
+
+ public function testInvalidContentType()
+ {
+ $bearer = new Bearer();
+ $request = TestRequest::createPost(array(
+ 'access_token' => 'ThisIsMyAccessToken'
+ ));
+ $request->server['CONTENT_TYPE'] = 'application/json; charset=UTF-8';
+
+ $param = $bearer->getAccessTokenParameter($request, $response = new Response());
+ $this->assertNull($param);
+ $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 testValidRequestUsingAuthorizationHeader()
+ {
+ $bearer = new Bearer();
+ $request = new TestRequest();
+ $request->headers['AUTHORIZATION'] = 'Bearer MyToken';
+ $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8';
+
+ $param = $bearer->getAccessTokenParameter($request, $response = new Response());
+ $this->assertEquals('MyToken', $param);
+ }
+
+ public function testValidRequestUsingAuthorizationHeaderCaseInsensitive()
+ {
+ $bearer = new Bearer();
+ $request = new TestRequest();
+ $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8';
+ $request->headers['Authorization'] = 'Bearer MyToken';
+
+ $param = $bearer->getAccessTokenParameter($request, $response = new Response());
+ $this->assertEquals('MyToken', $param);
+ }
+}
diff --git a/library/oauth2/test/bootstrap.php b/library/oauth2/test/bootstrap.php
new file mode 100644
index 000000000..0a4af0716
--- /dev/null
+++ b/library/oauth2/test/bootstrap.php
@@ -0,0 +1,12 @@
+<?php
+
+require_once(dirname(__FILE__).'/../src/OAuth2/Autoloader.php');
+OAuth2\Autoloader::register();
+
+// register test classes
+OAuth2\Autoloader::register(dirname(__FILE__).'/lib');
+
+// register vendors if possible
+if (file_exists(__DIR__.'/../vendor/autoload.php')) {
+ require_once(__DIR__.'/../vendor/autoload.php');
+}
diff --git a/library/oauth2/test/cleanup.php b/library/oauth2/test/cleanup.php
new file mode 100644
index 000000000..8663a901b
--- /dev/null
+++ b/library/oauth2/test/cleanup.php
@@ -0,0 +1,15 @@
+<?php
+
+require_once(dirname(__FILE__).'/../src/OAuth2/Autoloader.php');
+OAuth2\Autoloader::register();
+
+// register test classes
+OAuth2\Autoloader::register(dirname(__FILE__).'/lib');
+
+// register vendors if possible
+if (file_exists(__DIR__.'/../vendor/autoload.php')) {
+ require_once(__DIR__.'/../vendor/autoload.php');
+}
+
+// remove the dynamoDB database that was created for this build
+OAuth2\Storage\Bootstrap::getInstance()->cleanupTravisDynamoDb();
diff --git a/library/oauth2/test/config/keys/id_rsa b/library/oauth2/test/config/keys/id_rsa
new file mode 100644
index 000000000..e8b9eff2d
--- /dev/null
+++ b/library/oauth2/test/config/keys/id_rsa
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLsNjP+uAt2eO0cc5J9H5XV
+8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdwizIum8j0KzpsGYH5qReN
+QDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJuBe+FQpZTs8DewwIDAQAB
+AoGARfNxNknmtx/n1bskZ/01iZRzAge6BLEE0LV6Q4gS7mkRZu/Oyiv39Sl5vUlA
++WdGxLjkBwKNjxGN8Vxw9/ASd8rSsqeAUYIwAeifXrHhj5DBPQT/pDPkeFnp9B1w
+C6jo+3AbBQ4/b0ONSIEnCL2xGGglSIAxO17T1ViXp7lzXPECQQDe63nkRdWM0OCb
+oaHQPT3E26224maIstrGFUdt9yw3yJf4bOF7TtiPLlLuHsTTge3z+fG6ntC0xG56
+1cl37C3ZAkEA2HdVcRGugNp/qmVz4LJTpD+WZKi73PLAO47wDOrYh9Pn2I6fcEH0
+CPnggt1ko4ujvGzFTvRH64HXa6aPCv1j+wJBAMQMah3VQPNf/DlDVFEUmw9XeBZg
+VHaifX851aEjgXLp6qVj9IYCmLiLsAmVa9rr6P7p8asD418nZlaHUHE0eDkCQQCr
+uxis6GMx1Ka971jcJX2X696LoxXPd0KsvXySMupv79yagKPa8mgBiwPjrnK+EPVo
+cj6iochA/bSCshP/mwFrAkBHEKPi6V6gb94JinCT7x3weahbdp6bJ6/nzBH/p9VA
+HoT1JtwNFhGv9BCjmDydshQHfSWpY9NxlccBKL7ITm8R
+-----END RSA PRIVATE KEY----- \ No newline at end of file
diff --git a/library/oauth2/test/config/keys/id_rsa.pub b/library/oauth2/test/config/keys/id_rsa.pub
new file mode 100644
index 000000000..1ac15f5eb
--- /dev/null
+++ b/library/oauth2/test/config/keys/id_rsa.pub
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE-----
+MIICiDCCAfGgAwIBAgIBADANBgkqhkiG9w0BAQQFADA9MQswCQYDVQQGEwJVUzEL
+MAkGA1UECBMCVVQxITAfBgNVBAoTGFZpZ25ldHRlIENvcnBvcmF0aW9uIFNCWDAe
+Fw0xMTEwMTUwMzE4MjdaFw0zMTEwMTAwMzE4MjdaMD0xCzAJBgNVBAYTAlVTMQsw
+CQYDVQQIEwJVVDEhMB8GA1UEChMYVmlnbmV0dGUgQ29ycG9yYXRpb24gU0JYMIGf
+MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLs
+NjP+uAt2eO0cc5J9H5XV8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdw
+izIum8j0KzpsGYH5qReNQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJu
+Be+FQpZTs8DewwIDAQABo4GXMIGUMB0GA1UdDgQWBBRe8hrEXm+Yim4YlD5Nx+1K
+vCYs9DBlBgNVHSMEXjBcgBRe8hrEXm+Yim4YlD5Nx+1KvCYs9KFBpD8wPTELMAkG
+A1UEBhMCVVMxCzAJBgNVBAgTAlVUMSEwHwYDVQQKExhWaWduZXR0ZSBDb3Jwb3Jh
+dGlvbiBTQliCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBjhyRD
+lM7vnLn6drgQVftW5V9nDFAyPAuiGvMIKFSbiAf1PxXCRn5sfJquwWKsJUi4ZGNl
+aViXdFmN6/F13PSM+yg63tpKy0fYqMbTM+Oe5WuSHkSW1VuYNHV+24adgNk/FRDL
+FRrlM1f6s9VTLWvwGItjfrof0Ba8Uq7ZDSb9Xg==
+-----END CERTIFICATE----- \ No newline at end of file
diff --git a/library/oauth2/test/config/storage.json b/library/oauth2/test/config/storage.json
new file mode 100644
index 000000000..a31d3bca2
--- /dev/null
+++ b/library/oauth2/test/config/storage.json
@@ -0,0 +1,181 @@
+{
+ "authorization_codes": {
+ "testcode": {
+ "client_id": "Test Client ID",
+ "user_id": "",
+ "redirect_uri": "",
+ "expires": "9999999999",
+ "id_token": "IDTOKEN"
+ },
+ "testcode-with-scope": {
+ "client_id": "Test Client ID",
+ "user_id": "",
+ "redirect_uri": "",
+ "expires": "9999999999",
+ "scope": "scope1 scope2"
+ },
+ "testcode-expired": {
+ "client_id": "Test Client ID",
+ "user_id": "",
+ "redirect_uri": "",
+ "expires": "1356998400"
+ },
+ "testcode-empty-secret": {
+ "client_id": "Test Client ID Empty Secret",
+ "user_id": "",
+ "redirect_uri": "",
+ "expires": "9999999999"
+ },
+ "testcode-openid": {
+ "client_id": "Test Client ID",
+ "user_id": "",
+ "redirect_uri": "",
+ "expires": "9999999999",
+ "id_token": "test_id_token"
+ }
+ },
+ "client_credentials" : {
+ "Test Client ID": {
+ "client_secret": "TestSecret"
+ },
+ "Test Client ID with Redirect Uri": {
+ "client_secret": "TestSecret2",
+ "redirect_uri": "http://brentertainment.com"
+ },
+ "Test Client ID with Buggy Redirect Uri": {
+ "client_secret": "TestSecret2",
+ "redirect_uri": " http://brentertainment.com"
+ },
+ "Test Client ID with Multiple Redirect Uris": {
+ "client_secret": "TestSecret3",
+ "redirect_uri": "http://brentertainment.com http://morehazards.com"
+ },
+ "Test Client ID with Redirect Uri Parts": {
+ "client_secret": "TestSecret4",
+ "redirect_uri": "http://user:pass@brentertainment.com:2222/authorize/cb?auth_type=oauth&test=true"
+ },
+ "Test Some Other Client": {
+ "client_secret": "TestSecret3"
+ },
+ "Test Client ID Empty Secret": {
+ "client_secret": ""
+ },
+ "Test Client ID For Password Grant": {
+ "grant_types": "password",
+ "client_secret": ""
+ },
+ "Client ID With User ID": {
+ "client_secret": "TestSecret",
+ "user_id": "brent@brentertainment.com"
+ },
+ "oauth_test_client": {
+ "client_secret": "testpass",
+ "grant_types": "implicit password"
+ }
+ },
+ "user_credentials" : {
+ "test-username": {
+ "password": "testpass"
+ },
+ "testusername": {
+ "password": "testpass"
+ },
+ "testuser": {
+ "password": "password",
+ "email": "testuser@test.com",
+ "email_verified": true
+ },
+ "johndoe": {
+ "password": "password"
+ }
+ },
+ "refresh_tokens" : {
+ "test-refreshtoken": {
+ "refresh_token": "test-refreshtoken",
+ "client_id": "Test Client ID",
+ "user_id": "test-username",
+ "expires": 0,
+ "scope": null
+ },
+ "test-refreshtoken-with-scope": {
+ "refresh_token": "test-refreshtoken",
+ "client_id": "Test Client ID",
+ "user_id": "test-username",
+ "expires": 0,
+ "scope": "scope1 scope2"
+ }
+ },
+ "access_tokens" : {
+ "accesstoken-expired": {
+ "access_token": "accesstoken-expired",
+ "client_id": "Test Client ID",
+ "expires": 1234567,
+ "scope": null
+ },
+ "accesstoken-scope": {
+ "access_token": "accesstoken-scope",
+ "client_id": "Test Client ID",
+ "expires": 99999999900,
+ "scope": "testscope"
+ },
+ "accesstoken-openid-connect": {
+ "access_token": "accesstoken-openid-connect",
+ "client_id": "Test Client ID",
+ "user_id": "testuser",
+ "expires": 99999999900,
+ "scope": "openid email"
+ },
+ "accesstoken-malformed": {
+ "access_token": "accesstoken-mallformed",
+ "expires": 99999999900,
+ "scope": "testscope"
+ }
+ },
+ "jwt": {
+ "Test Client ID": {
+ "key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5/SxVlE8gnpFqCxgl2wjhzY7u\ncEi00s0kUg3xp7lVEvgLgYcAnHiWp+gtSjOFfH2zsvpiWm6Lz5f743j/FEzHIO1o\nwR0p4d9pOaJK07d01+RzoQLOIQAgXrr4T1CCWUesncwwPBVCyy2Mw3Nmhmr9MrF8\nUlvdRKBxriRnlP3qJQIDAQAB\n-----END PUBLIC KEY-----",
+ "subject": "testuser@ourdomain.com"
+ },
+ "Test Client ID PHP-5.2": {
+ "key": "mysecretkey",
+ "subject": "testuser@ourdomain.com"
+ },
+ "Missing Key Client": {
+ "key": null,
+ "subject": "testuser@ourdomain.com"
+ },
+ "Missing Key Client PHP-5.2": {
+ "key": null,
+ "subject": "testuser@ourdomain.com"
+ },
+ "oauth_test_client": {
+ "key": "-----BEGIN CERTIFICATE-----\nMIICiDCCAfGgAwIBAgIBADANBgkqhkiG9w0BAQQFADA9MQswCQYDVQQGEwJVUzEL\nMAkGA1UECBMCVVQxITAfBgNVBAoTGFZpZ25ldHRlIENvcnBvcmF0aW9uIFNCWDAe\nFw0xMTEwMTUwMzE4MjdaFw0zMTEwMTAwMzE4MjdaMD0xCzAJBgNVBAYTAlVTMQsw\nCQYDVQQIEwJVVDEhMB8GA1UEChMYVmlnbmV0dGUgQ29ycG9yYXRpb24gU0JYMIGf\nMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLs\nNjP+uAt2eO0cc5J9H5XV8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdw\nizIum8j0KzpsGYH5qReNQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJu\nBe+FQpZTs8DewwIDAQABo4GXMIGUMB0GA1UdDgQWBBRe8hrEXm+Yim4YlD5Nx+1K\nvCYs9DBlBgNVHSMEXjBcgBRe8hrEXm+Yim4YlD5Nx+1KvCYs9KFBpD8wPTELMAkG\nA1UEBhMCVVMxCzAJBgNVBAgTAlVUMSEwHwYDVQQKExhWaWduZXR0ZSBDb3Jwb3Jh\ndGlvbiBTQliCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBjhyRD\nlM7vnLn6drgQVftW5V9nDFAyPAuiGvMIKFSbiAf1PxXCRn5sfJquwWKsJUi4ZGNl\naViXdFmN6/F13PSM+yg63tpKy0fYqMbTM+Oe5WuSHkSW1VuYNHV+24adgNk/FRDL\nFRrlM1f6s9VTLWvwGItjfrof0Ba8Uq7ZDSb9Xg==\n-----END CERTIFICATE-----",
+ "subject": "test_subject"
+ }
+ },
+ "jti": [
+ {
+ "issuer": "Test Client ID",
+ "subject": "testuser@ourdomain.com",
+ "audience": "http://myapp.com/oauth/auth",
+ "expires": 99999999900,
+ "jti": "used_jti"
+ }
+ ],
+ "supported_scopes" : [
+ "scope1",
+ "scope2",
+ "scope3",
+ "clientscope1",
+ "clientscope2",
+ "clientscope3",
+ "supportedscope1",
+ "supportedscope2",
+ "supportedscope3",
+ "supportedscope4"
+ ],
+ "keys": {
+ "public_key": "-----BEGIN CERTIFICATE-----\nMIICiDCCAfGgAwIBAgIBADANBgkqhkiG9w0BAQQFADA9MQswCQYDVQQGEwJVUzEL\nMAkGA1UECBMCVVQxITAfBgNVBAoTGFZpZ25ldHRlIENvcnBvcmF0aW9uIFNCWDAe\nFw0xMTEwMTUwMzE4MjdaFw0zMTEwMTAwMzE4MjdaMD0xCzAJBgNVBAYTAlVTMQsw\nCQYDVQQIEwJVVDEhMB8GA1UEChMYVmlnbmV0dGUgQ29ycG9yYXRpb24gU0JYMIGf\nMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLs\nNjP+uAt2eO0cc5J9H5XV8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdw\nizIum8j0KzpsGYH5qReNQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJu\nBe+FQpZTs8DewwIDAQABo4GXMIGUMB0GA1UdDgQWBBRe8hrEXm+Yim4YlD5Nx+1K\nvCYs9DBlBgNVHSMEXjBcgBRe8hrEXm+Yim4YlD5Nx+1KvCYs9KFBpD8wPTELMAkG\nA1UEBhMCVVMxCzAJBgNVBAgTAlVUMSEwHwYDVQQKExhWaWduZXR0ZSBDb3Jwb3Jh\ndGlvbiBTQliCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQBjhyRD\nlM7vnLn6drgQVftW5V9nDFAyPAuiGvMIKFSbiAf1PxXCRn5sfJquwWKsJUi4ZGNl\naViXdFmN6/F13PSM+yg63tpKy0fYqMbTM+Oe5WuSHkSW1VuYNHV+24adgNk/FRDL\nFRrlM1f6s9VTLWvwGItjfrof0Ba8Uq7ZDSb9Xg==\n-----END CERTIFICATE-----",
+ "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQC8fpi06NfVYHAOAnxNMVnTXr/ptsLsNjP+uAt2eO0cc5J9H5XV\n8lFVujOrRu/JWi1TDmAvOaf/6A3BphIA1Pwp0AAqlZdwizIum8j0KzpsGYH5qReN\nQDwF3oUSKMsQCCGCDHrDYifG/pRi9bN1ZVjEXPr35HJuBe+FQpZTs8DewwIDAQAB\nAoGARfNxNknmtx/n1bskZ/01iZRzAge6BLEE0LV6Q4gS7mkRZu/Oyiv39Sl5vUlA\n+WdGxLjkBwKNjxGN8Vxw9/ASd8rSsqeAUYIwAeifXrHhj5DBPQT/pDPkeFnp9B1w\nC6jo+3AbBQ4/b0ONSIEnCL2xGGglSIAxO17T1ViXp7lzXPECQQDe63nkRdWM0OCb\noaHQPT3E26224maIstrGFUdt9yw3yJf4bOF7TtiPLlLuHsTTge3z+fG6ntC0xG56\n1cl37C3ZAkEA2HdVcRGugNp/qmVz4LJTpD+WZKi73PLAO47wDOrYh9Pn2I6fcEH0\nCPnggt1ko4ujvGzFTvRH64HXa6aPCv1j+wJBAMQMah3VQPNf/DlDVFEUmw9XeBZg\nVHaifX851aEjgXLp6qVj9IYCmLiLsAmVa9rr6P7p8asD418nZlaHUHE0eDkCQQCr\nuxis6GMx1Ka971jcJX2X696LoxXPd0KsvXySMupv79yagKPa8mgBiwPjrnK+EPVo\ncj6iochA/bSCshP/mwFrAkBHEKPi6V6gb94JinCT7x3weahbdp6bJ6/nzBH/p9VA\nHoT1JtwNFhGv9BCjmDydshQHfSWpY9NxlccBKL7ITm8R\n-----END RSA PRIVATE KEY-----"
+ }
+}
diff --git a/library/oauth2/test/lib/OAuth2/Request/TestRequest.php b/library/oauth2/test/lib/OAuth2/Request/TestRequest.php
new file mode 100644
index 000000000..7bbce28a4
--- /dev/null
+++ b/library/oauth2/test/lib/OAuth2/Request/TestRequest.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace OAuth2\Request;
+
+use OAuth2\Request;
+use OAuth2\RequestInterface;
+
+/**
+*
+*/
+class TestRequest extends Request implements RequestInterface
+{
+ public $query, $request, $server, $headers;
+
+ public function __construct()
+ {
+ $this->query = $_GET;
+ $this->request = $_POST;
+ $this->server = $_SERVER;
+ $this->headers = array();
+ }
+
+ public function query($name, $default = null)
+ {
+ return isset($this->query[$name]) ? $this->query[$name] : $default;
+ }
+
+ public function request($name, $default = null)
+ {
+ return isset($this->request[$name]) ? $this->request[$name] : $default;
+ }
+
+ public function server($name, $default = null)
+ {
+ return isset($this->server[$name]) ? $this->server[$name] : $default;
+ }
+
+ public function getAllQueryParameters()
+ {
+ return $this->query;
+ }
+
+ public function setQuery(array $query)
+ {
+ $this->query = $query;
+ }
+
+ public function setPost(array $params)
+ {
+ $this->server['REQUEST_METHOD'] = 'POST';
+ $this->request = $params;
+ }
+
+ public static function createPost(array $params = array())
+ {
+ $request = new self();
+ $request->setPost($params);
+
+ return $request;
+ }
+}
diff --git a/library/oauth2/test/lib/OAuth2/Storage/BaseTest.php b/library/oauth2/test/lib/OAuth2/Storage/BaseTest.php
new file mode 100755
index 000000000..921d52500
--- /dev/null
+++ b/library/oauth2/test/lib/OAuth2/Storage/BaseTest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace OAuth2\Storage;
+
+abstract class BaseTest extends \PHPUnit_Framework_TestCase
+{
+ public function provideStorage()
+ {
+ $memory = Bootstrap::getInstance()->getMemoryStorage();
+ $sqlite = Bootstrap::getInstance()->getSqlitePdo();
+ $mysql = Bootstrap::getInstance()->getMysqlPdo();
+ $postgres = Bootstrap::getInstance()->getPostgresPdo();
+ $mongo = Bootstrap::getInstance()->getMongo();
+ $redis = Bootstrap::getInstance()->getRedisStorage();
+ $cassandra = Bootstrap::getInstance()->getCassandraStorage();
+ $dynamodb = Bootstrap::getInstance()->getDynamoDbStorage();
+ $couchbase = Bootstrap::getInstance()->getCouchbase();
+
+ /* hack until we can fix "default_scope" dependencies in other tests */
+ $memory->defaultScope = 'defaultscope1 defaultscope2';
+
+ return array(
+ array($memory),
+ array($sqlite),
+ array($mysql),
+ array($postgres),
+ array($mongo),
+ array($redis),
+ array($cassandra),
+ array($dynamodb),
+ array($couchbase),
+ );
+ }
+}
diff --git a/library/oauth2/test/lib/OAuth2/Storage/Bootstrap.php b/library/oauth2/test/lib/OAuth2/Storage/Bootstrap.php
new file mode 100755
index 000000000..4ac9022b1
--- /dev/null
+++ b/library/oauth2/test/lib/OAuth2/Storage/Bootstrap.php
@@ -0,0 +1,888 @@
+<?php
+
+namespace OAuth2\Storage;
+
+class Bootstrap
+{
+ const DYNAMODB_PHP_VERSION = 'none';
+
+ protected static $instance;
+ private $mysql;
+ private $sqlite;
+ private $postgres;
+ private $mongo;
+ private $redis;
+ private $cassandra;
+ private $configDir;
+ private $dynamodb;
+ private $couchbase;
+
+ public function __construct()
+ {
+ $this->configDir = __DIR__.'/../../../config';
+ }
+
+ public static function getInstance()
+ {
+ if (!self::$instance) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ public function getSqlitePdo()
+ {
+ if (!$this->sqlite) {
+ $this->removeSqliteDb();
+ $pdo = new \PDO(sprintf('sqlite://%s', $this->getSqliteDir()));
+ $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ $this->createSqliteDb($pdo);
+
+ $this->sqlite = new Pdo($pdo);
+ }
+
+ return $this->sqlite;
+ }
+
+ public function getPostgresPdo()
+ {
+ if (!$this->postgres) {
+ if (in_array('pgsql', \PDO::getAvailableDrivers())) {
+ $this->removePostgresDb();
+ $this->createPostgresDb();
+ if ($pdo = $this->getPostgresDriver()) {
+ $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ $this->populatePostgresDb($pdo);
+ $this->postgres = new Pdo($pdo);
+ }
+ } else {
+ $this->postgres = new NullStorage('Postgres', 'Missing postgres PDO extension.');
+ }
+ }
+
+ return $this->postgres;
+ }
+
+ public function getPostgresDriver()
+ {
+ try {
+ $pdo = new \PDO('pgsql:host=localhost;dbname=oauth2_server_php', 'postgres');
+
+ return $pdo;
+ } catch (\PDOException $e) {
+ $this->postgres = new NullStorage('Postgres', $e->getMessage());
+ }
+ }
+
+ public function getMemoryStorage()
+ {
+ return new Memory(json_decode(file_get_contents($this->configDir. '/storage.json'), true));
+ }
+
+ public function getRedisStorage()
+ {
+ if (!$this->redis) {
+ if (class_exists('Predis\Client')) {
+ $redis = new \Predis\Client();
+ if ($this->testRedisConnection($redis)) {
+ $redis->flushdb();
+ $this->redis = new Redis($redis);
+ $this->createRedisDb($this->redis);
+ } else {
+ $this->redis = new NullStorage('Redis', 'Unable to connect to redis server on port 6379');
+ }
+ } else {
+ $this->redis = new NullStorage('Redis', 'Missing redis library. Please run "composer.phar require predis/predis:dev-master"');
+ }
+ }
+
+ return $this->redis;
+ }
+
+ private function testRedisConnection(\Predis\Client $redis)
+ {
+ try {
+ $redis->connect();
+ } catch (\Predis\CommunicationException $exception) {
+ // we were unable to connect to the redis server
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getMysqlPdo()
+ {
+ if (!$this->mysql) {
+ $pdo = null;
+ try {
+ $pdo = new \PDO('mysql:host=localhost;', 'root');
+ } catch (\PDOException $e) {
+ $this->mysql = new NullStorage('MySQL', 'Unable to connect to MySQL on root@localhost');
+ }
+
+ if ($pdo) {
+ $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ $this->removeMysqlDb($pdo);
+ $this->createMysqlDb($pdo);
+
+ $this->mysql = new Pdo($pdo);
+ }
+ }
+
+ return $this->mysql;
+ }
+
+ public function getMongo()
+ {
+ if (!$this->mongo) {
+ $skipMongo = $this->getEnvVar('SKIP_MONGO_TESTS');
+ if (!$skipMongo && class_exists('MongoClient')) {
+ $mongo = new \MongoClient('mongodb://localhost:27017', array('connect' => false));
+ if ($this->testMongoConnection($mongo)) {
+ $db = $mongo->oauth2_server_php;
+ $this->removeMongoDb($db);
+ $this->createMongoDb($db);
+
+ $this->mongo = new Mongo($db);
+ } else {
+ $this->mongo = new NullStorage('Mongo', 'Unable to connect to mongo server on "localhost:27017"');
+ }
+ } else {
+ $this->mongo = new NullStorage('Mongo', 'Missing mongo php extension. Please install mongo.so');
+ }
+ }
+
+ return $this->mongo;
+ }
+
+ private function testMongoConnection(\MongoClient $mongo)
+ {
+ try {
+ $mongo->connect();
+ } catch (\MongoConnectionException $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getCouchbase()
+ {
+ if (!$this->couchbase) {
+ if ($this->getEnvVar('SKIP_COUCHBASE_TESTS')) {
+ $this->couchbase = new NullStorage('Couchbase', 'Skipping Couchbase tests');
+ } elseif (!class_exists('Couchbase')) {
+ $this->couchbase = new NullStorage('Couchbase', 'Missing Couchbase php extension. Please install couchbase.so');
+ } else {
+ // round-about way to make sure couchbase is working
+ // this is required because it throws a "floating point exception" otherwise
+ $code = "new \Couchbase(array('localhost:8091'), '', '', 'auth', false);";
+ $exec = sprintf('php -r "%s"', $code);
+ $ret = exec($exec, $test, $var);
+ if ($ret != 0) {
+ $couchbase = new \Couchbase(array('localhost:8091'), '', '', 'auth', false);
+ if ($this->testCouchbaseConnection($couchbase)) {
+ $this->clearCouchbase($couchbase);
+ $this->createCouchbaseDB($couchbase);
+
+ $this->couchbase = new CouchbaseDB($couchbase);
+ } else {
+ $this->couchbase = new NullStorage('Couchbase', 'Unable to connect to Couchbase server on "localhost:8091"');
+ }
+ } else {
+ $this->couchbase = new NullStorage('Couchbase', 'Error while trying to connect to Couchbase');
+ }
+ }
+ }
+
+ return $this->couchbase;
+ }
+
+ private function testCouchbaseConnection(\Couchbase $couchbase)
+ {
+ try {
+ if (count($couchbase->getServers()) > 0) {
+ return true;
+ }
+ } catch (\CouchbaseException $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getCassandraStorage()
+ {
+ if (!$this->cassandra) {
+ if (class_exists('phpcassa\ColumnFamily')) {
+ $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_test', array('127.0.0.1:9160'));
+ if ($this->testCassandraConnection($cassandra)) {
+ $this->removeCassandraDb();
+ $this->cassandra = new Cassandra($cassandra);
+ $this->createCassandraDb($this->cassandra);
+ } else {
+ $this->cassandra = new NullStorage('Cassandra', 'Unable to connect to cassandra server on "127.0.0.1:9160"');
+ }
+ } else {
+ $this->cassandra = new NullStorage('Cassandra', 'Missing cassandra library. Please run "composer.phar require thobbs/phpcassa:dev-master"');
+ }
+ }
+
+ return $this->cassandra;
+ }
+
+ private function testCassandraConnection(\phpcassa\Connection\ConnectionPool $cassandra)
+ {
+ try {
+ new \phpcassa\SystemManager('localhost:9160');
+ } catch (\Exception $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private function removeCassandraDb()
+ {
+ $sys = new \phpcassa\SystemManager('localhost:9160');
+
+ try {
+ $sys->drop_keyspace('oauth2_test');
+ } catch (\cassandra\InvalidRequestException $e) {
+
+ }
+ }
+
+ private function createCassandraDb(Cassandra $storage)
+ {
+ // create the cassandra keyspace and column family
+ $sys = new \phpcassa\SystemManager('localhost:9160');
+
+ $sys->create_keyspace('oauth2_test', array(
+ "strategy_class" => \phpcassa\Schema\StrategyClass::SIMPLE_STRATEGY,
+ "strategy_options" => array('replication_factor' => '1')
+ ));
+
+ $sys->create_column_family('oauth2_test', 'auth');
+ $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_test', array('127.0.0.1:9160'));
+ $cf = new \phpcassa\ColumnFamily($cassandra, 'auth');
+
+ // populate the data
+ $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password');
+ $storage->setAccessToken("testtoken", "Some Client", '', time() + 1000);
+ $storage->setAuthorizationCode("testcode", "Some Client", '', '', time() + 1000);
+
+ $storage->setScope('supportedscope1 supportedscope2 supportedscope3 supportedscope4');
+ $storage->setScope('defaultscope1 defaultscope2', null, 'default');
+
+ $storage->setScope('clientscope1 clientscope2', 'Test Client ID');
+ $storage->setScope('clientscope1 clientscope2', 'Test Client ID', 'default');
+
+ $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Client ID 2');
+ $storage->setScope('clientscope1 clientscope2', 'Test Client ID 2', 'default');
+
+ $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID');
+ $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID', 'default');
+
+ $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Default Scope Client ID 2');
+ $storage->setScope('clientscope3', 'Test Default Scope Client ID 2', 'default');
+
+ $storage->setClientKey('oauth_test_client', $this->getTestPublicKey(), 'test_subject');
+
+ $cf->insert("oauth_public_keys:ClientID_One", array('__data' => json_encode(array("public_key" => "client_1_public", "private_key" => "client_1_private", "encryption_algorithm" => "RS256"))));
+ $cf->insert("oauth_public_keys:ClientID_Two", array('__data' => json_encode(array("public_key" => "client_2_public", "private_key" => "client_2_private", "encryption_algorithm" => "RS256"))));
+ $cf->insert("oauth_public_keys:", array('__data' => json_encode(array("public_key" => $this->getTestPublicKey(), "private_key" => $this->getTestPrivateKey(), "encryption_algorithm" => "RS256"))));
+
+ $cf->insert("oauth_users:testuser", array('__data' =>json_encode(array("password" => "password", "email" => "testuser@test.com", "email_verified" => true))));
+
+ }
+
+ private function createSqliteDb(\PDO $pdo)
+ {
+ $this->runPdoSql($pdo);
+ }
+
+ private function removeSqliteDb()
+ {
+ if (file_exists($this->getSqliteDir())) {
+ unlink($this->getSqliteDir());
+ }
+ }
+
+ private function createMysqlDb(\PDO $pdo)
+ {
+ $pdo->exec('CREATE DATABASE oauth2_server_php');
+ $pdo->exec('USE oauth2_server_php');
+ $this->runPdoSql($pdo);
+ }
+
+ private function removeMysqlDb(\PDO $pdo)
+ {
+ $pdo->exec('DROP DATABASE IF EXISTS oauth2_server_php');
+ }
+
+ private function createPostgresDb()
+ {
+ if (!`psql postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='postgres'"`) {
+ `createuser -s -r postgres`;
+ }
+
+ `createdb -O postgres oauth2_server_php`;
+ }
+
+ private function populatePostgresDb(\PDO $pdo)
+ {
+ $this->runPdoSql($pdo);
+ }
+
+ private function removePostgresDb()
+ {
+ if (trim(`psql -l | grep oauth2_server_php | wc -l`)) {
+ `dropdb oauth2_server_php`;
+ }
+ }
+
+ public function runPdoSql(\PDO $pdo)
+ {
+ $storage = new Pdo($pdo);
+ foreach (explode(';', $storage->getBuildSql()) as $statement) {
+ $result = $pdo->exec($statement);
+ }
+
+ // set up scopes
+ $sql = 'INSERT INTO oauth_scopes (scope) VALUES (?)';
+ foreach (explode(' ', 'supportedscope1 supportedscope2 supportedscope3 supportedscope4 clientscope1 clientscope2 clientscope3') as $supportedScope) {
+ $pdo->prepare($sql)->execute(array($supportedScope));
+ }
+
+ $sql = 'INSERT INTO oauth_scopes (scope, is_default) VALUES (?, ?)';
+ foreach (array('defaultscope1', 'defaultscope2') as $defaultScope) {
+ $pdo->prepare($sql)->execute(array($defaultScope, true));
+ }
+
+ // set up clients
+ $sql = 'INSERT INTO oauth_clients (client_id, client_secret, scope, grant_types) VALUES (?, ?, ?, ?)';
+ $pdo->prepare($sql)->execute(array('Test Client ID', 'TestSecret', 'clientscope1 clientscope2', null));
+ $pdo->prepare($sql)->execute(array('Test Client ID 2', 'TestSecret', 'clientscope1 clientscope2 clientscope3', null));
+ $pdo->prepare($sql)->execute(array('Test Default Scope Client ID', 'TestSecret', 'clientscope1 clientscope2', null));
+ $pdo->prepare($sql)->execute(array('oauth_test_client', 'testpass', null, 'implicit password'));
+
+ // set up misc
+ $sql = 'INSERT INTO oauth_access_tokens (access_token, client_id, expires, user_id) VALUES (?, ?, ?, ?)';
+ $pdo->prepare($sql)->execute(array('testtoken', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour')), null));
+ $pdo->prepare($sql)->execute(array('accesstoken-openid-connect', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour')), 'testuser'));
+
+ $sql = 'INSERT INTO oauth_authorization_codes (authorization_code, client_id, expires) VALUES (?, ?, ?)';
+ $pdo->prepare($sql)->execute(array('testcode', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour'))));
+
+ $sql = 'INSERT INTO oauth_users (username, password, email, email_verified) VALUES (?, ?, ?, ?)';
+ $pdo->prepare($sql)->execute(array('testuser', 'password', 'testuser@test.com', true));
+
+ $sql = 'INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES (?, ?, ?, ?)';
+ $pdo->prepare($sql)->execute(array('ClientID_One', 'client_1_public', 'client_1_private', 'RS256'));
+ $pdo->prepare($sql)->execute(array('ClientID_Two', 'client_2_public', 'client_2_private', 'RS256'));
+
+ $sql = 'INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES (?, ?, ?, ?)';
+ $pdo->prepare($sql)->execute(array(null, $this->getTestPublicKey(), $this->getTestPrivateKey(), 'RS256'));
+
+ $sql = 'INSERT INTO oauth_jwt (client_id, subject, public_key) VALUES (?, ?, ?)';
+ $pdo->prepare($sql)->execute(array('oauth_test_client', 'test_subject', $this->getTestPublicKey()));
+ }
+
+ public function getSqliteDir()
+ {
+ return $this->configDir. '/test.sqlite';
+ }
+
+ public function getConfigDir()
+ {
+ return $this->configDir;
+ }
+
+ private function createCouchbaseDB(\Couchbase $db)
+ {
+ $db->set('oauth_clients-oauth_test_client',json_encode(array(
+ 'client_id' => "oauth_test_client",
+ 'client_secret' => "testpass",
+ 'redirect_uri' => "http://example.com",
+ 'grant_types' => 'implicit password'
+ )));
+
+ $db->set('oauth_access_tokens-testtoken',json_encode(array(
+ 'access_token' => "testtoken",
+ 'client_id' => "Some Client"
+ )));
+
+ $db->set('oauth_authorization_codes-testcode',json_encode(array(
+ 'access_token' => "testcode",
+ 'client_id' => "Some Client"
+ )));
+
+ $db->set('oauth_users-testuser',json_encode(array(
+ 'username' => 'testuser',
+ 'password' => 'password',
+ 'email' => 'testuser@test.com',
+ 'email_verified' => true,
+ )));
+
+ $db->set('oauth_jwt-oauth_test_client',json_encode(array(
+ 'client_id' => 'oauth_test_client',
+ 'key' => $this->getTestPublicKey(),
+ 'subject' => 'test_subject',
+ )));
+ }
+
+ private function clearCouchbase(\Couchbase $cb)
+ {
+ $cb->delete('oauth_authorization_codes-new-openid-code');
+ $cb->delete('oauth_access_tokens-newtoken');
+ $cb->delete('oauth_authorization_codes-newcode');
+ $cb->delete('oauth_refresh_tokens-refreshtoken');
+ }
+
+ private function createMongoDb(\MongoDB $db)
+ {
+ $db->oauth_clients->insert(array(
+ 'client_id' => "oauth_test_client",
+ 'client_secret' => "testpass",
+ 'redirect_uri' => "http://example.com",
+ 'grant_types' => 'implicit password'
+ ));
+
+ $db->oauth_access_tokens->insert(array(
+ 'access_token' => "testtoken",
+ 'client_id' => "Some Client"
+ ));
+
+ $db->oauth_authorization_codes->insert(array(
+ 'authorization_code' => "testcode",
+ 'client_id' => "Some Client"
+ ));
+
+ $db->oauth_users->insert(array(
+ 'username' => 'testuser',
+ 'password' => 'password',
+ 'email' => 'testuser@test.com',
+ 'email_verified' => true,
+ ));
+
+ $db->oauth_jwt->insert(array(
+ 'client_id' => 'oauth_test_client',
+ 'key' => $this->getTestPublicKey(),
+ 'subject' => 'test_subject',
+ ));
+ }
+
+ private function createRedisDb(Redis $storage)
+ {
+ $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password');
+ $storage->setAccessToken("testtoken", "Some Client", '', time() + 1000);
+ $storage->setAuthorizationCode("testcode", "Some Client", '', '', time() + 1000);
+ $storage->setUser("testuser", "password");
+
+ $storage->setScope('supportedscope1 supportedscope2 supportedscope3 supportedscope4');
+ $storage->setScope('defaultscope1 defaultscope2', null, 'default');
+
+ $storage->setScope('clientscope1 clientscope2', 'Test Client ID');
+ $storage->setScope('clientscope1 clientscope2', 'Test Client ID', 'default');
+
+ $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Client ID 2');
+ $storage->setScope('clientscope1 clientscope2', 'Test Client ID 2', 'default');
+
+ $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID');
+ $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID', 'default');
+
+ $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Default Scope Client ID 2');
+ $storage->setScope('clientscope3', 'Test Default Scope Client ID 2', 'default');
+
+ $storage->setClientKey('oauth_test_client', $this->getTestPublicKey(), 'test_subject');
+ }
+
+ public function removeMongoDb(\MongoDB $db)
+ {
+ $db->drop();
+ }
+
+ public function getTestPublicKey()
+ {
+ return file_get_contents(__DIR__.'/../../../config/keys/id_rsa.pub');
+ }
+
+ private function getTestPrivateKey()
+ {
+ return file_get_contents(__DIR__.'/../../../config/keys/id_rsa');
+ }
+
+ public function getDynamoDbStorage()
+ {
+ if (!$this->dynamodb) {
+ // only run once per travis build
+ if (true == $this->getEnvVar('TRAVIS')) {
+ if (self::DYNAMODB_PHP_VERSION != $this->getEnvVar('TRAVIS_PHP_VERSION')) {
+ $this->dynamodb = new NullStorage('DynamoDb', 'Skipping for travis.ci - only run once per build');
+
+ return;
+ }
+ }
+ if (class_exists('\Aws\DynamoDb\DynamoDbClient')) {
+ if ($client = $this->getDynamoDbClient()) {
+ // travis runs a unique set of tables per build, to avoid conflict
+ $prefix = '';
+ if ($build_id = $this->getEnvVar('TRAVIS_JOB_NUMBER')) {
+ $prefix = sprintf('build_%s_', $build_id);
+ } else {
+ if (!$this->deleteDynamoDb($client, $prefix, true)) {
+ return $this->dynamodb = new NullStorage('DynamoDb', 'Timed out while waiting for DynamoDB deletion (30 seconds)');
+ }
+ }
+ $this->createDynamoDb($client, $prefix);
+ $this->populateDynamoDb($client, $prefix);
+ $config = array(
+ 'client_table' => $prefix.'oauth_clients',
+ 'access_token_table' => $prefix.'oauth_access_tokens',
+ 'refresh_token_table' => $prefix.'oauth_refresh_tokens',
+ 'code_table' => $prefix.'oauth_authorization_codes',
+ 'user_table' => $prefix.'oauth_users',
+ 'jwt_table' => $prefix.'oauth_jwt',
+ 'scope_table' => $prefix.'oauth_scopes',
+ 'public_key_table' => $prefix.'oauth_public_keys',
+ );
+ $this->dynamodb = new DynamoDB($client, $config);
+ } elseif (!$this->dynamodb) {
+ $this->dynamodb = new NullStorage('DynamoDb', 'unable to connect to DynamoDB');
+ }
+ } else {
+ $this->dynamodb = new NullStorage('DynamoDb', 'Missing DynamoDB library. Please run "composer.phar require aws/aws-sdk-php:dev-master');
+ }
+ }
+
+ return $this->dynamodb;
+ }
+
+ private function getDynamoDbClient()
+ {
+ $config = array();
+ // check for environment variables
+ if (($key = $this->getEnvVar('AWS_ACCESS_KEY_ID')) && ($secret = $this->getEnvVar('AWS_SECRET_KEY'))) {
+ $config['key'] = $key;
+ $config['secret'] = $secret;
+ } else {
+ // fall back on ~/.aws/credentials file
+ // @see http://docs.aws.amazon.com/aws-sdk-php/guide/latest/credentials.html#credential-profiles
+ if (!file_exists($this->getEnvVar('HOME') . '/.aws/credentials')) {
+ $this->dynamodb = new NullStorage('DynamoDb', 'No aws credentials file found, and no AWS_ACCESS_KEY_ID or AWS_SECRET_KEY environment variable set');
+
+ return;
+ }
+
+ // set profile in AWS_PROFILE environment variable, defaults to "default"
+ $config['profile'] = $this->getEnvVar('AWS_PROFILE', 'default');
+ }
+
+ // set region in AWS_REGION environment variable, defaults to "us-east-1"
+ $config['region'] = $this->getEnvVar('AWS_REGION', \Aws\Common\Enum\Region::US_EAST_1);
+
+ return \Aws\DynamoDb\DynamoDbClient::factory($config);
+ }
+
+ private function deleteDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null, $waitForDeletion = false)
+ {
+ $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users');
+ $nbTables = count($tablesList);
+
+ // Delete all table.
+ foreach ($tablesList as $key => $table) {
+ try {
+ $client->deleteTable(array('TableName' => $prefix.$table));
+ } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) {
+ // Table does not exist : nothing to do
+ }
+ }
+
+ // Wait for deleting
+ if ($waitForDeletion) {
+ $retries = 5;
+ $nbTableDeleted = 0;
+ while ($nbTableDeleted != $nbTables) {
+ $nbTableDeleted = 0;
+ foreach ($tablesList as $key => $table) {
+ try {
+ $result = $client->describeTable(array('TableName' => $prefix.$table));
+ } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) {
+ // Table does not exist : nothing to do
+ $nbTableDeleted++;
+ }
+ }
+ if ($nbTableDeleted != $nbTables) {
+ if ($retries < 0) {
+ // we are tired of waiting
+ return false;
+ }
+ sleep(5);
+ echo "Sleeping 5 seconds for DynamoDB ($retries more retries)...\n";
+ $retries--;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private function createDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null)
+ {
+ $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users');
+ $nbTables = count($tablesList);
+ $client->createTable(array(
+ 'TableName' => $prefix.'oauth_access_tokens',
+ 'AttributeDefinitions' => array(
+ array('AttributeName' => 'access_token','AttributeType' => 'S')
+ ),
+ 'KeySchema' => array(array('AttributeName' => 'access_token','KeyType' => 'HASH')),
+ 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1)
+ ));
+
+ $client->createTable(array(
+ 'TableName' => $prefix.'oauth_authorization_codes',
+ 'AttributeDefinitions' => array(
+ array('AttributeName' => 'authorization_code','AttributeType' => 'S')
+ ),
+ 'KeySchema' => array(array('AttributeName' => 'authorization_code','KeyType' => 'HASH')),
+ 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1)
+ ));
+
+ $client->createTable(array(
+ 'TableName' => $prefix.'oauth_clients',
+ 'AttributeDefinitions' => array(
+ array('AttributeName' => 'client_id','AttributeType' => 'S')
+ ),
+ 'KeySchema' => array(array('AttributeName' => 'client_id','KeyType' => 'HASH')),
+ 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1)
+ ));
+
+ $client->createTable(array(
+ 'TableName' => $prefix.'oauth_jwt',
+ 'AttributeDefinitions' => array(
+ array('AttributeName' => 'client_id','AttributeType' => 'S'),
+ array('AttributeName' => 'subject','AttributeType' => 'S')
+ ),
+ 'KeySchema' => array(
+ array('AttributeName' => 'client_id','KeyType' => 'HASH'),
+ array('AttributeName' => 'subject','KeyType' => 'RANGE')
+ ),
+ 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1)
+ ));
+
+ $client->createTable(array(
+ 'TableName' => $prefix.'oauth_public_keys',
+ 'AttributeDefinitions' => array(
+ array('AttributeName' => 'client_id','AttributeType' => 'S')
+ ),
+ 'KeySchema' => array(array('AttributeName' => 'client_id','KeyType' => 'HASH')),
+ 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1)
+ ));
+
+ $client->createTable(array(
+ 'TableName' => $prefix.'oauth_refresh_tokens',
+ 'AttributeDefinitions' => array(
+ array('AttributeName' => 'refresh_token','AttributeType' => 'S')
+ ),
+ 'KeySchema' => array(array('AttributeName' => 'refresh_token','KeyType' => 'HASH')),
+ 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1)
+ ));
+
+ $client->createTable(array(
+ 'TableName' => $prefix.'oauth_scopes',
+ 'AttributeDefinitions' => array(
+ array('AttributeName' => 'scope','AttributeType' => 'S'),
+ array('AttributeName' => 'is_default','AttributeType' => 'S')
+ ),
+ 'KeySchema' => array(array('AttributeName' => 'scope','KeyType' => 'HASH')),
+ 'GlobalSecondaryIndexes' => array(
+ array(
+ 'IndexName' => 'is_default-index',
+ 'KeySchema' => array(array('AttributeName' => 'is_default', 'KeyType' => 'HASH')),
+ 'Projection' => array('ProjectionType' => 'ALL'),
+ 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1)
+ ),
+ ),
+ 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1)
+ ));
+
+ $client->createTable(array(
+ 'TableName' => $prefix.'oauth_users',
+ 'AttributeDefinitions' => array(array('AttributeName' => 'username','AttributeType' => 'S')),
+ 'KeySchema' => array(array('AttributeName' => 'username','KeyType' => 'HASH')),
+ 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1)
+ ));
+
+ // Wait for creation
+ $nbTableCreated = 0;
+ while ($nbTableCreated != $nbTables) {
+ $nbTableCreated = 0;
+ foreach ($tablesList as $key => $table) {
+ try {
+ $result = $client->describeTable(array('TableName' => $prefix.$table));
+ if ($result['Table']['TableStatus'] == 'ACTIVE') {
+ $nbTableCreated++;
+ }
+ } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) {
+ // Table does not exist : nothing to do
+ $nbTableCreated++;
+ }
+ }
+ if ($nbTableCreated != $nbTables) {
+ sleep(1);
+ }
+ }
+ }
+
+ private function populateDynamoDb($client, $prefix = null)
+ {
+ // set up scopes
+ foreach (explode(' ', 'supportedscope1 supportedscope2 supportedscope3 supportedscope4 clientscope1 clientscope2 clientscope3') as $supportedScope) {
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_scopes',
+ 'Item' => array('scope' => array('S' => $supportedScope))
+ ));
+ }
+
+ foreach (array('defaultscope1', 'defaultscope2') as $defaultScope) {
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_scopes',
+ 'Item' => array('scope' => array('S' => $defaultScope), 'is_default' => array('S' => "true"))
+ ));
+ }
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_clients',
+ 'Item' => array(
+ 'client_id' => array('S' => 'Test Client ID'),
+ 'client_secret' => array('S' => 'TestSecret'),
+ 'scope' => array('S' => 'clientscope1 clientscope2')
+ )
+ ));
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_clients',
+ 'Item' => array(
+ 'client_id' => array('S' => 'Test Client ID 2'),
+ 'client_secret' => array('S' => 'TestSecret'),
+ 'scope' => array('S' => 'clientscope1 clientscope2 clientscope3')
+ )
+ ));
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_clients',
+ 'Item' => array(
+ 'client_id' => array('S' => 'Test Default Scope Client ID'),
+ 'client_secret' => array('S' => 'TestSecret'),
+ 'scope' => array('S' => 'clientscope1 clientscope2')
+ )
+ ));
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_clients',
+ 'Item' => array(
+ 'client_id' => array('S' => 'oauth_test_client'),
+ 'client_secret' => array('S' => 'testpass'),
+ 'grant_types' => array('S' => 'implicit password')
+ )
+ ));
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_access_tokens',
+ 'Item' => array(
+ 'access_token' => array('S' => 'testtoken'),
+ 'client_id' => array('S' => 'Some Client'),
+ )
+ ));
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_access_tokens',
+ 'Item' => array(
+ 'access_token' => array('S' => 'accesstoken-openid-connect'),
+ 'client_id' => array('S' => 'Some Client'),
+ 'user_id' => array('S' => 'testuser'),
+ )
+ ));
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_authorization_codes',
+ 'Item' => array(
+ 'authorization_code' => array('S' => 'testcode'),
+ 'client_id' => array('S' => 'Some Client'),
+ )
+ ));
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_users',
+ 'Item' => array(
+ 'username' => array('S' => 'testuser'),
+ 'password' => array('S' => 'password'),
+ 'email' => array('S' => 'testuser@test.com'),
+ 'email_verified' => array('S' => 'true'),
+ )
+ ));
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_public_keys',
+ 'Item' => array(
+ 'client_id' => array('S' => 'ClientID_One'),
+ 'public_key' => array('S' => 'client_1_public'),
+ 'private_key' => array('S' => 'client_1_private'),
+ 'encryption_algorithm' => array('S' => 'RS256'),
+ )
+ ));
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_public_keys',
+ 'Item' => array(
+ 'client_id' => array('S' => 'ClientID_Two'),
+ 'public_key' => array('S' => 'client_2_public'),
+ 'private_key' => array('S' => 'client_2_private'),
+ 'encryption_algorithm' => array('S' => 'RS256'),
+ )
+ ));
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_public_keys',
+ 'Item' => array(
+ 'client_id' => array('S' => '0'),
+ 'public_key' => array('S' => $this->getTestPublicKey()),
+ 'private_key' => array('S' => $this->getTestPrivateKey()),
+ 'encryption_algorithm' => array('S' => 'RS256'),
+ )
+ ));
+
+ $client->putItem(array(
+ 'TableName' => $prefix.'oauth_jwt',
+ 'Item' => array(
+ 'client_id' => array('S' => 'oauth_test_client'),
+ 'subject' => array('S' => 'test_subject'),
+ 'public_key' => array('S' => $this->getTestPublicKey()),
+ )
+ ));
+ }
+
+ public function cleanupTravisDynamoDb($prefix = null)
+ {
+ if (is_null($prefix)) {
+ // skip this when not applicable
+ if (!$this->getEnvVar('TRAVIS') || self::DYNAMODB_PHP_VERSION != $this->getEnvVar('TRAVIS_PHP_VERSION')) {
+ return;
+ }
+
+ $prefix = sprintf('build_%s_', $this->getEnvVar('TRAVIS_JOB_NUMBER'));
+ }
+
+ $client = $this->getDynamoDbClient();
+ $this->deleteDynamoDb($client, $prefix);
+ }
+
+ private function getEnvVar($var, $default = null)
+ {
+ return isset($_SERVER[$var]) ? $_SERVER[$var] : (getenv($var) ?: $default);
+ }
+}
diff --git a/library/oauth2/test/lib/OAuth2/Storage/NullStorage.php b/library/oauth2/test/lib/OAuth2/Storage/NullStorage.php
new file mode 100644
index 000000000..6caa62068
--- /dev/null
+++ b/library/oauth2/test/lib/OAuth2/Storage/NullStorage.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace OAuth2\Storage;
+
+/**
+*
+*/
+class NullStorage extends Memory
+{
+ private $name;
+ private $description;
+
+ public function __construct($name, $description = null)
+ {
+ $this->name = $name;
+ $this->description = $description;
+ }
+
+ public function __toString()
+ {
+ return $this->name;
+ }
+
+ public function getMessage()
+ {
+ if ($this->description) {
+ return $this->description;
+ }
+
+ return $this->name;
+ }
+}