summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLukas Reschke <lukas@statuscode.ch>2014-05-28 19:06:47 +0200
committerLukas Reschke <lukas@statuscode.ch>2014-05-28 19:06:47 +0200
commitce9d5df6df37e51587dcde638086dfe501892b56 (patch)
tree3a5e9a2cb22ede6c9c21f9847f05ec2ec7dd5826
parent6d57e4c491aa8e09ff20f7ef9e2fb5618f75de48 (diff)
parent3a21c3e2f46fd01cbbab237d36a3e8abbbca3cca (diff)
downloadnextcloud-server-ce9d5df6df37e51587dcde638086dfe501892b56.tar.gz
nextcloud-server-ce9d5df6df37e51587dcde638086dfe501892b56.zip
Merge pull request #8681 from owncloud/logintimestamp
Record login timestamp per user. Required for new user managament.
-rw-r--r--core/command/user/lastseen.php47
-rw-r--r--core/register_command.php1
-rw-r--r--lib/base.php24
-rw-r--r--lib/private/user.php11
-rw-r--r--lib/private/user/manager.php6
-rw-r--r--lib/private/user/session.php37
-rw-r--r--lib/private/user/user.php25
-rw-r--r--tests/lib/user/session.php182
8 files changed, 314 insertions, 19 deletions
diff --git a/core/command/user/lastseen.php b/core/command/user/lastseen.php
new file mode 100644
index 00000000000..7a8db013e3a
--- /dev/null
+++ b/core/command/user/lastseen.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * Copyright (c) 2014 Arthur Schiwon <blizzz@owncloud.com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ * See the COPYING-README file.
+ */
+
+namespace OC\Core\Command\User;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+
+class LastSeen extends Command {
+ protected function configure() {
+ $this
+ ->setName('user:lastseen')
+ ->setDescription('shows when the user was logged it last time')
+ ->addArgument(
+ 'uid',
+ InputArgument::REQUIRED,
+ 'the username'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $userManager = \OC::$server->getUserManager();
+ $user = $userManager->get($input->getArgument('uid'));
+ if(is_null($user)) {
+ $output->writeln('User does not exist');
+ return;
+ }
+
+ $lastLogin = $user->getLastLogin();
+ if($lastLogin === 0) {
+ $output->writeln('User ' . $user->getUID() .
+ ' has never logged in, yet.');
+ } else {
+ $date = new \DateTime();
+ $date->setTimestamp($lastLogin);
+ $output->writeln($user->getUID() .
+ '`s last login: ' . $date->format('d.m.Y H:i'));
+ }
+ }
+}
diff --git a/core/register_command.php b/core/register_command.php
index f1361c859fc..dfb5134eff9 100644
--- a/core/register_command.php
+++ b/core/register_command.php
@@ -17,3 +17,4 @@ $application->add(new OC\Core\Command\App\Enable());
$application->add(new OC\Core\Command\App\ListApps());
$application->add(new OC\Core\Command\Maintenance\Repair(new \OC\Repair()));
$application->add(new OC\Core\Command\User\Report());
+$application->add(new OC\Core\Command\User\LastSeen());
diff --git a/lib/base.php b/lib/base.php
index a93f1caded4..63df94319f6 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -884,30 +884,24 @@ class OC {
if (defined("DEBUG") && DEBUG) {
OC_Log::write('core', 'Trying to login from cookie', OC_Log::DEBUG);
}
- // confirm credentials in cookie
- if (isset($_COOKIE['oc_token']) && OC_User::userExists($_COOKIE['oc_username'])) {
- // delete outdated cookies
+
+ if(OC_User::userExists($_COOKIE['oc_username'])) {
self::cleanupLoginTokens($_COOKIE['oc_username']);
- // get stored tokens
- $tokens = OC_Preferences::getKeys($_COOKIE['oc_username'], 'login_token');
- // test cookies token against stored tokens
- if (in_array($_COOKIE['oc_token'], $tokens, true)) {
- // replace successfully used token with a new one
- OC_Preferences::deleteKey($_COOKIE['oc_username'], 'login_token', $_COOKIE['oc_token']);
- $token = OC_Util::generateRandomBytes(32);
- OC_Preferences::setValue($_COOKIE['oc_username'], 'login_token', $token, time());
- OC_User::setMagicInCookie($_COOKIE['oc_username'], $token);
- // login
- OC_User::setUserId($_COOKIE['oc_username']);
+ // verify whether the supplied "remember me" token was valid
+ $granted = OC_User::loginWithCookie(
+ $_COOKIE['oc_username'], $_COOKIE['oc_token']);
+ if($granted === true) {
OC_Util::redirectToDefaultPage();
// doesn't return
}
+ OC_Log::write('core', 'Authentication cookie rejected for user ' .
+ $_COOKIE['oc_username'], OC_Log::WARN);
// if you reach this point you have changed your password
// or you are an attacker
// we can not delete tokens here because users may reach
// this point multiple times after a password change
- OC_Log::write('core', 'Authentication cookie rejected for user ' . $_COOKIE['oc_username'], OC_Log::WARN);
}
+
OC_User::unsetMagicInCookie();
return true;
}
diff --git a/lib/private/user.php b/lib/private/user.php
index 9276d7923c9..5d3ebb57c8c 100644
--- a/lib/private/user.php
+++ b/lib/private/user.php
@@ -236,6 +236,17 @@ class OC_User {
}
/**
+ * Try to login a user using the magic cookie (remember login)
+ *
+ * @param string $uid The username of the user to log in
+ * @param string $token
+ * @return bool
+ */
+ public static function loginWithCookie($uid, $token) {
+ return self::getUserSession()->loginWithCookie($uid, $token);
+ }
+
+ /**
* Try to login a user, assuming authentication
* has already happened (e.g. via Single Sign On).
*
diff --git a/lib/private/user/manager.php b/lib/private/user/manager.php
index 0fcf1ceb6ab..f2964fecca3 100644
--- a/lib/private/user/manager.php
+++ b/lib/private/user/manager.php
@@ -52,6 +52,12 @@ class Manager extends PublicEmitter {
unset($cachedUsers[$i]);
}
});
+ $this->listen('\OC\User', 'postLogin', function ($user) {
+ $user->updateLastLoginTimestamp();
+ });
+ $this->listen('\OC\User', 'postRememberedLogin', function ($user) {
+ $user->updateLastLoginTimestamp();
+ });
}
/**
diff --git a/lib/private/user/session.php b/lib/private/user/session.php
index 3d10b134b83..5f0dee607ae 100644
--- a/lib/private/user/session.php
+++ b/lib/private/user/session.php
@@ -22,7 +22,9 @@ use OC\Hooks\Emitter;
* - preCreateUser(string $uid, string $password)
* - postCreateUser(\OC\User\User $user)
* - preLogin(string $user, string $password)
- * - postLogin(\OC\User\User $user)
+ * - postLogin(\OC\User\User $user, string $password)
+ * - preRememberedLogin(string $uid)
+ * - postRememberedLogin(\OC\User\User $user)
* - logout()
*
* @package OC\User
@@ -171,6 +173,39 @@ class Session implements Emitter, \OCP\IUserSession {
}
/**
+ * perform login using the magic cookie (remember login)
+ *
+ * @param string $uid the username
+ * @param string $currentToken
+ * @return bool
+ */
+ public function loginWithCookie($uid, $currentToken) {
+ $this->manager->emit('\OC\User', 'preRememberedLogin', array($uid));
+ $user = $this->manager->get($uid);
+ if(is_null($user)) {
+ // user does not exist
+ return false;
+ }
+
+ // get stored tokens
+ $tokens = \OC_Preferences::getKeys($uid, 'login_token');
+ // test cookies token against stored tokens
+ if(!in_array($currentToken, $tokens, true)) {
+ return false;
+ }
+ // replace successfully used token with a new one
+ \OC_Preferences::deleteKey($uid, 'login_token', $currentToken);
+ $newToken = \OC_Util::generateRandomBytes(32);
+ \OC_Preferences::setValue($uid, 'login_token', $newToken, time());
+ $this->setMagicInCookie($user->getUID(), $newToken);
+
+ //login
+ $this->setUser($user);
+ $this->manager->emit('\OC\User', 'postRememberedLogin', array($user));
+ return true;
+ }
+
+ /**
* logout the user from the session
*/
public function logout() {
diff --git a/lib/private/user/user.php b/lib/private/user/user.php
index bc5c541e521..8aba7188e24 100644
--- a/lib/private/user/user.php
+++ b/lib/private/user/user.php
@@ -43,6 +43,11 @@ class User {
private $home;
/**
+ * @var int $lastLogin
+ */
+ private $lastLogin;
+
+ /**
* @var \OC\AllConfig $config
*/
private $config;
@@ -64,6 +69,7 @@ class User {
} else {
$this->enabled = true;
}
+ $this->lastLogin = \OC_Preferences::getValue($uid, 'login', 'lastLogin', 0);
}
/**
@@ -108,6 +114,25 @@ class User {
}
/**
+ * returns the timestamp of the user's last login or 0 if the user did never
+ * login
+ *
+ * @return int
+ */
+ public function getLastLogin() {
+ return $this->lastLogin;
+ }
+
+ /**
+ * updates the timestamp of the most recent login of this user
+ */
+ public function updateLastLoginTimestamp() {
+ $this->lastLogin = time();
+ \OC_Preferences::setValue(
+ $this->uid, 'login', 'lastLogin', $this->lastLogin);
+ }
+
+ /**
* Delete the user
*
* @return bool
diff --git a/tests/lib/user/session.php b/tests/lib/user/session.php
index 46b268b3624..2845a9c964a 100644
--- a/tests/lib/user/session.php
+++ b/tests/lib/user/session.php
@@ -67,7 +67,17 @@ class Session extends \PHPUnit_Framework_TestCase {
},
'foo'));
- $manager = $this->getMock('\OC\User\Manager');
+ $managerMethods = get_class_methods('\OC\User\Manager');
+ //keep following methods intact in order to ensure hooks are
+ //working
+ $doNotMock = array('__construct', 'emit', 'listen');
+ foreach($doNotMock as $methodName) {
+ $i = array_search($methodName, $managerMethods, true);
+ if($i !== false) {
+ unset($managerMethods[$i]);
+ }
+ }
+ $manager = $this->getMock('\OC\User\Manager', $managerMethods, array());
$backend = $this->getMock('OC_User_Dummy');
@@ -78,6 +88,8 @@ class Session extends \PHPUnit_Framework_TestCase {
$user->expects($this->any())
->method('getUID')
->will($this->returnValue('foo'));
+ $user->expects($this->once())
+ ->method('updateLastLoginTimestamp');
$manager->expects($this->once())
->method('checkPassword')
@@ -94,7 +106,17 @@ class Session extends \PHPUnit_Framework_TestCase {
$session->expects($this->never())
->method('set');
- $manager = $this->getMock('\OC\User\Manager');
+ $managerMethods = get_class_methods('\OC\User\Manager');
+ //keep following methods intact in order to ensure hooks are
+ //working
+ $doNotMock = array('__construct', 'emit', 'listen');
+ foreach($doNotMock as $methodName) {
+ $i = array_search($methodName, $managerMethods, true);
+ if($i !== false) {
+ unset($managerMethods[$i]);
+ }
+ }
+ $manager = $this->getMock('\OC\User\Manager', $managerMethods, array());
$backend = $this->getMock('OC_User_Dummy');
@@ -102,6 +124,8 @@ class Session extends \PHPUnit_Framework_TestCase {
$user->expects($this->once())
->method('isEnabled')
->will($this->returnValue(false));
+ $user->expects($this->never())
+ ->method('updateLastLoginTimestamp');
$manager->expects($this->once())
->method('checkPassword')
@@ -117,13 +141,25 @@ class Session extends \PHPUnit_Framework_TestCase {
$session->expects($this->never())
->method('set');
- $manager = $this->getMock('\OC\User\Manager');
+ $managerMethods = get_class_methods('\OC\User\Manager');
+ //keep following methods intact in order to ensure hooks are
+ //working
+ $doNotMock = array('__construct', 'emit', 'listen');
+ foreach($doNotMock as $methodName) {
+ $i = array_search($methodName, $managerMethods, true);
+ if($i !== false) {
+ unset($managerMethods[$i]);
+ }
+ }
+ $manager = $this->getMock('\OC\User\Manager', $managerMethods, array());
$backend = $this->getMock('OC_User_Dummy');
$user = $this->getMock('\OC\User\User', array(), array('foo', $backend));
$user->expects($this->never())
->method('isEnabled');
+ $user->expects($this->never())
+ ->method('updateLastLoginTimestamp');
$manager->expects($this->once())
->method('checkPassword')
@@ -151,4 +187,144 @@ class Session extends \PHPUnit_Framework_TestCase {
$userSession = new \OC\User\Session($manager, $session);
$userSession->login('foo', 'bar');
}
+
+ public function testRememberLoginValidToken() {
+ $session = $this->getMock('\OC\Session\Memory', array(), array(''));
+ $session->expects($this->exactly(1))
+ ->method('set')
+ ->with($this->callback(function($key) {
+ switch($key) {
+ case 'user_id':
+ return true;
+ default:
+ return false;
+ }
+ },
+ 'foo'));
+
+ $managerMethods = get_class_methods('\OC\User\Manager');
+ //keep following methods intact in order to ensure hooks are
+ //working
+ $doNotMock = array('__construct', 'emit', 'listen');
+ foreach($doNotMock as $methodName) {
+ $i = array_search($methodName, $managerMethods, true);
+ if($i !== false) {
+ unset($managerMethods[$i]);
+ }
+ }
+ $manager = $this->getMock('\OC\User\Manager', $managerMethods, array());
+
+ $backend = $this->getMock('OC_User_Dummy');
+
+ $user = $this->getMock('\OC\User\User', array(), array('foo', $backend));
+
+ $user->expects($this->any())
+ ->method('getUID')
+ ->will($this->returnValue('foo'));
+ $user->expects($this->once())
+ ->method('updateLastLoginTimestamp');
+
+ $manager->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->will($this->returnValue($user));
+
+ //prepare login token
+ $token = 'goodToken';
+ \OC_Preferences::setValue('foo', 'login_token', $token, time());
+
+ $userSession = $this->getMock(
+ '\OC\User\Session',
+ //override, otherwise tests will fail because of setcookie()
+ array('setMagicInCookie'),
+ //there are passed as parameters to the constructor
+ array($manager, $session));
+
+ $granted = $userSession->loginWithCookie('foo', $token);
+
+ $this->assertSame($granted, true);
+ }
+
+ public function testRememberLoginInvalidToken() {
+ $session = $this->getMock('\OC\Session\Memory', array(), array(''));
+ $session->expects($this->never())
+ ->method('set');
+
+ $managerMethods = get_class_methods('\OC\User\Manager');
+ //keep following methods intact in order to ensure hooks are
+ //working
+ $doNotMock = array('__construct', 'emit', 'listen');
+ foreach($doNotMock as $methodName) {
+ $i = array_search($methodName, $managerMethods, true);
+ if($i !== false) {
+ unset($managerMethods[$i]);
+ }
+ }
+ $manager = $this->getMock('\OC\User\Manager', $managerMethods, array());
+
+ $backend = $this->getMock('OC_User_Dummy');
+
+ $user = $this->getMock('\OC\User\User', array(), array('foo', $backend));
+
+ $user->expects($this->any())
+ ->method('getUID')
+ ->will($this->returnValue('foo'));
+ $user->expects($this->never())
+ ->method('updateLastLoginTimestamp');
+
+ $manager->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->will($this->returnValue($user));
+
+ //prepare login token
+ $token = 'goodToken';
+ \OC_Preferences::setValue('foo', 'login_token', $token, time());
+
+ $userSession = new \OC\User\Session($manager, $session);
+ $granted = $userSession->loginWithCookie('foo', 'badToken');
+
+ $this->assertSame($granted, false);
+ }
+
+ public function testRememberLoginInvalidUser() {
+ $session = $this->getMock('\OC\Session\Memory', array(), array(''));
+ $session->expects($this->never())
+ ->method('set');
+
+ $managerMethods = get_class_methods('\OC\User\Manager');
+ //keep following methods intact in order to ensure hooks are
+ //working
+ $doNotMock = array('__construct', 'emit', 'listen');
+ foreach($doNotMock as $methodName) {
+ $i = array_search($methodName, $managerMethods, true);
+ if($i !== false) {
+ unset($managerMethods[$i]);
+ }
+ }
+ $manager = $this->getMock('\OC\User\Manager', $managerMethods, array());
+
+ $backend = $this->getMock('OC_User_Dummy');
+
+ $user = $this->getMock('\OC\User\User', array(), array('foo', $backend));
+
+ $user->expects($this->never())
+ ->method('getUID');
+ $user->expects($this->never())
+ ->method('updateLastLoginTimestamp');
+
+ $manager->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->will($this->returnValue(null));
+
+ //prepare login token
+ $token = 'goodToken';
+ \OC_Preferences::setValue('foo', 'login_token', $token, time());
+
+ $userSession = new \OC\User\Session($manager, $session);
+ $granted = $userSession->loginWithCookie('foo', $token);
+
+ $this->assertSame($granted, false);
+ }
}