* composer require thobbs/phpcassa:dev-master * * * Once this is done, instantiate the connection: * * $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160')); * * * Then, register the storage client: * * $storage = new OAuth2\Storage\Cassandra($cassandra); * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); * * * @see test/lib/OAuth2/Storage/Bootstrap::getCassandraStorage */ class Cassandra implements AuthorizationCodeInterface, AccessTokenInterface, ClientCredentialsInterface, UserCredentialsInterface, RefreshTokenInterface, JwtBearerInterface, ScopeInterface, PublicKeyInterface, UserClaimsInterface, OpenIDAuthorizationCodeInterface { private $cache; /** * @var ConnectionPool */ protected $cassandra; /** * @var array */ protected $config; /** * Cassandra Storage! uses phpCassa * * @param ConnectionPool|array $connection * @param array $config * * @throws InvalidArgumentException */ 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); } /** * @param $key * @return bool|mixed */ 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); } /** * @param $key * @param $value * @param int $expire * @return bool */ 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; } /** * @param $key * @return bool */ 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; } /** * @param string $code * @return bool|mixed */ public function getAuthorizationCode($code) { return $this->getValue($this->config['code_key'] . $code); } /** * @param string $authorization_code * @param mixed $client_id * @param mixed $user_id * @param string $redirect_uri * @param int $expires * @param string $scope * @param string $id_token * @return bool */ public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { return $this->setValue( $this->config['code_key'] . $authorization_code, compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method'), $expires ); } /** * @param string $code * @return bool */ public function expireAuthorizationCode($code) { $key = $this->config['code_key'] . $code; unset($this->cache[$key]); return $this->expireValue($key); } /** * @param string $username * @param string $password * @return bool */ 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 * * @param array $user * @param string $password * @return bool */ 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); } /** * @param string $username * @return array|bool|false */ public function getUserDetails($username) { return $this->getUser($username); } /** * @param string $username * @return array|bool */ 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); } /** * @param string $username * @param string $password * @param string $first_name * @param string $last_name * @return bool */ 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') ); } /** * @param mixed $client_id * @param string $client_secret * @return bool */ 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; } /** * @param $client_id * @return bool */ public function isPublicClient($client_id) { if (!$client = $this->getClientDetails($client_id)) { return false; } return empty($client['client_secret']); } /** * @param $client_id * @return array|bool|mixed */ public function getClientDetails($client_id) { return $this->getValue($this->config['client_key'] . $client_id); } /** * @param $client_id * @param null $client_secret * @param null $redirect_uri * @param null $grant_types * @param null $scope * @param null $user_id * @return bool */ 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') ); } /** * @param $client_id * @param $grant_type * @return bool */ 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; } /** * @param $refresh_token * @return bool|mixed */ public function getRefreshToken($refresh_token) { return $this->getValue($this->config['refresh_token_key'] . $refresh_token); } /** * @param $refresh_token * @param $client_id * @param $user_id * @param $expires * @param null $scope * @return bool */ 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 ); } /** * @param $refresh_token * @return bool */ public function unsetRefreshToken($refresh_token) { return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); } /** * @param string $access_token * @return array|bool|mixed|null */ public function getAccessToken($access_token) { return $this->getValue($this->config['access_token_key'].$access_token); } /** * @param string $access_token * @param mixed $client_id * @param mixed $user_id * @param int $expires * @param null $scope * @return bool */ 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 ); } /** * @param $access_token * @return bool */ public function unsetAccessToken($access_token) { return $this->expireValue($this->config['access_token_key'] . $access_token); } /** * @param $scope * @return bool */ 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); } /** * @param null $client_id * @return bool|mixed */ 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; } /** * @param $scope * @param null $client_id * @param string $type * @return bool * @throws \InvalidArgumentException */ 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); } /** * @param $client_id * @param $subject * @return bool|null */ 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; } /** * @param $client_id * @param $key * @param null $subject * @return bool */ public function setClientKey($client_id, $key, $subject = null) { return $this->setValue($this->config['jwt_key'] . $client_id, array( 'key' => $key, 'subject' => $subject )); } /** * @param $client_id * @return bool|null */ public function getClientScope($client_id) { if (!$clientDetails = $this->getClientDetails($client_id)) { return false; } if (isset($clientDetails['scope'])) { return $clientDetails['scope']; } return null; } /** * @param $client_id * @param $subject * @param $audience * @param $expiration * @param $jti * @throws \Exception */ public function getJti($client_id, $subject, $audience, $expiration, $jti) { //TODO: Needs cassandra implementation. throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.'); } /** * @param $client_id * @param $subject * @param $audience * @param $expiration * @param $jti * @throws \Exception */ public function setJti($client_id, $subject, $audience, $expiration, $jti) { //TODO: Needs cassandra implementation. throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.'); } /** * @param string $client_id * @return mixed */ 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']; } } /** * @param string $client_id * @return mixed */ 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']; } } /** * @param null $client_id * @return mixed|string */ 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'; } /** * @param mixed $user_id * @param string $claims * @return array|bool */ 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; } /** * @param $claim * @param $userDetails * @return array */ 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; } }