Add two factor auth to core

This commit is contained in:
Christoph Wurst 2016-05-11 11:23:25 +02:00
parent dec3f9ebcb
commit dfb4d426c2
No known key found for this signature in database
GPG key ID: FEECD2543CA6EAF0
24 changed files with 1305 additions and 8 deletions

View file

@ -103,6 +103,7 @@ class Auth extends AbstractBasic {
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());

View file

@ -33,6 +33,7 @@ use OC\Core\Controller\AvatarController;
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 @@ class Application extends App {
$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 @@ class Application extends App {
$container->registerService('DefaultEmailAddress', function() {
return Util::getDefaultEmailAddress('lostpassword-noreply');
});
$container->registerService('TwoFactorAuthManager', function(SimpleContainer $c) {
return $c->query('ServerContainer')->getTwoFactorAuthManager();
});
}
}

View file

@ -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 @@ class LoginController extends Controller {
*/
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 @@ class LoginController extends Controller {
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 @

View file

@ -0,0 +1,134 @@
<?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\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 string $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()]));
}
}

View file

@ -0,0 +1,117 @@
<?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\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\AppFramework\Utility\IControllerMethodReflector;
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;
/** @var IControllerMethodReflector */
private $reflector;
/**
* @param Manager $twoFactorManager
* @param Session $userSession
* @param ISession $session
* @param IURLGenerator $urlGenerator
*/
public function __construct(Manager $twoFactorManager, Session $userSession, ISession $session,
IURLGenerator $urlGenerator, IControllerMethodReflector $reflector) {
$this->twoFactorManager = $twoFactorManager;
$this->userSession = $userSession;
$this->session = $session;
$this->urlGenerator = $urlGenerator;
$this->reflector = $reflector;
}
/**
* @param Controller $controller
* @param string $methodName
*/
public function beforeController($controller, $methodName) {
if ($this->reflector->hasAnnotation('PublicPage')) {
// Don't block public pages
return;
}
if ($this->userSession->isLoggedIn()) {
$user = $this->userSession->getUser();
if ($this->twoFactorManager->isTwoFactorAuthenticated($user)) {
$this->checkTwoFactor($controller, $methodName);
}
}
// TODO: dont check/enforce 2FA if a auth token is used
}
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'));
}
}
}

View file

@ -31,6 +31,12 @@ body {
background-size: cover;
}
.two-factor-provider {
text-align: center;
width: 100%;
display: inline-block;
}
.float-spinner {
height: 32px;
display: none;

View file

@ -46,6 +46,9 @@ $application->registerRoutes($this, [
['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'],
],
]);

View file

@ -0,0 +1,16 @@
<fieldset class="warning">
<legend><strong><?php p($l->t('Two-step verification')) ?></strong></legend>
<p><?php p($l->t('Enhanced security has been enabled for your account. Please authenticate using a second factor.')) ?></p>
</fieldset>
<fieldset class="warning">
<ul>
<?php foreach ($_['providers'] as $provider): ?>
<li>
<a class="two-factor-provider"
href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.showChallenge', ['challengeProviderId' => $provider->getId()])) ?>">
<?php p($provider->getDescription()) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</fieldset>

View file

@ -0,0 +1,19 @@
<?php
/** @var $l OC_L10N */
/** @var $_ array */
/* @var $error boolean */
$error = $_['error'];
/* @var $provider OCP\Authentication\TwoFactorAuth\IProvider */
$provider = $_['provider'];
/* @var $template string */
$template = $_['template'];
?>
<fieldset class="warning">
<legend><strong><?php p($provider->getDisplayName()); ?></strong></legend>
<p><?php p($l->t('Please authenticate using the selected factor.')) ?></p>
</fieldset>
<?php if ($error): ?>
<span class="warning"><?php p($l->t('An error occured while verifying the token')); ?></span>
<?php endif; ?>
<?php print_unescaped($template); ?>

View file

@ -301,7 +301,7 @@ class AppManager implements IAppManager {
*
* @param string $appId app id
*
* @return array app iinfo
* @return array app info
*
* @internal
*/

View file

@ -92,6 +92,9 @@ class InfoParser {
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) {

View file

@ -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,21 @@ class DIContainer extends SimpleContainer implements IAppContainer {
);
});
$this->registerService('TwoFactorMiddleware', function (SimpleContainer $c) use ($app) {
$twoFactorManager = $c->getServer()->getTwoFactorAuthManager();
$userSession = $app->getServer()->getUserSession();
$session = $app->getServer()->getSession();
$urlGenerator = $app->getServer()->getURLGenerator();
$reflector = $c['ControllerMethodReflector'];
return new TwoFactorMiddleware($twoFactorManager, $userSession, $session, $urlGenerator, $reflector);
});
$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]);

View file

@ -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 LoginRequiredException extends Exception {
}

View file

@ -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 TwoFactorAuthRequiredException extends Exception {
}

View file

@ -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 UserAlreadyLoggedInException extends Exception {
}

View file

@ -0,0 +1,141 @@
<?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\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 isset($providers[$challengeProviderId]) ? $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) {
// Provider class can not be resolved, ignore it
}
}
}
return array_filter($providers, function ($provider) use ($user) {
/* @var $provider IProvider */
return $provider->isTwoFactorAuthEnabledForUser($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());
}
}

View file

@ -275,6 +275,11 @@ class Server extends ServerContainer implements IServerContainer {
});
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();
});
@ -854,6 +859,13 @@ class Server extends ServerContainer implements IServerContainer {
return $this->query('UserSession')->setSession($session);
}
/**
* @return \OC\Authentication\TwoFactorAuth\Manager
*/
public function getTwoFactorAuthManager() {
return $this->query('\OC\Authentication\TwoFactorAuth\Manager');
}
/**
* @return \OC\NavigationManager
*/

View file

@ -332,6 +332,7 @@ class Session implements IUserSession, Emitter {
/**
* 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
*/

View file

@ -0,0 +1,93 @@
<?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 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
*
* Example: "Email"
*
* @since 9.1.0
*
* @return string
*/
public function getDisplayName();
/**
* Get the description for selecting the 2FA provider
*
* Example: "Get a token via e-mail"
*
* @since 9.1.0
*
* @return string
*/
public function getDescription();
/**
* 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 enabled for the given user
*
* @since 9.1.0
*
* @param IUser $user
* @return boolean
*/
public function isTwoFactorAuthEnabledForUser(IUser $user);
}

View file

@ -21,6 +21,7 @@
namespace Tests\Core\Controller;
use OC\Authentication\TwoFactorAuth\Manager;
use OC\Core\Controller\LoginController;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
@ -47,6 +48,8 @@ class LoginControllerTest extends TestCase {
private $userSession;
/** @var IURLGenerator */
private $urlGenerator;
/** @var Manager */
private $twoFactorManager;
public function setUp() {
parent::setUp();
@ -58,6 +61,9 @@ class LoginControllerTest extends TestCase {
->disableOriginalConstructor()
->getMock();
$this->urlGenerator = $this->getMock('\\OCP\\IURLGenerator');
$this->twoFactorManager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager')
->disableOriginalConstructor()
->getMock();
$this->loginController = new LoginController(
'core',
@ -66,7 +72,8 @@ class LoginControllerTest extends TestCase {
$this->config,
$this->session,
$this->userSession,
$this->urlGenerator
$this->urlGenerator,
$this->twoFactorManager
);
}
@ -299,6 +306,10 @@ class LoginControllerTest extends TestCase {
$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')
@ -336,5 +347,32 @@ class LoginControllerTest extends TestCase {
$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));
}
}

View file

@ -0,0 +1,219 @@
<?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 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('<html/>'));
$expected = new \OCP\AppFramework\Http\TemplateResponse('core', 'twofactorshowchallenge', [
'error' => true,
'provider' => $provider,
'template' => '<html/>',
], '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'));
}
}

View file

@ -0,0 +1,182 @@
<?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\Middleware;
use Test\TestCase;
class TwoFactorMiddlewareTest extends TestCase {
private $twoFactorManager;
private $userSession;
private $session;
private $urlGenerator;
private $reflector;
/** @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->reflector = $this->getMock('\OCP\AppFramework\Utility\IControllerMethodReflector');
$this->middleware = new TwoFactorMiddleware($this->twoFactorManager, $this->userSession, $this->session, $this->urlGenerator, $this->reflector);
}
public function testBeforeControllerNotLoggedIn() {
$this->reflector->expects($this->once())
->method('hasAnnotation')
->with('PublicPage')
->will($this->returnValue(false));
$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 testBeforeControllerPublicPage() {
$this->reflector->expects($this->once())
->method('hasAnnotation')
->with('PublicPage')
->will($this->returnValue(true));
$this->userSession->expects($this->never())
->method('isLoggedIn');
$this->middleware->beforeController(null, 'create');
}
public function testBeforeControllerNoTwoFactorCheckNeeded() {
$user = $this->getMock('\OCP\IUser');
$this->reflector->expects($this->once())
->method('hasAnnotation')
->with('PublicPage')
->will($this->returnValue(false));
$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->reflector->expects($this->once())
->method('hasAnnotation')
->with('PublicPage')
->will($this->returnValue(false));
$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->reflector->expects($this->once())
->method('hasAnnotation')
->with('PublicPage')
->will($this->returnValue(false));
$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));
}
}

View file

@ -75,5 +75,6 @@
"live-migration": [],
"uninstall": []
},
"background-jobs": []
"background-jobs": [],
"two-factor-providers": []
}

View file

@ -0,0 +1,187 @@
<?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\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\IConfig */
private $config;
/** @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->config = $this->getMock('\OCP\IConfig');
$this->manager = new Manager($this->appManager, $this->session, $this->config);
$this->fakeProvider = $this->getMock('\OCP\Authentication\TwoFactorAuth\IProvider');
$this->fakeProvider->expects($this->any())
->method('getId')
->will($this->returnValue('email'));
$this->fakeProvider->expects($this->any())
->method('isTwoFactorAuthEnabledForUser')
->will($this->returnValue(true));
\OC::$server->registerService('\OCA\MyCustom2faApp\FakeProvider', function() {
return $this->fakeProvider;
});
}
private function prepareProviders() {
$this->appManager->expects($this->any())
->method('getEnabledAppsForUser')
->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() {
$this->prepareProviders();
$user = $this->getMock('\OCP\IUser');
$user->expects($this->once())
->method('getUID')
->will($this->returnValue('user123'));
$this->config->expects($this->once())
->method('getUserValue')
->with('user123', 'core', 'two_factor_auth_disabled', 0)
->will($this->returnValue(0));
$this->assertTrue($this->manager->isTwoFactorAuthenticated($user));
}
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 = [
'email' => $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);
}
}