DefaultTokenProvider.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2016, ownCloud, Inc.
  5. * @copyright Copyright (c) 2016, Christoph Wurst <christoph@winzerhof-wurst.at>
  6. *
  7. * @author Christoph Wurst <christoph@owncloud.com>
  8. * @author Lukas Reschke <lukas@statuscode.ch>
  9. * @author Marcel Waldvogel <marcel.waldvogel@uni-konstanz.de>
  10. * @author Martin <github@diemattels.at>
  11. * @author Robin Appelman <robin@icewind.nl>
  12. *
  13. * @license AGPL-3.0
  14. *
  15. * This code is free software: you can redistribute it and/or modify
  16. * it under the terms of the GNU Affero General Public License, version 3,
  17. * as published by the Free Software Foundation.
  18. *
  19. * This program is distributed in the hope that it will be useful,
  20. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. * GNU Affero General Public License for more details.
  23. *
  24. * You should have received a copy of the GNU Affero General Public License, version 3,
  25. * along with this program. If not, see <http://www.gnu.org/licenses/>
  26. *
  27. */
  28. namespace OC\Authentication\Token;
  29. use Exception;
  30. use OC\Authentication\Exceptions\ExpiredTokenException;
  31. use OC\Authentication\Exceptions\InvalidTokenException;
  32. use OC\Authentication\Exceptions\PasswordlessTokenException;
  33. use OCP\AppFramework\Db\DoesNotExistException;
  34. use OCP\AppFramework\Utility\ITimeFactory;
  35. use OCP\IConfig;
  36. use OCP\ILogger;
  37. use OCP\Security\ICrypto;
  38. class DefaultTokenProvider implements IProvider {
  39. /** @var DefaultTokenMapper */
  40. private $mapper;
  41. /** @var ICrypto */
  42. private $crypto;
  43. /** @var IConfig */
  44. private $config;
  45. /** @var ILogger $logger */
  46. private $logger;
  47. /** @var ITimeFactory $time */
  48. private $time;
  49. /**
  50. * @param DefaultTokenMapper $mapper
  51. * @param ICrypto $crypto
  52. * @param IConfig $config
  53. * @param ILogger $logger
  54. * @param ITimeFactory $time
  55. */
  56. public function __construct(DefaultTokenMapper $mapper,
  57. ICrypto $crypto,
  58. IConfig $config,
  59. ILogger $logger,
  60. ITimeFactory $time) {
  61. $this->mapper = $mapper;
  62. $this->crypto = $crypto;
  63. $this->config = $config;
  64. $this->logger = $logger;
  65. $this->time = $time;
  66. }
  67. /**
  68. * Create and persist a new token
  69. *
  70. * @param string $token
  71. * @param string $uid
  72. * @param string $loginName
  73. * @param string|null $password
  74. * @param string $name
  75. * @param int $type token type
  76. * @param int $remember whether the session token should be used for remember-me
  77. * @return IToken
  78. */
  79. public function generateToken(string $token,
  80. string $uid,
  81. string $loginName,
  82. $password,
  83. string $name,
  84. int $type = IToken::TEMPORARY_TOKEN,
  85. int $remember = IToken::DO_NOT_REMEMBER): IToken {
  86. $dbToken = new DefaultToken();
  87. $dbToken->setUid($uid);
  88. $dbToken->setLoginName($loginName);
  89. if (!is_null($password)) {
  90. $dbToken->setPassword($this->encryptPassword($password, $token));
  91. }
  92. $dbToken->setName($name);
  93. $dbToken->setToken($this->hashToken($token));
  94. $dbToken->setType($type);
  95. $dbToken->setRemember($remember);
  96. $dbToken->setLastActivity($this->time->getTime());
  97. $dbToken->setLastCheck($this->time->getTime());
  98. $dbToken->setVersion(DefaultToken::VERSION);
  99. $this->mapper->insert($dbToken);
  100. return $dbToken;
  101. }
  102. /**
  103. * Save the updated token
  104. *
  105. * @param IToken $token
  106. * @throws InvalidTokenException
  107. */
  108. public function updateToken(IToken $token) {
  109. if (!($token instanceof DefaultToken)) {
  110. throw new InvalidTokenException();
  111. }
  112. $this->mapper->update($token);
  113. }
  114. /**
  115. * Update token activity timestamp
  116. *
  117. * @throws InvalidTokenException
  118. * @param IToken $token
  119. */
  120. public function updateTokenActivity(IToken $token) {
  121. if (!($token instanceof DefaultToken)) {
  122. throw new InvalidTokenException();
  123. }
  124. /** @var DefaultToken $token */
  125. $now = $this->time->getTime();
  126. if ($token->getLastActivity() < ($now - 60)) {
  127. // Update token only once per minute
  128. $token->setLastActivity($now);
  129. $this->mapper->update($token);
  130. }
  131. }
  132. public function getTokenByUser(string $uid): array {
  133. return $this->mapper->getTokenByUser($uid);
  134. }
  135. /**
  136. * Get a token by token
  137. *
  138. * @param string $tokenId
  139. * @throws InvalidTokenException
  140. * @throws ExpiredTokenException
  141. * @return IToken
  142. */
  143. public function getToken(string $tokenId): IToken {
  144. try {
  145. $token = $this->mapper->getToken($this->hashToken($tokenId));
  146. } catch (DoesNotExistException $ex) {
  147. throw new InvalidTokenException();
  148. }
  149. if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
  150. throw new ExpiredTokenException($token);
  151. }
  152. return $token;
  153. }
  154. /**
  155. * Get a token by token id
  156. *
  157. * @param int $tokenId
  158. * @throws InvalidTokenException
  159. * @throws ExpiredTokenException
  160. * @return IToken
  161. */
  162. public function getTokenById(int $tokenId): IToken {
  163. try {
  164. $token = $this->mapper->getTokenById($tokenId);
  165. } catch (DoesNotExistException $ex) {
  166. throw new InvalidTokenException();
  167. }
  168. if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
  169. throw new ExpiredTokenException($token);
  170. }
  171. return $token;
  172. }
  173. /**
  174. * @param string $oldSessionId
  175. * @param string $sessionId
  176. * @throws InvalidTokenException
  177. */
  178. public function renewSessionToken(string $oldSessionId, string $sessionId) {
  179. $token = $this->getToken($oldSessionId);
  180. $newToken = new DefaultToken();
  181. $newToken->setUid($token->getUID());
  182. $newToken->setLoginName($token->getLoginName());
  183. if (!is_null($token->getPassword())) {
  184. $password = $this->decryptPassword($token->getPassword(), $oldSessionId);
  185. $newToken->setPassword($this->encryptPassword($password, $sessionId));
  186. }
  187. $newToken->setName($token->getName());
  188. $newToken->setToken($this->hashToken($sessionId));
  189. $newToken->setType(IToken::TEMPORARY_TOKEN);
  190. $newToken->setRemember($token->getRemember());
  191. $newToken->setLastActivity($this->time->getTime());
  192. $this->mapper->insert($newToken);
  193. $this->mapper->delete($token);
  194. }
  195. /**
  196. * @param IToken $savedToken
  197. * @param string $tokenId session token
  198. * @throws InvalidTokenException
  199. * @throws PasswordlessTokenException
  200. * @return string
  201. */
  202. public function getPassword(IToken $savedToken, string $tokenId): string {
  203. $password = $savedToken->getPassword();
  204. if (is_null($password)) {
  205. throw new PasswordlessTokenException();
  206. }
  207. return $this->decryptPassword($password, $tokenId);
  208. }
  209. /**
  210. * Encrypt and set the password of the given token
  211. *
  212. * @param IToken $token
  213. * @param string $tokenId
  214. * @param string $password
  215. * @throws InvalidTokenException
  216. */
  217. public function setPassword(IToken $token, string $tokenId, string $password) {
  218. if (!($token instanceof DefaultToken)) {
  219. throw new InvalidTokenException();
  220. }
  221. /** @var DefaultToken $token */
  222. $token->setPassword($this->encryptPassword($password, $tokenId));
  223. $this->mapper->update($token);
  224. }
  225. /**
  226. * Invalidate (delete) the given session token
  227. *
  228. * @param string $token
  229. */
  230. public function invalidateToken(string $token) {
  231. $this->mapper->invalidate($this->hashToken($token));
  232. }
  233. public function invalidateTokenById(string $uid, int $id) {
  234. $this->mapper->deleteById($uid, $id);
  235. }
  236. /**
  237. * Invalidate (delete) old session tokens
  238. */
  239. public function invalidateOldTokens() {
  240. $olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24);
  241. $this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
  242. $this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER);
  243. $rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
  244. $this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']);
  245. $this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER);
  246. }
  247. /**
  248. * Rotate the token. Usefull for for example oauth tokens
  249. *
  250. * @param IToken $token
  251. * @param string $oldTokenId
  252. * @param string $newTokenId
  253. * @return IToken
  254. */
  255. public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken {
  256. try {
  257. $password = $this->getPassword($token, $oldTokenId);
  258. $token->setPassword($this->encryptPassword($password, $newTokenId));
  259. } catch (PasswordlessTokenException $e) {
  260. }
  261. $token->setToken($this->hashToken($newTokenId));
  262. $this->updateToken($token);
  263. return $token;
  264. }
  265. /**
  266. * @param string $token
  267. * @return string
  268. */
  269. private function hashToken(string $token): string {
  270. $secret = $this->config->getSystemValue('secret');
  271. return hash('sha512', $token . $secret);
  272. }
  273. /**
  274. * Encrypt the given password
  275. *
  276. * The token is used as key
  277. *
  278. * @param string $password
  279. * @param string $token
  280. * @return string encrypted password
  281. */
  282. private function encryptPassword(string $password, string $token): string {
  283. $secret = $this->config->getSystemValue('secret');
  284. return $this->crypto->encrypt($password, $token . $secret);
  285. }
  286. /**
  287. * Decrypt the given password
  288. *
  289. * The token is used as key
  290. *
  291. * @param string $password
  292. * @param string $token
  293. * @throws InvalidTokenException
  294. * @return string the decrypted key
  295. */
  296. private function decryptPassword(string $password, string $token): string {
  297. $secret = $this->config->getSystemValue('secret');
  298. try {
  299. return $this->crypto->decrypt($password, $token . $secret);
  300. } catch (Exception $ex) {
  301. // Delete the invalid token
  302. $this->invalidateToken($token);
  303. throw new InvalidTokenException();
  304. }
  305. }
  306. public function markPasswordInvalid(IToken $token, string $tokenId) {
  307. if (!($token instanceof DefaultToken)) {
  308. throw new InvalidTokenException();
  309. }
  310. //No need to mark as invalid. We just invalide default tokens
  311. $this->invalidateToken($tokenId);
  312. }
  313. public function updatePasswords(string $uid, string $password) {
  314. // Nothing to do here
  315. }
  316. }