DefaultTokenProvider.php 9.7 KB

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