From 6f85fe9cab984b31d2fb430f6cae081214385316 Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Wed, 11 May 2016 11:23:25 +0200 Subject: [PATCH] Add two factor auth to core and add a dummy provider app for demonstration --- .gitignore | 1 + apps/dav/lib/Connector/Sabre/Auth.php | 1 + apps/twofactor_email/appinfo/info.xml | 23 ++ apps/twofactor_email/js/challenge.js | 5 + .../lib/Provider/TwoFactorEmailProvider.php | 94 ++++++++ apps/twofactor_email/templates/challenge.php | 11 + apps/twofactor_email/tests/bootstrap.php | 13 ++ core/Application.php | 16 +- core/Controller/LoginController.php | 16 +- .../TwoFactorChallengeController.php | 134 +++++++++++ core/Middleware/TwoFactorMiddleware.php | 109 +++++++++ core/routes.php | 3 + core/templates/twofactorselectchallenge.php | 10 + core/templates/twofactorshowchallenge.php | 16 ++ lib/private/App/AppManager.php | 2 +- lib/private/App/InfoParser.php | 3 + .../DependencyInjection/DIContainer.php | 14 +- .../Exceptions/LoginRequiredException.php | 29 +++ .../TwoFactorAuthRequiredException.php | 29 +++ .../UserAlreadyLoggedInException.php | 29 +++ .../Authentication/TwoFactorAuth/Manager.php | 141 +++++++++++ lib/private/Server.php | 12 + lib/private/User/Session.php | 1 + .../TwoFactorAuth/IProvider.php | 80 +++++++ tests/core/controller/LoginControllerTest.php | 40 +++- .../TwoFactorChallengeControllerTest.php | 219 ++++++++++++++++++ .../middleware/TwoFactorMiddlewareTest.php | 153 ++++++++++++ tests/data/app/expected-info.json | 3 +- .../twofactorauth/managertest.php | 169 ++++++++++++++ 29 files changed, 1368 insertions(+), 8 deletions(-) create mode 100644 apps/twofactor_email/appinfo/info.xml create mode 100644 apps/twofactor_email/js/challenge.js create mode 100644 apps/twofactor_email/lib/Provider/TwoFactorEmailProvider.php create mode 100644 apps/twofactor_email/templates/challenge.php create mode 100644 apps/twofactor_email/tests/bootstrap.php create mode 100644 core/Controller/TwoFactorChallengeController.php create mode 100644 core/Middleware/TwoFactorMiddleware.php create mode 100644 core/templates/twofactorselectchallenge.php create mode 100644 core/templates/twofactorshowchallenge.php create mode 100644 lib/private/Authentication/Exceptions/LoginRequiredException.php create mode 100644 lib/private/Authentication/Exceptions/TwoFactorAuthRequiredException.php create mode 100644 lib/private/Authentication/Exceptions/UserAlreadyLoggedInException.php create mode 100644 lib/private/Authentication/TwoFactorAuth/Manager.php create mode 100644 lib/public/Authentication/TwoFactorAuth/IProvider.php create mode 100644 tests/core/controller/TwoFactorChallengeControllerTest.php create mode 100644 tests/core/middleware/TwoFactorMiddlewareTest.php create mode 100644 tests/lib/authentication/twofactorauth/managertest.php diff --git a/.gitignore b/.gitignore index 73f57989a8da..807f9565551e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ !/apps/user_ldap !/apps/provisioning_api !/apps/systemtags +!/apps/twofactor_email !/apps/updatenotification /apps/files_external/3rdparty/irodsphp/PHPUnitTest /apps/files_external/3rdparty/irodsphp/web diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php index b8047e779f5b..69301096b496 100644 --- a/apps/dav/lib/Connector/Sabre/Auth.php +++ b/apps/dav/lib/Connector/Sabre/Auth.php @@ -103,6 +103,7 @@ protected function validateUserPass($username, $password) { return true; } else { \OC_Util::setUpFS(); //login hooks may need early access to the filesystem + // TODO: do not allow basic auth if the user is 2FA enforced if($this->userSession->login($username, $password)) { $this->userSession->createSessionToken($this->request, $username, $password); \OC_Util::setUpFS($this->userSession->getUser()->getUID()); diff --git a/apps/twofactor_email/appinfo/info.xml b/apps/twofactor_email/appinfo/info.xml new file mode 100644 index 000000000000..918014ffb3af --- /dev/null +++ b/apps/twofactor_email/appinfo/info.xml @@ -0,0 +1,23 @@ + + + twofactor_email + Two Factor Email Provider + An Two-Factor-Auth Provider for ownCloud 9.1+ + AGPL + Christoph Wurst + 0.0.1 + TwoFactor_Email + other + + + + + + + OCA\TwoFactor_Email\Provider\TwoFactorEmailProvider + + + + + + diff --git a/apps/twofactor_email/js/challenge.js b/apps/twofactor_email/js/challenge.js new file mode 100644 index 000000000000..52154859e690 --- /dev/null +++ b/apps/twofactor_email/js/challenge.js @@ -0,0 +1,5 @@ +(function() { + 'use strict'; + + console.log('if you can see this, 2FA providers can add JS files'); +})(); diff --git a/apps/twofactor_email/lib/Provider/TwoFactorEmailProvider.php b/apps/twofactor_email/lib/Provider/TwoFactorEmailProvider.php new file mode 100644 index 000000000000..67ce9026cc47 --- /dev/null +++ b/apps/twofactor_email/lib/Provider/TwoFactorEmailProvider.php @@ -0,0 +1,94 @@ + + * + * @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 OCA\TwoFactor_Email\Provider; + +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\IUser; +use OCP\Template; + +class TwoFactorEmailProvider implements IProvider { + + /** + * Get unique identifier of this 2FA provider + * + * @since 9.1.0 + * + * @return string + */ + public function getId() { + return 'email'; + } + + /** + * Get the display name for selecting the 2FA provider + * + * @since 9.1.0 + * + * @return string + */ + public function getDisplayName() { + // TODO: L10N + return 'Email'; + } + + /** + * Get the template for rending the 2FA provider view + * + * @since 9.1.0 + * + * @param IUser $user + * @return Template + */ + public function getTemplate(IUser $user) { + return new Template('twofactor_email', 'challenge'); + } + + /** + * Verify the given challenge + * + * @since 9.1.0 + * + * @param IUser $user + * @param string $challenge + */ + public function verifyChallenge(IUser $user, $challenge) { + if ($challenge === 'passme') { + return true; + } + return false; + } + + /** + * Decides whether 2FA is enforced for the given user + * + * @since 9.1.0 + * + * @param IUser $user + * @return boolean + */ + public function isTwoFactorAuthEnforcedForUser(IUser $user) { + // 2FA is enforced for all users + return true; + } + +} diff --git a/apps/twofactor_email/templates/challenge.php b/apps/twofactor_email/templates/challenge.php new file mode 100644 index 000000000000..92a084057531 --- /dev/null +++ b/apps/twofactor_email/templates/challenge.php @@ -0,0 +1,11 @@ + + +
+ + +
diff --git a/apps/twofactor_email/tests/bootstrap.php b/apps/twofactor_email/tests/bootstrap.php new file mode 100644 index 000000000000..93a0859c8002 --- /dev/null +++ b/apps/twofactor_email/tests/bootstrap.php @@ -0,0 +1,13 @@ + + * @copyright Christoph Wurst 2016 + */ + +require_once __DIR__ . '/../../../tests/bootstrap.php'; +require_once __DIR__ . '/../appinfo/autoload.php'; diff --git a/core/Application.php b/core/Application.php index a835dc7fbb26..e00f2c906dac 100644 --- a/core/Application.php +++ b/core/Application.php @@ -33,6 +33,7 @@ use OC\Core\Controller\LoginController; use OC\Core\Controller\LostController; use OC\Core\Controller\TokenController; +use OC\Core\Controller\TwoFactorChallengeController; use OC\Core\Controller\UserController; use OC_Defaults; use OCP\AppFramework\App; @@ -101,9 +102,19 @@ public function __construct(array $urlParams=array()){ $c->query('Config'), $c->query('Session'), $c->query('UserSession'), - $c->query('URLGenerator') + $c->query('URLGenerator'), + $c->query('TwoFactorAuthManager') ); }); + $container->registerService('TwoFactorChallengeController', function (SimpleContainer $c) { + return new TwoFactorChallengeController( + $c->query('AppName'), + $c->query('Request'), + $c->query('TwoFactorAuthManager'), + $c->query('UserSession'), + $c->query('Session'), + $c->query('URLGenerator')); + }); $container->registerService('TokenController', function(SimpleContainer $c) { return new TokenController( $c->query('AppName'), @@ -168,6 +179,9 @@ public function __construct(array $urlParams=array()){ $container->registerService('DefaultEmailAddress', function() { return Util::getDefaultEmailAddress('lostpassword-noreply'); }); + $container->registerService('TwoFactorAuthManager', function(SimpleContainer $c) { + return $c->query('ServerContainer')->getTwoFactorAuthManager(); + }); } } diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 59d40ca14e2a..ea857bb57dfd 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -23,7 +23,7 @@ namespace OC\Core\Controller; -use OC; +use OC\Authentication\TwoFactorAuth\Manager; use OC\User\Session; use OC_App; use OC_Util; @@ -54,6 +54,9 @@ class LoginController extends Controller { /** @var IURLGenerator */ private $urlGenerator; + /** @var Manager */ + private $twoFactorManager; + /** * @param string $appName * @param IRequest $request @@ -62,15 +65,17 @@ class LoginController extends Controller { * @param ISession $session * @param Session $userSession * @param IURLGenerator $urlGenerator + * @param Manager $twoFactorManager */ function __construct($appName, IRequest $request, IUserManager $userManager, IConfig $config, ISession $session, - Session $userSession, IURLGenerator $urlGenerator) { + Session $userSession, IURLGenerator $urlGenerator, Manager $twoFactorManager) { parent::__construct($appName, $request); $this->userManager = $userManager; $this->config = $config; $this->session = $session; $this->userSession = $userSession; $this->urlGenerator = $urlGenerator; + $this->twoFactorManager = $twoFactorManager; } /** @@ -167,6 +172,7 @@ public function showLoginForm($user, $redirect_url, $remember_login) { */ public function tryLogin($user, $password, $redirect_url) { // TODO: Add all the insane error handling + /* @var $loginResult IUser */ $loginResult = $this->userManager->checkPassword($user, $password); if ($loginResult === false) { $users = $this->userManager->getByEmail($user); @@ -185,6 +191,12 @@ public function tryLogin($user, $password, $redirect_url) { return new RedirectResponse($this->urlGenerator->linkToRoute('core.login.showLoginForm', $args)); } $this->userSession->createSessionToken($this->request, $loginResult->getUID(), $password); + + if ($this->twoFactorManager->isTwoFactorAuthenticated($loginResult)) { + $this->twoFactorManager->prepareTwoFactorLogin($loginResult); + return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge')); + } + if (!is_null($redirect_url) && $this->userSession->isLoggedIn()) { $location = $this->urlGenerator->getAbsoluteURL(urldecode($redirect_url)); // Deny the redirect if the URL contains a @ diff --git a/core/Controller/TwoFactorChallengeController.php b/core/Controller/TwoFactorChallengeController.php new file mode 100644 index 000000000000..f86663e1aa1a --- /dev/null +++ b/core/Controller/TwoFactorChallengeController.php @@ -0,0 +1,134 @@ + + * + * @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\Authentication\TwoFactorAuth\Manager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\IUserSession; + +class TwoFactorChallengeController extends Controller { + + /** @var Manager */ + private $twoFactorManager; + + /** @var IUserSession */ + private $userSession; + + /** @var ISession */ + private $session; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** + * @param string $appName + * @param IRequest $request + * @param Manager $twoFactorManager + * @param IUserSession $userSession + * @param ISession $session + * @param IURLGenerator $urlGenerator + */ + public function __construct($appName, IRequest $request, Manager $twoFactorManager, IUserSession $userSession, + ISession $session, IURLGenerator $urlGenerator) { + parent::__construct($appName, $request); + $this->twoFactorManager = $twoFactorManager; + $this->userSession = $userSession; + $this->session = $session; + $this->urlGenerator = $urlGenerator; + } + + /** + * @NoCSRFRequired + * @PublicPage + * + * @return TemplateResponse + */ + public function selectChallenge() { + $user = $this->userSession->getUser(); + $providers = $this->twoFactorManager->getProviders($user); + + $data = [ + 'providers' => $providers, + ]; + return new TemplateResponse($this->appName, 'twofactorselectchallenge', $data, 'guest'); + } + + /** + * @NoCSRFRequired + * @PublicPage + * @UseSession + * + * @param string $challengeProviderId + * @return TemplateResponse + */ + public function showChallenge($challengeProviderId) { + $user = $this->userSession->getUser(); + $provider = $this->twoFactorManager->getProvider($user, $challengeProviderId); + if (is_null($provider)) { + return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge')); + } + + if ($this->session->exists('two_factor_auth_error')) { + $this->session->remove('two_factor_auth_error'); + $error = true; + } else { + $error = false; + } + $data = [ + 'error' => $error, + 'provider' => $provider, + 'template' => $provider->getTemplate($user)->fetchPage(), + ]; + return new TemplateResponse($this->appName, 'twofactorshowchallenge', $data, 'guest'); + } + + /** + * @NoCSRFRequired + * @PublicPage + * @UseSession + * + * @param int $challengeProviderId + * @param string $challenge + * @return RedirectResponse + */ + public function solveChallenge($challengeProviderId, $challenge) { + $user = $this->userSession->getUser(); + $provider = $this->twoFactorManager->getProvider($user, $challengeProviderId); + if (is_null($provider)) { + return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge')); + } + + if ($this->twoFactorManager->verifyChallenge($challengeProviderId, $user, $challenge)) { + return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index')); + } + + $this->session->set('two_factor_auth_error', true); + return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.showChallenge', ['challengeProviderId' => $provider->getId()])); + } + +} diff --git a/core/Middleware/TwoFactorMiddleware.php b/core/Middleware/TwoFactorMiddleware.php new file mode 100644 index 000000000000..5f03bd7649c8 --- /dev/null +++ b/core/Middleware/TwoFactorMiddleware.php @@ -0,0 +1,109 @@ + + * + * @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\Middleware; + +use Exception; +use OC\Authentication\Exceptions\TwoFactorAuthRequiredException; +use OC\Authentication\Exceptions\UserAlreadyLoggedInException; +use OC\Authentication\TwoFactorAuth\Manager; +use OC\Core\Controller\TwoFactorChallengeController; +use OC\User\Session; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Middleware; +use OCP\ISession; +use OCP\IURLGenerator; + +class TwoFactorMiddleware extends Middleware { + + /** @var Manager */ + private $twoFactorManager; + + /** @var Session */ + private $userSession; + + /** @var ISession */ + private $session; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** + * @param Manager $twoFactorManager + * @param Session $userSession + * @param ISession $session + * @param IURLGenerator $urlGenerator + */ + public function __construct(Manager $twoFactorManager, Session $userSession, ISession $session, + IURLGenerator $urlGenerator) { + $this->twoFactorManager = $twoFactorManager; + $this->userSession = $userSession; + $this->session = $session; + $this->urlGenerator = $urlGenerator; + } + + /** + * @param Controller $controller + * @param string $methodName + */ + public function beforeController($controller, $methodName) { + if ($this->userSession->isLoggedIn()) { + $user = $this->userSession->getUser(); + + if ($this->twoFactorManager->isTwoFactorAuthenticated($user)) { + $this->checkTwoFactor($controller, $methodName); + } + } + // TODO: force login if controller != LoginController + // TODO: dont check/enforce 2FA if a auth token is used + // TODO: throw exceptions if necessary + } + + private function checkTwoFactor($controller, $methodName) { + // If two-factor auth is in progress disallow access to any controllers + // defined within "LoginController". + $needsSecondFactor = $this->twoFactorManager->needsSecondFactor(); + $twoFactor = $controller instanceof TwoFactorChallengeController; + + // Disallow access to any controller if 2FA needs to be checked + if ($needsSecondFactor && !$twoFactor) { + throw new TwoFactorAuthRequiredException(); + } + + // Allow access to the two-factor controllers only if two-factor authentication + // is in progress. + if (!$needsSecondFactor && $twoFactor) { + throw new UserAlreadyLoggedInException(); + } + } + + public function afterException($controller, $methodName, Exception $exception) { + if ($exception instanceof TwoFactorAuthRequiredException) { + return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge')); + } + if ($exception instanceof UserAlreadyLoggedInException) { + return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index')); + } + } + +} diff --git a/core/routes.php b/core/routes.php index 70909352000c..053c5d888a76 100644 --- a/core/routes.php +++ b/core/routes.php @@ -46,6 +46,9 @@ ['name' => 'login#showLoginForm', 'url' => '/login', 'verb' => 'GET'], ['name' => 'login#logout', 'url' => '/logout', 'verb' => 'GET'], ['name' => 'token#generateToken', 'url' => '/token/generate', 'verb' => 'POST'], + ['name' => 'TwoFactorChallenge#selectChallenge', 'url' => '/login/selectchallenge', 'verb' => 'GET'], + ['name' => 'TwoFactorChallenge#showChallenge', 'url' => '/login/challenge/{challengeProviderId}', 'verb' => 'GET'], + ['name' => 'TwoFactorChallenge#solveChallenge', 'url' => '/login/challenge/{challengeProviderId}', 'verb' => 'POST'], ], ]); diff --git a/core/templates/twofactorselectchallenge.php b/core/templates/twofactorselectchallenge.php new file mode 100644 index 000000000000..23ee53396549 --- /dev/null +++ b/core/templates/twofactorselectchallenge.php @@ -0,0 +1,10 @@ +t('Enhanced security has been enabled for your account. Please authenticate using a second factor.')) ?> +
    + +
  1. + + getDisplayName()) ?> + +
  2. + +
diff --git a/core/templates/twofactorshowchallenge.php b/core/templates/twofactorshowchallenge.php new file mode 100644 index 000000000000..ce71cfaa5e87 --- /dev/null +++ b/core/templates/twofactorshowchallenge.php @@ -0,0 +1,16 @@ + + +

getDisplayName()); ?>

+ +t('An error occured while verifying the token')); ?> + + diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index 69e5334774e5..37cfb83012cd 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -301,7 +301,7 @@ public function getAppsNeedingUpgrade($ocVersion) { * * @param string $appId app id * - * @return array app iinfo + * @return array app info * * @internal */ diff --git a/lib/private/App/InfoParser.php b/lib/private/App/InfoParser.php index e9456550206e..7e6e5df54110 100644 --- a/lib/private/App/InfoParser.php +++ b/lib/private/App/InfoParser.php @@ -92,6 +92,9 @@ public function parse($file) { if (!array_key_exists('background-jobs', $array)) { $array['background-jobs'] = []; } + if (!array_key_exists('two-factor-providers', $array)) { + $array['two-factor-providers'] = []; + } if (array_key_exists('documentation', $array) && is_array($array['documentation'])) { foreach ($array['documentation'] as $key => $url) { diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 439b631b50f4..6a81b22effad 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -31,15 +31,16 @@ namespace OC\AppFramework\DependencyInjection; use OC; +use OC\AppFramework\Core\API; use OC\AppFramework\Http; use OC\AppFramework\Http\Dispatcher; use OC\AppFramework\Http\Output; -use OC\AppFramework\Core\API; use OC\AppFramework\Middleware\MiddlewareDispatcher; -use OC\AppFramework\Middleware\Security\SecurityMiddleware; use OC\AppFramework\Middleware\Security\CORSMiddleware; +use OC\AppFramework\Middleware\Security\SecurityMiddleware; use OC\AppFramework\Middleware\SessionMiddleware; use OC\AppFramework\Utility\SimpleContainer; +use OC\Core\Middleware\TwoFactorMiddleware; use OCP\AppFramework\IApi; use OCP\AppFramework\IAppContainer; @@ -357,11 +358,20 @@ public function __construct($appName, $urlParams = array()){ ); }); + $this->registerService('TwoFactorMiddleware', function (SimpleContainer $c) use ($app) { + $twoFactorManager = $c->getServer()->getTwoFactorAuthManager(); + $userSession = $app->getServer()->getUserSession(); + $session = $app->getServer()->getSession(); + $urlGenerator = $app->getServer()->getURLGenerator(); + return new TwoFactorMiddleware($twoFactorManager, $userSession, $session, $urlGenerator); + }); + $middleWares = &$this->middleWares; $this->registerService('MiddlewareDispatcher', function($c) use (&$middleWares) { $dispatcher = new MiddlewareDispatcher(); $dispatcher->registerMiddleware($c['CORSMiddleware']); $dispatcher->registerMiddleware($c['SecurityMiddleware']); + $dispatcher->registerMiddleWare($c['TwoFactorMiddleware']); foreach($middleWares as $middleWare) { $dispatcher->registerMiddleware($c[$middleWare]); diff --git a/lib/private/Authentication/Exceptions/LoginRequiredException.php b/lib/private/Authentication/Exceptions/LoginRequiredException.php new file mode 100644 index 000000000000..1c388cc1e3f7 --- /dev/null +++ b/lib/private/Authentication/Exceptions/LoginRequiredException.php @@ -0,0 +1,29 @@ + + * + * @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\Authentication\Exceptions; + +use Exception; + +class LoginRequiredException extends Exception { + +} diff --git a/lib/private/Authentication/Exceptions/TwoFactorAuthRequiredException.php b/lib/private/Authentication/Exceptions/TwoFactorAuthRequiredException.php new file mode 100644 index 000000000000..dc9f8937504b --- /dev/null +++ b/lib/private/Authentication/Exceptions/TwoFactorAuthRequiredException.php @@ -0,0 +1,29 @@ + + * + * @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\Authentication\Exceptions; + +use Exception; + +class TwoFactorAuthRequiredException extends Exception { + +} diff --git a/lib/private/Authentication/Exceptions/UserAlreadyLoggedInException.php b/lib/private/Authentication/Exceptions/UserAlreadyLoggedInException.php new file mode 100644 index 000000000000..28a46323f020 --- /dev/null +++ b/lib/private/Authentication/Exceptions/UserAlreadyLoggedInException.php @@ -0,0 +1,29 @@ + + * + * @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\Authentication\Exceptions; + +use Exception; + +class UserAlreadyLoggedInException extends Exception { + +} diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php new file mode 100644 index 000000000000..3d4cab46af73 --- /dev/null +++ b/lib/private/Authentication/TwoFactorAuth/Manager.php @@ -0,0 +1,141 @@ + + * + * @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\Authentication\TwoFactorAuth; + +use OC; +use OC\App\AppManager; +use OCP\AppFramework\QueryException; +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\ISession; +use OCP\IUser; + +class Manager { + + const SESSION_UID_KEY = 'two_factor_auth_uid'; + + /** @var AppManager */ + private $appManager; + + /** @var ISession */ + private $session; + + /** + * @param AppManager $appManager + * @param ISession $session + */ + public function __construct(AppManager $appManager, ISession $session) { + $this->appManager = $appManager; + $this->session = $session; + } + + /** + * Determine whether the user must provide a second factor challenge + * + * @param IUser $user + * @return boolean + */ + public function isTwoFactorAuthenticated(IUser $user) { + return count($this->getProviders($user)) > 0; + } + + /** + * Get a 2FA provider by its ID + * + * @param IUser $user + * @param string $challengeProviderId + * @return IProvider|null + */ + public function getProvider(IUser $user, $challengeProviderId) { + $providers = $this->getProviders($user); + return $providers[$challengeProviderId] ? : null; + } + + /** + * Get the list of 2FA providers for the given user + * + * @param IUser $user + * @return IProvider[] + */ + public function getProviders(IUser $user) { + $allApps = $this->appManager->getEnabledAppsForUser($user); + $providers = []; + + foreach ($allApps as $appId) { + $info = $this->appManager->getAppInfo($appId); + $providerClasses = $info['two-factor-providers']; + foreach ($providerClasses as $class) { + try { + $provider = OC::$server->query($class); + $providers[$provider->getId()] = $provider; + } catch (QueryException $exc) { + + } + } + } + + return array_filter($providers, function ($provider) use ($user) { + /* @var $provider IProvider */ + return $provider->isTwoFactorAuthEnforcedForUser($user); + }); + } + + /** + * Verify the given challenge + * + * @param string $providerId + * @param IUser $user + * @param string $challenge + * @return boolean + */ + public function verifyChallenge($providerId, IUser $user, $challenge) { + $provider = $this->getProvider($user, $providerId); + if (is_null($provider)) { + return false; + } + + $result = $provider->verifyChallenge($user, $challenge); + if ($result) { + $this->session->remove(self::SESSION_UID_KEY); + } + return $result; + } + + /** + * Check if the currently logged in user needs to pass 2FA + * + * @return boolean + */ + public function needsSecondFactor() { + return $this->session->exists(self::SESSION_UID_KEY); + } + + /** + * Prepare the 2FA login (set session value) + * + * @param IUser $user + */ + public function prepareTwoFactorLogin(IUser $user) { + $this->session->set(self::SESSION_UID_KEY, $user->getUID()); + } + +} diff --git a/lib/private/Server.php b/lib/private/Server.php index a6f1425d5452..153a7da5fc45 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -277,6 +277,11 @@ public function __construct($webRoot, \OC\Config $config) { }); return $userSession; }); + + $this->registerService('\OC\Authentication\TwoFactorAuth\Manager', function (Server $c) { + return new \OC\Authentication\TwoFactorAuth\Manager($c->getAppManager(), $c->getSession()); + }); + $this->registerService('NavigationManager', function ($c) { return new \OC\NavigationManager(); }); @@ -850,6 +855,13 @@ public function setSession(\OCP\ISession $session) { return $this->query('UserSession')->setSession($session); } + /** + * @return \OC\Authentication\TwoFactorAuth\Manager + */ + public function getTwoFactorAuthManager() { + return $this->query('\OC\Authentication\TwoFactorAuth\Manager'); + } + /** * @return \OC\NavigationManager */ diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index c9f42d7e414b..f56ba78ed682 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -350,6 +350,7 @@ protected function prepareUserLogin() { /** * Tries to login the user with HTTP Basic Authentication * + * @todo do not allow basic auth if the user is 2FA enforced * @param IRequest $request * @return boolean if the login was successful */ diff --git a/lib/public/Authentication/TwoFactorAuth/IProvider.php b/lib/public/Authentication/TwoFactorAuth/IProvider.php new file mode 100644 index 000000000000..f591c7c67e44 --- /dev/null +++ b/lib/public/Authentication/TwoFactorAuth/IProvider.php @@ -0,0 +1,80 @@ + + * + * @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 OCP\Authentication\TwoFactorAuth; + +use OCP\IUser; +use OCP\Template; + +/** + * @since 9.1.0 + */ +interface IProvider { + + /** + * Get unique identifier of this 2FA provider + * + * @since 9.1.0 + * + * @return string + */ + public function getId(); + + /** + * Get the display name for selecting the 2FA provider + * + * @since 9.1.0 + * + * @return string + */ + public function getDisplayName(); + + /** + * Get the template for rending the 2FA provider view + * + * @since 9.1.0 + * + * @param IUser $user + * @return Template + */ + public function getTemplate(IUser $user); + + /** + * Verify the given challenge + * + * @since 9.1.0 + * + * @param IUser $user + * @param string $challenge + */ + public function verifyChallenge(IUser $user, $challenge); + + /** + * Decides whether 2FA is enforced for the given user + * + * @since 9.1.0 + * + * @param IUser $user + * @return boolean + */ + public function isTwoFactorAuthEnforcedForUser(IUser $user); +} diff --git a/tests/core/controller/LoginControllerTest.php b/tests/core/controller/LoginControllerTest.php index 139d48ad7dac..c81c7fab41a6 100644 --- a/tests/core/controller/LoginControllerTest.php +++ b/tests/core/controller/LoginControllerTest.php @@ -21,6 +21,7 @@ namespace OC\Core\Controller; +use OC\Authentication\TwoFactorAuth\Manager; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\IConfig; @@ -46,6 +47,8 @@ class LoginControllerTest extends TestCase { private $userSession; /** @var IURLGenerator */ private $urlGenerator; + /** @var Manager */ + private $twoFactorManager; public function setUp() { parent::setUp(); @@ -57,6 +60,9 @@ public function setUp() { ->disableOriginalConstructor() ->getMock(); $this->urlGenerator = $this->getMock('\\OCP\\IURLGenerator'); + $this->twoFactorManager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager') + ->disableOriginalConstructor() + ->getMock(); $this->loginController = new LoginController( 'core', @@ -65,7 +71,8 @@ public function setUp() { $this->config, $this->session, $this->userSession, - $this->urlGenerator + $this->urlGenerator, + $this->twoFactorManager ); } @@ -298,6 +305,10 @@ public function testLoginWithValidCredentials() { $this->userSession->expects($this->once()) ->method('createSessionToken') ->with($this->request, $user->getUID(), $password); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->with($user) + ->will($this->returnValue(false)); $this->urlGenerator->expects($this->once()) ->method('linkToRoute') ->with('files.view.index') @@ -335,5 +346,32 @@ public function testLoginWithValidCredentialsAndRedirectUrl() { $expected = new \OCP\AppFramework\Http\RedirectResponse(urldecode($redirectUrl)); $this->assertEquals($expected, $this->loginController->tryLogin($user->getUID(), $password, $originalUrl)); } + + public function testLoginWithTwoFactorEnforced() { + $user = $this->getMock('\OCP\IUser'); + $password = 'secret'; + $challengeUrl = 'challenge/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->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->with($user) + ->will($this->returnValue(true)); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with($user); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('core.TwoFactorChallenge.selectChallenge') + ->will($this->returnValue($challengeUrl)); + + $expected = new \OCP\AppFramework\Http\RedirectResponse($challengeUrl); + $this->assertEquals($expected, $this->loginController->tryLogin($user, $password, null)); + } } diff --git a/tests/core/controller/TwoFactorChallengeControllerTest.php b/tests/core/controller/TwoFactorChallengeControllerTest.php new file mode 100644 index 000000000000..c65625ec329d --- /dev/null +++ b/tests/core/controller/TwoFactorChallengeControllerTest.php @@ -0,0 +1,219 @@ + + * + * @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 Test\TestCase; + +class TwoFactorChallengeControllerTest extends TestCase { + + private $request; + private $twoFactorManager; + private $userSession; + private $session; + private $urlGenerator; + + /** TwoFactorChallengeController */ + private $controller; + + protected function setUp() { + parent::setUp(); + + $this->request = $this->getMock('\OCP\IRequest'); + $this->twoFactorManager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager') + ->disableOriginalConstructor() + ->getMock(); + $this->userSession = $this->getMock('\OCP\IUserSession'); + $this->session = $this->getMock('\OCP\ISession'); + $this->urlGenerator = $this->getMock('\OCP\IURLGenerator'); + + $this->controller = new TwoFactorChallengeController( + 'core', $this->request, $this->twoFactorManager, $this->userSession, $this->session, $this->urlGenerator + ); + } + + public function testSelectChallenge() { + $user = $this->getMock('\OCP\IUser'); + $providers = [ + 'prov1', + 'prov2', + ]; + + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $this->twoFactorManager->expects($this->once()) + ->method('getProviders') + ->with($user) + ->will($this->returnValue($providers)); + + $expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorselectchallenge', [ + 'providers' => $providers, + ], 'guest'); + + $this->assertEquals($expected, $this->controller->selectChallenge()); + } + + public function testShowChallenge() { + $user = $this->getMock('\OCP\IUser'); + $provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider') + ->disableOriginalConstructor() + ->getMock(); + $tmpl = $this->getMockBuilder('\OCP\Template') + ->disableOriginalConstructor() + ->getMock(); + + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $this->twoFactorManager->expects($this->once()) + ->method('getProvider') + ->with($user, 'myprovider') + ->will($this->returnValue($provider)); + + $this->session->expects($this->once()) + ->method('exists') + ->with('two_factor_auth_error') + ->will($this->returnValue(true)); + $this->session->expects($this->once()) + ->method('remove') + ->with('two_factor_auth_error'); + $provider->expects($this->once()) + ->method('getTemplate') + ->with($user) + ->will($this->returnValue($tmpl)); + $tmpl->expects($this->once()) + ->method('fetchPage') + ->will($this->returnValue('')); + + $expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorshowchallenge', [ + 'error' => true, + 'provider' => $provider, + 'template' => '', + ], 'guest'); + + $this->assertEquals($expected, $this->controller->showChallenge('myprovider')); + } + + public function testShowInvalidChallenge() { + $user = $this->getMock('\OCP\IUser'); + + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $this->twoFactorManager->expects($this->once()) + ->method('getProvider') + ->with($user, 'myprovider') + ->will($this->returnValue(null)); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('core.TwoFactorChallenge.selectChallenge') + ->will($this->returnValue('select/challenge/url')); + + $expected = new \OCP\AppFramework\Http\RedirectResponse('select/challenge/url'); + + $this->assertEquals($expected, $this->controller->showChallenge('myprovider')); + } + + public function testSolveChallenge() { + $user = $this->getMock('\OCP\IUser'); + $provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider') + ->disableOriginalConstructor() + ->getMock(); + + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $this->twoFactorManager->expects($this->once()) + ->method('getProvider') + ->with($user, 'myprovider') + ->will($this->returnValue($provider)); + + $this->twoFactorManager->expects($this->once()) + ->method('verifyChallenge') + ->with('myprovider', $user, 'token') + ->will($this->returnValue(true)); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('files.view.index') + ->will($this->returnValue('files/index/url')); + + $expected = new \OCP\AppFramework\Http\RedirectResponse('files/index/url'); + $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token')); + } + + public function testSolveChallengeInvalidProvider() { + $user = $this->getMock('\OCP\IUser'); + + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $this->twoFactorManager->expects($this->once()) + ->method('getProvider') + ->with($user, 'myprovider') + ->will($this->returnValue(null)); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('core.TwoFactorChallenge.selectChallenge') + ->will($this->returnValue('select/challenge/url')); + + $expected = new \OCP\AppFramework\Http\RedirectResponse('select/challenge/url'); + + $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token')); + } + + public function testSolveInvalidChallenge() { + $user = $this->getMock('\OCP\IUser'); + $provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider') + ->disableOriginalConstructor() + ->getMock(); + + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $this->twoFactorManager->expects($this->once()) + ->method('getProvider') + ->with($user, 'myprovider') + ->will($this->returnValue($provider)); + + $this->twoFactorManager->expects($this->once()) + ->method('verifyChallenge') + ->with('myprovider', $user, 'token') + ->will($this->returnValue(false)); + $this->session->expects($this->once()) + ->method('set') + ->with('two_factor_auth_error', true); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('core.TwoFactorChallenge.showChallenge', [ + 'challengeProviderId' => 'myprovider', + ]) + ->will($this->returnValue('files/index/url')); + $provider->expects($this->once()) + ->method('getId') + ->will($this->returnValue('myprovider')); + + $expected = new \OCP\AppFramework\Http\RedirectResponse('files/index/url'); + $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token')); + } + +} diff --git a/tests/core/middleware/TwoFactorMiddlewareTest.php b/tests/core/middleware/TwoFactorMiddlewareTest.php new file mode 100644 index 000000000000..deb9f0d62a3b --- /dev/null +++ b/tests/core/middleware/TwoFactorMiddlewareTest.php @@ -0,0 +1,153 @@ + + * + * @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\Middleware; + +use Test\TestCase; + +class TwoFactorMiddlewareTest extends TestCase { + + private $twoFactorManager; + private $userSession; + private $session; + private $urlGenerator; + + /** @var TwoFactorMiddleware */ + private $middleware; + + protected function setUp() { + parent::setUp(); + + $this->twoFactorManager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager') + ->disableOriginalConstructor() + ->getMock(); + $this->userSession = $this->getMockBuilder('\OC\User\Session') + ->disableOriginalConstructor() + ->getMock(); + $this->session = $this->getMock('\OCP\ISession'); + $this->urlGenerator = $this->getMock('\OCP\IURLGenerator'); + + $this->middleware = new TwoFactorMiddleware($this->twoFactorManager, $this->userSession, $this->session, $this->urlGenerator); + } + + public function testBeforeControllerNotLoggedIn() { + $this->userSession->expects($this->once()) + ->method('isLoggedIn') + ->will($this->returnValue(false)); + + $this->userSession->expects($this->never()) + ->method('getUser'); + + $this->middleware->beforeController(null, 'index'); + } + + public function testBeforeControllerNoTwoFactorCheckNeeded() { + $user = $this->getMock('\OCP\IUser'); + + $this->userSession->expects($this->once()) + ->method('isLoggedIn') + ->will($this->returnValue(true)); + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->with($user) + ->will($this->returnValue(false)); + + $this->middleware->beforeController(null, 'index'); + } + + /** + * @expectedException \OC\Authentication\Exceptions\TwoFactorAuthRequiredException + */ + public function testBeforeControllerTwoFactorAuthRequired() { + $user = $this->getMock('\OCP\IUser'); + + $this->userSession->expects($this->once()) + ->method('isLoggedIn') + ->will($this->returnValue(true)); + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->with($user) + ->will($this->returnValue(true)); + $this->twoFactorManager->expects($this->once()) + ->method('needsSecondFactor') + ->will($this->returnValue(true)); + + $this->middleware->beforeController(null, 'index'); + } + + /** + * @expectedException \OC\Authentication\Exceptions\UserAlreadyLoggedInException + */ + public function testBeforeControllerUserAlreadyLoggedIn() { + $user = $this->getMock('\OCP\IUser'); + + $this->userSession->expects($this->once()) + ->method('isLoggedIn') + ->will($this->returnValue(true)); + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->with($user) + ->will($this->returnValue(true)); + $this->twoFactorManager->expects($this->once()) + ->method('needsSecondFactor') + ->will($this->returnValue(false)); + + $twoFactorChallengeController = $this->getMockBuilder('\OC\Core\Controller\TwoFactorChallengeController') + ->disableOriginalConstructor() + ->getMock(); + $this->middleware->beforeController($twoFactorChallengeController, 'index'); + } + + public function testAfterExceptionTwoFactorAuthRequired() { + $ex = new \OC\Authentication\Exceptions\TwoFactorAuthRequiredException(); + + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('core.TwoFactorChallenge.selectChallenge') + ->will($this->returnValue('redirect/url')); + $expected = new \OCP\AppFramework\Http\RedirectResponse('redirect/url'); + + $this->assertEquals($expected, $this->middleware->afterException(null, 'index', $ex)); + } + + public function testAfterException() { + $ex = new \OC\Authentication\Exceptions\UserAlreadyLoggedInException(); + + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('files.view.index') + ->will($this->returnValue('redirect/url')); + $expected = new \OCP\AppFramework\Http\RedirectResponse('redirect/url'); + + $this->assertEquals($expected, $this->middleware->afterException(null, 'index', $ex)); + } + +} diff --git a/tests/data/app/expected-info.json b/tests/data/app/expected-info.json index 81de5341efa1..6d8d85ab5524 100644 --- a/tests/data/app/expected-info.json +++ b/tests/data/app/expected-info.json @@ -75,5 +75,6 @@ "live-migration": [], "uninstall": [] }, - "background-jobs": [] + "background-jobs": [], + "two-factor-providers": [] } diff --git a/tests/lib/authentication/twofactorauth/managertest.php b/tests/lib/authentication/twofactorauth/managertest.php new file mode 100644 index 000000000000..2f79f857e6b6 --- /dev/null +++ b/tests/lib/authentication/twofactorauth/managertest.php @@ -0,0 +1,169 @@ + + * + * @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 Test\Authentication\TwoFactorAuth; + +use Test\TestCase; +use OC\Authentication\TwoFactorAuth\Manager; + +class ManagerTest extends TestCase { + + /** @var OCP\IUser */ + private $user; + + /** @var OC\App\AppManager */ + private $appManager; + + /** @var OCP\ISession */ + private $session; + + /** @var Manager */ + private $manager; + + /** @var \OCP\Authentication\TwoFactorAuth\IProvider */ + private $fakeProvider; + + protected function setUp() { + parent::setUp(); + + $this->user = $this->getMock('\OCP\IUser'); + $this->appManager = $this->getMockBuilder('\OC\App\AppManager') + ->disableOriginalConstructor() + ->getMock(); + $this->session = $this->getMock('\OCP\ISession'); + + $this->manager = new Manager($this->appManager, $this->session); + + $this->fakeProvider = $this->getMock('\OCP\Authentication\TwoFactorAuth\IProvider'); + $this->fakeProvider->expects($this->any()) + ->method('getId') + ->will($this->returnValue('email')); + \OC::$server->registerService('\OCA\MyCustom2faApp\FakeProvider', function() { + return $this->fakeProvider; + }); + } + + private function prepareProviders() { + $this->appManager->expects($this->once()) + ->method('getEnabledAppsForUsers') + ->with($this->user) + ->will($this->returnValue(['mycustom2faapp'])); + + $this->appManager->expects($this->once()) + ->method('getAppInfo') + ->with('mycustom2faapp') + ->will($this->returnValue([ + 'two-factor-providers' => [ + '\OCA\MyCustom2faApp\FakeProvider', + ], + ])); + } + + public function testIsTwoFactorAuthenticated() { + + } + + public function testGetProvider() { + $this->prepareProviders(); + + $this->assertSame($this->fakeProvider, $this->manager->getProvider($this->user, 'email')); + } + + public function testGetInvalidProvider() { + $this->prepareProviders(); + + $this->assertSame(null, $this->manager->getProvider($this->user, 'nonexistent')); + } + + public function testGetProviders() { + $this->prepareProviders(); + $expectedProviders = [ + $this->fakeProvider, + ]; + + $this->assertEquals($expectedProviders, $this->manager->getProviders($this->user)); + } + + public function testVerifyChallenge() { + $this->prepareProviders(); + + $challenge = 'passme'; + $this->fakeProvider->expects($this->once()) + ->method('verifyChallenge') + ->with($this->user, $challenge) + ->will($this->returnValue(true)); + $this->session->expects($this->once()) + ->method('remove') + ->with('two_factor_auth_uid'); + + $this->assertEquals(true, $this->manager->verifyChallenge('email', $this->user, $challenge)); + } + + public function testVerifyChallengeInvalidProviderId() { + $this->prepareProviders(); + + $challenge = 'passme'; + $this->fakeProvider->expects($this->never()) + ->method('verifyChallenge') + ->with($this->user, $challenge); + $this->session->expects($this->never()) + ->method('remove'); + + $this->assertEquals(false, $this->manager->verifyChallenge('dontexist', $this->user, $challenge)); + } + + public function testVerifyInvalidChallenge() { + $this->prepareProviders(); + + $challenge = 'dontpassme'; + $this->fakeProvider->expects($this->once()) + ->method('verifyChallenge') + ->with($this->user, $challenge) + ->will($this->returnValue(false)); + $this->session->expects($this->never()) + ->method('remove'); + + $this->assertEquals(false, $this->manager->verifyChallenge('email', $this->user, $challenge)); + } + + public function testNeedsSecondFactor() { + $this->session->expects($this->once()) + ->method('exists') + ->with('two_factor_auth_uid') + ->will($this->returnValue(false)); + + $this->assertequals(false, $this->manager->needsSecondFactor()); + } + + public function testPrepareTwoFactorLogin() { + $this->user->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('ferdinand')); + + $this->session->expects($this->once()) + ->method('set') + ->with('two_factor_auth_uid', 'ferdinand'); + + $this->manager->prepareTwoFactorLogin($this->user); + } + +}