diff --git a/.travis.yml b/.travis.yml index 6bc9251da..77f50b57b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ php: - 5.6 - 7.0 - 7.1 -- hhvm env: global: - secure: Bc5ZqvZ1YYpoPZNNuU2eCB8DS6vBYrAdfBtTenBs5NSxzb+Vjven4kWakbzaMvZjb/Ib7Uph7DGuOtJXpmxnvBXPLd707LZ89oFWN/yqQlZKCcm8iErvJCB5XL+/ONHj2iPdR242HJweMcat6bMCwbVWoNDidjtWMH0U2mYFy3M= @@ -22,10 +21,11 @@ services: - cassandra before_install: - phpenv config-rm xdebug.ini || return 0 +- phpenv version-name | grep ^5.[3-6] && composer remove mongodb/mongodb --dev && echo "extension=mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true +- phpenv version-name | grep ^7 && echo "extension=mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true install: -- composer install --no-interaction +- composer install before_script: - psql -c 'create database oauth2_server_php;' -U postgres -- phpenv version-name | grep ^5.[3-6] && echo "extension=mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true after_script: - php test/cleanup.php diff --git a/composer.json b/composer.json index b1f28c707..561699f5e 100644 --- a/composer.json +++ b/composer.json @@ -12,22 +12,23 @@ } ], "homepage": "http://github.com/bshaffer/oauth2-server-php", - "require":{ - "php":">=5.3.9" - }, "autoload": { "psr-0": { "OAuth2": "src/" } }, - "suggest": { - "predis/predis": "Required to use the Redis storage engine", - "thobbs/phpcassa": "Required to use the Cassandra storage engine", - "aws/aws-sdk-php": "~2.8 is required to use the DynamoDB storage engine", - "firebase/php-jwt": "~2.2 is required to use JWT features" + "require":{ + "php":">=5.3.9" }, "require-dev": { "aws/aws-sdk-php": "~2.8", "firebase/php-jwt": "~2.2", "predis/predis": "dev-master", - "thobbs/phpcassa": "dev-master" + "thobbs/phpcassa": "dev-master", + "mongodb/mongodb": "^1.1" + }, + "suggest": { + "predis/predis": "Required to use Redis storage", + "thobbs/phpcassa": "Required to use Cassandra storage", + "aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage", + "firebase/php-jwt": "~1.1 is required to use MondoDB storage" } } diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php index cef35e5e9..eea06e315 100644 --- a/src/OAuth2/Storage/Mongo.php +++ b/src/OAuth2/Storage/Mongo.php @@ -22,6 +22,7 @@ class Mongo implements AuthorizationCodeInterface, UserCredentialsInterface, RefreshTokenInterface, JwtBearerInterface, + PublicKeyInterface, OpenIDAuthorizationCodeInterface { protected $db; @@ -46,6 +47,7 @@ public function __construct($connection, $config = array()) 'refresh_token_table' => 'oauth_refresh_tokens', 'code_table' => 'oauth_authorization_codes', 'user_table' => 'oauth_users', + 'key_table' => 'oauth_keys', 'jwt_table' => 'oauth_jwt', ), $config); } @@ -336,4 +338,55 @@ public function setJti($client_id, $subject, $audience, $expiration, $jti) //TODO: Needs mongodb implementation. throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); } + + public function getPublicKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['public_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['public_key']; + } + + public function getPrivateKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['private_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['encryption_algorithm']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? 'RS256' : $result['encryption_algorithm']; + } } diff --git a/src/OAuth2/Storage/MongoDB.php b/src/OAuth2/Storage/MongoDB.php new file mode 100644 index 000000000..64f740fc1 --- /dev/null +++ b/src/OAuth2/Storage/MongoDB.php @@ -0,0 +1,380 @@ + + */ +class MongoDB implements AuthorizationCodeInterface, + UserCredentialsInterface, + AccessTokenInterface, + ClientCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + PublicKeyInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $config; + + public function __construct($connection, $config = array()) + { + if ($connection instanceof Database) { + $this->db = $connection; + } else { + if (!is_array($connection)) { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB\Database or a configuration array'); + } + $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']); + $m = new Client($server); + $this->db = $m->selectDatabase($connection['database']); + } + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'jti_table' => 'oauth_jti', + 'scope_table' => 'oauth_scopes', + 'key_table' => 'oauth_keys', + ), $config); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return $result['client_secret'] == $client_secret; + } + return false; + } + + public function isPublicClient($client_id) + { + if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return false; + } + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + $result = $this->collection('client_table')->findOne(array('client_id' => $client_id)); + return is_null($result) ? false : $result; + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + if ($this->getClientDetails($client_id)) { + $result = $this->collection('client_table')->updateOne( + array('client_id' => $client_id), + array('$set' => array( + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )) + ); + return $result->getMatchedCount() > 0; + } + $client = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ); + $result = $this->collection('client_table')->insertOne($client); + return $result->getInsertedCount() > 0; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + $details = $this->getClientDetails($client_id); + if (isset($details['grant_types'])) { + $grant_types = explode(' ', $details['grant_types']); + return in_array($grant_type, $grant_types); + } + // if grant_types are not defined, then none are restricted + return true; + } + + /* AccessTokenInterface */ + public function getAccessToken($access_token) + { + $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token)); + return is_null($token) ? false : $token; + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + // if it exists, update it. + if ($this->getAccessToken($access_token)) { + $result = $this->collection('access_token_table')->updateOne( + array('access_token' => $access_token), + array('$set' => array( + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )) + ); + return $result->getMatchedCount() > 0; + } + $token = array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + ); + $result = $this->collection('access_token_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function unsetAccessToken($access_token) + { + $result = $this->collection('access_token_table')->deleteOne(array( + 'access_token' => $access_token + )); + return $result->getDeletedCount() > 0; + } + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + $code = $this->collection('code_table')->findOne(array( + 'authorization_code' => $code + )); + return is_null($code) ? false : $code; + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + // if it exists, update it. + if ($this->getAuthorizationCode($code)) { + $result = $this->collection('code_table')->updateOne( + array('authorization_code' => $code), + array('$set' => array( + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )) + ); + return $result->getMatchedCount() > 0; + } + $token = array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + ); + $result = $this->collection('code_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function expireAuthorizationCode($code) + { + $result = $this->collection('code_table')->deleteOne(array( + 'authorization_code' => $code + )); + return $result->getDeletedCount() > 0; + } + + /* UserCredentialsInterface */ + public function checkUserCredentials($username, $password) + { + if ($user = $this->getUser($username)) { + return $this->checkPassword($user, $password); + } + return false; + } + + public function getUserDetails($username) + { + if ($user = $this->getUser($username)) { + $user['user_id'] = $user['username']; + } + return $user; + } + + /* RefreshTokenInterface */ + public function getRefreshToken($refresh_token) + { + $token = $this->collection('refresh_token_table')->findOne(array( + 'refresh_token' => $refresh_token + )); + return is_null($token) ? false : $token; + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $token = array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + ); + $result = $this->collection('refresh_token_table')->insertOne($token); + return $result->getInsertedCount() > 0; + } + + public function unsetRefreshToken($refresh_token) + { + $result = $this->collection('refresh_token_table')->deleteOne(array( + 'refresh_token' => $refresh_token + )); + return $result->getDeletedCount() > 0; + } + + // plaintext passwords are bad! Override this for your application + protected function checkPassword($user, $password) + { + return $user['password'] == $password; + } + + public function getUser($username) + { + $result = $this->collection('user_table')->findOne(array('username' => $username)); + return is_null($result) ? false : $result; + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + if ($this->getUser($username)) { + $result = $this->collection('user_table')->updateOne( + array('username' => $username), + array('$set' => array( + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )) + ); + + return $result->getMatchedCount() > 0; + } + + $user = array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + ); + $result = $this->collection('user_table')->insertOne($user); + return $result->getInsertedCount() > 0; + } + + public function getClientKey($client_id, $subject) + { + $result = $this->collection('jwt_table')->findOne(array( + 'client_id' => $client_id, + 'subject' => $subject + )); + return is_null($result) ? false : $result['key']; + } + + public function getClientScope($client_id) + { + if (!$clientDetails = $this->getClientDetails($client_id)) { + return false; + } + if (isset($clientDetails['scope'])) { + return $clientDetails['scope']; + } + return null; + } + + public function getJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.'); + } + + public function setJti($client_id, $subject, $audience, $expires, $jti) + { + //TODO: Needs mongodb implementation. + throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); + } + + public function getPublicKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['public_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['public_key']; + } + + public function getPrivateKey($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['private_key']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? false : $result['private_key']; + } + + public function getEncryptionAlgorithm($client_id = null) + { + if ($client_id) { + $result = $this->collection('key_table')->findOne(array( + 'client_id' => $client_id + )); + if ($result) { + return $result['encryption_algorithm']; + } + } + + $result = $this->collection('key_table')->findOne(array( + 'client_id' => null + )); + return is_null($result) ? 'RS256' : $result['encryption_algorithm']; + } + + // Helper function to access a MongoDB collection by `type`: + protected function collection($name) + { + return $this->db->{$this->config[$name]}; + } +} \ No newline at end of file diff --git a/test/lib/OAuth2/Storage/BaseTest.php b/test/lib/OAuth2/Storage/BaseTest.php index 921d52500..f0b1274a2 100755 --- a/test/lib/OAuth2/Storage/BaseTest.php +++ b/test/lib/OAuth2/Storage/BaseTest.php @@ -11,6 +11,7 @@ public function provideStorage() $mysql = Bootstrap::getInstance()->getMysqlPdo(); $postgres = Bootstrap::getInstance()->getPostgresPdo(); $mongo = Bootstrap::getInstance()->getMongo(); + $mongoDb = Bootstrap::getInstance()->getMongoDB(); $redis = Bootstrap::getInstance()->getRedisStorage(); $cassandra = Bootstrap::getInstance()->getCassandraStorage(); $dynamodb = Bootstrap::getInstance()->getDynamoDbStorage(); @@ -25,6 +26,7 @@ public function provideStorage() array($mysql), array($postgres), array($mongo), + array($mongoDb), array($redis), array($cassandra), array($dynamodb), diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php index 4ac9022b1..3d7bdd4e9 100755 --- a/test/lib/OAuth2/Storage/Bootstrap.php +++ b/test/lib/OAuth2/Storage/Bootstrap.php @@ -11,6 +11,7 @@ class Bootstrap private $sqlite; private $postgres; private $mongo; + private $mongoDb; private $redis; private $cassandra; private $configDir; @@ -137,13 +138,12 @@ public function getMysqlPdo() public function getMongo() { if (!$this->mongo) { - $skipMongo = $this->getEnvVar('SKIP_MONGO_TESTS'); - if (!$skipMongo && class_exists('MongoClient')) { + if (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); + $db = $mongo->oauth2_server_php_legacy; + $this->removeMongo($db); + $this->createMongo($db); $this->mongo = new Mongo($db); } else { @@ -157,6 +157,28 @@ public function getMongo() return $this->mongo; } + public function getMongoDb() + { + if (!$this->mongoDb) { + if (class_exists('MongoDB\Client')) { + $mongoDb = new \MongoDB\Client('mongodb://localhost:27017'); + if ($this->testMongoDBConnection($mongoDb)) { + $db = $mongoDb->oauth2_server_php; + $this->removeMongoDb($db); + $this->createMongoDb($db); + + $this->mongoDb = new MongoDB($db); + } else { + $this->mongoDb = new NullStorage('MongoDB', 'Unable to connect to mongo server on "localhost:27017"'); + } + } else { + $this->mongoDb = new NullStorage('MongoDB', 'Missing MongoDB php extension. Please install mongodb.so'); + } + } + + return $this->mongoDb; + } + private function testMongoConnection(\MongoClient $mongo) { try { @@ -168,6 +190,11 @@ private function testMongoConnection(\MongoClient $mongo) return true; } + private function testMongoDBConnection(\MongoDB\Client $mongo) + { + return true; + } + public function getCouchbase() { if (!$this->couchbase) { @@ -442,7 +469,7 @@ private function clearCouchbase(\Couchbase $cb) $cb->delete('oauth_refresh_tokens-refreshtoken'); } - private function createMongoDb(\MongoDB $db) + private function createMongo(\MongoDB $db) { $db->oauth_clients->insert(array( 'client_id' => "oauth_test_client", @@ -468,13 +495,70 @@ private function createMongoDb(\MongoDB $db) 'email_verified' => true, )); + $db->oauth_keys->insert(array( + 'client_id' => null, + 'public_key' => $this->getTestPublicKey(), + 'private_key' => $this->getTestPrivateKey(), + 'encryption_algorithm' => 'RS256' + )); + $db->oauth_jwt->insert(array( 'client_id' => 'oauth_test_client', - 'key' => $this->getTestPublicKey(), + 'key' => $this->getTestPublicKey(), 'subject' => 'test_subject', )); } + public function removeMongo(\MongoDB $db) + { + $db->drop(); + } + + private function createMongoDB(\MongoDB\Database $db) + { + $db->oauth_clients->insertOne(array( + 'client_id' => "oauth_test_client", + 'client_secret' => "testpass", + 'redirect_uri' => "http://example.com", + 'grant_types' => 'implicit password' + )); + + $db->oauth_access_tokens->insertOne(array( + 'access_token' => "testtoken", + 'client_id' => "Some Client" + )); + + $db->oauth_authorization_codes->insertOne(array( + 'authorization_code' => "testcode", + 'client_id' => "Some Client" + )); + + $db->oauth_users->insertOne(array( + 'username' => 'testuser', + 'password' => 'password', + 'email' => 'testuser@test.com', + 'email_verified' => true, + )); + + $db->oauth_keys->insertOne(array( + 'client_id' => null, + 'public_key' => $this->getTestPublicKey(), + 'private_key' => $this->getTestPrivateKey(), + 'encryption_algorithm' => 'RS256' + )); + + $db->oauth_jwt->insertOne(array( + 'client_id' => 'oauth_test_client', + 'key' => $this->getTestPublicKey(), + 'subject' => 'test_subject', + )); + } + + public function removeMongoDB(\MongoDB\Database $db) + { + $db->drop(); + } + private function createRedisDb(Redis $storage) { $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password'); @@ -500,11 +584,6 @@ private function createRedisDb(Redis $storage) $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');