diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 884eea8869..71478470ff 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -196,9 +196,10 @@ class LoginController extends Controller { * @param string $user * @param string $password * @param string $redirect_url + * @param boolean $remember_login * @return RedirectResponse */ - public function tryLogin($user, $password, $redirect_url) { + public function tryLogin($user, $password, $redirect_url, $remember_login = false) { $currentDelay = $this->throttler->getDelay($this->request->getRemoteAddress()); $this->throttler->sleepDelay($this->request->getRemoteAddress()); @@ -236,13 +237,13 @@ class LoginController extends Controller { // TODO: remove password checks from above and let the user session handle failures // requires https://github.com/owncloud/core/pull/24616 $this->userSession->login($user, $password); - $this->userSession->createSessionToken($this->request, $loginResult->getUID(), $user, $password); + $this->userSession->createSessionToken($this->request, $loginResult->getUID(), $user, $password, $remember_login); // User has successfully logged in, now remove the password reset link, when it is available $this->config->deleteUserValue($loginResult->getUID(), 'core', 'lostpassword'); if ($this->twoFactorManager->isTwoFactorAuthenticated($loginResult)) { - $this->twoFactorManager->prepareTwoFactorLogin($loginResult); + $this->twoFactorManager->prepareTwoFactorLogin($loginResult, $remember_login); $providers = $this->twoFactorManager->getProviders($loginResult); if (count($providers) === 1) { @@ -265,6 +266,10 @@ class LoginController extends Controller { return new RedirectResponse($this->urlGenerator->linkToRoute($url, $urlParams)); } + if ($remember_login) { + $this->userSession->createRememberMeToken($loginResult); + } + return $this->generateRedirect($redirect_url); } diff --git a/db_structure.xml b/db_structure.xml index 77f6d76898..09dbde710d 100644 --- a/db_structure.xml +++ b/db_structure.xml @@ -1125,6 +1125,15 @@ 2 + + remember + integer + 0 + true + true + 1 + + last_activity integer diff --git a/lib/base.php b/lib/base.php index e7bedb6959..28fc57161c 100644 --- a/lib/base.php +++ b/lib/base.php @@ -1035,6 +1035,12 @@ class OC { if ($userSession->tryTokenLogin($request)) { return true; } + if (isset($_COOKIE['nc_username']) + && isset($_COOKIE['nc_token']) + && isset($_COOKIE['nc_session_id']) + && $userSession->loginWithCookie($_COOKIE['nc_username'], $_COOKIE['nc_token'], $_COOKIE['nc_session_id'])) { + return true; + } if ($userSession->tryBasicAuthLogin($request, \OC::$server->getBruteForceThrottler())) { return true; } diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index a1e845f132..e1516c47ed 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -290,6 +290,7 @@ class DIContainer extends SimpleContainer implements IAppContainer { $this->registerService('OCP\\IUserSession', function($c) { return $this->getServer()->getUserSession(); }); + $this->registerAlias(\OC\User\Session::class, \OCP\IUserSession::class); $this->registerService('OCP\\ISession', function($c) { return $this->getServer()->getSession(); diff --git a/lib/private/Authentication/Token/DefaultToken.php b/lib/private/Authentication/Token/DefaultToken.php index 904df9baa2..faef2f73b3 100644 --- a/lib/private/Authentication/Token/DefaultToken.php +++ b/lib/private/Authentication/Token/DefaultToken.php @@ -35,6 +35,8 @@ use OCP\AppFramework\Db\Entity; * @method string getToken() * @method void setType(string $type) * @method int getType() + * @method void setRemember(int $remember) + * @method int getRemember() * @method void setLastActivity(int $lastActivity) * @method int getLastActivity() */ @@ -70,6 +72,11 @@ class DefaultToken extends Entity implements IToken { */ protected $type; + /** + * @var int + */ + protected $remember; + /** * @var int */ diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php index 0ce26197cc..752974ff24 100644 --- a/lib/private/Authentication/Token/DefaultTokenMapper.php +++ b/lib/private/Authentication/Token/DefaultTokenMapper.php @@ -40,24 +40,25 @@ class DefaultTokenMapper extends Mapper { * @param string $token */ public function invalidate($token) { + /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') - ->andWhere($qb->expr()->eq('token', $qb->createParameter('token'))) + ->where($qb->expr()->eq('token', $qb->createParameter('token'))) ->setParameter('token', $token) ->execute(); } /** * @param int $olderThan + * @param int $remember */ - public function invalidateOld($olderThan) { + public function invalidateOld($olderThan, $remember = IToken::DO_NOT_REMEMBER) { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') - ->where($qb->expr()->lt('last_activity', $qb->createParameter('last_activity'))) - ->andWhere($qb->expr()->eq('type', $qb->createParameter('type'))) - ->setParameter('last_activity', $olderThan, IQueryBuilder::PARAM_INT) - ->setParameter('type', IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT) + ->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -71,7 +72,7 @@ class DefaultTokenMapper extends Mapper { public function getToken($token) { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'type', 'token', 'last_activity', 'last_check') + $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'type', 'remember', 'token', 'last_activity', 'last_check') ->from('authtoken') ->where($qb->expr()->eq('token', $qb->createParameter('token'))) ->setParameter('token', $token) @@ -97,7 +98,7 @@ class DefaultTokenMapper extends Mapper { public function getTokenByUser(IUser $user) { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('id', 'uid', 'login_name', 'password', 'name', 'type', 'token', 'last_activity', 'last_check') + $qb->select('id', 'uid', 'login_name', 'password', 'name', 'type', 'remember', 'token', 'last_activity', 'last_check') ->from('authtoken') ->where($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))) ->setMaxResults(1000); diff --git a/lib/private/Authentication/Token/DefaultTokenProvider.php b/lib/private/Authentication/Token/DefaultTokenProvider.php index b0fbeb9b47..af1d600e4c 100644 --- a/lib/private/Authentication/Token/DefaultTokenProvider.php +++ b/lib/private/Authentication/Token/DefaultTokenProvider.php @@ -73,9 +73,10 @@ class DefaultTokenProvider implements IProvider { * @param string|null $password * @param string $name * @param int $type token type + * @param int $remember whether the session token should be used for remember-me * @return IToken */ - public function generateToken($token, $uid, $loginName, $password, $name, $type = IToken::TEMPORARY_TOKEN) { + public function generateToken($token, $uid, $loginName, $password, $name, $type = IToken::TEMPORARY_TOKEN, $remember = IToken::DO_NOT_REMEMBER) { $dbToken = new DefaultToken(); $dbToken->setUid($uid); $dbToken->setLoginName($loginName); @@ -85,6 +86,7 @@ class DefaultTokenProvider implements IProvider { $dbToken->setName($name); $dbToken->setToken($this->hashToken($token)); $dbToken->setType($type); + $dbToken->setRemember($remember); $dbToken->setLastActivity($this->time->getTime()); $this->mapper->insert($dbToken); @@ -151,6 +153,27 @@ class DefaultTokenProvider implements IProvider { } } + /** + * @param string $oldSessionId + * @param string $sessionId + */ + public function renewSessionToken($oldSessionId, $sessionId) { + $token = $this->getToken($oldSessionId); + + $newToken = new DefaultToken(); + $newToken->setUid($token->getUID()); + $newToken->setLoginName($token->getLoginName()); + if (!is_null($token->getPassword())) { + $password = $this->decryptPassword($token->getPassword(), $oldSessionId); + $newToken->setPassword($this->encryptPassword($password, $sessionId)); + } + $newToken->setName($token->getName()); + $newToken->setToken($this->hashToken($sessionId)); + $newToken->setType(IToken::TEMPORARY_TOKEN); + $newToken->setLastActivity($this->time->getTime()); + $this->mapper->insert($newToken); + } + /** * @param IToken $savedToken * @param string $tokenId session token @@ -207,8 +230,11 @@ class DefaultTokenProvider implements IProvider { */ public function invalidateOldTokens() { $olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24); - $this->logger->info('Invalidating tokens older than ' . date('c', $olderThan)); - $this->mapper->invalidateOld($olderThan); + $this->logger->info('Invalidating session tokens older than ' . date('c', $olderThan)); + $this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER); + $rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); + $this->logger->info('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold)); + $this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER); } /** diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php index 65b515960e..b8c15571df 100644 --- a/lib/private/Authentication/Token/IProvider.php +++ b/lib/private/Authentication/Token/IProvider.php @@ -28,6 +28,7 @@ use OCP\IUser; interface IProvider { + /** * Create and persist a new token * @@ -37,9 +38,10 @@ interface IProvider { * @param string|null $password * @param string $name * @param int $type token type + * @param int $remember whether the session token should be used for remember-me * @return IToken */ - public function generateToken($token, $uid, $loginName, $password, $name, $type = IToken::TEMPORARY_TOKEN); + public function generateToken($token, $uid, $loginName, $password, $name, $type = IToken::TEMPORARY_TOKEN, $remember = IToken::DO_NOT_REMEMBER); /** * Get a token by token id @@ -50,6 +52,12 @@ interface IProvider { */ public function getToken($tokenId) ; + /** + * @param string $oldSessionId + * @param string $sessionId + */ + public function renewSessionToken($oldSessionId, $sessionId); + /** * Invalidate (delete) the given session token * diff --git a/lib/private/Authentication/Token/IToken.php b/lib/private/Authentication/Token/IToken.php index e1e78ca369..14811dd320 100644 --- a/lib/private/Authentication/Token/IToken.php +++ b/lib/private/Authentication/Token/IToken.php @@ -28,6 +28,8 @@ interface IToken extends JsonSerializable { const TEMPORARY_TOKEN = 0; const PERMANENT_TOKEN = 1; + const DO_NOT_REMEMBER = 0; + const REMEMBER = 1; /** * Get the token ID diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php index 1bea7aa347..5f47b2cfaa 100644 --- a/lib/private/Authentication/TwoFactorAuth/Manager.php +++ b/lib/private/Authentication/TwoFactorAuth/Manager.php @@ -37,6 +37,7 @@ class Manager { const SESSION_UID_KEY = 'two_factor_auth_uid'; const BACKUP_CODES_APP_ID = 'twofactor_backupcodes'; const BACKUP_CODES_PROVIDER_ID = 'backup_codes'; + const REMEBER_LOGIN = 'two_factor_remember_login'; /** @var AppManager */ private $appManager; @@ -51,6 +52,7 @@ class Manager { * @param AppManager $appManager * @param ISession $session * @param IConfig $config + * @param Session $userSession */ public function __construct(AppManager $appManager, ISession $session, IConfig $config) { $this->appManager = $appManager; @@ -171,11 +173,16 @@ class Manager { return false; } - $result = $provider->verifyChallenge($user, $challenge); - if ($result) { + $passed = $provider->verifyChallenge($user, $challenge); + if ($passed) { + if ($this->session->get(self::REMEBER_LOGIN) === true) { + // TODO: resolve cyclic dependency and use DI + \OC::$server->getUserSession()->createRememberMeToken($user); + } $this->session->remove(self::SESSION_UID_KEY); + $this->session->remove(self::REMEBER_LOGIN); } - return $result; + return $passed; } /** @@ -202,12 +209,14 @@ class Manager { } /** - * Prepare the 2FA login (set session value) + * Prepare the 2FA login * * @param IUser $user + * @param boolean $rememberMe */ - public function prepareTwoFactorLogin(IUser $user) { + public function prepareTwoFactorLogin(IUser $user, $rememberMe) { $this->session->set(self::SESSION_UID_KEY, $user->getUID()); + $this->session->set(self::REMEBER_LOGIN, $rememberMe); } } diff --git a/lib/private/Server.php b/lib/private/Server.php index 21ec311401..7931e08074 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -283,7 +283,7 @@ class Server extends ServerContainer implements IServerContainer { return $userSession; }); - $this->registerService('\OC\Authentication\TwoFactorAuth\Manager', function (Server $c) { + $this->registerService(\OC\Authentication\TwoFactorAuth\Manager::class, function (Server $c) { return new \OC\Authentication\TwoFactorAuth\Manager($c->getAppManager(), $c->getSession(), $c->getConfig()); }); diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index a213ee48c2..68f17747a0 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -526,9 +526,10 @@ class Session implements IUserSession, Emitter { * @param string $uid user UID * @param string $loginName login name * @param string $password + * @param int $remember * @return boolean */ - public function createSessionToken(IRequest $request, $uid, $loginName, $password = null) { + public function createSessionToken(IRequest $request, $uid, $loginName, $password = null, $remember = IToken::DO_NOT_REMEMBER) { if (is_null($this->manager->get($uid))) { // User does not exist return false; @@ -537,7 +538,7 @@ class Session implements IUserSession, Emitter { try { $sessionId = $this->session->getId(); $pwd = $this->getPassword($password); - $this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name); + $this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name, IToken::TEMPORARY_TOKEN, IToken::REMEMBER); return true; } catch (SessionNotAvailableException $ex) { // This can happen with OCC, where a memory session is used @@ -680,9 +681,10 @@ class Session implements IUserSession, Emitter { * * @param string $uid the username * @param string $currentToken + * @param string $oldSessionId * @return bool */ - public function loginWithCookie($uid, $currentToken) { + public function loginWithCookie($uid, $currentToken, $oldSessionId) { $this->session->regenerateId(); $this->manager->emit('\OC\User', 'preRememberedLogin', array($uid)); $user = $this->manager->get($uid); @@ -692,15 +694,26 @@ class Session implements IUserSession, Emitter { } // get stored tokens - $tokens = OC::$server->getConfig()->getUserKeys($uid, 'login_token'); + $tokens = $this->config->getUserKeys($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::$server->getConfig()->deleteUserValue($uid, 'login_token', $currentToken); + $this->config->deleteUserValue($uid, 'login_token', $currentToken); $newToken = OC::$server->getSecureRandom()->generate(32); - OC::$server->getConfig()->setUserValue($uid, 'login_token', $newToken, time()); + $this->config->setUserValue($uid, 'login_token', $newToken, $this->timeFacory->getTime()); + + try { + $sessionId = $this->session->getId(); + $this->tokenProvider->renewSessionToken($oldSessionId, $sessionId); + } catch (SessionNotAvailableException $ex) { + return false; + } catch (InvalidTokenException $ex) { + \OC::$server->getLogger()->warning('Renewing session token failed', ['app' => 'core']); + return false; + } + $this->setMagicInCookie($user->getUID(), $newToken); //login @@ -709,6 +722,15 @@ class Session implements IUserSession, Emitter { return true; } + /** + * @param IUser $user + */ + public function createRememberMeToken(IUser $user) { + $token = OC::$server->getSecureRandom()->generate(32); + $this->config->setUserValue($user->getUID(), 'login_token', $token, time()); + $this->setMagicInCookie($user->getUID(), $token); + } + /** * logout the user from the session */ @@ -736,10 +758,19 @@ class Session implements IUserSession, Emitter { */ public function setMagicInCookie($username, $token) { $secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https'; - $expires = time() + OC::$server->getConfig()->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); - setcookie('oc_username', $username, $expires, OC::$WEBROOT, '', $secureCookie, true); - setcookie('oc_token', $token, $expires, OC::$WEBROOT, '', $secureCookie, true); - setcookie('oc_remember_login', '1', $expires, OC::$WEBROOT, '', $secureCookie, true); + $webRoot = \OC::$WEBROOT; + if ($webRoot === '') { + $webRoot = '/'; + } + + $expires = $this->timeFacory->getTime() + OC::$server->getConfig()->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); + setcookie('nc_username', $username, $expires, $webRoot, '', $secureCookie, true); + setcookie('nc_token', $token, $expires, $webRoot, '', $secureCookie, true); + try { + setcookie('nc_session_id', $this->session->getId(), $expires, $webRoot, '', $secureCookie, true); + } catch (SessionNotAvailableException $ex) { + // ignore + } } /** @@ -749,17 +780,17 @@ class Session implements IUserSession, Emitter { //TODO: DI for cookies and IRequest $secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https'; - unset($_COOKIE['oc_username']); //TODO: DI - unset($_COOKIE['oc_token']); - unset($_COOKIE['oc_remember_login']); - setcookie('oc_username', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true); - setcookie('oc_token', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true); - setcookie('oc_remember_login', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true); + unset($_COOKIE['nc_username']); //TODO: DI + unset($_COOKIE['nc_token']); + unset($_COOKIE['nc_session_id']); + setcookie('nc_username', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true); + setcookie('nc_token', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true); + setcookie('nc_session_id', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true); // old cookies might be stored under /webroot/ instead of /webroot // and Firefox doesn't like it! - setcookie('oc_username', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); - setcookie('oc_token', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); - setcookie('oc_remember_login', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); + setcookie('nc_username', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); + setcookie('nc_token', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); + setcookie('nc_session_id', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); } /** @@ -779,4 +810,5 @@ class Session implements IUserSession, Emitter { } } + } diff --git a/lib/private/legacy/user.php b/lib/private/legacy/user.php index af2382dbb8..ed0d14a1ab 100644 --- a/lib/private/legacy/user.php +++ b/lib/private/legacy/user.php @@ -155,10 +155,11 @@ class OC_User { * @deprecated use \OCP\IUserSession::loginWithCookie() * @param string $uid The username of the user to log in * @param string $token + * @param string $oldSessionId * @return bool */ - public static function loginWithCookie($uid, $token) { - return self::getUserSession()->loginWithCookie($uid, $token); + public static function loginWithCookie($uid, $token, $oldSessionId) { + return self::getUserSession()->loginWithCookie($uid, $token, $oldSessionId); } /** diff --git a/lib/public/IRequest.php b/lib/public/IRequest.php index 11242c481f..b36a934b0c 100644 --- a/lib/public/IRequest.php +++ b/lib/public/IRequest.php @@ -145,7 +145,7 @@ interface IRequest { * Shortcut for getting cookie variables * * @param string $key the key that will be taken from the $_COOKIE array - * @return string the value in the $_COOKIE element + * @return string|null the value in the $_COOKIE element * @since 6.0.0 */ public function getCookie($key); diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php index ff50ac98fb..d16b9b114f 100644 --- a/tests/Core/Controller/LoginControllerTest.php +++ b/tests/Core/Controller/LoginControllerTest.php @@ -322,6 +322,8 @@ class LoginControllerTest extends TestCase { $this->userSession->expects($this->never()) ->method('createSessionToken'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $this->config->expects($this->never()) ->method('deleteUserValue'); @@ -363,7 +365,7 @@ class LoginControllerTest extends TestCase { ->with($user, $password); $this->userSession->expects($this->once()) ->method('createSessionToken') - ->with($this->request, $user->getUID(), $user, $password); + ->with($this->request, $user->getUID(), $user, $password, false); $this->twoFactorManager->expects($this->once()) ->method('isTwoFactorAuthenticated') ->with($user) @@ -371,11 +373,63 @@ class LoginControllerTest extends TestCase { $this->config->expects($this->once()) ->method('deleteUserValue') ->with('uid', 'core', 'lostpassword'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new \OCP\AppFramework\Http\RedirectResponse($indexPageUrl); $this->assertEquals($expected, $this->loginController->tryLogin($user, $password, null)); } + public function testLoginWithValidCredentialsAndRememberMe() { + /** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->getMockBuilder('\OCP\IUser')->getMock(); + $user->expects($this->any()) + ->method('getUID') + ->will($this->returnValue('uid')); + $password = 'secret'; + $indexPageUrl = \OC_Util::getDefaultPageUrl(); + + $this->request + ->expects($this->exactly(2)) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->request + ->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(true); + $this->throttler + ->expects($this->once()) + ->method('sleepDelay') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(200); + $this->userManager->expects($this->once()) + ->method('checkPassword') + ->will($this->returnValue($user)); + $this->userSession->expects($this->once()) + ->method('login') + ->with($user, $password); + $this->userSession->expects($this->once()) + ->method('createSessionToken') + ->with($this->request, $user->getUID(), $user, $password, true); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->with($user) + ->will($this->returnValue(false)); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with('uid', 'core', 'lostpassword'); + $this->userSession->expects($this->once()) + ->method('createRememberMeToken') + ->with($user); + + $expected = new \OCP\AppFramework\Http\RedirectResponse($indexPageUrl); + $this->assertEquals($expected, $this->loginController->tryLogin($user, $password, null, true)); + } + public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn() { /** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */ $user = $this->getMockBuilder('\OCP\IUser')->getMock(); @@ -408,6 +462,8 @@ class LoginControllerTest extends TestCase { ->will($this->returnValue(false)); $this->config->expects($this->never()) ->method('deleteUserValue'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new \OCP\AppFramework\Http\RedirectResponse(\OC_Util::getDefaultPageUrl()); $this->assertEquals($expected, $this->loginController->tryLogin('Jane', $password, $originalUrl)); @@ -450,6 +506,8 @@ class LoginControllerTest extends TestCase { ->will($this->returnValue($redirectUrl)); $this->config->expects($this->never()) ->method('deleteUserValue'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new \OCP\AppFramework\Http\RedirectResponse($redirectUrl); $this->assertEquals($expected, $this->loginController->tryLogin('Jane', $password, $originalUrl)); @@ -488,7 +546,7 @@ class LoginControllerTest extends TestCase { ->will($this->returnValue($user)); $this->userSession->expects($this->once()) ->method('createSessionToken') - ->with($this->request, $user->getUID(), 'Jane', $password); + ->with($this->request, $user->getUID(), 'Jane', $password, false); $this->userSession->expects($this->once()) ->method('isLoggedIn') ->with() @@ -540,7 +598,7 @@ class LoginControllerTest extends TestCase { ->with('john@doe.com', $password); $this->userSession->expects($this->once()) ->method('createSessionToken') - ->with($this->request, $user->getUID(), 'john@doe.com', $password); + ->with($this->request, $user->getUID(), 'john@doe.com', $password, false); $this->twoFactorManager->expects($this->once()) ->method('isTwoFactorAuthenticated') ->with($user) @@ -564,6 +622,8 @@ class LoginControllerTest extends TestCase { $this->config->expects($this->once()) ->method('deleteUserValue') ->with('john', 'core', 'lostpassword'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new RedirectResponse($challengeUrl); $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', $password, null)); @@ -605,7 +665,7 @@ class LoginControllerTest extends TestCase { ->with('john@doe.com', $password); $this->userSession->expects($this->once()) ->method('createSessionToken') - ->with($this->request, $user->getUID(), 'john@doe.com', $password); + ->with($this->request, $user->getUID(), 'john@doe.com', $password, false); $this->twoFactorManager->expects($this->once()) ->method('isTwoFactorAuthenticated') ->with($user) @@ -628,6 +688,8 @@ class LoginControllerTest extends TestCase { $this->config->expects($this->once()) ->method('deleteUserValue') ->with('john', 'core', 'lostpassword'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new RedirectResponse($challengeUrl); $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', $password, null)); @@ -680,6 +742,8 @@ class LoginControllerTest extends TestCase { ->with('login', '192.168.0.1', ['user' => 'john@doe.com']); $this->config->expects($this->never()) ->method('deleteUserValue'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new RedirectResponse(''); $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', 'just wrong', null)); diff --git a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php index d71d946847..418a4d14f6 100644 --- a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php @@ -130,6 +130,7 @@ class DefaultTokenMapperTest extends TestCase { $token->setName('Firefox on Android'); $token->setToken('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'); $token->setType(IToken::TEMPORARY_TOKEN); + $token->setRemember(IToken::DO_NOT_REMEMBER); $token->setLastActivity($this->time - 60 * 60 * 24 * 3); $token->setLastCheck($this->time - 10); diff --git a/tests/lib/Authentication/Token/DefaultTokenProviderTest.php b/tests/lib/Authentication/Token/DefaultTokenProviderTest.php index 7f90cf051f..cd6bf7bad5 100644 --- a/tests/lib/Authentication/Token/DefaultTokenProviderTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenProviderTest.php @@ -25,7 +25,6 @@ namespace Test\Authentication\Token; use OC\Authentication\Token\DefaultToken; use OC\Authentication\Token\DefaultTokenProvider; use OC\Authentication\Token\IToken; -use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\ILogger; @@ -81,6 +80,7 @@ class DefaultTokenProviderTest extends TestCase { $toInsert->setName($name); $toInsert->setToken(hash('sha512', $token . '1f4h9s')); $toInsert->setType($type); + $toInsert->setRemember(IToken::DO_NOT_REMEMBER); $toInsert->setLastActivity($this->time); $this->config->expects($this->any()) @@ -95,7 +95,7 @@ class DefaultTokenProviderTest extends TestCase { ->method('insert') ->with($this->equalTo($toInsert)); - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type); + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); $this->assertEquals($toInsert, $actual); } @@ -245,13 +245,19 @@ class DefaultTokenProviderTest extends TestCase { public function testInvalidateOldTokens() { $defaultSessionLifetime = 60 * 60 * 24; - $this->config->expects($this->once()) + $defaultRememberMeLifetime = 60 * 60 * 24 * 15; + $this->config->expects($this->exactly(2)) ->method('getSystemValue') - ->with('session_lifetime', $defaultSessionLifetime) - ->will($this->returnValue(150)); - $this->mapper->expects($this->once()) + ->will($this->returnValueMap([ + ['session_lifetime', $defaultSessionLifetime, 150], + ['remember_login_cookie_lifetime', $defaultRememberMeLifetime, 300], + ])); + $this->mapper->expects($this->at(0)) ->method('invalidateOld') ->with($this->time - 150); + $this->mapper->expects($this->at(1)) + ->method('invalidateOld') + ->with($this->time - 300); $this->tokenProvider->invalidateOldTokens(); } diff --git a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php index 72b70d817d..52f3ca2850 100644 --- a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php +++ b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php @@ -233,8 +233,15 @@ class ManagerTest extends TestCase { ->with($this->user, $challenge) ->will($this->returnValue(true)); $this->session->expects($this->once()) + ->method('get') + ->with('two_factor_remember_login') + ->will($this->returnValue(false)); + $this->session->expects($this->at(1)) ->method('remove') ->with('two_factor_auth_uid'); + $this->session->expects($this->at(2)) + ->method('remove') + ->with('two_factor_remember_login'); $this->assertTrue($this->manager->verifyChallenge('email', $this->user, $challenge)); } @@ -304,11 +311,29 @@ class ManagerTest extends TestCase { ->method('getUID') ->will($this->returnValue('ferdinand')); - $this->session->expects($this->once()) + $this->session->expects($this->at(0)) ->method('set') ->with('two_factor_auth_uid', 'ferdinand'); + $this->session->expects($this->at(1)) + ->method('set') + ->with('two_factor_remember_login', true); - $this->manager->prepareTwoFactorLogin($this->user); + $this->manager->prepareTwoFactorLogin($this->user, true); + } + + public function testPrepareTwoFactorLoginDontRemember() { + $this->user->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('ferdinand')); + + $this->session->expects($this->at(0)) + ->method('set') + ->with('two_factor_auth_uid', 'ferdinand'); + $this->session->expects($this->at(1)) + ->method('set') + ->with('two_factor_remember_login', false); + + $this->manager->prepareTwoFactorLogin($this->user, false); } } diff --git a/tests/lib/User/SessionTest.php b/tests/lib/User/SessionTest.php index 268d8e10e5..c324870a60 100644 --- a/tests/lib/User/SessionTest.php +++ b/tests/lib/User/SessionTest.php @@ -52,6 +52,8 @@ class SessionTest extends \Test\TestCase { $this->tokenProvider = $this->createMock(IProvider::class); $this->config = $this->createMock(IConfig::class); $this->throttler = $this->createMock(Throttler::class); + + \OC_User::setIncognitoMode(false); } public function testGetUser() { @@ -100,7 +102,7 @@ class SessionTest extends \Test\TestCase { ->method('updateTokenActivity') ->with($token); - $manager->expects($this->any()) + $manager->expects($this->once()) ->method('get') ->with($expectedUser->getUID()) ->will($this->returnValue($expectedUser)); @@ -181,16 +183,9 @@ class SessionTest extends \Test\TestCase { }, 'foo')); $managerMethods = get_class_methods(Manager::class); - //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->getMockBuilder(Manager::class) + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class)->setMethods($mockedManagerMethods)->getMock(); ->setMethods($managerMethods) ->setConstructorArgs([$this->config]) ->getMock(); @@ -238,17 +233,10 @@ class SessionTest extends \Test\TestCase { ->with('bar') ->will($this->throwException(new \OC\Authentication\Exceptions\InvalidTokenException())); - $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->getMockBuilder(Manager::class) + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class)->setMethods($mockedManagerMethods)->getMock(); ->setMethods($managerMethods) ->setConstructorArgs([$this->config]) ->getMock(); @@ -273,17 +261,10 @@ class SessionTest extends \Test\TestCase { public function testLoginInvalidPassword() { $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); - $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->getMockBuilder(Manager::class) + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class)->setMethods($mockedManagerMethods)->getMock(); ->setMethods($managerMethods) ->setConstructorArgs([$this->config]) ->getMock(); @@ -513,156 +494,208 @@ class SessionTest extends \Test\TestCase { public function testRememberLoginValidToken() { $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); - $session->expects($this->exactly(1)) - ->method('set') - ->with($this->callback(function ($key) { - switch ($key) { - case 'user_id': - return true; - default: - return false; - } - }, 'foo')); - $session->expects($this->once()) - ->method('regenerateId'); + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class)->setMethods($mockedManagerMethods)->getMock(); + $userSession = $this->getMockBuilder(Session::class) + //override, otherwise tests will fail because of setcookie() + ->setMethods(['setMagicInCookie']) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) + ->getMock(); - $managerMethods = get_class_methods(Manager::class); - //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->getMockBuilder(Manager::class) + $user = $this->createMock(IUser::class); + $token = 'goodToken'; + $oldSessionId = 'sess321'; + $sessionId = 'sess123'; ->setMethods($managerMethods) ->setConstructorArgs([$this->config]) ->getMock(); - $backend = $this->createMock(\Test\Util\User\Dummy::class); - - $user = $this->getMockBuilder(User::class)->setConstructorArgs(['foo', $backend])->getMock(); - - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('foo')); - $user->expects($this->once()) - ->method('updateLastLoginTimestamp'); - + $session->expects($this->once()) + ->method('regenerateId'); $manager->expects($this->once()) ->method('get') ->with('foo') ->will($this->returnValue($user)); + $this->config->expects($this->once()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->will($this->returnValue([$token])); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with('foo', 'login_token', $token); + $this->config->expects($this->once()) + ->method('setUserValue'); // TODO: mock new random value - //prepare login token - $token = 'goodToken'; - \OC::$server->getConfig()->setUserValue('foo', 'login_token', $token, time()); + $session->expects($this->once()) + ->method('getId') + ->will($this->returnValue($sessionId)); + $this->tokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with($oldSessionId, $sessionId) + ->will($this->returnValue(true)); + $user->expects($this->any()) + ->method('getUID') + ->will($this->returnValue('foo')); + $userSession->expects($this->once()) + ->method('setMagicInCookie'); + $user->expects($this->once()) + ->method('updateLastLoginTimestamp'); + $session->expects($this->once()) + ->method('set') + ->with('user_id', 'foo'); + + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); + + $this->assertTrue($granted); + } + + public function testRememberLoginInvalidSessionToken() { + $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class)->setMethods($mockedManagerMethods)->getMock(); $userSession = $this->getMockBuilder(Session::class) //override, otherwise tests will fail because of setcookie() ->setMethods(['setMagicInCookie']) - //there are passed as parameters to the constructor ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) ->getMock(); - $granted = $userSession->loginWithCookie('foo', $token); + $user = $this->createMock(IUser::class); + $token = 'goodToken'; + $oldSessionId = 'sess321'; + $sessionId = 'sess123'; - $this->assertSame($granted, true); + $session->expects($this->once()) + ->method('regenerateId'); + $manager->expects($this->once()) + ->method('get') + ->with('foo') + ->will($this->returnValue($user)); + $this->config->expects($this->once()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->will($this->returnValue([$token])); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with('foo', 'login_token', $token); + $this->config->expects($this->once()) + ->method('setUserValue'); // TODO: mock new random value + + $session->expects($this->once()) + ->method('getId') + ->will($this->returnValue($sessionId)); + $this->tokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with($oldSessionId, $sessionId) + ->will($this->throwException(new \OC\Authentication\Exceptions\InvalidTokenException())); + ->setMethods($managerMethods) + ->setConstructorArgs([$this->config]) + ->getMock(); + + $user->expects($this->never()) + ->method('getUID') + ->will($this->returnValue('foo')); + $userSession->expects($this->never()) + ->method('setMagicInCookie'); + $user->expects($this->never()) + ->method('updateLastLoginTimestamp'); + $session->expects($this->never()) + ->method('set') + ->with('user_id', 'foo'); + + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); + + $this->assertFalse($granted); } public function testRememberLoginInvalidToken() { $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); - $session->expects($this->never()) - ->method('set'); - $session->expects($this->once()) - ->method('regenerateId'); - - $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->getMockBuilder(Manager::class) - ->setMethods($managerMethods) - ->setConstructorArgs([$this->config]) + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class)->setMethods($mockedManagerMethods)->getMock(); + $userSession = $this->getMockBuilder(Session::class) + //override, otherwise tests will fail because of setcookie() + ->setMethods(['setMagicInCookie']) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) ->getMock(); - $backend = $this->createMock(\Test\Util\User\Dummy::class); - - $user = $this->getMockBuilder(User::class)->setConstructorArgs(['foo', $backend])->getMock(); - - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('foo')); - $user->expects($this->never()) - ->method('updateLastLoginTimestamp'); + $user = $this->createMock(IUser::class); + $token = 'goodToken'; + $oldSessionId = 'sess321'; + $session->expects($this->once()) + ->method('regenerateId'); $manager->expects($this->once()) ->method('get') ->with('foo') ->will($this->returnValue($user)); + $this->config->expects($this->once()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->will($this->returnValue(['anothertoken'])); + $this->config->expects($this->never()) + ->method('deleteUserValue') + ->with('foo', 'login_token', $token); - //prepare login token - $token = 'goodToken'; - \OC::$server->getConfig()->setUserValue('foo', 'login_token', $token, time()); + $this->tokenProvider->expects($this->never()) + ->method('renewSessionToken'); + $userSession->expects($this->never()) + ->method('setMagicInCookie'); + $user->expects($this->never()) + ->method('updateLastLoginTimestamp'); + $session->expects($this->never()) + ->method('set') + ->with('user_id', 'foo'); - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); - $granted = $userSession->loginWithCookie('foo', 'badToken'); + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); - $this->assertSame($granted, false); + $this->assertFalse($granted); } public function testRememberLoginInvalidUser() { $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); - $session->expects($this->never()) - ->method('set'); + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class)->setMethods($mockedManagerMethods)->getMock(); + $userSession = $this->getMockBuilder(Session::class) + //override, otherwise tests will fail because of setcookie() + ->setMethods(['setMagicInCookie']) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) + ->getMock(); + $token = 'goodToken'; + $oldSessionId = 'sess321'; + $session->expects($this->once()) ->method('regenerateId'); - - $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->getMockBuilder(Manager::class) ->setMethods($managerMethods) ->setConstructorArgs([$this->config]) ->getMock(); - - $backend = $this->createMock(\Test\Util\User\Dummy::class); - - $user = $this->getMockBuilder(User::class)->setConstructorArgs(['foo', $backend])->getMock(); - - $user->expects($this->never()) - ->method('getUID'); - $user->expects($this->never()) - ->method('updateLastLoginTimestamp'); - $manager->expects($this->once()) ->method('get') ->with('foo') ->will($this->returnValue(null)); + $this->config->expects($this->never()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->will($this->returnValue(['anothertoken'])); - //prepare login token - $token = 'goodToken'; - \OC::$server->getConfig()->setUserValue('foo', 'login_token', $token, time()); + $this->tokenProvider->expects($this->never()) + ->method('renewSessionToken'); + $userSession->expects($this->never()) + ->method('setMagicInCookie'); + $session->expects($this->never()) + ->method('set') + ->with('user_id', 'foo'); - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); - $granted = $userSession->loginWithCookie('foo', $token); + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); - $this->assertSame($granted, false); + $this->assertFalse($granted); } public function testActiveUserAfterSetSession() { diff --git a/version.php b/version.php index 96725a6bb4..e6de2e2bde 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ // We only can count up. The 4. digit is only for the internal patchlevel to trigger DB upgrades // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = array(9, 2, 0, 4); +$OC_Version = array(9, 2, 0, 5); // The human readable string $OC_VersionString = '11.0 alpha';