diff --git a/apps/dav/lib/connector/sabre/auth.php b/apps/dav/lib/connector/sabre/auth.php index b63efa3a1b..b8047e779f 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 b81a5e003b..a0168e435e 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 0000000000..43aa618bd0 --- /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 0000000000..5f36b199e0 --- /dev/null +++ b/build/integration/features/bootstrap/Auth.php @@ -0,0 +1,117 @@ +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 31be33165e..b8fb516fad 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 0a54386a2c..a835dc7fbb 100644 --- a/core/Application.php +++ b/core/Application.php @@ -1,6 +1,7 @@ + * @author Christoph Wurst * @author Lukas Reschke * @author Morris Jobke * @author Roeland Jago Douma @@ -28,12 +29,14 @@ namespace OC\Core; use OC\AppFramework\Utility\SimpleContainer; use OC\AppFramework\Utility\TimeFactory; -use OC\Core\Controller\LoginController; -use \OCP\AppFramework\App; -use OC\Core\Controller\LostController; -use OC\Core\Controller\UserController; use OC\Core\Controller\AvatarController; -use \OCP\Util; +use OC\Core\Controller\LoginController; +use OC\Core\Controller\LostController; +use OC\Core\Controller\TokenController; +use OC\Core\Controller\UserController; +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 796706d364..6985e2be87 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -1,5 +1,7 @@ * @author Lukas Reschke * * @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 0000000000..6606a3c834 --- /dev/null +++ b/core/Controller/TokenController.php @@ -0,0 +1,90 @@ + + * + * @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 + * + */ + +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 a9c800af4e..7090935200 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 86c186928c..45814fc71d 100644 --- a/core/templates/login.php +++ b/core/templates/login.php @@ -9,7 +9,7 @@ script('core', [ ?> -
+
'); diff --git a/db_structure.xml b/db_structure.xml index 99541a4f90..7b4a3b5329 100644 --- a/db_structure.xml +++ b/db_structure.xml @@ -1031,6 +1031,92 @@ + + *dbprefix*authtoken + + + + + id + integer + 0 + true + 1 + true + 4 + + + + + uid + text + + true + 64 + + + + password + clob + + true + 4000 + + + + name + text + + true + 100 + + + + token + text + + true + 200 + + + + type + integer + 0 + true + true + 2 + + + + last_activity + integer + 0 + true + true + 4 + + + + authtoken_token_index + true + + token + ascending + + + + + authtoken_last_activity_index + + last_activity + ascending + + + + +
+