Cassandra.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. <?php
  2. namespace OAuth2\Storage;
  3. use phpcassa\ColumnFamily;
  4. use phpcassa\ColumnSlice;
  5. use phpcassa\Connection\ConnectionPool;
  6. use OAuth2\OpenID\Storage\UserClaimsInterface;
  7. use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
  8. use InvalidArgumentException;
  9. /**
  10. * Cassandra storage for all storage types
  11. *
  12. * To use, install "thobbs/phpcassa" via composer:
  13. * <code>
  14. * composer require thobbs/phpcassa:dev-master
  15. * </code>
  16. *
  17. * Once this is done, instantiate the connection:
  18. * <code>
  19. * $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160'));
  20. * </code>
  21. *
  22. * Then, register the storage client:
  23. * <code>
  24. * $storage = new OAuth2\Storage\Cassandra($cassandra);
  25. * $storage->setClientDetails($client_id, $client_secret, $redirect_uri);
  26. * </code>
  27. *
  28. * @see test/lib/OAuth2/Storage/Bootstrap::getCassandraStorage
  29. */
  30. class Cassandra implements AuthorizationCodeInterface,
  31. AccessTokenInterface,
  32. ClientCredentialsInterface,
  33. UserCredentialsInterface,
  34. RefreshTokenInterface,
  35. JwtBearerInterface,
  36. ScopeInterface,
  37. PublicKeyInterface,
  38. UserClaimsInterface,
  39. OpenIDAuthorizationCodeInterface
  40. {
  41. private $cache;
  42. /**
  43. * @var ConnectionPool
  44. */
  45. protected $cassandra;
  46. /**
  47. * @var array
  48. */
  49. protected $config;
  50. /**
  51. * Cassandra Storage! uses phpCassa
  52. *
  53. * @param ConnectionPool|array $connection
  54. * @param array $config
  55. *
  56. * @throws InvalidArgumentException
  57. */
  58. public function __construct($connection = array(), array $config = array())
  59. {
  60. if ($connection instanceof ConnectionPool) {
  61. $this->cassandra = $connection;
  62. } else {
  63. if (!is_array($connection)) {
  64. throw new InvalidArgumentException('First argument to OAuth2\Storage\Cassandra must be an instance of phpcassa\Connection\ConnectionPool or a configuration array');
  65. }
  66. $connection = array_merge(array(
  67. 'keyspace' => 'oauth2',
  68. 'servers' => null,
  69. ), $connection);
  70. $this->cassandra = new ConnectionPool($connection['keyspace'], $connection['servers']);
  71. }
  72. $this->config = array_merge(array(
  73. // cassandra config
  74. 'column_family' => 'auth',
  75. // key names
  76. 'client_key' => 'oauth_clients:',
  77. 'access_token_key' => 'oauth_access_tokens:',
  78. 'refresh_token_key' => 'oauth_refresh_tokens:',
  79. 'code_key' => 'oauth_authorization_codes:',
  80. 'user_key' => 'oauth_users:',
  81. 'jwt_key' => 'oauth_jwt:',
  82. 'scope_key' => 'oauth_scopes:',
  83. 'public_key_key' => 'oauth_public_keys:',
  84. ), $config);
  85. }
  86. /**
  87. * @param $key
  88. * @return bool|mixed
  89. */
  90. protected function getValue($key)
  91. {
  92. if (isset($this->cache[$key])) {
  93. return $this->cache[$key];
  94. }
  95. $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
  96. try {
  97. $value = $cf->get($key, new ColumnSlice("", ""));
  98. $value = array_shift($value);
  99. } catch (\cassandra\NotFoundException $e) {
  100. return false;
  101. }
  102. return json_decode($value, true);
  103. }
  104. /**
  105. * @param $key
  106. * @param $value
  107. * @param int $expire
  108. * @return bool
  109. */
  110. protected function setValue($key, $value, $expire = 0)
  111. {
  112. $this->cache[$key] = $value;
  113. $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
  114. $str = json_encode($value);
  115. if ($expire > 0) {
  116. try {
  117. $seconds = $expire - time();
  118. // __data key set as C* requires a field, note: max TTL can only be 630720000 seconds
  119. $cf->insert($key, array('__data' => $str), null, $seconds);
  120. } catch (\Exception $e) {
  121. return false;
  122. }
  123. } else {
  124. try {
  125. // __data key set as C* requires a field
  126. $cf->insert($key, array('__data' => $str));
  127. } catch (\Exception $e) {
  128. return false;
  129. }
  130. }
  131. return true;
  132. }
  133. /**
  134. * @param $key
  135. * @return bool
  136. */
  137. protected function expireValue($key)
  138. {
  139. unset($this->cache[$key]);
  140. $cf = new ColumnFamily($this->cassandra, $this->config['column_family']);
  141. if ($cf->get_count($key) > 0) {
  142. try {
  143. // __data key set as C* requires a field
  144. $cf->remove($key, array('__data'));
  145. } catch (\Exception $e) {
  146. return false;
  147. }
  148. return true;
  149. }
  150. return false;
  151. }
  152. /**
  153. * @param string $code
  154. * @return bool|mixed
  155. */
  156. public function getAuthorizationCode($code)
  157. {
  158. return $this->getValue($this->config['code_key'] . $code);
  159. }
  160. /**
  161. * @param string $authorization_code
  162. * @param mixed $client_id
  163. * @param mixed $user_id
  164. * @param string $redirect_uri
  165. * @param int $expires
  166. * @param string $scope
  167. * @param string $id_token
  168. * @return bool
  169. */
  170. public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
  171. {
  172. return $this->setValue(
  173. $this->config['code_key'] . $authorization_code,
  174. compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'),
  175. $expires
  176. );
  177. }
  178. /**
  179. * @param string $code
  180. * @return bool
  181. */
  182. public function expireAuthorizationCode($code)
  183. {
  184. $key = $this->config['code_key'] . $code;
  185. unset($this->cache[$key]);
  186. return $this->expireValue($key);
  187. }
  188. /**
  189. * @param string $username
  190. * @param string $password
  191. * @return bool
  192. */
  193. public function checkUserCredentials($username, $password)
  194. {
  195. if ($user = $this->getUser($username)) {
  196. return $this->checkPassword($user, $password);
  197. }
  198. return false;
  199. }
  200. /**
  201. * plaintext passwords are bad! Override this for your application
  202. *
  203. * @param array $user
  204. * @param string $password
  205. * @return bool
  206. */
  207. protected function checkPassword($user, $password)
  208. {
  209. return $user['password'] == $this->hashPassword($password);
  210. }
  211. // use a secure hashing algorithm when storing passwords. Override this for your application
  212. protected function hashPassword($password)
  213. {
  214. return sha1($password);
  215. }
  216. /**
  217. * @param string $username
  218. * @return array|bool|false
  219. */
  220. public function getUserDetails($username)
  221. {
  222. return $this->getUser($username);
  223. }
  224. /**
  225. * @param string $username
  226. * @return array|bool
  227. */
  228. public function getUser($username)
  229. {
  230. if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) {
  231. return false;
  232. }
  233. // the default behavior is to use "username" as the user_id
  234. return array_merge(array(
  235. 'user_id' => $username,
  236. ), $userInfo);
  237. }
  238. /**
  239. * @param string $username
  240. * @param string $password
  241. * @param string $first_name
  242. * @param string $last_name
  243. * @return bool
  244. */
  245. public function setUser($username, $password, $first_name = null, $last_name = null)
  246. {
  247. $password = $this->hashPassword($password);
  248. return $this->setValue(
  249. $this->config['user_key'] . $username,
  250. compact('username', 'password', 'first_name', 'last_name')
  251. );
  252. }
  253. /**
  254. * @param mixed $client_id
  255. * @param string $client_secret
  256. * @return bool
  257. */
  258. public function checkClientCredentials($client_id, $client_secret = null)
  259. {
  260. if (!$client = $this->getClientDetails($client_id)) {
  261. return false;
  262. }
  263. return isset($client['client_secret'])
  264. && $client['client_secret'] == $client_secret;
  265. }
  266. /**
  267. * @param $client_id
  268. * @return bool
  269. */
  270. public function isPublicClient($client_id)
  271. {
  272. if (!$client = $this->getClientDetails($client_id)) {
  273. return false;
  274. }
  275. return empty($client['client_secret']);
  276. }
  277. /**
  278. * @param $client_id
  279. * @return array|bool|mixed
  280. */
  281. public function getClientDetails($client_id)
  282. {
  283. return $this->getValue($this->config['client_key'] . $client_id);
  284. }
  285. /**
  286. * @param $client_id
  287. * @param null $client_secret
  288. * @param null $redirect_uri
  289. * @param null $grant_types
  290. * @param null $scope
  291. * @param null $user_id
  292. * @return bool
  293. */
  294. public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
  295. {
  296. return $this->setValue(
  297. $this->config['client_key'] . $client_id,
  298. compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id')
  299. );
  300. }
  301. /**
  302. * @param $client_id
  303. * @param $grant_type
  304. * @return bool
  305. */
  306. public function checkRestrictedGrantType($client_id, $grant_type)
  307. {
  308. $details = $this->getClientDetails($client_id);
  309. if (isset($details['grant_types'])) {
  310. $grant_types = explode(' ', $details['grant_types']);
  311. return in_array($grant_type, (array) $grant_types);
  312. }
  313. // if grant_types are not defined, then none are restricted
  314. return true;
  315. }
  316. /**
  317. * @param $refresh_token
  318. * @return bool|mixed
  319. */
  320. public function getRefreshToken($refresh_token)
  321. {
  322. return $this->getValue($this->config['refresh_token_key'] . $refresh_token);
  323. }
  324. /**
  325. * @param $refresh_token
  326. * @param $client_id
  327. * @param $user_id
  328. * @param $expires
  329. * @param null $scope
  330. * @return bool
  331. */
  332. public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
  333. {
  334. return $this->setValue(
  335. $this->config['refresh_token_key'] . $refresh_token,
  336. compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'),
  337. $expires
  338. );
  339. }
  340. /**
  341. * @param $refresh_token
  342. * @return bool
  343. */
  344. public function unsetRefreshToken($refresh_token)
  345. {
  346. return $this->expireValue($this->config['refresh_token_key'] . $refresh_token);
  347. }
  348. /**
  349. * @param string $access_token
  350. * @return array|bool|mixed|null
  351. */
  352. public function getAccessToken($access_token)
  353. {
  354. return $this->getValue($this->config['access_token_key'].$access_token);
  355. }
  356. /**
  357. * @param string $access_token
  358. * @param mixed $client_id
  359. * @param mixed $user_id
  360. * @param int $expires
  361. * @param null $scope
  362. * @return bool
  363. */
  364. public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
  365. {
  366. return $this->setValue(
  367. $this->config['access_token_key'].$access_token,
  368. compact('access_token', 'client_id', 'user_id', 'expires', 'scope'),
  369. $expires
  370. );
  371. }
  372. /**
  373. * @param $access_token
  374. * @return bool
  375. */
  376. public function unsetAccessToken($access_token)
  377. {
  378. return $this->expireValue($this->config['access_token_key'] . $access_token);
  379. }
  380. /**
  381. * @param $scope
  382. * @return bool
  383. */
  384. public function scopeExists($scope)
  385. {
  386. $scope = explode(' ', $scope);
  387. $result = $this->getValue($this->config['scope_key'].'supported:global');
  388. $supportedScope = explode(' ', (string) $result);
  389. return (count(array_diff($scope, $supportedScope)) == 0);
  390. }
  391. /**
  392. * @param null $client_id
  393. * @return bool|mixed
  394. */
  395. public function getDefaultScope($client_id = null)
  396. {
  397. if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) {
  398. $result = $this->getValue($this->config['scope_key'].'default:global');
  399. }
  400. return $result;
  401. }
  402. /**
  403. * @param $scope
  404. * @param null $client_id
  405. * @param string $type
  406. * @return bool
  407. * @throws \InvalidArgumentException
  408. */
  409. public function setScope($scope, $client_id = null, $type = 'supported')
  410. {
  411. if (!in_array($type, array('default', 'supported'))) {
  412. throw new \InvalidArgumentException('"$type" must be one of "default", "supported"');
  413. }
  414. if (is_null($client_id)) {
  415. $key = $this->config['scope_key'].$type.':global';
  416. } else {
  417. $key = $this->config['scope_key'].$type.':'.$client_id;
  418. }
  419. return $this->setValue($key, $scope);
  420. }
  421. /**
  422. * @param $client_id
  423. * @param $subject
  424. * @return bool|null
  425. */
  426. public function getClientKey($client_id, $subject)
  427. {
  428. if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) {
  429. return false;
  430. }
  431. if (isset($jwt['subject']) && $jwt['subject'] == $subject ) {
  432. return $jwt['key'];
  433. }
  434. return null;
  435. }
  436. /**
  437. * @param $client_id
  438. * @param $key
  439. * @param null $subject
  440. * @return bool
  441. */
  442. public function setClientKey($client_id, $key, $subject = null)
  443. {
  444. return $this->setValue($this->config['jwt_key'] . $client_id, array(
  445. 'key' => $key,
  446. 'subject' => $subject
  447. ));
  448. }
  449. /**
  450. * @param $client_id
  451. * @return bool|null
  452. */
  453. public function getClientScope($client_id)
  454. {
  455. if (!$clientDetails = $this->getClientDetails($client_id)) {
  456. return false;
  457. }
  458. if (isset($clientDetails['scope'])) {
  459. return $clientDetails['scope'];
  460. }
  461. return null;
  462. }
  463. /**
  464. * @param $client_id
  465. * @param $subject
  466. * @param $audience
  467. * @param $expiration
  468. * @param $jti
  469. * @throws \Exception
  470. */
  471. public function getJti($client_id, $subject, $audience, $expiration, $jti)
  472. {
  473. //TODO: Needs cassandra implementation.
  474. throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.');
  475. }
  476. /**
  477. * @param $client_id
  478. * @param $subject
  479. * @param $audience
  480. * @param $expiration
  481. * @param $jti
  482. * @throws \Exception
  483. */
  484. public function setJti($client_id, $subject, $audience, $expiration, $jti)
  485. {
  486. //TODO: Needs cassandra implementation.
  487. throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.');
  488. }
  489. /**
  490. * @param string $client_id
  491. * @return mixed
  492. */
  493. public function getPublicKey($client_id = '')
  494. {
  495. $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
  496. if (is_array($public_key)) {
  497. return $public_key['public_key'];
  498. }
  499. $public_key = $this->getValue($this->config['public_key_key']);
  500. if (is_array($public_key)) {
  501. return $public_key['public_key'];
  502. }
  503. }
  504. /**
  505. * @param string $client_id
  506. * @return mixed
  507. */
  508. public function getPrivateKey($client_id = '')
  509. {
  510. $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
  511. if (is_array($public_key)) {
  512. return $public_key['private_key'];
  513. }
  514. $public_key = $this->getValue($this->config['public_key_key']);
  515. if (is_array($public_key)) {
  516. return $public_key['private_key'];
  517. }
  518. }
  519. /**
  520. * @param null $client_id
  521. * @return mixed|string
  522. */
  523. public function getEncryptionAlgorithm($client_id = null)
  524. {
  525. $public_key = $this->getValue($this->config['public_key_key'] . $client_id);
  526. if (is_array($public_key)) {
  527. return $public_key['encryption_algorithm'];
  528. }
  529. $public_key = $this->getValue($this->config['public_key_key']);
  530. if (is_array($public_key)) {
  531. return $public_key['encryption_algorithm'];
  532. }
  533. return 'RS256';
  534. }
  535. /**
  536. * @param mixed $user_id
  537. * @param string $claims
  538. * @return array|bool
  539. */
  540. public function getUserClaims($user_id, $claims)
  541. {
  542. $userDetails = $this->getUserDetails($user_id);
  543. if (!is_array($userDetails)) {
  544. return false;
  545. }
  546. $claims = explode(' ', trim($claims));
  547. $userClaims = array();
  548. // for each requested claim, if the user has the claim, set it in the response
  549. $validClaims = explode(' ', self::VALID_CLAIMS);
  550. foreach ($validClaims as $validClaim) {
  551. if (in_array($validClaim, $claims)) {
  552. if ($validClaim == 'address') {
  553. // address is an object with subfields
  554. $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
  555. } else {
  556. $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
  557. }
  558. }
  559. }
  560. return $userClaims;
  561. }
  562. /**
  563. * @param $claim
  564. * @param $userDetails
  565. * @return array
  566. */
  567. protected function getUserClaim($claim, $userDetails)
  568. {
  569. $userClaims = array();
  570. $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
  571. $claimValues = explode(' ', $claimValuesString);
  572. foreach ($claimValues as $value) {
  573. if ($value == 'email_verified') {
  574. $userClaims[$value] = $userDetails[$value]=='true' ? true : false;
  575. } else {
  576. $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
  577. }
  578. }
  579. return $userClaims;
  580. }
  581. }