diff --git a/src/OAuth2/Storage/CassandraCQL.php b/src/OAuth2/Storage/CassandraCQL.php new file mode 100644 index 000000000..033a691db --- /dev/null +++ b/src/OAuth2/Storage/CassandraCQL.php @@ -0,0 +1,448 @@ + + * $cassandraCluster = \Cassandra::cluster()->withContactPoints('192.168.0.100')->withPort(9042)->build(); + * $cassandraSession = $cassandraCluster->connect( 'my_keyspace' ); + * + * + * You need the following tables in cassandra for this storage + * + * $sql = "CREATE TABLE IF NOT EXISTS ".$this->config['column_family_data']." (key varchar, data text, PRIMARY KEY(key))"; + * $statement = new \Cassandra\SimpleStatement($sql); + * $result = $this->getSession()->execute($statement); + * + * $sql = "CREATE TABLE IF NOT EXISTS ".$this->config['column_family_clients']." (key text, data text, PRIMARY KEY(key))"; + * $statement = new \Cassandra\SimpleStatement($sql); + * $result = $this->getSession()->execute($statement); + * + * + * Then, register the storage client: + * + * $storage = new OAuth2\Storage\Cassandra($cassandra); + * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); + * + * + */ +class CassandraCQL implements AuthorizationCodeInterface, + AccessTokenInterface, + ClientCredentialsInterface, + UserCredentialsInterface, + RefreshTokenInterface, + JwtBearerInterface, + ScopeInterface, + PublicKeyInterface, + UserClaimsInterface, + OpenIDAuthorizationCodeInterface +{ + + protected $cassandraSession; + + protected $config; + + + public function __construct($cassandraSession, array $config = array()) + { + $this->cassandraSession = $cassandraSession; + + $this->config = array_merge(array( + // cassandra config + 'column_family_data' => 'oauth2_data', + 'column_family_clients' => 'oauth2_clients', + + // key names + '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); + + /* + $sql = "CREATE TABLE IF NOT EXISTS ".$this->config['column_family_data']." (key varchar, data text, PRIMARY KEY(key))"; + $statement = new \Cassandra\SimpleStatement($sql); + $result = $this->getSession()->execute($statement); + + $sql = "CREATE TABLE IF NOT EXISTS ".$this->config['column_family_clients']." (key text, data text, PRIMARY KEY(key))"; + $statement = new \Cassandra\SimpleStatement($sql); + $result = $this->getSession()->execute($statement); + */ + } + + protected function getSession() + { + return $this->cassandraSession; + } + + protected function getValue($key) + { + $sql = "SELECT * FROM ".$this->config['column_family_data']. " WHERE key = '".$key."' "; + $statement = new \Cassandra\SimpleStatement($sql); + $result = $this->getSession()->execute($statement); + if ($result->count() === 0) + { + return false; + } + $row = $result[0]; + return json_decode($row['data'], true); + + + } + + protected function setValue($key, $value, $expire = 0) + { + $str = json_encode($value); + $sql = "UPDATE ".$this->config['column_family_data']." SET data='".$str."' WHERE key='".$key."'"; + $this->getSession()->execute(new \Cassandra\SimpleStatement($sql)); + return true; + } + + protected function expireValue($key) + { + $sql = "DELETE FROM ".$this->config['column_family_data']." WHERE key='".$key."' "; + $this->getSession()->execute(new \Cassandra\SimpleStatement($sql)); + return true; + } + + /* 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; + + 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'] == 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 = sha1($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) + { + $sql = "SELECT * FROM ".$this->config['column_family_clients']. " WHERE key = '".$client_id."' "; + $statement = new \Cassandra\SimpleStatement($sql); + $result = $this->getSession()->execute($statement); + if ($result->count() === 0) + { + return false; + } + $row = $result[0]; + return json_decode($row['data'], true); + } + + public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) + { + $value = compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id'); + $str = json_encode($value); + + $sql = "UPDATE ".$this->config['column_family_clients']." SET data='".$str."' WHERE key='".$client_id."'"; + $this->getSession()->execute(new \Cassandra\SimpleStatement($sql)); + 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; + } + + /* 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/test/lib/OAuth2/Storage/BaseTest.php b/test/lib/OAuth2/Storage/BaseTest.php index 921d52500..0b1ec02cc 100755 --- a/test/lib/OAuth2/Storage/BaseTest.php +++ b/test/lib/OAuth2/Storage/BaseTest.php @@ -15,6 +15,7 @@ public function provideStorage() $cassandra = Bootstrap::getInstance()->getCassandraStorage(); $dynamodb = Bootstrap::getInstance()->getDynamoDbStorage(); $couchbase = Bootstrap::getInstance()->getCouchbase(); + $cassandraCql = Bootstrap::getInstance()->getCassandraCqlStorage(); /* hack until we can fix "default_scope" dependencies in other tests */ $memory->defaultScope = 'defaultscope1 defaultscope2'; @@ -29,6 +30,7 @@ public function provideStorage() array($cassandra), array($dynamodb), array($couchbase), + array($cassandraCql), ); } } diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php index efb6644c2..9f69df09c 100755 --- a/test/lib/OAuth2/Storage/Bootstrap.php +++ b/test/lib/OAuth2/Storage/Bootstrap.php @@ -16,6 +16,7 @@ class Bootstrap private $configDir; private $dynamodb; private $couchbase; + private $cassandraStorage; public function __construct() { @@ -881,4 +882,113 @@ private function getEnvVar($var, $default = null) { return isset($_SERVER[$var]) ? $_SERVER[$var] : (getenv($var) ?: $default); } + + + + public function getCassandraCqlStorage() + { + if (!$this->cassandraStorage) { + if (class_exists('\Cassandra')) { + try { + $cassandraCluster = \Cassandra::cluster()->withContactPoints('127.0.0.1')->withPort(9042)->build(); + $this->cassandraSession = $cassandraCluster->connect(); // 'oauth2_test' ); + + } catch (\Exception $e) { + $this->cassandraStorage = new NullStorage('CassandraCQL', 'Unable to connect to cassandra server on "127.0.0.1:9042"'); + } + + $this->removeCassandraCqlDb($this->cassandraSession); + $this->cassandraStorage = new CassandraCQL($this->cassandraSession); + $this->createCassandraCqlDb($this->cassandraSession, $this->cassandraStorage); + } + else { + $this->cassandraStorage = new NullStorage('CassandraCQL', 'datastax cassandra library is missing.'); + } + } + return $this->cassandraStorage; + } + + private function removeCassandraCqlDb($session) + { + try { + $statement = new \Cassandra\SimpleStatement( + 'DROP KEYSPACE oauth2_test' + ); + $result = $session->execute($statement); + } catch (\Exception $e) { + + } + } + + private function createCassandraCqlDb($session, $storage) + { + $statement = new \Cassandra\SimpleStatement( + "CREATE KEYSPACE oauth2_test WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }" + ); + $result = $session->execute($statement); + + $statement = new \Cassandra\SimpleStatement( + 'USE oauth2_test' + ); + $result = $session->execute($statement); + + $statement = new \Cassandra\SimpleStatement( + 'CREATE COLUMNFAMILY oauth2_data (key text, data text, PRIMARY KEY(key))' + ); + $result = $session->execute($statement); + + $statement = new \Cassandra\SimpleStatement( + 'CREATE COLUMNFAMILY oauth2_clients (key text, data text, PRIMARY KEY(key))' + ); + $result = $session->execute($statement); + + + // 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'); + + $statement = new \Cassandra\SimpleStatement( + "UPDATE oauth2_data SET data = '".json_encode(array("public_key" => "client_1_public", "private_key" => "client_1_private", "encryption_algorithm" => "RS256"))."' WHERE key = 'oauth_public_keys:ClientID_One'" + ); + $result = $session->execute($statement); + + $statement = new \Cassandra\SimpleStatement( + "UPDATE oauth2_data SET data = '".json_encode(array("public_key" => "client_2_public", "private_key" => "client_2_private", "encryption_algorithm" => "RS256"))."' WHERE key = 'oauth_public_keys:ClientID_Two'" + ); + $result = $session->execute($statement); + + $statement = new \Cassandra\SimpleStatement( + "UPDATE oauth2_data SET data = '".json_encode(array("public_key" => $this->getTestPublicKey(), "private_key" => $this->getTestPrivateKey(), "encryption_algorithm" => "RS256"))."' WHERE key = 'oauth_public_keys:'" + ); + $result = $session->execute($statement); + + $statement = new \Cassandra\SimpleStatement( + "UPDATE oauth2_data SET data = '".json_encode(array("password" => "password", "email" => "testuser@test.com", "email_verified" => true))."' WHERE key = 'oauth_users:testuser'" + ); + $result = $session->execute($statement); + + } + + + + + }