diff --git a/.gitignore b/.gitignore index c43a667d4..8a0c06d6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -# Test Files # -test/config/test.sqlite -vendor -composer.lock -.idea +# Test Files # +test/config/test.sqlite +vendor +composer.lock +.idea +nbproject \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index dd4aae4a6..4f634b00c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,11 @@ services: - cassandra before_install: - phpenv config-rm xdebug.ini || return 0 +- if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.0" ]]; then + phpenv config-add testing/mongodb.ini; + elif [[ ${TRAVIS_PHP_VERSION:0:3} != "5.3" ]]; then + pecl install mongodb || /bin/true; + fi install: - composer install --no-interaction before_script: diff --git a/src/OAuth2/Storage/MongoDB.php b/src/OAuth2/Storage/MongoDB.php new file mode 100644 index 000000000..b2df645c0 --- /dev/null +++ b/src/OAuth2/Storage/MongoDB.php @@ -0,0 +1,364 @@ + + */ +class MongoDB implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + OpenIDAuthorizationCodeInterface +{ + protected $db; + protected $database; + protected $config; + + public function __construct($connection, $config = array()) + { + if (is_string ($connection)) { + $this->db = new Manager($connection); + if (preg_match('/^mongodb:\\/\\/.+\\/([^?&]+)/s', $connection, $matches)) { + $this->database = $matches[1]; + } else { + throw new \InvalidArgumentException("Unable to determine Database Name from dsn."); + } + } elseif (is_array($connection)) { + $a = array('mongodb://'); + if (!empty($connection['username'])) { + $a[] = $connection['username'] . ':'; + } + if (!empty($connection['password'])) { + $a[] = rawurlencode($connection['password']) . '@'; + } + if (!empty($connection['host'])) { + $a[] = $connection['host']; + } + if (!empty($connection['port'])) { + $a[] = ':' . $connection['port']; + } + if (!empty($connection['database'])) { + $a[] = '/' . $connection['database']; + $this->database = $connection['database']; + } + $dsn = implode('', $a); + + $o = !empty($connection['options']) ? $connection['options'] : array(); + $options = array_merge(array( + 'w' => \MongoDB\Driver\WriteConcern::MAJORITY, + 'j' => true, + 'readPreference' => \MongoDB\Driver\ReadPreference::RP_NEAREST + ), $o); + $driverOptions = !empty($connection['driverOptions']) ? $connection['driverOptions'] : array(); + + $this->db = new Manager($dsn, $options, $driverOptions); + } else { + throw new \InvalidArgumentException('First argument to OAuth2\Storage\MongoDB must be a string or a configuration array'); + } + + $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); + } + + /* ClientCredentialsInterface */ + public function checkClientCredentials($client_id, $client_secret = null) + { + if ($result = $this->findOne('client_table', array('client_id' => $client_id))) { + return $result['client_secret'] == $client_secret; + } + return false; + } + + /** + * @param string $client_id + * @return boolean + */ + public function isPublicClient($client_id) + { + if (!$result = $this->findOne('client_table', array('client_id' => $client_id))) { + return false; + } + return empty($result['client_secret']); + } + + /* ClientInterface */ + public function getClientDetails($client_id) + { + return $this->findOne('client_table', array('client_id' => $client_id)); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + $bulk = new BulkWrite(); + $bulk->update( + array('client_id' => $client_id), + array('$set' => array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + )), + array('upsert' => true) + ); + $this->db->executeBulkWrite($this->collection('client_table'), $bulk); + return true; + } + + public function unsetClientDetails($client_id) + { + $this->delete('client_table', array('client_id' => $client_id)); + return true; + } + + public function checkRestrictedGrantType($client_id, $grant_type) + { + if ($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) + { + return $this->findOne('access_token_table', array('access_token' => $access_token)); + } + + public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) + { + $bulk = new BulkWrite(); + $bulk->update( + array('access_token' => $access_token), + array('$set' => array( + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope + )), + array('upsert' => true) + ); + $this->db->executeBulkWrite($this->collection('access_token_table'), $bulk); + return true; + } + + public function unsetAccessToken($access_token) + { + $this->delete('access_token_table', array('access_token' => $access_token)); + return true; + } + + + /* AuthorizationCodeInterface */ + public function getAuthorizationCode($code) + { + return $this->findOne('code_table', array('authorization_code' => $code)); + } + + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + { + $bulk = new BulkWrite(); + $bulk->update( + array('authorization_code' => $code), + array('$set' => array( + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + )), + array('upsert' => true) + ); + $this->db->executeBulkWrite($this->collection('code_table'), $bulk); + return true; + } + + public function expireAuthorizationCode($code) + { + $this->delete('code_table', 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) + { + return $this->findOne('refresh_token_table', array('refresh_token' => $refresh_token)); + } + + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) + { + $bulk = new BulkWrite(); + $bulk->update( + array('refresh_token' => $refresh_token), + array('$set' => array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + )), + array('upsert' => true) + ); + $this->db->executeBulkWrite($this->collection('refresh_token_table'), $bulk); + return true; + } + + public function unsetRefreshToken($refresh_token) + { + $this->delete('refresh_token_table', array('refresh_token' => $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) + { + return $this->findOne('user_table', array('username' => $username)); + } + + public function setUser($username, $password, $firstName = null, $lastName = null) + { + $bulk = new BulkWrite(); + $bulk->update( + array('username' => $username), + array('$set' => array( + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName + )), + array('upsert' => true) + ); + $this->db->executeBulkWrite($this->collection('user_table'), $bulk); + return true; + } + + public function getClientKey($client_id, $subject) + { + $result = $this->findOne('jwt_table', array( + 'client_id' => $client_id, + 'subject' => $subject + )); + return $result ? $result['key'] : 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 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.'); + } + + + protected function collection($name) + { + return $this->database . '.' . $this->config[$name]; + } + + /** + * + * @param string $collection + * @param array $filter + * @return array | false + */ + protected function findOne($collection, $filter) + { + $query = new Query($filter, array('limit' => 1, array('sort' => array('_id' => -1)))); + $cursor = $this->db + ->executeQuery($this->collection($collection), $query); + $cursor->setTypeMap(array( + 'root' => 'array', + 'document' => 'array' + )); + $result = $cursor->toArray(); + return current($result); + } + + /** + * + * @param string $collection + * @param array $filter + * @return boolean + */ + protected function delete($collection, $filter) + { + $bulk = new BulkWrite(); + $bulk->delete($filter); + $this->db->executeBulkWrite($this->collection($collection), $bulk); + return true; + } +} diff --git a/test/OAuth2/Storage/MongoDBTest.php b/test/OAuth2/Storage/MongoDBTest.php new file mode 100644 index 000000000..8163bfe67 --- /dev/null +++ b/test/OAuth2/Storage/MongoDBTest.php @@ -0,0 +1,132 @@ + + */ +class MongoDBTest extends BaseTest +{ + + public function __construct() + { + $mongodb = Bootstrap::getInstance()->getMongoDB(); + } + + public function testSetClientDetails() + { + $db = new \OAuth2\Storage\MongoDB('mongodb://localhost:27017/oauth2_server_php'); + +// comment with Bootstrap::getInstance()->getMongoDB(); +// $db->setClientDetails('oauth_test_client', 'testpass', 'redirect_uri', [], 'map green', 'oauth_test_user_id'); + + $this->assertFalse($db->checkClientCredentials('oauth_test_client')); + $this->assertTrue($db->checkClientCredentials('oauth_test_client', 'testpass')); + $this->assertFalse($db->checkClientCredentials('oauth_test_client', 'testpass!!!!')); + + $db->setClientDetails('oauth_test_client', 'testpass!!!', 'redirect_uri', [], 'map green', 'oauth_test_user_id'); + $this->assertFalse($db->checkClientCredentials('oauth_test_client', 'testpass')); + + $db->unsetClientDetails('oauth_test_client'); + $this->assertFalse($db->checkClientCredentials('oauth_test_client', 'testpass')); + + $db->setClientDetails('oauth_test_client', 'testpass', 'redirect_uri', [], 'map green', 'oauth_test_user_id'); + $this->assertTrue($db->checkClientCredentials('oauth_test_client', 'testpass')); + } + + public function testConnection() + { + $db = new \OAuth2\Storage\MongoDB('mongodb://localhost:27017/oauth2_server_php'); + $this->assertNotFalse($db->getClientDetails('oauth_test_client')); + $db = new \OAuth2\Storage\MongoDB(['host' => 'localhost', 'port' => '27017', 'database' => 'oauth2_server_php']); + $this->assertNotFalse($db->getClientDetails('oauth_test_client')); + } + + public function testIsPublicClient() + { + $db = new \OAuth2\Storage\MongoDB('mongodb://localhost:27017/oauth2_server_php'); + + $res = $db->isPublicClient('oauth_test_client'); + $this->assertFalse($res); + } + + public function testAccessToken() + { + $db = new \OAuth2\Storage\MongoDB('mongodb://localhost:27017/oauth2_server_php'); + + $this->assertNotFalse($db->getAccessToken('testtoken')); + $db->setAccessToken('testtoken', 'Some Client!!!', 'oauth_test_user_id', 1); + $this->assertEquals($db->getAccessToken('testtoken')['client_id'], 'Some Client!!!'); + $db->unsetAccessToken('testtoken'); + $this->assertFalse($db->getAccessToken('testtoken')); + $db->setAccessToken('testtoken', 'Some Client', 'oauth_test_user_id', 2); + $this->assertNotFalse($db->getAccessToken('testtoken')); + + } + + public function testAuthorizationCode() + { + $db = new \OAuth2\Storage\MongoDB('mongodb://localhost:27017/oauth2_server_php'); + + $this->assertNotFalse($db->getAuthorizationCode('testcode')); + $db->setAuthorizationCode('testcode', 'Some Client!!!', 'oauth_test_user_id', 'http://example.com', 0); + $this->assertEquals($db->getAuthorizationCode('testcode')['client_id'], 'Some Client!!!'); + $db->expireAuthorizationCode('testcode'); + $this->assertFalse($db->getAuthorizationCode('testcode')); + $db->setAuthorizationCode('testcode', 'Some Client', 'oauth_test_user_id', 'http://example.com', 23442); + $this->assertNotFalse($db->getAuthorizationCode('testcode')); + } + + public function testRefreshToken() + { + $db = new \OAuth2\Storage\MongoDB('mongodb://localhost:27017/oauth2_server_php'); + + $this->assertNotFalse($db->getRefreshToken('testrefreshtoken')); + $db->setRefreshToken('testrefreshtoken', 'Some Client!!!', 'oauth_test_user_id', 0); + $this->assertEquals($db->getRefreshToken('testrefreshtoken')['client_id'], 'Some Client!!!'); + $db->unsetRefreshToken('testrefreshtoken'); + $this->assertFalse($db->getRefreshToken('testrefreshtoken')); + $db->setRefreshToken('testrefreshtoken', 'Some Client', 'oauth_test_user_id', 232342); + $this->assertNotFalse($db->getRefreshToken('testrefreshtoken')); + } + + public function testCheckUserCredentials() + { + $db = new \OAuth2\Storage\MongoDB('mongodb://localhost:27017/oauth2_server_php'); + + $this->assertTrue($db->checkUserCredentials('testuser', 'password')); + } + + public function testGetUserDetails() + { + $db = new \OAuth2\Storage\MongoDB('mongodb://localhost:27017/oauth2_server_php'); + $this->assertNotFalse($db->getUserDetails('testuser')); + } + + public function testUser() + { + $db = new \OAuth2\Storage\MongoDB('mongodb://localhost:27017/oauth2_server_php'); + + $this->assertNotFalse($db->getUser('testuser')); + $db->setUser('testuser', 'password123123', 'First Name', 'Last Name'); + $this->assertTrue($db->checkUserCredentials('testuser', 'password123123')); + } + + public function testGetClientKey() + { + $db = new \OAuth2\Storage\MongoDB('mongodb://localhost:27017/oauth2_server_php'); + + $this->assertNotFalse($db->getClientKey('oauth_test_client', 'test_subject')); + } + + public function testGetClientScope() + { + $db = new \OAuth2\Storage\MongoDB('mongodb://localhost:27017/oauth2_server_php'); + + $this->assertStringMatchesFormat('%S', $db->getClientScope('oauth_test_client')); + + } + +} diff --git a/test/lib/OAuth2/Storage/BaseTest.php b/test/lib/OAuth2/Storage/BaseTest.php index 921d52500..672c504ce 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,10 +26,11 @@ public function provideStorage() array($mysql), array($postgres), array($mongo), + array($mongodb), array($redis), array($cassandra), array($dynamodb), array($couchbase), ); } -} +} \ No newline at end of file diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php index 4ac9022b1..6a9601092 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; @@ -142,9 +143,8 @@ public function getMongo() $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->removeMongo($db); + $this->createMongo($db); $this->mongo = new Mongo($db); } else { $this->mongo = new NullStorage('Mongo', 'Unable to connect to mongo server on "localhost:27017"'); @@ -167,7 +167,32 @@ private function testMongoConnection(\MongoClient $mongo) return true; } + + public function getMongoDB() + { + if (!$this->mongodb) { + if ('5.3' === substr(phpversion(), 0, 3)) { + $this->mongodb = new NullStorage('MongoDB', 'The mongodb.so extension is not compatible with PHP 5.3'); + } elseif ($this->getEnvVar('SKIP_MONGODB_TESTS')) { + $this->mongodb = new NullStorage('MongoDB', 'Skipping MongoDB tests'); + } elseif (class_exists('\MongoDB\Driver\Manager')) { + try { + $mongodb = new \MongoDB\Driver\Manager('mongodb://localhost:27017'); + $mongodb->selectServer($mongodb->getReadPreference()); // checking servers + $this->removeMongoDB($mongodb); + $this->createMongoDB($mongodb); + $this->mongodb = new MongoDB('mongodb://localhost:27017'); + } catch (\Exception $e) { + $this->mongodb = new NullStorage('MongoDB', 'Unable to connect to MongoDB server on "localhost:27017"'); + } + } else { + $this->mongodb = new NullStorage('MongoDB', 'Missing MongoDB php extension. Please install mongodb.so'); + } + } + return $this->mongodb; + } + public function getCouchbase() { if (!$this->couchbase) { @@ -442,7 +467,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", @@ -474,6 +499,87 @@ private function createMongoDb(\MongoDB $db) 'subject' => 'test_subject', )); } + + private function createMongoDB(\MongoDB\Driver\Manager $mongodb) + { + $bulk = new \MongoDB\Driver\BulkWrite(); + $bulk->insert(array( + 'client_id' => "oauth_test_client", + 'client_secret' => "testpass", + 'redirect_uri' => "http://example.com", + 'grant_types' => 'implicit password', + 'scope' => 'clientscope1 clientscope2' + )); + $bulk->insert(array( + 'client_id' => "oauth_test_client2", + 'client_secret' => "testpass2", + 'redirect_uri' => "http://example.com", + 'grant_types' => 'implicit password', + 'scope' => 'clientscope3 clientscope4' + )); + $mongodb->executeBulkWrite('oauth2_server_php.oauth_clients', $bulk); + + $bulk = new \MongoDB\Driver\BulkWrite(); + $bulk->insert(array( + 'access_token' => "testtoken", + 'client_id' => "Some Client" + )); + $bulk->insert(array( + 'access_token' => "testtoken2", + 'client_id' => "Some Client2" + )); + $mongodb->executeBulkWrite('oauth2_server_php.oauth_access_tokens', $bulk); + + $bulk = new \MongoDB\Driver\BulkWrite(); + $bulk->insert(array( + 'authorization_code' => "testcode", + 'client_id' => "Some Client" + )); + $bulk->insert(array( + 'authorization_code' => "testcode2", + 'client_id' => "Some Client2" + )); + $mongodb->executeBulkWrite('oauth2_server_php.oauth_authorization_codes', $bulk); + + $bulk = new \MongoDB\Driver\BulkWrite(); + $bulk->insert(array( + 'refresh_token' => 'testrefreshtoken', + 'client_id' => 'Some Client', + )); + $bulk->insert(array( + 'refresh_token' => 'testrefreshtoken2', + 'client_id' => 'Some Client2', + )); + $mongodb->executeBulkWrite('oauth2_server_php.oauth_refresh_tokens', $bulk); + + $bulk = new \MongoDB\Driver\BulkWrite(); + $bulk->insert(array( + 'username' => 'testuser', + 'password' => 'password', + 'email' => 'testuser@test.com', + 'email_verified' => true, + )); + $bulk->insert(array( + 'username' => 'testuser2', + 'password' => 'password2', + 'email' => 'testuser@test.com', + 'email_verified' => true, + )); + $mongodb->executeBulkWrite('oauth2_server_php.oauth_users', $bulk); + + $bulk = new \MongoDB\Driver\BulkWrite(); + $bulk->insert(array( + 'client_id' => 'oauth_test_client', + 'key' => $this->getTestPublicKey(), + 'subject' => 'test_subject', + )); + $bulk->insert(array( + 'client_id' => 'oauth_test_client2', + 'key' => $this->getTestPublicKey(), + 'subject' => 'test_subject2', + )); + $mongodb->executeBulkWrite('oauth2_server_php.oauth_jwt', $bulk); + } private function createRedisDb(Redis $storage) { @@ -500,10 +606,16 @@ private function createRedisDb(Redis $storage) $storage->setClientKey('oauth_test_client', $this->getTestPublicKey(), 'test_subject'); } - public function removeMongoDb(\MongoDB $db) + public function removeMongo(\MongoDB $db) { $db->drop(); } + + public function removeMongoDB(\MongoDB\Driver\Manager $mongodb) + { + $command = new \MongoDB\Driver\Command(array('dropDatabase' => 1)); + $mongodb->executeCommand('oauth2_server_php', $command); + } public function getTestPublicKey() {