diff options
author | Lukas Reschke <lukas@statuscode.ch> | 2014-05-28 19:06:47 +0200 |
---|---|---|
committer | Lukas Reschke <lukas@statuscode.ch> | 2014-05-28 19:06:47 +0200 |
commit | ce9d5df6df37e51587dcde638086dfe501892b56 (patch) | |
tree | 3a5e9a2cb22ede6c9c21f9847f05ec2ec7dd5826 | |
parent | 6d57e4c491aa8e09ff20f7ef9e2fb5618f75de48 (diff) | |
parent | 3a21c3e2f46fd01cbbab237d36a3e8abbbca3cca (diff) | |
download | nextcloud-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.php | 47 | ||||
-rw-r--r-- | core/register_command.php | 1 | ||||
-rw-r--r-- | lib/base.php | 24 | ||||
-rw-r--r-- | lib/private/user.php | 11 | ||||
-rw-r--r-- | lib/private/user/manager.php | 6 | ||||
-rw-r--r-- | lib/private/user/session.php | 37 | ||||
-rw-r--r-- | lib/private/user/user.php | 25 | ||||
-rw-r--r-- | tests/lib/user/session.php | 182 |
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); + } } |