diff options
author | Thomas Müller <DeepDiver1975@users.noreply.github.com> | 2016-05-11 13:52:45 +0200 |
---|---|---|
committer | Thomas Müller <DeepDiver1975@users.noreply.github.com> | 2016-05-11 13:52:45 +0200 |
commit | efa545f8f0697896a538587bee644fc7c3699185 (patch) | |
tree | 8a3d24a3af825ca93583df5e639b8865deacefaf | |
parent | f39e163d4a6ee63444bfb6a797e12a482bd0a49f (diff) | |
parent | 0486d750aade407739977911cc1aab40e65dc460 (diff) | |
download | nextcloud-server-efa545f8f0697896a538587bee644fc7c3699185.tar.gz nextcloud-server-efa545f8f0697896a538587bee644fc7c3699185.zip |
Merge pull request #24189 from owncloud/pluggable-auth
Pluggable auth
33 files changed, 2058 insertions, 426 deletions
diff --git a/apps/dav/lib/connector/sabre/auth.php b/apps/dav/lib/connector/sabre/auth.php index b63efa3a1ba..b8047e779f5 100644 --- a/apps/dav/lib/connector/sabre/auth.php +++ b/apps/dav/lib/connector/sabre/auth.php @@ -33,7 +33,7 @@ use Exception; use OC\AppFramework\Http\Request; use OCP\IRequest; use OCP\ISession; -use OCP\IUserSession; +use OC\User\Session; use Sabre\DAV\Auth\Backend\AbstractBasic; use Sabre\DAV\Exception\NotAuthenticated; use Sabre\DAV\Exception\ServiceUnavailable; @@ -45,7 +45,7 @@ class Auth extends AbstractBasic { /** @var ISession */ private $session; - /** @var IUserSession */ + /** @var Session */ private $userSession; /** @var IRequest */ private $request; @@ -54,12 +54,12 @@ class Auth extends AbstractBasic { /** * @param ISession $session - * @param IUserSession $userSession + * @param Session $userSession * @param IRequest $request * @param string $principalPrefix */ public function __construct(ISession $session, - IUserSession $userSession, + Session $userSession, IRequest $request, $principalPrefix = 'principals/users/') { $this->session = $session; @@ -104,6 +104,7 @@ class Auth extends AbstractBasic { } else { \OC_Util::setUpFS(); //login hooks may need early access to the filesystem if($this->userSession->login($username, $password)) { + $this->userSession->createSessionToken($this->request, $username, $password); \OC_Util::setUpFS($this->userSession->getUser()->getUID()); $this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID()); $this->session->close(); diff --git a/apps/dav/tests/unit/connector/sabre/auth.php b/apps/dav/tests/unit/connector/sabre/auth.php index b81a5e003b5..a0168e435e2 100644 --- a/apps/dav/tests/unit/connector/sabre/auth.php +++ b/apps/dav/tests/unit/connector/sabre/auth.php @@ -28,7 +28,7 @@ use OCP\IRequest; use OCP\IUser; use Test\TestCase; use OCP\ISession; -use OCP\IUserSession; +use OC\User\Session; /** * Class Auth @@ -41,7 +41,7 @@ class Auth extends TestCase { private $session; /** @var \OCA\DAV\Connector\Sabre\Auth */ private $auth; - /** @var IUserSession */ + /** @var Session */ private $userSession; /** @var IRequest */ private $request; @@ -50,7 +50,7 @@ class Auth extends TestCase { parent::setUp(); $this->session = $this->getMockBuilder('\OCP\ISession') ->disableOriginalConstructor()->getMock(); - $this->userSession = $this->getMockBuilder('\OCP\IUserSession') + $this->userSession = $this->getMockBuilder('\OC\User\Session') ->disableOriginalConstructor()->getMock(); $this->request = $this->getMockBuilder('\OCP\IRequest') ->disableOriginalConstructor()->getMock(); @@ -170,6 +170,10 @@ class Auth extends TestCase { ->method('login') ->with('MyTestUser', 'MyTestPassword') ->will($this->returnValue(true)); + $this->userSession + ->expects($this->once()) + ->method('createSessionToken') + ->with($this->request, 'MyTestUser', 'MyTestPassword'); $this->session ->expects($this->once()) ->method('set') @@ -559,6 +563,9 @@ class Auth extends TestCase { ->method('login') ->with('username', 'password') ->will($this->returnValue(true)); + $this->userSession + ->expects($this->once()) + ->method('createSessionToken'); $user = $this->getMockBuilder('\OCP\IUser') ->disableOriginalConstructor() ->getMock(); diff --git a/build/integration/features/auth.feature b/build/integration/features/auth.feature new file mode 100644 index 00000000000..43aa618bd00 --- /dev/null +++ b/build/integration/features/auth.feature @@ -0,0 +1,78 @@ +Feature: auth + + Background: + Given user "user0" exists + Given a new client token is used + + + # FILES APP + + Scenario: access files app anonymously + When requesting "/index.php/apps/files" with "GET" + Then the HTTP status code should be "401" + + Scenario: access files app with basic auth + When requesting "/index.php/apps/files" with "GET" using basic auth + Then the HTTP status code should be "200" + + Scenario: access files app with basic token auth + When requesting "/index.php/apps/files" with "GET" using basic token auth + Then the HTTP status code should be "200" + + Scenario: access files app with a client token + When requesting "/index.php/apps/files" with "GET" using a client token + Then the HTTP status code should be "200" + + Scenario: access files app with browser session + Given a new browser session is started + When requesting "/index.php/apps/files" with "GET" using browser session + Then the HTTP status code should be "200" + + + # WebDAV + + Scenario: using WebDAV anonymously + When requesting "/remote.php/webdav" with "PROPFIND" + Then the HTTP status code should be "401" + + Scenario: using WebDAV with basic auth + When requesting "/remote.php/webdav" with "PROPFIND" using basic auth + Then the HTTP status code should be "207" + + Scenario: using WebDAV with token auth + When requesting "/remote.php/webdav" with "PROPFIND" using basic token auth + Then the HTTP status code should be "207" + + # DAV token auth is not possible yet + #Scenario: using WebDAV with a client token + # When requesting "/remote.php/webdav" with "PROPFIND" using a client token + # Then the HTTP status code should be "207" + + Scenario: using WebDAV with browser session + Given a new browser session is started + When requesting "/remote.php/webdav" with "PROPFIND" using browser session + Then the HTTP status code should be "207" + + + # OCS + + Scenario: using OCS anonymously + When requesting "/ocs/v1.php/apps/files_sharing/api/v1/remote_shares" with "GET" + Then the OCS status code should be "997" + + Scenario: using OCS with basic auth + When requesting "/ocs/v1.php/apps/files_sharing/api/v1/remote_shares" with "GET" using basic auth + Then the OCS status code should be "100" + + Scenario: using OCS with token auth + When requesting "/ocs/v1.php/apps/files_sharing/api/v1/remote_shares" with "GET" using basic token auth + Then the OCS status code should be "100" + + Scenario: using OCS with client token + When requesting "/ocs/v1.php/apps/files_sharing/api/v1/remote_shares" with "GET" using a client token + Then the OCS status code should be "100" + + Scenario: using OCS with browser session + Given a new browser session is started + When requesting "/ocs/v1.php/apps/files_sharing/api/v1/remote_shares" with "GET" using browser session + Then the OCS status code should be "100"
\ No newline at end of file diff --git a/build/integration/features/bootstrap/Auth.php b/build/integration/features/bootstrap/Auth.php new file mode 100644 index 00000000000..5f36b199e06 --- /dev/null +++ b/build/integration/features/bootstrap/Auth.php @@ -0,0 +1,117 @@ +<?php + +use GuzzleHttp\Client; +use GuzzleHttp\Exception\ClientException; + +require __DIR__ . '/../../vendor/autoload.php'; + +trait Auth { + + private $clientToken; + + /** @BeforeScenario */ + public function tearUpScenario() { + $this->client = new Client(); + $this->responseXml = ''; + } + + /** + * @When requesting :url with :method + */ + public function requestingWith($url, $method) { + $this->sendRequest($url, $method); + } + + private function sendRequest($url, $method, $authHeader = null, $useCookies = false) { + $fullUrl = substr($this->baseUrl, 0, -5) . $url; + try { + if ($useCookies) { + $request = $this->client->createRequest($method, $fullUrl, [ + 'cookies' => $this->cookieJar, + ]); + } else { + $request = $this->client->createRequest($method, $fullUrl); + } + if ($authHeader) { + $request->setHeader('Authorization', $authHeader); + } + $request->setHeader('OCS_APIREQUEST', 'true'); + $request->setHeader('requesttoken', $this->requestToken); + $this->response = $this->client->send($request); + } catch (ClientException $ex) { + $this->response = $ex->getResponse(); + } + } + + /** + * @Given a new client token is used + */ + public function aNewClientTokenIsUsed() { + $client = new Client(); + $resp = $client->post(substr($this->baseUrl, 0, -5) . '/token/generate', [ + 'json' => [ + 'user' => 'user0', + 'password' => '123456', + ] + ]); + $this->clientToken = json_decode($resp->getBody()->getContents())->token; + } + + /** + * @When requesting :url with :method using basic auth + */ + public function requestingWithBasicAuth($url, $method) { + $this->sendRequest($url, $method, 'basic ' . base64_encode('user0:123456')); + } + + /** + * @When requesting :url with :method using basic token auth + */ + public function requestingWithBasicTokenAuth($url, $method) { + $this->sendRequest($url, $method, 'basic ' . base64_encode('user0:' . $this->clientToken)); + } + + /** + * @When requesting :url with :method using a client token + */ + public function requestingWithUsingAClientToken($url, $method) { + $this->sendRequest($url, $method, 'token ' . $this->clientToken); + } + + /** + * @When requesting :url with :method using browser session + */ + public function requestingWithBrowserSession($url, $method) { + $this->sendRequest($url, $method, null, true); + } + + /** + * @Given a new browser session is started + */ + public function aNewBrowserSessionIsStarted() { + $loginUrl = substr($this->baseUrl, 0, -5) . '/login'; + // Request a new session and extract CSRF token + $client = new Client(); + $response = $client->get( + $loginUrl, [ + 'cookies' => $this->cookieJar, + ] + ); + $this->extracRequestTokenFromResponse($response); + + // Login and extract new token + $client = new Client(); + $response = $client->post( + $loginUrl, [ + 'body' => [ + 'user' => 'user0', + 'password' => '123456', + 'requesttoken' => $this->requestToken, + ], + 'cookies' => $this->cookieJar, + ] + ); + $this->extracRequestTokenFromResponse($response); + } + +} diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index 31be33165e6..b8fb516fada 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -6,6 +6,9 @@ use GuzzleHttp\Message\ResponseInterface; require __DIR__ . '/../../vendor/autoload.php'; trait BasicStructure { + + use Auth; + /** @var string */ private $currentUser = ''; @@ -176,7 +179,7 @@ trait BasicStructure { * @param string $user */ public function loggingInUsingWebAs($user) { - $loginUrl = substr($this->baseUrl, 0, -5); + $loginUrl = substr($this->baseUrl, 0, -5) . '/login'; // Request a new session and extract CSRF token $client = new Client(); $response = $client->get( diff --git a/core/Application.php b/core/Application.php index 0a54386a2ce..a835dc7fbb2 100644 --- a/core/Application.php +++ b/core/Application.php @@ -1,6 +1,7 @@ <?php /** * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Christoph Wurst <christoph@owncloud.com> * @author Lukas Reschke <lukas@owncloud.com> * @author Morris Jobke <hey@morrisjobke.de> * @author Roeland Jago Douma <rullzer@owncloud.com> @@ -28,12 +29,14 @@ namespace OC\Core; use OC\AppFramework\Utility\SimpleContainer; use OC\AppFramework\Utility\TimeFactory; +use OC\Core\Controller\AvatarController; use OC\Core\Controller\LoginController; -use \OCP\AppFramework\App; use OC\Core\Controller\LostController; +use OC\Core\Controller\TokenController; use OC\Core\Controller\UserController; -use OC\Core\Controller\AvatarController; -use \OCP\Util; +use OC_Defaults; +use OCP\AppFramework\App; +use OCP\Util; /** * Class Application @@ -101,6 +104,15 @@ class Application extends App { $c->query('URLGenerator') ); }); + $container->registerService('TokenController', function(SimpleContainer $c) { + return new TokenController( + $c->query('AppName'), + $c->query('Request'), + $c->query('UserManager'), + $c->query('OC\Authentication\Token\DefaultTokenProvider'), + $c->query('SecureRandom') + ); + }); /** * Core class wrappers @@ -132,6 +144,9 @@ class Application extends App { $container->registerService('UserSession', function(SimpleContainer $c) { return $c->query('ServerContainer')->getUserSession(); }); + $container->registerService('Session', function(SimpleContainer $c) { + return $c->query('ServerContainer')->getSession(); + }); $container->registerService('Cache', function(SimpleContainer $c) { return $c->query('ServerContainer')->getCache(); }); @@ -139,7 +154,7 @@ class Application extends App { return $c->query('ServerContainer')->getUserFolder(); }); $container->registerService('Defaults', function() { - return new \OC_Defaults; + return new OC_Defaults; }); $container->registerService('Mailer', function(SimpleContainer $c) { return $c->query('ServerContainer')->getMailer(); diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 796706d364a..6985e2be87e 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -1,5 +1,7 @@ <?php + /** + * @author Christoph Wurst <christoph@owncloud.com> * @author Lukas Reschke <lukas@owncloud.com> * * @copyright Copyright (c) 2016, ownCloud, Inc. @@ -21,7 +23,10 @@ namespace OC\Core\Controller; -use OC\Setup; +use OC; +use OC\User\Session; +use OC_App; +use OC_Util; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; @@ -31,17 +36,21 @@ use OCP\ISession; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; -use OCP\IUserSession; class LoginController extends Controller { + /** @var IUserManager */ private $userManager; + /** @var IConfig */ private $config; + /** @var ISession */ private $session; - /** @var IUserSession */ + + /** @var Session */ private $userSession; + /** @var IURLGenerator */ private $urlGenerator; @@ -51,16 +60,11 @@ class LoginController extends Controller { * @param IUserManager $userManager * @param IConfig $config * @param ISession $session - * @param IUserSession $userSession + * @param Session $userSession * @param IURLGenerator $urlGenerator */ - function __construct($appName, - IRequest $request, - IUserManager $userManager, - IConfig $config, - ISession $session, - IUserSession $userSession, - IURLGenerator $urlGenerator) { + function __construct($appName, IRequest $request, IUserManager $userManager, IConfig $config, ISession $session, + Session $userSession, IURLGenerator $urlGenerator) { parent::__construct($appName, $request); $this->userManager = $userManager; $this->config = $config; @@ -94,20 +98,18 @@ class LoginController extends Controller { * @param string $redirect_url * @param string $remember_login * - * @return TemplateResponse + * @return TemplateResponse|RedirectResponse */ - public function showLoginForm($user, - $redirect_url, - $remember_login) { - if($this->userSession->isLoggedIn()) { - return new RedirectResponse(\OC_Util::getDefaultPageUrl()); + public function showLoginForm($user, $redirect_url, $remember_login) { + if ($this->userSession->isLoggedIn()) { + return new RedirectResponse(OC_Util::getDefaultPageUrl()); } $parameters = array(); $loginMessages = $this->session->get('loginMessages'); $errors = []; $messages = []; - if(is_array($loginMessages)) { + if (is_array($loginMessages)) { list($errors, $messages) = $loginMessages; } $this->session->remove('loginMessages'); @@ -137,8 +139,8 @@ class LoginController extends Controller { } } - $parameters['alt_login'] = \OC_App::getAlternativeLogIns(); - $parameters['rememberLoginAllowed'] = \OC_Util::rememberLoginAllowed(); + $parameters['alt_login'] = OC_App::getAlternativeLogIns(); + $parameters['rememberLoginAllowed'] = OC_Util::rememberLoginAllowed(); $parameters['rememberLoginState'] = !empty($remember_login) ? $remember_login : 0; if (!is_null($user) && $user !== '') { @@ -150,11 +152,49 @@ class LoginController extends Controller { } return new TemplateResponse( - $this->appName, - 'login', - $parameters, - 'guest' + $this->appName, 'login', $parameters, 'guest' ); } + /** + * @PublicPage + * @UseSession + * + * @param string $user + * @param string $password + * @param string $redirect_url + * @return RedirectResponse + */ + public function tryLogin($user, $password, $redirect_url) { + // TODO: Add all the insane error handling + $loginResult = $this->userManager->checkPassword($user, $password); + if ($loginResult === false) { + $users = $this->userManager->getByEmail($user); + // we only allow login by email if unique + if (count($users) === 1) { + $user = $users[0]->getUID(); + $loginResult = $this->userManager->checkPassword($user, $password); + } + } + if ($loginResult === false) { + $this->session->set('loginMessages', [ + [], + ['invalidpassword'] + ]); + // Read current user and append if possible + $args = !is_null($user) ? ['user' => $user] : []; + return new RedirectResponse($this->urlGenerator->linkToRoute('core.login.showLoginForm', $args)); + } + $this->userSession->createSessionToken($this->request, $loginResult->getUID(), $password); + if (!is_null($redirect_url) && $this->userSession->isLoggedIn()) { + $location = $this->urlGenerator->getAbsoluteURL(urldecode($redirect_url)); + // Deny the redirect if the URL contains a @ + // This prevents unvalidated redirects like ?redirect_url=:user@domain.com + if (strpos($location, '@') === false) { + return new RedirectResponse($location); + } + } + return new RedirectResponse($this->urlGenerator->linkTo('files', 'index')); + } + } diff --git a/core/Controller/TokenController.php b/core/Controller/TokenController.php new file mode 100644 index 00000000000..6606a3c8345 --- /dev/null +++ b/core/Controller/TokenController.php @@ -0,0 +1,90 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Core\Controller; + +use OC\AppFramework\Http; +use OC\Authentication\Token\DefaultTokenProvider; +use OC\Authentication\Token\IToken; +use OC\User\Manager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; +use OCP\IRequest; +use OCP\Security\ISecureRandom; + +class TokenController extends Controller { + + /** @var Manager */ + private $userManager; + + /** @var DefaultTokenProvider */ + private $tokenProvider; + + /** @var ISecureRandom */ + private $secureRandom; + + /** + * @param string $appName + * @param IRequest $request + * @param Manager $userManager + * @param DefaultTokenProvider $tokenProvider + * @param ISecureRandom $secureRandom + */ + public function __construct($appName, IRequest $request, Manager $userManager, DefaultTokenProvider $tokenProvider, + ISecureRandom $secureRandom) { + parent::__construct($appName, $request); + $this->userManager = $userManager; + $this->tokenProvider = $tokenProvider; + $this->secureRandom = $secureRandom; + } + + /** + * Generate a new access token clients can authenticate with + * + * @PublicPage + * @NoCSRFRequired + * + * @param string $user + * @param string $password + * @param string $name the name of the client + * @return JSONResponse + */ + public function generateToken($user, $password, $name = 'unknown client') { + if (is_null($user) || is_null($password)) { + $response = new Response(); + $response->setStatus(Http::STATUS_UNPROCESSABLE_ENTITY); + return $response; + } + if ($this->userManager->checkPassword($user, $password) === false) { + $response = new Response(); + $response->setStatus(Http::STATUS_UNAUTHORIZED); + return $response; + } + $token = $this->secureRandom->generate(128); + $this->tokenProvider->generateToken($token, $user, $password, $name, IToken::PERMANENT_TOKEN); + return [ + 'token' => $token, + ]; + } + +} diff --git a/core/routes.php b/core/routes.php index a9c800af4e8..70909352000 100644 --- a/core/routes.php +++ b/core/routes.php @@ -42,9 +42,11 @@ $application->registerRoutes($this, [ ['name' => 'avatar#postCroppedAvatar', 'url' => '/avatar/cropped', 'verb' => 'POST'], ['name' => 'avatar#getTmpAvatar', 'url' => '/avatar/tmp', 'verb' => 'GET'], ['name' => 'avatar#postAvatar', 'url' => '/avatar/', 'verb' => 'POST'], + ['name' => 'login#tryLogin', 'url' => '/login', 'verb' => 'POST'], ['name' => 'login#showLoginForm', 'url' => '/login', 'verb' => 'GET'], ['name' => 'login#logout', 'url' => '/logout', 'verb' => 'GET'], - ] + ['name' => 'token#generateToken', 'url' => '/token/generate', 'verb' => 'POST'], + ], ]); // Post installation check diff --git a/core/templates/login.php b/core/templates/login.php index 86c186928cc..45814fc71d9 100644 --- a/core/templates/login.php +++ b/core/templates/login.php @@ -9,7 +9,7 @@ script('core', [ ?> <!--[if IE 8]><style>input[type="checkbox"]{padding:0;}</style><![endif]--> -<form method="post" name="login" action="<?php p(OC::$WEBROOT) ?>/"> +<form method="post" name="login"> <fieldset> <?php if (!empty($_['redirect_url'])) { print_unescaped('<input type="hidden" name="redirect_url" value="' . \OCP\Util::sanitizeHTML($_['redirect_url']) . '">'); diff --git a/db_structure.xml b/db_structure.xml index 99541a4f901..7b4a3b53294 100644 --- a/db_structure.xml +++ b/db_structure.xml @@ -1032,6 +1032,92 @@ </table> <table> + <name>*dbprefix*authtoken</name> + + <declaration> + + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <!-- Foreign Key users::uid --> + <field> + <name>uid</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>64</length> + </field> + + <field> + <name>password</name> + <type>clob</type> + <default></default> + <notnull>true</notnull> + <length>4000</length> + </field> + + <field> + <name>name</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>100</length> + </field> + + <field> + <name>token</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>200</length> + </field> + + <field> + <name>type</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + <length>2</length> + </field> + + <field> + <name>last_activity</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <index> + <name>authtoken_token_index</name> + <unique>true</unique> + <field> + <name>token</name> + <sorting>ascending</sorting> + </field> + </index> + + <index> + <name>authtoken_last_activity_index</name> + <field> + <name>last_activity</name> + <sorting>ascending</sorting> + </field> + </index> + + </declaration> + </table> + + <table> <!-- List of tags (category) + a unique tag id (id) per user (uid) and type. diff --git a/lib/base.php b/lib/base.php index 0ea8a117e98..16ce0973a95 100644 --- a/lib/base.php +++ b/lib/base.php @@ -856,7 +856,7 @@ class OC { } else { // For guests: Load only filesystem and logging OC_App::loadApps(array('filesystem', 'logging')); - \OC_User::tryBasicAuthLogin(); + self::handleLogin($request); } } @@ -878,17 +878,6 @@ class OC { } } - // Handle redirect URL for logged in users - if (isset($_REQUEST['redirect_url']) && OC_User::isLoggedIn()) { - $location = \OC::$server->getURLGenerator()->getAbsoluteURL(urldecode($_REQUEST['redirect_url'])); - - // Deny the redirect if the URL contains a @ - // This prevents unvalidated redirects like ?redirect_url=:user@domain.com - if (strpos($location, '@') === false) { - header('Location: ' . $location); - return; - } - } // Handle WebDAV if ($_SERVER['REQUEST_METHOD'] == 'PROPFIND') { // not allowed any more to prevent people @@ -904,12 +893,33 @@ class OC { OC_App::loadApps(); OC_User::setupBackends(); OC_Util::setupFS(); + // FIXME // Redirect to default application OC_Util::redirectToDefaultPage(); } else { // Not handled and not logged in - self::handleLogin(); + header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute('core.login.showLoginForm')); + } + } + + /** + * Check login: apache auth, auth token, basic auth + * + * @param OCP\IRequest $request + * @return boolean + */ + private static function handleLogin(OCP\IRequest $request) { + $userSession = self::$server->getUserSession(); + if (OC_User::handleApacheAuth()) { + return true; } + if ($userSession->tryTokenLogin($request)) { + return true; + } + if ($userSession->tryBasicAuthLogin($request)) { + return true; + } + return false; } protected static function handleAuthHeaders() { @@ -932,163 +942,6 @@ class OC { } } } - - protected static function handleLogin() { - OC_App::loadApps(array('prelogin')); - $error = array(); - $messages = []; - - try { - // auth possible via apache module? - if (OC::tryApacheAuth()) { - $error[] = 'apacheauthfailed'; - } // remember was checked after last login - elseif (OC::tryRememberLogin()) { - $error[] = 'invalidcookie'; - } // logon via web form - elseif (OC::tryFormLogin()) { - $error[] = 'invalidpassword'; - } - } catch (\OC\User\LoginException $e) { - $messages[] = $e->getMessage(); - } catch (\Exception $ex) { - \OCP\Util::logException('handleLogin', $ex); - // do not disclose information. show generic error - $error[] = 'internalexception'; - } - - if(!\OC::$server->getUserSession()->isLoggedIn()) { - $loginMessages = array(array_unique($error), $messages); - \OC::$server->getSession()->set('loginMessages', $loginMessages); - // Read current user and append if possible - $args = []; - if(isset($_POST['user'])) { - $args['user'] = $_POST['user']; - } - - $redirectionTarget = \OC::$server->getURLGenerator()->linkToRoute('core.login.showLoginForm', $args); - header('Location: ' . $redirectionTarget); - } - } - - /** - * Remove outdated and therefore invalid tokens for a user - * @param string $user - */ - protected static function cleanupLoginTokens($user) { - $config = \OC::$server->getConfig(); - $cutoff = time() - $config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); - $tokens = $config->getUserKeys($user, 'login_token'); - foreach ($tokens as $token) { - $time = $config->getUserValue($user, 'login_token', $token); - if ($time < $cutoff) { - $config->deleteUserValue($user, 'login_token', $token); - } - } - } - - /** - * Try to login a user via HTTP authentication - * @return bool|void - */ - protected static function tryApacheAuth() { - $return = OC_User::handleApacheAuth(); - - // if return is true we are logged in -> redirect to the default page - if ($return === true) { - $_REQUEST['redirect_url'] = \OC::$server->getRequest()->getRequestUri(); - OC_Util::redirectToDefaultPage(); - exit; - } - - // in case $return is null apache based auth is not enabled - return is_null($return) ? false : true; - } - - /** - * Try to login a user using the remember me cookie. - * @return bool Whether the provided cookie was valid - */ - protected static function tryRememberLogin() { - if (!isset($_COOKIE["oc_remember_login"]) - || !isset($_COOKIE["oc_token"]) - || !isset($_COOKIE["oc_username"]) - || !$_COOKIE["oc_remember_login"] - || !OC_Util::rememberLoginAllowed() - ) { - return false; - } - - if (\OC::$server->getConfig()->getSystemValue('debug', false)) { - \OCP\Util::writeLog('core', 'Trying to login from cookie', \OCP\Util::DEBUG); - } - - if(OC_User::userExists($_COOKIE['oc_username'])) { - self::cleanupLoginTokens($_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 - } - \OCP\Util::writeLog('core', 'Authentication cookie rejected for user ' . - $_COOKIE['oc_username'], \OCP\Util::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_User::unsetMagicInCookie(); - return true; - } - - /** - * Tries to login a user using the form based authentication - * @return bool|void - */ - protected static function tryFormLogin() { - if (!isset($_POST["user"]) || !isset($_POST['password'])) { - return false; - } - - if(!(\OC::$server->getRequest()->passesCSRFCheck())) { - return false; - } - OC_App::loadApps(); - - //setup extra user backends - OC_User::setupBackends(); - - if (OC_User::login((string)$_POST["user"], (string)$_POST["password"])) { - $userId = OC_User::getUser(); - - // setting up the time zone - if (isset($_POST['timezone-offset'])) { - self::$server->getSession()->set('timezone', (string)$_POST['timezone-offset']); - self::$server->getConfig()->setUserValue($userId, 'core', 'timezone', (string)$_POST['timezone']); - } - - self::cleanupLoginTokens($userId); - if (!empty($_POST["remember_login"])) { - $config = self::$server->getConfig(); - if ($config->getSystemValue('debug', false)) { - self::$server->getLogger()->debug('Setting remember login to cookie', array('app' => 'core')); - } - $token = \OC::$server->getSecureRandom()->generate(32); - $config->setUserValue($userId, 'login_token', $token, time()); - OC_User::setMagicInCookie($userId, $token); - } else { - OC_User::unsetMagicInCookie(); - } - OC_Util::redirectToDefaultPage(); - exit(); - } - return true; - } - } - OC::init(); diff --git a/lib/private/Authentication/Exceptions/InvalidTokenException.php b/lib/private/Authentication/Exceptions/InvalidTokenException.php new file mode 100644 index 00000000000..3e52d3b78f0 --- /dev/null +++ b/lib/private/Authentication/Exceptions/InvalidTokenException.php @@ -0,0 +1,29 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Authentication\Exceptions; + +use Exception; + +class InvalidTokenException extends Exception { + +} diff --git a/lib/private/Authentication/Token/DefaultToken.php b/lib/private/Authentication/Token/DefaultToken.php new file mode 100644 index 00000000000..25caf675a43 --- /dev/null +++ b/lib/private/Authentication/Token/DefaultToken.php @@ -0,0 +1,81 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Authentication\Token; + +use OCP\AppFramework\Db\Entity; + +/** + * @method void setId(int $id) + * @method void setUid(string $uid); + * @method void setPassword(string $password) + * @method string getPassword() + * @method void setName(string $name) + * @method string getName() + * @method void setToken(string $token) + * @method string getToken() + * @method void setType(string $type) + * @method int getType() + * @method void setLastActivity(int $lastActivity) + * @method int getLastActivity() + */ +class DefaultToken extends Entity implements IToken { + + /** + * @var string user UID + */ + protected $uid; + + /** + * @var string encrypted user password + */ + protected $password; + + /** + * @var string token name (e.g. browser/OS) + */ + protected $name; + + /** + * @var string + */ + protected $token; + + /** + * @var int + */ + protected $type; + + /** + * @var int + */ + protected $lastActivity; + + public function getId() { + return $this->id; + } + + public function getUID() { + return $this->uid; + } + +} diff --git a/lib/private/Authentication/Token/DefaultTokenCleanupJob.php b/lib/private/Authentication/Token/DefaultTokenCleanupJob.php new file mode 100644 index 00000000000..4d1290eb623 --- /dev/null +++ b/lib/private/Authentication/Token/DefaultTokenCleanupJob.php @@ -0,0 +1,36 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Authentication\Token; + +use OC; +use OC\BackgroundJob\Job; + +class DefaultTokenCleanupJob extends Job { + + protected function run($argument) { + /* @var $provider DefaultTokenProvider */ + $provider = OC::$server->query('OC\Authentication\Token\DefaultTokenProvider'); + $provider->invalidateOldTokens(); + } + +} diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php new file mode 100644 index 00000000000..18adbe48d78 --- /dev/null +++ b/lib/private/Authentication/Token/DefaultTokenMapper.php @@ -0,0 +1,86 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Authentication\Token; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\Mapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class DefaultTokenMapper extends Mapper { + + public function __construct(IDBConnection $db) { + parent::__construct($db, 'authtoken'); + } + + /** + * Invalidate (delete) a given token + * + * @param string $token + */ + public function invalidate($token) { + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->andWhere($qb->expr()->eq('token', $qb->createParameter('token'))) + ->setParameter('token', $token) + ->execute(); + } + + /** + * @param int $olderThan + */ + public function invalidateOld($olderThan) { + /* @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) + ->execute(); + } + + /** + * Get the user UID for the given token + * + * @param string $token + * @throws DoesNotExistException + * @return DefaultToken + */ + public function getToken($token) { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $result = $qb->select('id', 'uid', 'password', 'name', 'type', 'token', 'last_activity') + ->from('authtoken') + ->where($qb->expr()->eq('token', $qb->createParameter('token'))) + ->setParameter('token', $token) + ->execute(); + + $data = $result->fetch(); + if ($data === false) { + throw new DoesNotExistException('token does not exist'); + } + return DefaultToken::fromRow($data); + } + +} diff --git a/lib/private/Authentication/Token/DefaultTokenProvider.php b/lib/private/Authentication/Token/DefaultTokenProvider.php new file mode 100644 index 00000000000..a6641277cf9 --- /dev/null +++ b/lib/private/Authentication/Token/DefaultTokenProvider.php @@ -0,0 +1,205 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Authentication\Token; + +use Exception; +use OC\Authentication\Exceptions\InvalidTokenException; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\ILogger; +use OCP\Security\ICrypto; + +class DefaultTokenProvider implements IProvider { + + /** @var DefaultTokenMapper */ + private $mapper; + + /** @var ICrypto */ + private $crypto; + + /** @var IConfig */ + private $config; + + /** @var ILogger $logger */ + private $logger; + + /** @var ITimeFactory $time */ + private $time; + + /** + * @param DefaultTokenMapper $mapper + * @param ICrypto $crypto + * @param IConfig $config + * @param ILogger $logger + * @param ITimeFactory $time + */ + public function __construct(DefaultTokenMapper $mapper, ICrypto $crypto, IConfig $config, ILogger $logger, ITimeFactory $time) { + $this->mapper = $mapper; + $this->crypto = $crypto; + $this->config = $config; + $this->logger = $logger; + $this->time = $time; + } + + /** + * Create and persist a new token + * + * @param string $token + * @param string $uid + * @param string $password + * @param string $name + * @param int $type token type + * @return DefaultToken + */ + public function generateToken($token, $uid, $password, $name, $type = IToken::TEMPORARY_TOKEN) { + $dbToken = new DefaultToken(); + $dbToken->setUid($uid); + $dbToken->setPassword($this->encryptPassword($password, $token)); + $dbToken->setName($name); + $dbToken->setToken($this->hashToken($token)); + $dbToken->setType($type); + $dbToken->setLastActivity($this->time->getTime()); + + $this->mapper->insert($dbToken); + + return $dbToken; + } + + /** + * Update token activity timestamp + * + * @throws InvalidTokenException + * @param IToken $token + */ + public function updateToken(IToken $token) { + if (!($token instanceof DefaultToken)) { + throw new InvalidTokenException(); + } + /** @var DefaultToken $token */ + $token->setLastActivity($this->time->getTime()); + + $this->mapper->update($token); + } + + /** + * @param string $token + * @throws InvalidTokenException + * @return DefaultToken + */ + 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 + * @return string + */ + public function getPassword(DefaultToken $savedToken, $token) { + return $this->decryptPassword($savedToken->getPassword(), $token); + } + + /** + * Invalidate (delete) the given session token + * + * @param string $token + */ + public function invalidateToken($token) { + $this->mapper->invalidate($this->hashToken($token)); + } + + /** + * Invalidate (delete) old session tokens + */ + 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); + } + + /** + * @param string $token + * @throws InvalidTokenException + * @return DefaultToken user UID + */ + public function validateToken($token) { + $this->logger->debug('validating default token <' . $token . '>'); + try { + $dbToken = $this->mapper->getToken($this->hashToken($token)); + $this->logger->debug('valid token for ' . $dbToken->getUID()); + return $dbToken; + } catch (DoesNotExistException $ex) { + $this->logger->warning('invalid token'); + throw new InvalidTokenException(); + } + } + + /** + * @param string $token + * @return string + */ + private function hashToken($token) { + $secret = $this->config->getSystemValue('secret'); + return hash('sha512', $token . $secret); + } + + /** + * 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'); + try { + return $this->crypto->decrypt($password, $token . $secret); + } catch (Exception $ex) { + // Delete the invalid token + $this->invalidateToken($token); + throw new InvalidTokenException(); + } + } + +} diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php new file mode 100644 index 00000000000..f8a3262ca8b --- /dev/null +++ b/lib/private/Authentication/Token/IProvider.php @@ -0,0 +1,42 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Authentication\Token; + +use OC\Authentication\Exceptions\InvalidTokenException; + +interface IProvider { + + /** + * @param string $token + * @throws InvalidTokenException + * @return IToken + */ + public function validateToken($token); + + /** + * Update token activity timestamp + * + * @param IToken $token + */ + public function updateToken(IToken $token); +} diff --git a/lib/private/Authentication/Token/IToken.php b/lib/private/Authentication/Token/IToken.php new file mode 100644 index 00000000000..9b2bd18f83b --- /dev/null +++ b/lib/private/Authentication/Token/IToken.php @@ -0,0 +1,46 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Authentication\Token; + +/** + * @since 9.1.0 + */ +interface IToken { + + const TEMPORARY_TOKEN = 0; + const PERMANENT_TOKEN = 1; + + /** + * Get the token ID + * + * @return string + */ + public function getId(); + + /** + * Get the user UID + * + * @return string + */ + public function getUID(); +} diff --git a/lib/private/Server.php b/lib/private/Server.php index bbe6b88876f..cbab1f09ebd 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -5,6 +5,7 @@ * @author Bernhard Posselt <dev@bernhard-posselt.com> * @author Bernhard Reiter <ockham@raz.or.at> * @author Björn Schießle <schiessle@owncloud.com> + * @author Christoph Wurst <christoph@owncloud.com> * @author Christopher Schäpers <kondou@ts.unde.re> * @author Joas Schilling <nickvergessen@owncloud.com> * @author Jörn Friedrich Dreyer <jfd@butonic.de> @@ -208,12 +209,35 @@ class Server extends ServerContainer implements IServerContainer { }); return $groupManager; }); + $this->registerService('OC\Authentication\Token\DefaultTokenMapper', function (Server $c) { + $dbConnection = $c->getDatabaseConnection(); + return new Authentication\Token\DefaultTokenMapper($dbConnection); + }); + $this->registerService('OC\Authentication\Token\DefaultTokenProvider', function (Server $c) { + $mapper = $c->query('OC\Authentication\Token\DefaultTokenMapper'); + $crypto = $c->getCrypto(); + $config = $c->getConfig(); + $logger = $c->getLogger(); + $timeFactory = new TimeFactory(); + return new \OC\Authentication\Token\DefaultTokenProvider($mapper, $crypto, $config, $logger, $timeFactory); + }); $this->registerService('UserSession', function (Server $c) { $manager = $c->getUserManager(); - $session = new \OC\Session\Memory(''); - - $userSession = new \OC\User\Session($manager, $session); + $timeFactory = new TimeFactory(); + // Token providers might require a working database. This code + // might however be called when ownCloud is not yet setup. + if (\OC::$server->getSystemConfig()->getValue('installed', false)) { + $defaultTokenProvider = $c->query('OC\Authentication\Token\DefaultTokenProvider'); + $tokenProviders = [ + $defaultTokenProvider, + ]; + } else { + $defaultTokenProvider = null; + $tokenProviders = []; + } + + $userSession = new \OC\User\Session($manager, $session, $timeFactory, $defaultTokenProvider, $tokenProviders); $userSession->listen('\OC\User', 'preCreateUser', function ($uid, $password) { \OC_Hook::emit('OC_User', 'pre_createUser', array('run' => true, 'uid' => $uid, 'password' => $password)); }); diff --git a/lib/private/Setup.php b/lib/private/Setup.php index 23c66f98b7c..67d714188ac 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -364,7 +364,14 @@ class Setup { $group =\OC::$server->getGroupManager()->createGroup('admin'); $group->addUser($user); - \OC_User::login($username, $password); + + // Create a session token for the newly created user + // The token provider requires a working db, so it's not injected on setup + /* @var $userSession User\Session */ + $userSession = \OC::$server->getUserSession(); + $defaultTokenProvider = \OC::$server->query('OC\Authentication\Token\DefaultTokenProvider'); + $userSession->setTokenProvider($defaultTokenProvider); + $userSession->createSessionToken($request, $username, $password); //guess what this does Installer::installShippedApps(); @@ -382,6 +389,8 @@ class Setup { $config->setSystemValue('logtimezone', date_default_timezone_get()); } + self::installBackgroundJobs(); + //and we are done $config->setSystemValue('installed', true); } @@ -389,6 +398,10 @@ class Setup { return $error; } + public static function installBackgroundJobs() { + \OC::$server->getJobList()->add('\OC\Authentication\Token\DefaultTokenCleanupJob'); + } + /** * @return string Absolute path to htaccess */ diff --git a/lib/private/Updater.php b/lib/private/Updater.php index 7ca3cd09362..dbcaccaad26 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -216,6 +216,8 @@ class Updater extends BasicEmitter { try { Setup::updateHtaccess(); Setup::protectDataDirectory(); + // TODO: replace with the new repair step mechanism https://github.com/owncloud/core/pull/24378 + Setup::installBackgroundJobs(); } catch (\Exception $e) { throw new \Exception($e->getMessage()); } diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index c7f8a6920de..c9f42d7e414 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -1,7 +1,9 @@ <?php + /** * @author Arthur Schiwon <blizzz@owncloud.com> * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Christoph Wurst <christoph@owncloud.com> * @author Joas Schilling <nickvergessen@owncloud.com> * @author Jörn Friedrich Dreyer <jfd@butonic.de> * @author Lukas Reschke <lukas@owncloud.com> @@ -31,10 +33,22 @@ namespace OC\User; +use OC; +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Token\DefaultTokenProvider; +use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IToken; use OC\Hooks\Emitter; +use OC_User; +use OC_Util; +use OCA\DAV\Connector\Sabre\Auth; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IRequest; use OCP\ISession; +use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; +use OCP\Session\Exceptions\SessionNotAvailableException; /** * Class Session @@ -55,22 +69,57 @@ use OCP\IUserSession; * @package OC\User */ class Session implements IUserSession, Emitter { - /** @var \OC\User\Manager $manager */ + /* + * @var Manager $manager + */ + private $manager; - /** @var \OC\Session\Session $session */ + /* + * @var ISession $session + */ private $session; - /** @var \OC\User\User $activeUser */ + /* + * @var ITimeFactory + */ + private $timeFacory; + + /** + * @var DefaultTokenProvider + */ + private $tokenProvider; + + /** + * @var IProvider[] + */ + private $tokenProviders; + + /** + * @var User $activeUser + */ protected $activeUser; /** * @param IUserManager $manager * @param ISession $session + * @param ITimeFactory $timeFacory + * @param IProvider $tokenProvider + * @param IProvider[] $tokenProviders */ - public function __construct(IUserManager $manager, ISession $session) { + public function __construct(IUserManager $manager, ISession $session, ITimeFactory $timeFacory, $tokenProvider, array $tokenProviders = []) { $this->manager = $manager; $this->session = $session; + $this->timeFacory = $timeFacory; + $this->tokenProvider = $tokenProvider; + $this->tokenProviders = $tokenProviders; + } + + /** + * @param DefaultTokenProvider $provider + */ + public function setTokenProvider(DefaultTokenProvider $provider) { + $this->tokenProvider = $provider; } /** @@ -94,7 +143,7 @@ class Session implements IUserSession, Emitter { /** * get the manager object * - * @return \OC\User\Manager + * @return Manager */ public function getManager() { return $this->manager; @@ -125,7 +174,7 @@ class Session implements IUserSession, Emitter { /** * set the currently active user * - * @param \OC\User\User|null $user + * @param User|null $user */ public function setUser($user) { if (is_null($user)) { @@ -139,25 +188,65 @@ class Session implements IUserSession, Emitter { /** * get the current active user * - * @return \OCP\IUser|null Current user, otherwise null + * @return IUser|null Current user, otherwise null */ public function getUser() { // FIXME: This is a quick'n dirty work-around for the incognito mode as // described at https://github.com/owncloud/core/pull/12912#issuecomment-67391155 - if (\OC_User::isIncognitoMode()) { + if (OC_User::isIncognitoMode()) { return null; } - if ($this->activeUser) { - return $this->activeUser; - } else { + if (is_null($this->activeUser)) { $uid = $this->session->get('user_id'); - if ($uid !== null) { - $this->activeUser = $this->manager->get($uid); - return $this->activeUser; - } else { + if (is_null($uid)) { + return null; + } + $this->activeUser = $this->manager->get($uid); + if (is_null($this->activeUser)) { return null; } + $this->validateSession($this->activeUser); } + return $this->activeUser; + } + + protected function validateSession(IUser $user) { + try { + $sessionId = $this->session->getId(); + } catch (SessionNotAvailableException $ex) { + return; + } + try { + $token = $this->tokenProvider->getToken($sessionId); + } catch (InvalidTokenException $ex) { + // Session was invalidated + $this->logout(); + return; + } + + // Check whether login credentials are still valid + // This check is performed each 5 minutes + $lastCheck = $this->session->get('last_login_check') ? : 0; + $now = $this->timeFacory->getTime(); + if ($lastCheck < ($now - 60 * 5)) { + try { + $pwd = $this->tokenProvider->getPassword($token, $sessionId); + } catch (InvalidTokenException $ex) { + // An invalid token password was used -> log user out + $this->logout(); + return; + } + + if ($this->manager->checkPassword($user->getUID(), $pwd) === false) { + // Password has changed -> log user out + $this->logout(); + return; + } + $this->session->set('last_login_check', $now); + } + + // Session is valid, so the token can be refreshed + $this->updateToken($this->tokenProvider, $token); } /** @@ -218,6 +307,11 @@ class Session implements IUserSession, Emitter { $this->session->regenerateId(); $this->manager->emit('\OC\User', 'preLogin', array($uid, $password)); $user = $this->manager->checkPassword($uid, $password); + if ($user === false) { + if ($this->validateToken($password)) { + $user = $this->getUser(); + } + } if ($user !== false) { if (!is_null($user)) { if ($user->isEnabled()) { @@ -225,6 +319,7 @@ class Session implements IUserSession, Emitter { $this->setLoginName($uid); $this->manager->emit('\OC\User', 'postLogin', array($user, $password)); if ($this->isLoggedIn()) { + $this->prepareUserLogin(); return true; } else { // injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory @@ -241,6 +336,142 @@ class Session implements IUserSession, Emitter { return false; } + protected function prepareUserLogin() { + // TODO: mock/inject/use non-static + // Refresh the token + \OC::$server->getCsrfTokenManager()->refreshToken(); + //we need to pass the user name, which may differ from login name + $user = $this->getUser()->getUID(); + OC_Util::setupFS($user); + //trigger creation of user home and /files folder + \OC::$server->getUserFolder($user); + } + + /** + * Tries to login the user with HTTP Basic Authentication + * + * @param IRequest $request + * @return boolean if the login was successful + */ + public function tryBasicAuthLogin(IRequest $request) { + if (!empty($request->server['PHP_AUTH_USER']) && !empty($request->server['PHP_AUTH_PW'])) { + $result = $this->login($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW']); + if ($result === true) { + /** + * Add DAV authenticated. This should in an ideal world not be + * necessary but the iOS App reads cookies from anywhere instead + * only the DAV endpoint. + * This makes sure that the cookies will be valid for the whole scope + * @see https://github.com/owncloud/core/issues/22893 + */ + $this->session->set( + Auth::DAV_AUTHENTICATED, $this->getUser()->getUID() + ); + return true; + } + } + return false; + } + + private function loginWithToken($uid) { + // TODO: $this->manager->emit('\OC\User', 'preTokenLogin', array($uid)); + $user = $this->manager->get($uid); + if (is_null($user)) { + // user does not exist + return false; + } + + //login + $this->setUser($user); + // TODO: $this->manager->emit('\OC\User', 'postTokenLogin', array($user)); + return true; + } + + /** + * Create a new session token for the given user credentials + * + * @param IRequest $request + * @param string $uid user UID + * @param string $password + * @return boolean + */ + public function createSessionToken(IRequest $request, $uid, $password) { + if (is_null($this->manager->get($uid))) { + // User does not exist + return false; + } + $name = isset($request->server['HTTP_USER_AGENT']) ? $request->server['HTTP_USER_AGENT'] : 'unknown browser'; + $loggedIn = $this->login($uid, $password); + if ($loggedIn) { + try { + $sessionId = $this->session->getId(); + $this->tokenProvider->generateToken($sessionId, $uid, $password, $name); + } catch (SessionNotAvailableException $ex) { + + } + } + return $loggedIn; + } + + /** + * @param string $token + * @return boolean + */ + private function validateToken($token) { + foreach ($this->tokenProviders as $provider) { + try { + $token = $provider->validateToken($token); + if (!is_null($token)) { + $result = $this->loginWithToken($token->getUID()); + if ($result) { + // Login success + $this->updateToken($provider, $token); + return true; + } + } + } catch (InvalidTokenException $ex) { + + } + } + return false; + } + + /** + * @param IProvider $provider + * @param IToken $token + */ + private function updateToken(IProvider $provider, IToken $token) { + // To save unnecessary DB queries, this is only done once a minute + $lastTokenUpdate = $this->session->get('last_token_update') ? : 0; + $now = $this->timeFacory->getTime(); + if ($lastTokenUpdate < ($now - 60)) { + $provider->updateToken($token); + $this->session->set('last_token_update', $now); + } + } + + /** + * Tries to login the user with auth token header + * + * @todo check remember me cookie + * @return boolean + */ + public function tryTokenLogin(IRequest $request) { + $authHeader = $request->getHeader('Authorization'); + if (strpos($authHeader, 'token ') === false) { + // No auth header, let's try session id + try { + $sessionId = $this->session->getId(); + return $this->validateToken($sessionId); + } catch (SessionNotAvailableException $ex) { + return false; + } + } else { + $token = substr($authHeader, 6); + return $this->validateToken($token); + } + } + /** * perform login using the magic cookie (remember login) * @@ -258,15 +489,15 @@ class Session implements IUserSession, Emitter { } // get stored tokens - $tokens = \OC::$server->getConfig()->getUserKeys($uid, 'login_token'); + $tokens = OC::$server->getConfig()->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); - $newToken = \OC::$server->getSecureRandom()->generate(32); - \OC::$server->getConfig()->setUserValue($uid, 'login_token', $newToken, time()); + OC::$server->getConfig()->deleteUserValue($uid, 'login_token', $currentToken); + $newToken = OC::$server->getSecureRandom()->generate(32); + OC::$server->getConfig()->setUserValue($uid, 'login_token', $newToken, time()); $this->setMagicInCookie($user->getUID(), $newToken); //login @@ -280,6 +511,14 @@ class Session implements IUserSession, Emitter { */ public function logout() { $this->manager->emit('\OC\User', 'logout'); + $user = $this->getUser(); + if (!is_null($user)) { + try { + $this->tokenProvider->invalidateToken($this->session->getId()); + } catch (SessionNotAvailableException $ex) { + + } + } $this->setUser(null); $this->setLoginName(null); $this->unsetMagicInCookie(); @@ -293,11 +532,11 @@ class Session implements IUserSession, Emitter { * @param string $token */ 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); + $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); } /** @@ -305,18 +544,19 @@ class Session implements IUserSession, Emitter { */ public function unsetMagicInCookie() { //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); + $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); // 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('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); } + } diff --git a/lib/private/legacy/api.php b/lib/private/legacy/api.php index 702b9df1927..60300c88b57 100644 --- a/lib/private/legacy/api.php +++ b/lib/private/legacy/api.php @@ -337,7 +337,7 @@ class OC_API { } // reuse existing login - $loggedIn = OC_User::isLoggedIn(); + $loggedIn = \OC::$server->getUserSession()->isLoggedIn(); if ($loggedIn === true) { $ocsApiRequest = isset($_SERVER['HTTP_OCS_APIREQUEST']) ? $_SERVER['HTTP_OCS_APIREQUEST'] === 'true' : false; if ($ocsApiRequest) { @@ -353,35 +353,25 @@ class OC_API { // basic auth - because OC_User::login will create a new session we shall only try to login // if user and pass are set - if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']) ) { - $authUser = $_SERVER['PHP_AUTH_USER']; - $authPw = $_SERVER['PHP_AUTH_PW']; - try { - $return = OC_User::login($authUser, $authPw); - } catch (\OC\User\LoginException $e) { - return false; + $userSession = \OC::$server->getUserSession(); + $request = \OC::$server->getRequest(); + try { + $loginSuccess = $userSession->tryTokenLogin($request); + if (!$loginSuccess) { + $loginSuccess = $userSession->tryBasicAuthLogin($request); } - if ($return === true) { - self::$logoutRequired = true; - - // initialize the user's filesystem - \OC_Util::setUpFS(\OC_User::getUser()); - self::$isLoggedIn = true; + } catch (\OC\User\LoginException $e) { + return false; + } + + if ($loginSuccess === true) { + self::$logoutRequired = true; - /** - * Add DAV authenticated. This should in an ideal world not be - * necessary but the iOS App reads cookies from anywhere instead - * only the DAV endpoint. - * This makes sure that the cookies will be valid for the whole scope - * @see https://github.com/owncloud/core/issues/22893 - */ - \OC::$server->getSession()->set( - \OCA\DAV\Connector\Sabre\Auth::DAV_AUTHENTICATED, - \OC::$server->getUserSession()->getUser()->getUID() - ); + // initialize the user's filesystem + \OC_Util::setUpFS(\OC_User::getUser()); + self::$isLoggedIn = true; - return \OC_User::getUser(); - } + return \OC_User::getUser(); } return false; diff --git a/lib/private/legacy/user.php b/lib/private/legacy/user.php index 7855b5e7059..499e916994a 100644 --- a/lib/private/legacy/user.php +++ b/lib/private/legacy/user.php @@ -6,6 +6,7 @@ * @author Bart Visscher <bartv@thisnet.nl> * @author Bartek Przybylski <bart.p.pl@gmail.com> * @author Björn Schießle <schiessle@owncloud.com> + * @author Christoph Wurst <christoph@owncloud.com> * @author Florian Preinstorfer <nblock@archlinux.us> * @author Georg Ehrke <georg@owncloud.com> * @author Jakob Sack <mail@jakobsack.de> @@ -67,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; /** @@ -148,37 +149,7 @@ class OC_User { } /** - * Try to login a user - * - * @param string $loginName The login name of the user to log in - * @param string $password The password of the user - * @return boolean|null - * - * Log in a user and regenerate a new session - if the password is ok - */ - public static function login($loginName, $password) { - $result = self::getUserSession()->login($loginName, $password); - if (!$result) { - $users = \OC::$server->getUserManager()->getByEmail($loginName); - // we only allow login by email if unique - if (count($users) === 1) { - $result = self::getUserSession()->login($users[0]->getUID(), $password); - } - } - if ($result) { - // Refresh the token - \OC::$server->getCsrfTokenManager()->refreshToken(); - //we need to pass the user name, which may differ from login name - $user = self::getUserSession()->getUser()->getUID(); - OC_Util::setupFS($user); - //trigger creation of user home and /files folder - \OC::$server->getUserFolder($user); - } - return $result; - } - - /** * Try to login a user using the magic cookie (remember login) * * @deprecated use \OCP\IUserSession::loginWithCookie() @@ -284,28 +255,6 @@ class OC_User { } /** - * Tries to login the user with HTTP Basic Authentication - */ - public static function tryBasicAuthLogin() { - if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) { - $result = \OC_User::login($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']); - if($result === true) { - /** - * Add DAV authenticated. This should in an ideal world not be - * necessary but the iOS App reads cookies from anywhere instead - * only the DAV endpoint. - * This makes sure that the cookies will be valid for the whole scope - * @see https://github.com/owncloud/core/issues/22893 - */ - \OC::$server->getSession()->set( - \OCA\DAV\Connector\Sabre\Auth::DAV_AUTHENTICATED, - \OC::$server->getUserSession()->getUser()->getUID() - ); - } - } - } - - /** * Check if the user is logged in, considers also the HTTP basic credentials * * @deprecated use \OC::$server->getUserSession()->isLoggedIn() diff --git a/lib/private/legacy/util.php b/lib/private/legacy/util.php index b3432470f03..4f7a8668dfc 100644 --- a/lib/private/legacy/util.php +++ b/lib/private/legacy/util.php @@ -957,8 +957,7 @@ class OC_Util { public static function checkLoggedIn() { // Check if we are a user if (!OC_User::isLoggedIn()) { - header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute( - 'core.login.showLoginForm', + header('Location: ' . \OCP\Util::linkToAbsolute('', 'index.php', [ 'redirect_url' => \OC::$server->getRequest()->getRequestUri() ] diff --git a/tests/core/controller/LoginControllerTest.php b/tests/core/controller/LoginControllerTest.php index f9a6080892b..0b5a143f4e3 100644 --- a/tests/core/controller/LoginControllerTest.php +++ b/tests/core/controller/LoginControllerTest.php @@ -53,7 +53,9 @@ class LoginControllerTest extends TestCase { $this->userManager = $this->getMock('\\OCP\\IUserManager'); $this->config = $this->getMock('\\OCP\\IConfig'); $this->session = $this->getMock('\\OCP\\ISession'); - $this->userSession = $this->getMock('\\OCP\\IUserSession'); + $this->userSession = $this->getMockBuilder('\\OC\\User\\Session') + ->disableOriginalConstructor() + ->getMock(); $this->urlGenerator = $this->getMock('\\OCP\\IURLGenerator'); $this->loginController = new LoginController( @@ -264,4 +266,74 @@ class LoginControllerTest extends TestCase { ); $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('0', '', '')); } + + public function testLoginWithInvalidCredentials() { + $user = $this->getMock('\OCP\IUser'); + $password = 'secret'; + $loginPageUrl = 'some url'; + + $this->userManager->expects($this->once()) + ->method('checkPassword') + ->will($this->returnValue(false)); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('core.login.showLoginForm') + ->will($this->returnValue($loginPageUrl)); + + $this->userSession->expects($this->never()) + ->method('createSessionToken'); + + $expected = new \OCP\AppFramework\Http\RedirectResponse($loginPageUrl); + $this->assertEquals($expected, $this->loginController->tryLogin($user, $password, '')); + } + + public function testLoginWithValidCredentials() { + $user = $this->getMock('\OCP\IUser'); + $password = 'secret'; + $indexPageUrl = 'some url'; + + $this->userManager->expects($this->once()) + ->method('checkPassword') + ->will($this->returnValue($user)); + $this->userSession->expects($this->once()) + ->method('createSessionToken') + ->with($this->request, $user->getUID(), $password); + $this->urlGenerator->expects($this->once()) + ->method('linkTo') + ->with('files', 'index') + ->will($this->returnValue($indexPageUrl)); + + $expected = new \OCP\AppFramework\Http\RedirectResponse($indexPageUrl); + $this->assertEquals($expected, $this->loginController->tryLogin($user, $password, null)); + } + + public function testLoginWithValidCredentialsAndRedirectUrl() { + $user = $this->getMock('\OCP\IUser'); + $user->expects($this->any()) + ->method('getUID') + ->will($this->returnValue('jane')); + $password = 'secret'; + $originalUrl = 'another%20url'; + $redirectUrl = 'http://localhost/another url'; + + $this->userManager->expects($this->once()) + ->method('checkPassword') + ->with('jane', $password) + ->will($this->returnValue($user)); + $this->userSession->expects($this->once()) + ->method('createSessionToken') + ->with($this->request, $user->getUID(), $password); + $this->userSession->expects($this->once()) + ->method('isLoggedIn') + ->with() + ->will($this->returnValue(true)); + $this->urlGenerator->expects($this->once()) + ->method('getAbsoluteURL') + ->with(urldecode($originalUrl)) + ->will($this->returnValue($redirectUrl)); + + $expected = new \OCP\AppFramework\Http\RedirectResponse(urldecode($redirectUrl)); + $this->assertEquals($expected, $this->loginController->tryLogin($user->getUID(), $password, $originalUrl)); + } + } diff --git a/tests/core/controller/TokenControllerTest.php b/tests/core/controller/TokenControllerTest.php new file mode 100644 index 00000000000..4635f96f48f --- /dev/null +++ b/tests/core/controller/TokenControllerTest.php @@ -0,0 +1,94 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Core\Controller; + +use OC\AppFramework\Http; +use OCP\AppFramework\Http\Response; +use Test\TestCase; + +class TokenControllerTest extends TestCase { + + /** \OC\Core\Controller\TokenController */ + private $tokenController; + private $request; + private $userManager; + private $tokenProvider; + private $secureRandom; + + protected function setUp() { + parent::setUp(); + + $this->request = $this->getMock('\OCP\IRequest'); + $this->userManager = $this->getMockBuilder('\OC\User\Manager') + ->disableOriginalConstructor() + ->getMock(); + $this->tokenProvider = $this->getMockBuilder('\OC\Authentication\Token\DefaultTokenProvider') + ->disableOriginalConstructor() + ->getMock(); + $this->secureRandom = $this->getMock('\OCP\Security\ISecureRandom'); + + $this->tokenController = new TokenController('core', $this->request, $this->userManager, $this->tokenProvider, + $this->secureRandom); + } + + public function testWithoutCredentials() { + $expected = new Response(); + $expected->setStatus(Http::STATUS_UNPROCESSABLE_ENTITY); + + $actual = $this->tokenController->generateToken(null, null); + + $this->assertEquals($expected, $actual); + } + + public function testWithInvalidCredentials() { + $this->userManager->expects($this->once()) + ->method('checkPassword') + ->with('john', 'passme') + ->will($this->returnValue(false)); + $expected = new Response(); + $expected->setStatus(Http::STATUS_UNAUTHORIZED); + + $actual = $this->tokenController->generateToken('john', 'passme'); + + $this->assertEquals($expected, $actual); + } + + public function testWithValidCredentials() { + $this->userManager->expects($this->once()) + ->method('checkPassword') + ->with('john', '123456') + ->will($this->returnValue(true)); + $this->secureRandom->expects($this->once()) + ->method('generate') + ->with(128) + ->will($this->returnValue('verysecurerandomtoken')); + $expected = [ + 'token' => 'verysecurerandomtoken' + ]; + + $actual = $this->tokenController->generateToken('john', '123456'); + + $this->assertEquals($expected, $actual); + } + +} diff --git a/tests/lib/authentication/token/defaulttokencleanupjobtest.php b/tests/lib/authentication/token/defaulttokencleanupjobtest.php new file mode 100644 index 00000000000..c9082c08b30 --- /dev/null +++ b/tests/lib/authentication/token/defaulttokencleanupjobtest.php @@ -0,0 +1,57 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace Test\Authentication\Token; + +use OC\Authentication\Token\DefaultTokenCleanupJob; +use Test\TestCase; + +class DefaultTokenCleanupJobTest extends TestCase { + + /** @var DefaultTokenCleanupJob */ + private $job; + private $tokenProvider; + + protected function setUp() { + parent::setUp(); + + $this->tokenProvider = $this->getMockBuilder('\OC\Authentication\Token\DefaultTokenProvider') + ->disableOriginalConstructor() + ->getMock(); + $this->overwriteService('\OC\Authentication\Token\DefaultTokenProvider', $this->tokenProvider); + $this->job = new DefaultTokenCleanupJob(); + } + + protected function tearDown() { + parent::tearDown(); + + $this->restoreService('\OC\Authentication\Token\DefaultTokenProvider'); + } + + public function testRun() { + $this->tokenProvider->expects($this->once()) + ->method('invalidateOldTokens') + ->with(); + $this->invokePrivate($this->job, 'run', [null]); + } + +} diff --git a/tests/lib/authentication/token/defaulttokenmappertest.php b/tests/lib/authentication/token/defaulttokenmappertest.php new file mode 100644 index 00000000000..9a21e143fb4 --- /dev/null +++ b/tests/lib/authentication/token/defaulttokenmappertest.php @@ -0,0 +1,144 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace Test\Authentication\Token; + +use OC; +use OC\Authentication\Token\DefaultToken; +use OC\Authentication\Token\DefaultTokenMapper; +use OC\Authentication\Token\IToken; +use OCP\DB\QueryBuilder\IQueryBuilder; +use Test\TestCase; + +/** + * Class DefaultTokenMapperTest + * + * @group DB + * @package Test\Authentication + */ +class DefaultTokenMapperTest extends TestCase { + + /** @var DefaultTokenMapper */ + private $mapper; + private $dbConnection; + private $time; + + protected function setUp() { + parent::setUp(); + + $this->dbConnection = OC::$server->getDatabaseConnection(); + $this->time = time(); + $this->resetDatabase(); + + $this->mapper = new DefaultTokenMapper($this->dbConnection); + } + + private function resetDatabase() { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete('authtoken')->execute(); + $qb->insert('authtoken')->values([ + 'uid' => $qb->createNamedParameter('user1'), + 'password' => $qb->createNamedParameter('a75c7116460c082912d8f6860a850904|3nz5qbG1nNSLLi6V|c55365a0e54cfdfac4a175bcf11a7612aea74492277bba6e5d96a24497fa9272488787cb2f3ad34d8b9b8060934fce02f008d371df3ff3848f4aa61944851ff0'), + 'name' => $qb->createNamedParameter('Firefox on Linux'), + 'token' => $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'), + 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), + 'last_activity' => $qb->createNamedParameter($this->time - 120, IQueryBuilder::PARAM_INT), // Two minutes ago + ])->execute(); + $qb->insert('authtoken')->values([ + 'uid' => $qb->createNamedParameter('user2'), + 'password' => $qb->createNamedParameter('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f'), + 'name' => $qb->createNamedParameter('Firefox on Android'), + 'token' => $qb->createNamedParameter('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'), + 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), + 'last_activity' => $qb->createNamedParameter($this->time - 60 * 60 * 24 * 3, IQueryBuilder::PARAM_INT), // Three days ago + ])->execute(); + $qb->insert('authtoken')->values([ + 'uid' => $qb->createNamedParameter('user1'), + 'password' => $qb->createNamedParameter('063de945d6f6b26862d9b6f40652f2d5|DZ/z520tfdXPtd0T|395f6b89be8d9d605e409e20b9d9abe477fde1be38a3223f9e508f979bf906e50d9eaa4dca983ca4fb22a241eb696c3f98654e7775f78c4caf13108f98642b53'), + 'name' => $qb->createNamedParameter('Iceweasel on Linux'), + 'token' => $qb->createNamedParameter('47af8697ba590fb82579b5f1b3b6e8066773a62100abbe0db09a289a62f5d980dc300fa3d98b01d7228468d1ab05c1aa14c8d14bd5b6eee9cdf1ac14864680c3'), + 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), + 'last_activity' => $qb->createNamedParameter($this->time - 120, IQueryBuilder::PARAM_INT), // Two minutes ago + ])->execute(); + } + + private function getNumberOfTokens() { + $qb = $this->dbConnection->getQueryBuilder(); + $result = $qb->select($qb->createFunction('count(*) as `count`')) + ->from('authtoken') + ->execute() + ->fetch(); + return (int) $result['count']; + } + + public function testInvalidate() { + $token = '9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'; + + $this->mapper->invalidate($token); + + $this->assertSame(2, $this->getNumberOfTokens()); + } + + public function testInvalidateInvalid() { + $token = 'youwontfindthisoneinthedatabase'; + + $this->mapper->invalidate($token); + + $this->assertSame(3, $this->getNumberOfTokens()); + } + + public function testInvalidateOld() { + $olderThan = $this->time - 60 * 60; // One hour + + $this->mapper->invalidateOld($olderThan); + + $this->assertSame(2, $this->getNumberOfTokens()); + } + + public function testGetToken() { + $token = '1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'; + $token = new DefaultToken(); + $token->setUid('user2'); + $token->setPassword('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f'); + $token->setName('Firefox on Android'); + $token->setToken('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'); + $token->setType(IToken::TEMPORARY_TOKEN); + $token->setLastActivity($this->time - 60 * 60 * 24 * 3); + + $dbToken = $this->mapper->getToken($token->getToken()); + + $token->setId($dbToken->getId()); // We don't know the ID + $token->resetUpdatedFields(); + + $this->assertEquals($token, $dbToken); + } + + /** + * @expectedException \OCP\AppFramework\Db\DoesNotExistException + */ + public function testGetInvalidToken() { + $token = 'thisisaninvalidtokenthatisnotinthedatabase'; + + $this->mapper->getToken($token); + } + +} diff --git a/tests/lib/authentication/token/defaulttokenprovidertest.php b/tests/lib/authentication/token/defaulttokenprovidertest.php new file mode 100644 index 00000000000..5ee33d0ec11 --- /dev/null +++ b/tests/lib/authentication/token/defaulttokenprovidertest.php @@ -0,0 +1,199 @@ +<?php + +/** + * @author Christoph Wurst <christoph@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +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 Test\TestCase; + +class DefaultTokenProviderTest extends TestCase { + + /** @var DefaultTokenProvider */ + private $tokenProvider; + private $mapper; + private $crypto; + private $config; + private $logger; + private $timeFactory; + private $time; + + protected function setUp() { + parent::setUp(); + + $this->mapper = $this->getMockBuilder('\OC\Authentication\Token\DefaultTokenMapper') + ->disableOriginalConstructor() + ->getMock(); + $this->crypto = $this->getMock('\OCP\Security\ICrypto'); + $this->config = $this->getMock('\OCP\IConfig'); + $this->logger = $this->getMock('\OCP\ILogger'); + $this->timeFactory = $this->getMock('\OCP\AppFramework\Utility\ITimeFactory'); + $this->time = 1313131; + $this->timeFactory->expects($this->any()) + ->method('getTime') + ->will($this->returnValue($this->time)); + + $this->tokenProvider = new DefaultTokenProvider($this->mapper, $this->crypto, $this->config, $this->logger, + $this->timeFactory); + } + + public function testGenerateToken() { + $token = 'token'; + $uid = 'user'; + $password = 'passme'; + $name = 'Some browser'; + $type = IToken::PERMANENT_TOKEN; + + $toInsert = new DefaultToken(); + $toInsert->setUid($uid); + $toInsert->setPassword('encryptedpassword'); + $toInsert->setName($name); + $toInsert->setToken(hash('sha512', $token . '1f4h9s')); + $toInsert->setType($type); + $toInsert->setLastActivity($this->time); + + $this->config->expects($this->any()) + ->method('getSystemValue') + ->with('secret') + ->will($this->returnValue('1f4h9s')); + $this->crypto->expects($this->once()) + ->method('encrypt') + ->with($password, $token . '1f4h9s') + ->will($this->returnValue('encryptedpassword')); + $this->mapper->expects($this->once()) + ->method('insert') + ->with($this->equalTo($toInsert)); + + $actual = $this->tokenProvider->generateToken($token, $uid, $password, $name, $type); + + $this->assertEquals($toInsert, $actual); + } + + public function testUpdateToken() { + $tk = new DefaultToken(); + $this->mapper->expects($this->once()) + ->method('update') + ->with($tk); + + $this->tokenProvider->updateToken($tk); + + $this->assertEquals($this->time, $tk->getLastActivity()); + } + + public function testGetPassword() { + $token = 'token1234'; + $tk = new DefaultToken(); + $tk->setPassword('someencryptedvalue'); + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('secret') + ->will($this->returnValue('1f4h9s')); + $this->crypto->expects($this->once()) + ->method('decrypt') + ->with('someencryptedvalue', $token . '1f4h9s') + ->will($this->returnValue('passme')); + + $actual = $this->tokenProvider->getPassword($tk, $token); + + $this->assertEquals('passme', $actual); + } + + /** + * @expectedException \OC\Authentication\Exceptions\InvalidTokenException + */ + public function testGetPasswordDeletesInvalidToken() { + $token = 'token1234'; + $tk = new DefaultToken(); + $tk->setPassword('someencryptedvalue'); + /* @var $tokenProvider DefaultTokenProvider */ + $tokenProvider = $this->getMockBuilder('\OC\Authentication\Token\DefaultTokenProvider') + ->setMethods([ + 'invalidateToken' + ]) + ->setConstructorArgs([$this->mapper, $this->crypto, $this->config, $this->logger, + $this->timeFactory]) + ->getMock(); + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('secret') + ->will($this->returnValue('1f4h9s')); + $this->crypto->expects($this->once()) + ->method('decrypt') + ->with('someencryptedvalue', $token . '1f4h9s') + ->will($this->throwException(new \Exception('some crypto error occurred'))); + $tokenProvider->expects($this->once()) + ->method('invalidateToken') + ->with($token); + + $tokenProvider->getPassword($tk, $token); + } + + public function testInvalidateToken() { + $this->mapper->expects($this->once()) + ->method('invalidate') + ->with(hash('sha512', 'token7')); + + $this->tokenProvider->invalidateToken('token7'); + } + + public function testInvalidateOldTokens() { + $defaultSessionLifetime = 60 * 60 * 24; + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('session_lifetime', $defaultSessionLifetime) + ->will($this->returnValue(150)); + $this->mapper->expects($this->once()) + ->method('invalidateOld') + ->with($this->time - 150); + + $this->tokenProvider->invalidateOldTokens(); + } + + public function testValidateToken() { + $token = 'sometoken'; + $dbToken = new DefaultToken(); + $this->mapper->expects($this->once()) + ->method('getToken') + ->with(hash('sha512', $token)) + ->will($this->returnValue($dbToken)); + + $actual = $this->tokenProvider->validateToken($token); + + $this->assertEquals($dbToken, $actual); + } + + /** + * @expectedException \OC\Authentication\Exceptions\InvalidTokenException + */ + public function testValidateInvalidToken() { + $token = 'sometoken'; + $this->mapper->expects($this->once()) + ->method('getToken') + ->with(hash('sha512', $token)) + ->will($this->throwException(new DoesNotExistException(''))); + + $this->tokenProvider->validateToken($token); + } + +} diff --git a/tests/lib/user/session.php b/tests/lib/user/session.php index 5a8ea57cb86..c6ddeb416fb 100644 --- a/tests/lib/user/session.php +++ b/tests/lib/user/session.php @@ -11,107 +11,138 @@ namespace Test\User; use OC\Session\Memory; use OC\User\User; -use OCP\ISession; -use OCP\IUserManager; -use OCP\UserInterface; -use Test\TestCase; /** * @group DB * @package Test\User */ -class Session extends TestCase { - public function testGetUser() { - /** @var ISession | \PHPUnit_Framework_MockObject_MockObject $session */ - $session = $this->getMock('\OC\Session\Memory', array(), array('')); - $session->expects($this->once()) - ->method('get') - ->with('user_id') - ->will($this->returnValue('foo')); +class Session extends \Test\TestCase { - /** @var UserInterface | \PHPUnit_Framework_MockObject_MockObject $backend */ - $backend = $this->getMock('\Test\Util\User\Dummy'); - $backend->expects($this->once()) - ->method('userExists') - ->with('foo') - ->will($this->returnValue(true)); + /** @var \OCP\AppFramework\Utility\ITimeFactory */ + private $timeFactory; - $manager = new \OC\User\Manager(); - $manager->registerBackend($backend); + /** @var \OC\Authentication\Token\DefaultTokenProvider */ + protected $defaultProvider; - $userSession = new \OC\User\Session($manager, $session); - $user = $userSession->getUser(); - $this->assertEquals('foo', $user->getUID()); + protected function setUp() { + parent::setUp(); + + $this->timeFactory = $this->getMock('\OCP\AppFramework\Utility\ITimeFactory'); + $this->timeFactory->expects($this->any()) + ->method('getTime') + ->will($this->returnValue(10000)); + $this->defaultProvider = $this->getMockBuilder('\OC\Authentication\Token\DefaultTokenProvider') + ->disableOriginalConstructor() + ->getMock(); } - public function testIsLoggedIn() { - /** @var ISession | \PHPUnit_Framework_MockObject_MockObject $session */ + public function testGetUser() { + $token = new \OC\Authentication\Token\DefaultToken(); + + $expectedUser = new User('foo', null); $session = $this->getMock('\OC\Session\Memory', array(), array('')); - $session->expects($this->once()) + $session->expects($this->at(0)) ->method('get') ->with('user_id') - ->will($this->returnValue('foo')); + ->will($this->returnValue($expectedUser->getUID())); + $sessionId = 'abcdef12345'; - /** @var UserInterface | \PHPUnit_Framework_MockObject_MockObject $backend */ - $backend = $this->getMock('\Test\Util\User\Dummy'); - $backend->expects($this->once()) - ->method('userExists') - ->with('foo') + $manager = $this->getMockBuilder('\OC\User\Manager') + ->disableOriginalConstructor() + ->getMock(); + $session->expects($this->once()) + ->method('getId') + ->will($this->returnValue($sessionId)); + $this->defaultProvider->expects($this->once()) + ->method('getToken') + ->will($this->returnValue($token)); + $session->expects($this->at(2)) + ->method('get') + ->with('last_login_check') + ->will($this->returnValue(null)); // No check has been run yet + $this->defaultProvider->expects($this->once()) + ->method('getPassword') + ->with($token, $sessionId) + ->will($this->returnValue('password123')); + $manager->expects($this->once()) + ->method('checkPassword') + ->with($expectedUser->getUID(), 'password123') ->will($this->returnValue(true)); + $session->expects($this->at(3)) + ->method('set') + ->with('last_login_check', 10000); + + $session->expects($this->at(4)) + ->method('get') + ->with('last_token_update') + ->will($this->returnValue(null)); // No check run so far + $this->defaultProvider->expects($this->once()) + ->method('updateToken') + ->with($token); + $session->expects($this->at(5)) + ->method('set') + ->with('last_token_update', $this->equalTo(10000)); + + $manager->expects($this->any()) + ->method('get') + ->with($expectedUser->getUID()) + ->will($this->returnValue($expectedUser)); - $manager = new \OC\User\Manager(); - $manager->registerBackend($backend); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->defaultProvider, [$this->defaultProvider]); + $user = $userSession->getUser(); + $this->assertSame($expectedUser, $user); + } - $userSession = new \OC\User\Session($manager, $session); - $isLoggedIn = $userSession->isLoggedIn(); - $this->assertTrue($isLoggedIn); + public function isLoggedInData() { + return [ + [true], + [false], + ]; } - public function testNotLoggedIn() { - /** @var ISession | \PHPUnit_Framework_MockObject_MockObject $session */ + /** + * @dataProvider isLoggedInData + */ + public function testIsLoggedIn($isLoggedIn) { $session = $this->getMock('\OC\Session\Memory', array(), array('')); - $session->expects($this->once()) - ->method('get') - ->with('user_id') - ->will($this->returnValue(null)); - - /** @var UserInterface | \PHPUnit_Framework_MockObject_MockObject $backend */ - $backend = $this->getMock('\Test\Util\User\Dummy'); - $backend->expects($this->never()) - ->method('userExists'); - $manager = new \OC\User\Manager(); - $manager->registerBackend($backend); + $manager = $this->getMockBuilder('\OC\User\Manager') + ->disableOriginalConstructor() + ->getMock(); - $userSession = new \OC\User\Session($manager, $session); - $isLoggedIn = $userSession->isLoggedIn(); - $this->assertFalse($isLoggedIn); + $userSession = $this->getMockBuilder('\OC\User\Session') + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->defaultProvider, [$this->defaultProvider]]) + ->setMethods([ + 'getUser' + ]) + ->getMock(); + $user = new User('sepp', null); + $userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($isLoggedIn ? $user : null)); + $this->assertEquals($isLoggedIn, $userSession->isLoggedIn()); } public function testSetUser() { - /** @var ISession | \PHPUnit_Framework_MockObject_MockObject $session */ $session = $this->getMock('\OC\Session\Memory', array(), array('')); $session->expects($this->once()) ->method('set') ->with('user_id', 'foo'); - /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject $manager */ $manager = $this->getMock('\OC\User\Manager'); $backend = $this->getMock('\Test\Util\User\Dummy'); - /** @var User | \PHPUnit_Framework_MockObject_MockObject $user */ $user = $this->getMock('\OC\User\User', array(), array('foo', $backend)); $user->expects($this->once()) ->method('getUID') ->will($this->returnValue('foo')); - $userSession = new \OC\User\Session($manager, $session); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->defaultProvider, [$this->defaultProvider]); $userSession->setUser($user); } public function testLoginValidPasswordEnabled() { - /** @var ISession | \PHPUnit_Framework_MockObject_MockObject $session */ $session = $this->getMock('\OC\Session\Memory', array(), array('')); $session->expects($this->once()) ->method('regenerateId'); @@ -127,8 +158,7 @@ class Session extends TestCase { return false; break; } - }, - 'foo')); + }, 'foo')); $managerMethods = get_class_methods('\OC\User\Manager'); //keep following methods intact in order to ensure hooks are @@ -140,13 +170,12 @@ class Session extends TestCase { unset($managerMethods[$i]); } } - /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject $manager */ $manager = $this->getMock('\OC\User\Manager', $managerMethods, array()); $backend = $this->getMock('\Test\Util\User\Dummy'); $user = $this->getMock('\OC\User\User', array(), array('foo', $backend)); - $user->expects($this->exactly(2)) + $user->expects($this->any()) ->method('isEnabled') ->will($this->returnValue(true)); $user->expects($this->any()) @@ -160,22 +189,27 @@ class Session extends TestCase { ->with('foo', 'bar') ->will($this->returnValue($user)); - $userSession = new \OC\User\Session($manager, $session); + $userSession = $this->getMockBuilder('\OC\User\Session') + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->defaultProvider, [$this->defaultProvider]]) + ->setMethods([ + 'prepareUserLogin' + ]) + ->getMock(); + $userSession->expects($this->once()) + ->method('prepareUserLogin'); $userSession->login('foo', 'bar'); $this->assertEquals($user, $userSession->getUser()); } /** * @expectedException \OC\User\LoginException - * @expectedExceptionMessage User disabled */ public function testLoginValidPasswordDisabled() { - /** @var ISession | \PHPUnit_Framework_MockObject_MockObject $session */ $session = $this->getMock('\OC\Session\Memory', array(), array('')); $session->expects($this->never()) ->method('set'); $session->expects($this->once()) - ->method('regenerateId'); + ->method('regenerateId'); $managerMethods = get_class_methods('\OC\User\Manager'); //keep following methods intact in order to ensure hooks are @@ -187,13 +221,12 @@ class Session extends TestCase { unset($managerMethods[$i]); } } - /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject $manager */ $manager = $this->getMock('\OC\User\Manager', $managerMethods, array()); $backend = $this->getMock('\Test\Util\User\Dummy'); $user = $this->getMock('\OC\User\User', array(), array('foo', $backend)); - $user->expects($this->once()) + $user->expects($this->any()) ->method('isEnabled') ->will($this->returnValue(false)); $user->expects($this->never()) @@ -204,17 +237,16 @@ class Session extends TestCase { ->with('foo', 'bar') ->will($this->returnValue($user)); - $userSession = new \OC\User\Session($manager, $session); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->defaultProvider, [$this->defaultProvider]); $userSession->login('foo', 'bar'); } public function testLoginInvalidPassword() { - /** @var ISession | \PHPUnit_Framework_MockObject_MockObject $session */ $session = $this->getMock('\OC\Session\Memory', array(), array('')); $session->expects($this->never()) ->method('set'); $session->expects($this->once()) - ->method('regenerateId'); + ->method('regenerateId'); $managerMethods = get_class_methods('\OC\User\Manager'); //keep following methods intact in order to ensure hooks are @@ -226,7 +258,6 @@ class Session extends TestCase { unset($managerMethods[$i]); } } - /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject $manager */ $manager = $this->getMock('\OC\User\Manager', $managerMethods, array()); $backend = $this->getMock('\Test\Util\User\Dummy'); @@ -242,32 +273,31 @@ class Session extends TestCase { ->with('foo', 'bar') ->will($this->returnValue(false)); - $userSession = new \OC\User\Session($manager, $session); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->defaultProvider, [$this->defaultProvider]); $userSession->login('foo', 'bar'); } public function testLoginNonExisting() { - /** @var ISession | \PHPUnit_Framework_MockObject_MockObject $session */ $session = $this->getMock('\OC\Session\Memory', array(), array('')); $session->expects($this->never()) ->method('set'); $session->expects($this->once()) - ->method('regenerateId'); + ->method('regenerateId'); - /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject $manager */ $manager = $this->getMock('\OC\User\Manager'); + $backend = $this->getMock('\Test\Util\User\Dummy'); + $manager->expects($this->once()) ->method('checkPassword') ->with('foo', 'bar') ->will($this->returnValue(false)); - $userSession = new \OC\User\Session($manager, $session); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->defaultProvider, [$this->defaultProvider]); $userSession->login('foo', 'bar'); } public function testRememberLoginValidToken() { - /** @var ISession | \PHPUnit_Framework_MockObject_MockObject $session */ $session = $this->getMock('\OC\Session\Memory', array(), array('')); $session->expects($this->exactly(1)) ->method('set') @@ -278,10 +308,9 @@ class Session extends TestCase { default: return false; } - }, - 'foo')); + }, 'foo')); $session->expects($this->once()) - ->method('regenerateId'); + ->method('regenerateId'); $managerMethods = get_class_methods('\OC\User\Manager'); //keep following methods intact in order to ensure hooks are @@ -314,13 +343,12 @@ class Session extends TestCase { $token = 'goodToken'; \OC::$server->getConfig()->setUserValue('foo', 'login_token', $token, time()); - /** @var \OC\User\Session $userSession */ $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)); + array($manager, $session, $this->timeFactory, $this->defaultProvider, [$this->defaultProvider])); $granted = $userSession->loginWithCookie('foo', $token); @@ -328,12 +356,11 @@ class Session extends TestCase { } public function testRememberLoginInvalidToken() { - /** @var ISession | \PHPUnit_Framework_MockObject_MockObject $session */ $session = $this->getMock('\OC\Session\Memory', array(), array('')); $session->expects($this->never()) ->method('set'); $session->expects($this->once()) - ->method('regenerateId'); + ->method('regenerateId'); $managerMethods = get_class_methods('\OC\User\Manager'); //keep following methods intact in order to ensure hooks are @@ -345,7 +372,6 @@ class Session extends TestCase { unset($managerMethods[$i]); } } - /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject $manager */ $manager = $this->getMock('\OC\User\Manager', $managerMethods, array()); $backend = $this->getMock('\Test\Util\User\Dummy'); @@ -367,19 +393,18 @@ class Session extends TestCase { $token = 'goodToken'; \OC::$server->getConfig()->setUserValue('foo', 'login_token', $token, time()); - $userSession = new \OC\User\Session($manager, $session); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->defaultProvider, [$this->defaultProvider]); $granted = $userSession->loginWithCookie('foo', 'badToken'); $this->assertSame($granted, false); } public function testRememberLoginInvalidUser() { - /** @var ISession | \PHPUnit_Framework_MockObject_MockObject $session */ $session = $this->getMock('\OC\Session\Memory', array(), array('')); $session->expects($this->never()) ->method('set'); $session->expects($this->once()) - ->method('regenerateId'); + ->method('regenerateId'); $managerMethods = get_class_methods('\OC\User\Manager'); //keep following methods intact in order to ensure hooks are @@ -391,7 +416,6 @@ class Session extends TestCase { unset($managerMethods[$i]); } } - /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject $manager */ $manager = $this->getMock('\OC\User\Manager', $managerMethods, array()); $backend = $this->getMock('\Test\Util\User\Dummy'); @@ -412,7 +436,7 @@ class Session extends TestCase { $token = 'goodToken'; \OC::$server->getConfig()->setUserValue('foo', 'login_token', $token, time()); - $userSession = new \OC\User\Session($manager, $session); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->defaultProvider, [$this->defaultProvider]); $granted = $userSession->loginWithCookie('foo', $token); $this->assertSame($granted, false); @@ -424,7 +448,6 @@ class Session extends TestCase { 'bar' => new User('bar', null) ); - /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject $manager */ $manager = $this->getMockBuilder('\OC\User\Manager') ->disableOriginalConstructor() ->getMock(); @@ -432,12 +455,20 @@ class Session extends TestCase { $manager->expects($this->any()) ->method('get') ->will($this->returnCallback(function ($uid) use ($users) { - return $users[$uid]; - })); + return $users[$uid]; + })); $session = new Memory(''); $session->set('user_id', 'foo'); - $userSession = new \OC\User\Session($manager, $session); + $userSession = $this->getMockBuilder('\OC\User\Session') + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->defaultProvider, [$this->defaultProvider]]) + ->setMethods([ + 'validateSession' + ]) + ->getMock(); + $userSession->expects($this->any()) + ->method('validateSession'); + $this->assertEquals($users['foo'], $userSession->getUser()); $session2 = new Memory(''); @@ -445,4 +476,5 @@ class Session extends TestCase { $userSession->setSession($session2); $this->assertEquals($users['bar'], $userSession->getUser()); } + } diff --git a/version.php b/version.php index a3840349811..81e0bc1b067 100644 --- a/version.php +++ b/version.php @@ -26,7 +26,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, 1, 0, 0); +$OC_Version = array(9, 1, 0, 1); // The human readable string $OC_VersionString = '9.1.0 pre alpha'; |