1
0

DynamoDB.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. <?php
  2. namespace OAuth2\Storage;
  3. use Aws\DynamoDb\DynamoDbClient;
  4. use OAuth2\OpenID\Storage\UserClaimsInterface;
  5. use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
  6. /**
  7. * DynamoDB storage for all storage types
  8. *
  9. * To use, install "aws/aws-sdk-php" via composer
  10. * <code>
  11. * composer require aws/aws-sdk-php:dev-master
  12. * </code>
  13. *
  14. * Once this is done, instantiate the DynamoDB client
  15. * <code>
  16. * $storage = new OAuth2\Storage\Dynamodb(array("key" => "YOURKEY", "secret" => "YOURSECRET", "region" => "YOURREGION"));
  17. * </code>
  18. *
  19. * Table :
  20. * - oauth_access_tokens (primary hash key : access_token)
  21. * - oauth_authorization_codes (primary hash key : authorization_code)
  22. * - oauth_clients (primary hash key : client_id)
  23. * - oauth_jwt (primary hash key : client_id, primary range key : subject)
  24. * - oauth_public_keys (primary hash key : client_id)
  25. * - oauth_refresh_tokens (primary hash key : refresh_token)
  26. * - oauth_scopes (primary hash key : scope, secondary index : is_default-index hash key is_default)
  27. * - oauth_users (primary hash key : username)
  28. *
  29. * @author Frederic AUGUSTE <frederic.auguste at gmail dot com>
  30. */
  31. class DynamoDB implements
  32. AuthorizationCodeInterface,
  33. AccessTokenInterface,
  34. ClientCredentialsInterface,
  35. UserCredentialsInterface,
  36. RefreshTokenInterface,
  37. JwtBearerInterface,
  38. ScopeInterface,
  39. PublicKeyInterface,
  40. UserClaimsInterface,
  41. OpenIDAuthorizationCodeInterface
  42. {
  43. protected $client;
  44. protected $config;
  45. public function __construct($connection, $config = array())
  46. {
  47. if (!($connection instanceof DynamoDbClient)) {
  48. if (!is_array($connection)) {
  49. throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region');
  50. }
  51. if (!array_key_exists("key",$connection) || !array_key_exists("secret",$connection) || !array_key_exists("region",$connection) ) {
  52. throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region');
  53. }
  54. $this->client = DynamoDbClient::factory(array(
  55. 'key' => $connection["key"],
  56. 'secret' => $connection["secret"],
  57. 'region' =>$connection["region"]
  58. ));
  59. } else {
  60. $this->client = $connection;
  61. }
  62. $this->config = array_merge(array(
  63. 'client_table' => 'oauth_clients',
  64. 'access_token_table' => 'oauth_access_tokens',
  65. 'refresh_token_table' => 'oauth_refresh_tokens',
  66. 'code_table' => 'oauth_authorization_codes',
  67. 'user_table' => 'oauth_users',
  68. 'jwt_table' => 'oauth_jwt',
  69. 'scope_table' => 'oauth_scopes',
  70. 'public_key_table' => 'oauth_public_keys',
  71. ), $config);
  72. }
  73. /* OAuth2\Storage\ClientCredentialsInterface */
  74. public function checkClientCredentials($client_id, $client_secret = null)
  75. {
  76. $result = $this->client->getItem(array(
  77. "TableName"=> $this->config['client_table'],
  78. "Key" => array('client_id' => array('S' => $client_id))
  79. ));
  80. return $result->count()==1 && $result["Item"]["client_secret"]["S"] == $client_secret;
  81. }
  82. public function isPublicClient($client_id)
  83. {
  84. $result = $this->client->getItem(array(
  85. "TableName"=> $this->config['client_table'],
  86. "Key" => array('client_id' => array('S' => $client_id))
  87. ));
  88. if ($result->count()==0) {
  89. return false ;
  90. }
  91. return empty($result["Item"]["client_secret"]);
  92. }
  93. /* OAuth2\Storage\ClientInterface */
  94. public function getClientDetails($client_id)
  95. {
  96. $result = $this->client->getItem(array(
  97. "TableName"=> $this->config['client_table'],
  98. "Key" => array('client_id' => array('S' => $client_id))
  99. ));
  100. if ($result->count()==0) {
  101. return false ;
  102. }
  103. $result = $this->dynamo2array($result);
  104. foreach (array('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') as $key => $val) {
  105. if (!array_key_exists ($val, $result)) {
  106. $result[$val] = null;
  107. }
  108. }
  109. return $result;
  110. }
  111. public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null)
  112. {
  113. $clientData = compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id');
  114. $clientData = array_filter($clientData, 'self::isNotEmpty');
  115. $result = $this->client->putItem(array(
  116. 'TableName' => $this->config['client_table'],
  117. 'Item' => $this->client->formatAttributes($clientData)
  118. ));
  119. return true;
  120. }
  121. public function checkRestrictedGrantType($client_id, $grant_type)
  122. {
  123. $details = $this->getClientDetails($client_id);
  124. if (isset($details['grant_types'])) {
  125. $grant_types = explode(' ', $details['grant_types']);
  126. return in_array($grant_type, (array) $grant_types);
  127. }
  128. // if grant_types are not defined, then none are restricted
  129. return true;
  130. }
  131. /* OAuth2\Storage\AccessTokenInterface */
  132. public function getAccessToken($access_token)
  133. {
  134. $result = $this->client->getItem(array(
  135. "TableName"=> $this->config['access_token_table'],
  136. "Key" => array('access_token' => array('S' => $access_token))
  137. ));
  138. if ($result->count()==0) {
  139. return false ;
  140. }
  141. $token = $this->dynamo2array($result);
  142. if (array_key_exists ('expires', $token)) {
  143. $token['expires'] = strtotime($token['expires']);
  144. }
  145. return $token;
  146. }
  147. public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
  148. {
  149. // convert expires to datestring
  150. $expires = date('Y-m-d H:i:s', $expires);
  151. $clientData = compact('access_token', 'client_id', 'user_id', 'expires', 'scope');
  152. $clientData = array_filter($clientData, 'self::isNotEmpty');
  153. $result = $this->client->putItem(array(
  154. 'TableName' => $this->config['access_token_table'],
  155. 'Item' => $this->client->formatAttributes($clientData)
  156. ));
  157. return true;
  158. }
  159. public function unsetAccessToken($access_token)
  160. {
  161. $result = $this->client->deleteItem(array(
  162. 'TableName' => $this->config['access_token_table'],
  163. 'Key' => $this->client->formatAttributes(array("access_token" => $access_token)),
  164. 'ReturnValues' => 'ALL_OLD',
  165. ));
  166. return null !== $result->get('Attributes');
  167. }
  168. /* OAuth2\Storage\AuthorizationCodeInterface */
  169. public function getAuthorizationCode($code)
  170. {
  171. $result = $this->client->getItem(array(
  172. "TableName"=> $this->config['code_table'],
  173. "Key" => array('authorization_code' => array('S' => $code))
  174. ));
  175. if ($result->count()==0) {
  176. return false ;
  177. }
  178. $token = $this->dynamo2array($result);
  179. if (!array_key_exists("id_token", $token )) {
  180. $token['id_token'] = null;
  181. }
  182. $token['expires'] = strtotime($token['expires']);
  183. return $token;
  184. }
  185. public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
  186. {
  187. // convert expires to datestring
  188. $expires = date('Y-m-d H:i:s', $expires);
  189. $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope');
  190. $clientData = array_filter($clientData, 'self::isNotEmpty');
  191. $result = $this->client->putItem(array(
  192. 'TableName' => $this->config['code_table'],
  193. 'Item' => $this->client->formatAttributes($clientData)
  194. ));
  195. return true;
  196. }
  197. public function expireAuthorizationCode($code)
  198. {
  199. $result = $this->client->deleteItem(array(
  200. 'TableName' => $this->config['code_table'],
  201. 'Key' => $this->client->formatAttributes(array("authorization_code" => $code))
  202. ));
  203. return true;
  204. }
  205. /* OAuth2\Storage\UserCredentialsInterface */
  206. public function checkUserCredentials($username, $password)
  207. {
  208. if ($user = $this->getUser($username)) {
  209. return $this->checkPassword($user, $password);
  210. }
  211. return false;
  212. }
  213. public function getUserDetails($username)
  214. {
  215. return $this->getUser($username);
  216. }
  217. /* UserClaimsInterface */
  218. public function getUserClaims($user_id, $claims)
  219. {
  220. if (!$userDetails = $this->getUserDetails($user_id)) {
  221. return false;
  222. }
  223. $claims = explode(' ', trim($claims));
  224. $userClaims = array();
  225. // for each requested claim, if the user has the claim, set it in the response
  226. $validClaims = explode(' ', self::VALID_CLAIMS);
  227. foreach ($validClaims as $validClaim) {
  228. if (in_array($validClaim, $claims)) {
  229. if ($validClaim == 'address') {
  230. // address is an object with subfields
  231. $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
  232. } else {
  233. $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
  234. }
  235. }
  236. }
  237. return $userClaims;
  238. }
  239. protected function getUserClaim($claim, $userDetails)
  240. {
  241. $userClaims = array();
  242. $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
  243. $claimValues = explode(' ', $claimValuesString);
  244. foreach ($claimValues as $value) {
  245. if ($value == 'email_verified') {
  246. $userClaims[$value] = $userDetails[$value]=='true' ? true : false;
  247. } else {
  248. $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
  249. }
  250. }
  251. return $userClaims;
  252. }
  253. /* OAuth2\Storage\RefreshTokenInterface */
  254. public function getRefreshToken($refresh_token)
  255. {
  256. $result = $this->client->getItem(array(
  257. "TableName"=> $this->config['refresh_token_table'],
  258. "Key" => array('refresh_token' => array('S' => $refresh_token))
  259. ));
  260. if ($result->count()==0) {
  261. return false ;
  262. }
  263. $token = $this->dynamo2array($result);
  264. $token['expires'] = strtotime($token['expires']);
  265. return $token;
  266. }
  267. public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
  268. {
  269. // convert expires to datestring
  270. $expires = date('Y-m-d H:i:s', $expires);
  271. $clientData = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope');
  272. $clientData = array_filter($clientData, 'self::isNotEmpty');
  273. $result = $this->client->putItem(array(
  274. 'TableName' => $this->config['refresh_token_table'],
  275. 'Item' => $this->client->formatAttributes($clientData)
  276. ));
  277. return true;
  278. }
  279. public function unsetRefreshToken($refresh_token)
  280. {
  281. $result = $this->client->deleteItem(array(
  282. 'TableName' => $this->config['refresh_token_table'],
  283. 'Key' => $this->client->formatAttributes(array("refresh_token" => $refresh_token))
  284. ));
  285. return true;
  286. }
  287. // plaintext passwords are bad! Override this for your application
  288. protected function checkPassword($user, $password)
  289. {
  290. return $user['password'] == $this->hashPassword($password);
  291. }
  292. // use a secure hashing algorithm when storing passwords. Override this for your application
  293. protected function hashPassword($password)
  294. {
  295. return sha1($password);
  296. }
  297. public function getUser($username)
  298. {
  299. $result = $this->client->getItem(array(
  300. "TableName"=> $this->config['user_table'],
  301. "Key" => array('username' => array('S' => $username))
  302. ));
  303. if ($result->count()==0) {
  304. return false ;
  305. }
  306. $token = $this->dynamo2array($result);
  307. $token['user_id'] = $username;
  308. return $token;
  309. }
  310. public function setUser($username, $password, $first_name = null, $last_name = null)
  311. {
  312. // do not store in plaintext
  313. $password = $this->hashPassword($password);
  314. $clientData = compact('username', 'password', 'first_name', 'last_name');
  315. $clientData = array_filter($clientData, 'self::isNotEmpty');
  316. $result = $this->client->putItem(array(
  317. 'TableName' => $this->config['user_table'],
  318. 'Item' => $this->client->formatAttributes($clientData)
  319. ));
  320. return true;
  321. }
  322. /* ScopeInterface */
  323. public function scopeExists($scope)
  324. {
  325. $scope = explode(' ', $scope);
  326. $scope_query = array();
  327. $count = 0;
  328. foreach ($scope as $key => $val) {
  329. $result = $this->client->query(array(
  330. 'TableName' => $this->config['scope_table'],
  331. 'Select' => 'COUNT',
  332. 'KeyConditions' => array(
  333. 'scope' => array(
  334. 'AttributeValueList' => array(array('S' => $val)),
  335. 'ComparisonOperator' => 'EQ'
  336. )
  337. )
  338. ));
  339. $count += $result['Count'];
  340. }
  341. return $count == count($scope);
  342. }
  343. public function getDefaultScope($client_id = null)
  344. {
  345. $result = $this->client->query(array(
  346. 'TableName' => $this->config['scope_table'],
  347. 'IndexName' => 'is_default-index',
  348. 'Select' => 'ALL_ATTRIBUTES',
  349. 'KeyConditions' => array(
  350. 'is_default' => array(
  351. 'AttributeValueList' => array(array('S' => 'true')),
  352. 'ComparisonOperator' => 'EQ',
  353. ),
  354. )
  355. ));
  356. $defaultScope = array();
  357. if ($result->count() > 0) {
  358. $array = $result->toArray();
  359. foreach ($array["Items"] as $item) {
  360. $defaultScope[] = $item['scope']['S'];
  361. }
  362. return empty($defaultScope) ? null : implode(' ', $defaultScope);
  363. }
  364. return null;
  365. }
  366. /* JWTBearerInterface */
  367. public function getClientKey($client_id, $subject)
  368. {
  369. $result = $this->client->getItem(array(
  370. "TableName"=> $this->config['jwt_table'],
  371. "Key" => array('client_id' => array('S' => $client_id), 'subject' => array('S' => $subject))
  372. ));
  373. if ($result->count()==0) {
  374. return false ;
  375. }
  376. $token = $this->dynamo2array($result);
  377. return $token['public_key'];
  378. }
  379. public function getClientScope($client_id)
  380. {
  381. if (!$clientDetails = $this->getClientDetails($client_id)) {
  382. return false;
  383. }
  384. if (isset($clientDetails['scope'])) {
  385. return $clientDetails['scope'];
  386. }
  387. return null;
  388. }
  389. public function getJti($client_id, $subject, $audience, $expires, $jti)
  390. {
  391. //TODO not use.
  392. }
  393. public function setJti($client_id, $subject, $audience, $expires, $jti)
  394. {
  395. //TODO not use.
  396. }
  397. /* PublicKeyInterface */
  398. public function getPublicKey($client_id = '0')
  399. {
  400. $result = $this->client->getItem(array(
  401. "TableName"=> $this->config['public_key_table'],
  402. "Key" => array('client_id' => array('S' => $client_id))
  403. ));
  404. if ($result->count()==0) {
  405. return false ;
  406. }
  407. $token = $this->dynamo2array($result);
  408. return $token['public_key'];
  409. }
  410. public function getPrivateKey($client_id = '0')
  411. {
  412. $result = $this->client->getItem(array(
  413. "TableName"=> $this->config['public_key_table'],
  414. "Key" => array('client_id' => array('S' => $client_id))
  415. ));
  416. if ($result->count()==0) {
  417. return false ;
  418. }
  419. $token = $this->dynamo2array($result);
  420. return $token['private_key'];
  421. }
  422. public function getEncryptionAlgorithm($client_id = null)
  423. {
  424. $result = $this->client->getItem(array(
  425. "TableName"=> $this->config['public_key_table'],
  426. "Key" => array('client_id' => array('S' => $client_id))
  427. ));
  428. if ($result->count()==0) {
  429. return 'RS256' ;
  430. }
  431. $token = $this->dynamo2array($result);
  432. return $token['encryption_algorithm'];
  433. }
  434. /**
  435. * Transform dynamodb resultset to an array.
  436. * @param $dynamodbResult
  437. * @return $array
  438. */
  439. private function dynamo2array($dynamodbResult)
  440. {
  441. $result = array();
  442. foreach ($dynamodbResult["Item"] as $key => $val) {
  443. $result[$key] = $val["S"];
  444. $result[] = $val["S"];
  445. }
  446. return $result;
  447. }
  448. private static function isNotEmpty($value)
  449. {
  450. return null !== $value && '' !== $value;
  451. }
  452. }