Merge pull request #8681 from owncloud/logintimestamp

Record login timestamp per user. Required for new user managament.
This commit is contained in:
Lukas Reschke 2014-05-28 19:06:47 +02:00
commit ce9d5df6df
8 changed files with 314 additions and 19 deletions

View File

@ -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'));
}
}
}

View File

@ -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());

View File

@ -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;
}

View File

@ -235,6 +235,17 @@ class OC_User {
return self::getUserSession()->login($uid, $password);
}
/**
* 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).

View File

@ -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();
});
}
/**

View File

@ -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
@ -170,6 +172,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
*/

View File

@ -42,6 +42,11 @@ class User {
*/
private $home;
/**
* @var int $lastLogin
*/
private $lastLogin;
/**
* @var \OC\AllConfig $config
*/
@ -64,6 +69,7 @@ class User {
} else {
$this->enabled = true;
}
$this->lastLogin = \OC_Preferences::getValue($uid, 'login', 'lastLogin', 0);
}
/**
@ -107,6 +113,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
*

View File

@ -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);
}
}