From 3ab922601a2e6b9b170007461b9e0718c70bddcd Mon Sep 17 00:00:00 2001
From: Christoph Wurst <christoph@owncloud.com>
Date: Tue, 26 Apr 2016 11:32:35 +0200
Subject: [PATCH] Check if session token is valid and log user out if the check
 fails * Update last_activity timestamp of the session token * Check user
 backend credentials once in 5 minutes

---
 core/Controller/LoginController.php           |  1 -
 db_structure.xml                              |  4 +-
 .../Authentication/Token/DefaultToken.php     |  7 +-
 .../Token/DefaultTokenMapper.php              |  6 +-
 .../Token/DefaultTokenProvider.php            | 67 +++++++++++++++++--
 lib/private/User/Session.php                  | 45 +++++++++++--
 lib/private/legacy/user.php                   |  2 +-
 7 files changed, 107 insertions(+), 25 deletions(-)

diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php
index fe1ad41aedb..e13d8ae10d2 100644
--- a/core/Controller/LoginController.php
+++ b/core/Controller/LoginController.php
@@ -26,7 +26,6 @@ namespace OC\Core\Controller;
 use OC;
 use OC\User\Session;
 use OC_App;
-use OC_User;
 use OC_Util;
 use OCP\AppFramework\Controller;
 use OCP\AppFramework\Http\RedirectResponse;
diff --git a/db_structure.xml b/db_structure.xml
index 68a812a6b8f..dcbf426e5b8 100644
--- a/db_structure.xml
+++ b/db_structure.xml
@@ -1057,10 +1057,10 @@
 
 			<field>
 				<name>password</name>
-				<type>text</type>
+				<type>clob</type>
 				<default></default>
 				<notnull>true</notnull>
-				<length>100</length>
+				<length>4000</length>
 			</field>
 
 			<field>
diff --git a/lib/private/Authentication/Token/DefaultToken.php b/lib/private/Authentication/Token/DefaultToken.php
index 9bdae789afd..6b859d7d063 100644
--- a/lib/private/Authentication/Token/DefaultToken.php
+++ b/lib/private/Authentication/Token/DefaultToken.php
@@ -51,13 +51,8 @@ class DefaultToken extends Entity implements IToken {
 	 */
 	protected $lastActivity;
 
-	/**
-	 * Get the token ID
-	 *
-	 * @return string
-	 */
 	public function getId() {
-		return $this->token;
+		return $this->id;
 	}
 
 }
diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php
index 9a73192c0d8..d54d2489399 100644
--- a/lib/private/Authentication/Token/DefaultTokenMapper.php
+++ b/lib/private/Authentication/Token/DefaultTokenMapper.php
@@ -61,10 +61,10 @@ class DefaultTokenMapper extends Mapper {
 	 *
 	 * @param string $token
 	 * @throws DoesNotExistException
-	 * @return string
+	 * @return DefaultToken
 	 */
-	public function getTokenUser($token) {
-		$sql = 'SELECT `uid` '
+	public function getToken($token) {
+		$sql = 'SELECT `id`, `uid`, `password`, `name`, `token`, `last_activity` '
 			. 'FROM `' . $this->getTableName() . '` '
 			. 'WHERE `token` = ?';
 		return $this->findEntity($sql, [
diff --git a/lib/private/Authentication/Token/DefaultTokenProvider.php b/lib/private/Authentication/Token/DefaultTokenProvider.php
index 71f798da370..b3564e0e81b 100644
--- a/lib/private/Authentication/Token/DefaultTokenProvider.php
+++ b/lib/private/Authentication/Token/DefaultTokenProvider.php
@@ -48,8 +48,7 @@ class DefaultTokenProvider implements IProvider {
 	 * @param IConfig $config
 	 * @param ILogger $logger
 	 */
-	public function __construct(DefaultTokenMapper $mapper, ICrypto $crypto,
-		IConfig $config, ILogger $logger) {
+	public function __construct(DefaultTokenMapper $mapper, ICrypto $crypto, IConfig $config, ILogger $logger) {
 		$this->mapper = $mapper;
 		$this->crypto = $crypto;
 		$this->config = $config;
@@ -67,8 +66,7 @@ class DefaultTokenProvider implements IProvider {
 	public function generateToken($token, $uid, $password, $name) {
 		$dbToken = new DefaultToken();
 		$dbToken->setUid($uid);
-		$secret = $this->config->getSystemValue('secret');
-		$dbToken->setPassword($this->crypto->encrypt($password . $secret));
+		$dbToken->setPassword($this->encryptPassword($password, $token));
 		$dbToken->setName($name);
 		$dbToken->setToken($this->hashToken($token));
 		$dbToken->setLastActivity(time());
@@ -78,6 +76,37 @@ class DefaultTokenProvider implements IProvider {
 		return $dbToken;
 	}
 
+	/**
+	 * Update token activity timestamp
+	 *
+	 * @param DefaultToken $token
+	 */
+	public function updateToken(DefaultToken $token) {
+		$token->setLastActivity(time());
+
+		$this->mapper->update($token);
+	}
+
+	/**
+	 * @param string $token
+	 * @throws InvalidTokenException
+	 */
+	public function getToken($token) {
+		try {
+			return $this->mapper->getToken($this->hashToken($token));
+		} catch (DoesNotExistException $ex) {
+			throw new InvalidTokenException();
+		}
+	}
+
+	/**
+	 * @param DefaultToken $savedToken
+	 * @param string $token session token
+	 */
+	public function getPassword(DefaultToken $savedToken, $token) {
+		return $this->decryptPassword($savedToken->getPassword(), $token);
+	}
+
 	/**
 	 * Invalidate (delete) the given session token
 	 *
@@ -104,7 +133,7 @@ class DefaultTokenProvider implements IProvider {
 	public function validateToken($token) {
 		$this->logger->debug('validating default token <' . $token . '>');
 		try {
-			$dbToken = $this->mapper->getTokenUser($this->hashToken($token));
+			$dbToken = $this->mapper->getToken($this->hashToken($token));
 			$this->logger->debug('valid token for ' . $dbToken->getUid());
 			return $dbToken->getUid();
 		} catch (DoesNotExistException $ex) {
@@ -121,4 +150,32 @@ class DefaultTokenProvider implements IProvider {
 		return hash('sha512', $token);
 	}
 
+	/**
+	 * Encrypt the given password
+	 *
+	 * The token is used as key
+	 *
+	 * @param string $password
+	 * @param string $token
+	 * @return string encrypted password
+	 */
+	private function encryptPassword($password, $token) {
+		$secret = $this->config->getSystemValue('secret');
+		return $this->crypto->encrypt($password, $token . $secret);
+	}
+
+	/**
+	 * Decrypt the given password
+	 *
+	 * The token is used as key
+	 *
+	 * @param string $password
+	 * @param string $token
+	 * @return string the decrypted key
+	 */
+	private function decryptPassword($password, $token) {
+		$secret = $this->config->getSystemValue('secret');
+		return $this->crypto->decrypt($password, $token . $secret);
+	}
+
 }
diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php
index 9db503e6add..7d4594e7205 100644
--- a/lib/private/User/Session.php
+++ b/lib/private/User/Session.php
@@ -96,8 +96,7 @@ class Session implements IUserSession, Emitter {
 	 * @param ISession $session
 	 * @param IProvider[] $tokenProviders
 	 */
-	public function __construct(IUserManager $manager, ISession $session,
-		DefaultTokenProvider $tokenProvider, array $tokenProviders = []) {
+	public function __construct(IUserManager $manager, ISession $session, DefaultTokenProvider $tokenProvider, array $tokenProviders = []) {
 		$this->manager = $manager;
 		$this->session = $session;
 		$this->tokenProvider = $tokenProvider;
@@ -118,8 +117,7 @@ class Session implements IUserSession, Emitter {
 	 * @param string $method optional
 	 * @param callable $callback optional
 	 */
-	public function removeListener($scope = null, $method = null,
-		callable $callback = null) {
+	public function removeListener($scope = null, $method = null, callable $callback = null) {
 		$this->manager->removeListener($scope, $method, $callback);
 	}
 
@@ -183,8 +181,7 @@ class Session implements IUserSession, Emitter {
 			return $this->activeUser;
 		} else {
 			$uid = $this->session->get('user_id');
-			if ($uid !== null) {
-				$this->activeUser = $this->manager->get($uid);
+			if ($uid !== null && $this->isValidSession($uid)) {
 				return $this->activeUser;
 			} else {
 				return null;
@@ -192,6 +189,41 @@ class Session implements IUserSession, Emitter {
 		}
 	}
 
+	private function isValidSession($uid) {
+		$this->activeUser = $this->manager->get($uid);
+		if (is_null($this->activeUser)) {
+			// User does not exist
+			return false;
+		}
+		// TODO: use ISession::getId(), https://github.com/owncloud/core/pull/24229
+		$sessionId = session_id();
+		try {
+			$token = $this->tokenProvider->getToken($sessionId);
+		} catch (InvalidTokenException $ex) {
+			// Session was inalidated
+			$this->logout();
+			return false;
+		}
+
+		// Check whether login credentials are still valid
+		// This check is performed each 5 minutes
+		$lastCheck = $this->session->get('last_login_check') ? : 0;
+		if ($lastCheck < (time() - 60 * 5)) {
+			$pwd = $this->tokenProvider->getPassword($token, $sessionId);
+			if ($this->manager->checkPassword($uid, $pwd) === false) {
+				// Password has changed -> log user out
+				$this->logout();
+				return false;
+			}
+			$this->session->set('last_login_check', time());
+		}
+
+		// Session is valid, so the token can be refreshed
+		$this->tokenProvider->updateToken($token);
+
+		return true;
+	}
+
 	/**
 	 * Checks whether the user is logged in
 	 *
@@ -334,7 +366,6 @@ class Session implements IUserSession, Emitter {
 	 * @return boolean
 	 */
 	private function validateToken(IRequest $request, $token) {
-		// TODO: hash token
 		foreach ($this->tokenProviders as $provider) {
 			try {
 				$user = $provider->validateToken($token);
diff --git a/lib/private/legacy/user.php b/lib/private/legacy/user.php
index 575011d3985..ca408d347bd 100644
--- a/lib/private/legacy/user.php
+++ b/lib/private/legacy/user.php
@@ -68,7 +68,7 @@ class OC_User {
 
 	private static $_setupedBackends = array();
 
-	// bool, stores if a user want to access a resource anonymously, e.g if he opens a public link
+	// bool, stores if a user want to access a resource anonymously, e.g if they open a public link
 	private static $incognitoMode = false;
 
 	/**
-- 
GitLab