You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

353 lines
10 KiB

3 years ago
  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\ILogger;
  39. use OCP\Security\ICrypto;
  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 ILogger */
  48. private $logger;
  49. /** @var ITimeFactory */
  50. private $time;
  51. public function __construct(DefaultTokenMapper $mapper,
  52. ICrypto $crypto,
  53. IConfig $config,
  54. ILogger $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. * Create and persist a new token
  64. *
  65. * @param string $token
  66. * @param string $uid
  67. * @param string $loginName
  68. * @param string|null $password
  69. * @param string $name
  70. * @param int $type token type
  71. * @param int $remember whether the session token should be used for remember-me
  72. * @return IToken
  73. */
  74. public function generateToken(string $token,
  75. string $uid,
  76. string $loginName,
  77. $password,
  78. string $name,
  79. int $type = IToken::TEMPORARY_TOKEN,
  80. int $remember = IToken::DO_NOT_REMEMBER): IToken {
  81. $dbToken = new DefaultToken();
  82. $dbToken->setUid($uid);
  83. $dbToken->setLoginName($loginName);
  84. if (!is_null($password)) {
  85. $dbToken->setPassword($this->encryptPassword($password, $token));
  86. }
  87. $dbToken->setName($name);
  88. $dbToken->setToken($this->hashToken($token));
  89. $dbToken->setType($type);
  90. $dbToken->setRemember($remember);
  91. $dbToken->setLastActivity($this->time->getTime());
  92. $dbToken->setLastCheck($this->time->getTime());
  93. $dbToken->setVersion(DefaultToken::VERSION);
  94. $this->mapper->insert($dbToken);
  95. return $dbToken;
  96. }
  97. /**
  98. * Save the updated token
  99. *
  100. * @param IToken $token
  101. * @throws InvalidTokenException
  102. */
  103. public function updateToken(IToken $token) {
  104. if (!($token instanceof DefaultToken)) {
  105. throw new InvalidTokenException("Invalid token type");
  106. }
  107. $this->mapper->update($token);
  108. }
  109. /**
  110. * Update token activity timestamp
  111. *
  112. * @throws InvalidTokenException
  113. * @param IToken $token
  114. */
  115. public function updateTokenActivity(IToken $token) {
  116. if (!($token instanceof DefaultToken)) {
  117. throw new InvalidTokenException("Invalid token type");
  118. }
  119. /** @var DefaultToken $token */
  120. $now = $this->time->getTime();
  121. if ($token->getLastActivity() < ($now - 60)) {
  122. // Update token only once per minute
  123. $token->setLastActivity($now);
  124. $this->mapper->update($token);
  125. }
  126. }
  127. public function getTokenByUser(string $uid): array {
  128. return $this->mapper->getTokenByUser($uid);
  129. }
  130. /**
  131. * Get a token by token
  132. *
  133. * @param string $tokenId
  134. * @throws InvalidTokenException
  135. * @throws ExpiredTokenException
  136. * @return IToken
  137. */
  138. public function getToken(string $tokenId): IToken {
  139. try {
  140. $token = $this->mapper->getToken($this->hashToken($tokenId));
  141. } catch (DoesNotExistException $ex) {
  142. throw new InvalidTokenException("Token does not exist", 0, $ex);
  143. }
  144. if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
  145. throw new ExpiredTokenException($token);
  146. }
  147. return $token;
  148. }
  149. /**
  150. * Get a token by token id
  151. *
  152. * @param int $tokenId
  153. * @throws InvalidTokenException
  154. * @throws ExpiredTokenException
  155. * @return IToken
  156. */
  157. public function getTokenById(int $tokenId): IToken {
  158. try {
  159. $token = $this->mapper->getTokenById($tokenId);
  160. } catch (DoesNotExistException $ex) {
  161. throw new InvalidTokenException("Token with ID $tokenId does not exist", 0, $ex);
  162. }
  163. if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
  164. throw new ExpiredTokenException($token);
  165. }
  166. return $token;
  167. }
  168. /**
  169. * @param string $oldSessionId
  170. * @param string $sessionId
  171. * @throws InvalidTokenException
  172. * @return IToken
  173. */
  174. public function renewSessionToken(string $oldSessionId, string $sessionId): IToken {
  175. $token = $this->getToken($oldSessionId);
  176. $newToken = new DefaultToken();
  177. $newToken->setUid($token->getUID());
  178. $newToken->setLoginName($token->getLoginName());
  179. if (!is_null($token->getPassword())) {
  180. $password = $this->decryptPassword($token->getPassword(), $oldSessionId);
  181. $newToken->setPassword($this->encryptPassword($password, $sessionId));
  182. }
  183. $newToken->setName($token->getName());
  184. $newToken->setToken($this->hashToken($sessionId));
  185. $newToken->setType(IToken::TEMPORARY_TOKEN);
  186. $newToken->setRemember($token->getRemember());
  187. $newToken->setLastActivity($this->time->getTime());
  188. $this->mapper->insert($newToken);
  189. $this->mapper->delete($token);
  190. return $newToken;
  191. }
  192. /**
  193. * @param IToken $savedToken
  194. * @param string $tokenId session token
  195. * @throws InvalidTokenException
  196. * @throws PasswordlessTokenException
  197. * @return string
  198. */
  199. public function getPassword(IToken $savedToken, string $tokenId): string {
  200. $password = $savedToken->getPassword();
  201. if (is_null($password)) {
  202. throw new PasswordlessTokenException();
  203. }
  204. return $this->decryptPassword($password, $tokenId);
  205. }
  206. /**
  207. * Encrypt and set the password of the given token
  208. *
  209. * @param IToken $token
  210. * @param string $tokenId
  211. * @param string $password
  212. * @throws InvalidTokenException
  213. */
  214. public function setPassword(IToken $token, string $tokenId, string $password) {
  215. if (!($token instanceof DefaultToken)) {
  216. throw new InvalidTokenException("Invalid token type");
  217. }
  218. /** @var DefaultToken $token */
  219. $token->setPassword($this->encryptPassword($password, $tokenId));
  220. $this->mapper->update($token);
  221. }
  222. /**
  223. * Invalidate (delete) the given session token
  224. *
  225. * @param string $token
  226. */
  227. public function invalidateToken(string $token) {
  228. $this->mapper->invalidate($this->hashToken($token));
  229. }
  230. public function invalidateTokenById(string $uid, int $id) {
  231. $this->mapper->deleteById($uid, $id);
  232. }
  233. /**
  234. * Invalidate (delete) old session tokens
  235. */
  236. public function invalidateOldTokens() {
  237. $olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24);
  238. $this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
  239. $this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER);
  240. $rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
  241. $this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']);
  242. $this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER);
  243. }
  244. /**
  245. * Rotate the token. Usefull for for example oauth tokens
  246. *
  247. * @param IToken $token
  248. * @param string $oldTokenId
  249. * @param string $newTokenId
  250. * @return IToken
  251. */
  252. public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken {
  253. try {
  254. $password = $this->getPassword($token, $oldTokenId);
  255. $token->setPassword($this->encryptPassword($password, $newTokenId));
  256. } catch (PasswordlessTokenException $e) {
  257. }
  258. $token->setToken($this->hashToken($newTokenId));
  259. $this->updateToken($token);
  260. return $token;
  261. }
  262. /**
  263. * @param string $token
  264. * @return string
  265. */
  266. private function hashToken(string $token): string {
  267. $secret = $this->config->getSystemValue('secret');
  268. return hash('sha512', $token . $secret);
  269. }
  270. /**
  271. * Encrypt the given password
  272. *
  273. * The token is used as key
  274. *
  275. * @param string $password
  276. * @param string $token
  277. * @return string encrypted password
  278. */
  279. private function encryptPassword(string $password, string $token): string {
  280. $secret = $this->config->getSystemValue('secret');
  281. return $this->crypto->encrypt($password, $token . $secret);
  282. }
  283. /**
  284. * Decrypt the given password
  285. *
  286. * The token is used as key
  287. *
  288. * @param string $password
  289. * @param string $token
  290. * @throws InvalidTokenException
  291. * @return string the decrypted key
  292. */
  293. private function decryptPassword(string $password, string $token): string {
  294. $secret = $this->config->getSystemValue('secret');
  295. try {
  296. return $this->crypto->decrypt($password, $token . $secret);
  297. } catch (Exception $ex) {
  298. // Delete the invalid token
  299. $this->invalidateToken($token);
  300. throw new InvalidTokenException("Can not decrypt token password: " . $ex->getMessage(), 0, $ex);
  301. }
  302. }
  303. public function markPasswordInvalid(IToken $token, string $tokenId) {
  304. if (!($token instanceof DefaultToken)) {
  305. throw new InvalidTokenException("Invalid token type");
  306. }
  307. //No need to mark as invalid. We just invalide default tokens
  308. $this->invalidateToken($tokenId);
  309. }
  310. public function updatePasswords(string $uid, string $password) {
  311. // Nothing to do here
  312. }
  313. }