DynamoDB.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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, function ($value) { return !is_null($value); });
  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, function ($value) { return !empty($value); });
  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. ));
  165. return true;
  166. }
  167. /* OAuth2\Storage\AuthorizationCodeInterface */
  168. public function getAuthorizationCode($code)
  169. {
  170. $result = $this->client->getItem(array(
  171. "TableName"=> $this->config['code_table'],
  172. "Key" => array('authorization_code' => array('S' => $code))
  173. ));
  174. if ($result->count()==0) {
  175. return false ;
  176. }
  177. $token = $this->dynamo2array($result);
  178. if (!array_key_exists("id_token", $token )) {
  179. $token['id_token'] = null;
  180. }
  181. $token['expires'] = strtotime($token['expires']);
  182. return $token;
  183. }
  184. public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
  185. {
  186. // convert expires to datestring
  187. $expires = date('Y-m-d H:i:s', $expires);
  188. $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope');
  189. $clientData = array_filter($clientData, function ($value) { return !empty($value); });
  190. $result = $this->client->putItem(array(
  191. 'TableName' => $this->config['code_table'],
  192. 'Item' => $this->client->formatAttributes($clientData)
  193. ));
  194. return true;
  195. }
  196. public function expireAuthorizationCode($code)
  197. {
  198. $result = $this->client->deleteItem(array(
  199. 'TableName' => $this->config['code_table'],
  200. 'Key' => $this->client->formatAttributes(array("authorization_code" => $code))
  201. ));
  202. return true;
  203. }
  204. /* OAuth2\Storage\UserCredentialsInterface */
  205. public function checkUserCredentials($username, $password)
  206. {
  207. if ($user = $this->getUser($username)) {
  208. return $this->checkPassword($user, $password);
  209. }
  210. return false;
  211. }
  212. public function getUserDetails($username)
  213. {
  214. return $this->getUser($username);
  215. }
  216. /* UserClaimsInterface */
  217. public function getUserClaims($user_id, $claims)
  218. {
  219. if (!$userDetails = $this->getUserDetails($user_id)) {
  220. return false;
  221. }
  222. $claims = explode(' ', trim($claims));
  223. $userClaims = array();
  224. // for each requested claim, if the user has the claim, set it in the response
  225. $validClaims = explode(' ', self::VALID_CLAIMS);
  226. foreach ($validClaims as $validClaim) {
  227. if (in_array($validClaim, $claims)) {
  228. if ($validClaim == 'address') {
  229. // address is an object with subfields
  230. $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
  231. } else {
  232. $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails));
  233. }
  234. }
  235. }
  236. return $userClaims;
  237. }
  238. protected function getUserClaim($claim, $userDetails)
  239. {
  240. $userClaims = array();
  241. $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
  242. $claimValues = explode(' ', $claimValuesString);
  243. foreach ($claimValues as $value) {
  244. if ($value == 'email_verified') {
  245. $userClaims[$value] = $userDetails[$value]=='true' ? true : false;
  246. } else {
  247. $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
  248. }
  249. }
  250. return $userClaims;
  251. }
  252. /* OAuth2\Storage\RefreshTokenInterface */
  253. public function getRefreshToken($refresh_token)
  254. {
  255. $result = $this->client->getItem(array(
  256. "TableName"=> $this->config['refresh_token_table'],
  257. "Key" => array('refresh_token' => array('S' => $refresh_token))
  258. ));
  259. if ($result->count()==0) {
  260. return false ;
  261. }
  262. $token = $this->dynamo2array($result);
  263. $token['expires'] = strtotime($token['expires']);
  264. return $token;
  265. }
  266. public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null)
  267. {
  268. // convert expires to datestring
  269. $expires = date('Y-m-d H:i:s', $expires);
  270. $clientData = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope');
  271. $clientData = array_filter($clientData, function ($value) { return !empty($value); });
  272. $result = $this->client->putItem(array(
  273. 'TableName' => $this->config['refresh_token_table'],
  274. 'Item' => $this->client->formatAttributes($clientData)
  275. ));
  276. return true;
  277. }
  278. public function unsetRefreshToken($refresh_token)
  279. {
  280. $result = $this->client->deleteItem(array(
  281. 'TableName' => $this->config['refresh_token_table'],
  282. 'Key' => $this->client->formatAttributes(array("refresh_token" => $refresh_token))
  283. ));
  284. return true;
  285. }
  286. // plaintext passwords are bad! Override this for your application
  287. protected function checkPassword($user, $password)
  288. {
  289. return $user['password'] == sha1($password);
  290. }
  291. public function getUser($username)
  292. {
  293. $result = $this->client->getItem(array(
  294. "TableName"=> $this->config['user_table'],
  295. "Key" => array('username' => array('S' => $username))
  296. ));
  297. if ($result->count()==0) {
  298. return false ;
  299. }
  300. $token = $this->dynamo2array($result);
  301. $token['user_id'] = $username;
  302. return $token;
  303. }
  304. public function setUser($username, $password, $first_name = null, $last_name = null)
  305. {
  306. // do not store in plaintext
  307. $password = sha1($password);
  308. $clientData = compact('username', 'password', 'first_name', 'last_name');
  309. $clientData = array_filter($clientData, function ($value) { return !is_null($value); });
  310. $result = $this->client->putItem(array(
  311. 'TableName' => $this->config['user_table'],
  312. 'Item' => $this->client->formatAttributes($clientData)
  313. ));
  314. return true;
  315. }
  316. /* ScopeInterface */
  317. public function scopeExists($scope)
  318. {
  319. $scope = explode(' ', $scope);
  320. $scope_query = array();
  321. $count = 0;
  322. foreach ($scope as $key => $val) {
  323. $result = $this->client->query(array(
  324. 'TableName' => $this->config['scope_table'],
  325. 'Select' => 'COUNT',
  326. 'KeyConditions' => array(
  327. 'scope' => array(
  328. 'AttributeValueList' => array(array('S' => $val)),
  329. 'ComparisonOperator' => 'EQ'
  330. )
  331. )
  332. ));
  333. $count += $result['Count'];
  334. }
  335. return $count == count($scope);
  336. }
  337. public function getDefaultScope($client_id = null)
  338. {
  339. $result = $this->client->query(array(
  340. 'TableName' => $this->config['scope_table'],
  341. 'IndexName' => 'is_default-index',
  342. 'Select' => 'ALL_ATTRIBUTES',
  343. 'KeyConditions' => array(
  344. 'is_default' => array(
  345. 'AttributeValueList' => array(array('S' => 'true')),
  346. 'ComparisonOperator' => 'EQ',
  347. ),
  348. )
  349. ));
  350. $defaultScope = array();
  351. if ($result->count() > 0) {
  352. $array = $result->toArray();
  353. foreach ($array["Items"] as $item) {
  354. $defaultScope[] = $item['scope']['S'];
  355. }
  356. return empty($defaultScope) ? null : implode(' ', $defaultScope);
  357. }
  358. return null;
  359. }
  360. /* JWTBearerInterface */
  361. public function getClientKey($client_id, $subject)
  362. {
  363. $result = $this->client->getItem(array(
  364. "TableName"=> $this->config['jwt_table'],
  365. "Key" => array('client_id' => array('S' => $client_id), 'subject' => array('S' => $subject))
  366. ));
  367. if ($result->count()==0) {
  368. return false ;
  369. }
  370. $token = $this->dynamo2array($result);
  371. return $token['public_key'];
  372. }
  373. public function getClientScope($client_id)
  374. {
  375. if (!$clientDetails = $this->getClientDetails($client_id)) {
  376. return false;
  377. }
  378. if (isset($clientDetails['scope'])) {
  379. return $clientDetails['scope'];
  380. }
  381. return null;
  382. }
  383. public function getJti($client_id, $subject, $audience, $expires, $jti)
  384. {
  385. //TODO not use.
  386. }
  387. public function setJti($client_id, $subject, $audience, $expires, $jti)
  388. {
  389. //TODO not use.
  390. }
  391. /* PublicKeyInterface */
  392. public function getPublicKey($client_id = '0')
  393. {
  394. $result = $this->client->getItem(array(
  395. "TableName"=> $this->config['public_key_table'],
  396. "Key" => array('client_id' => array('S' => $client_id))
  397. ));
  398. if ($result->count()==0) {
  399. return false ;
  400. }
  401. $token = $this->dynamo2array($result);
  402. return $token['public_key'];
  403. }
  404. public function getPrivateKey($client_id = '0')
  405. {
  406. $result = $this->client->getItem(array(
  407. "TableName"=> $this->config['public_key_table'],
  408. "Key" => array('client_id' => array('S' => $client_id))
  409. ));
  410. if ($result->count()==0) {
  411. return false ;
  412. }
  413. $token = $this->dynamo2array($result);
  414. return $token['private_key'];
  415. }
  416. public function getEncryptionAlgorithm($client_id = null)
  417. {
  418. $result = $this->client->getItem(array(
  419. "TableName"=> $this->config['public_key_table'],
  420. "Key" => array('client_id' => array('S' => $client_id))
  421. ));
  422. if ($result->count()==0) {
  423. return 'RS256' ;
  424. }
  425. $token = $this->dynamo2array($result);
  426. return $token['encryption_algorithm'];
  427. }
  428. /**
  429. * Transform dynamodb resultset to an array.
  430. * @param $dynamodbResult
  431. * @return $array
  432. */
  433. private function dynamo2array($dynamodbResult)
  434. {
  435. $result = array();
  436. foreach ($dynamodbResult["Item"] as $key => $val) {
  437. $result[$key] = $val["S"];
  438. $result[] = $val["S"];
  439. }
  440. return $result;
  441. }
  442. }