diff options
Diffstat (limited to 'library/oauth2/src/OAuth2/Storage/Cassandra.php')
-rw-r--r-- | library/oauth2/src/OAuth2/Storage/Cassandra.php | 480 |
1 files changed, 480 insertions, 0 deletions
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; + } + +} |