From 579162d7b94465d5041a7bf1229f68e6d92d7b58 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Fri, 5 Apr 2019 18:21:08 +0200 Subject: [PATCH] Allow 2FA to be setup on first login Once 2FA is enforced for a user and they have no 2FA setup yet this will now prompt them with a setup screen. Given that providers are enabled that allow setup then. Signed-off-by: Roeland Jago Douma Signed-off-by: Christoph Wurst --- .../TwoFactorChallengeController.php | 67 +++++++ core/Middleware/TwoFactorMiddleware.php | 8 +- core/routes.php | 3 + core/templates/twofactorselectchallenge.php | 15 +- core/templates/twofactorsetupchallenge.php | 16 ++ core/templates/twofactorsetupselection.php | 58 ++++++ lib/composer/composer/autoload_classmap.php | 3 + lib/composer/composer/autoload_static.php | 3 + .../Authentication/Login/TwoFactorCommand.php | 21 +- .../Authentication/TwoFactorAuth/Manager.php | 14 ++ .../TwoFactorAuth/ALoginSetupController.php | 34 ++++ .../TwoFactorAuth/IActivatableAtLogin.php | 43 +++++ .../TwoFactorAuth/ILoginSetupProvider.php | 41 ++++ .../TwoFactorChallengeControllerTest.php | 127 +++++++++++- .../Middleware/TwoFactorMiddlewareTest.php | 34 ++++ .../Login/TwoFactorCommandTest.php | 180 ++++++++++++++++++ .../TwoFactorAuth/ManagerTest.php | 19 ++ 17 files changed, 677 insertions(+), 9 deletions(-) create mode 100644 core/templates/twofactorsetupchallenge.php create mode 100644 core/templates/twofactorsetupselection.php create mode 100644 lib/public/Authentication/TwoFactorAuth/ALoginSetupController.php create mode 100644 lib/public/Authentication/TwoFactorAuth/IActivatableAtLogin.php create mode 100644 lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php diff --git a/core/Controller/TwoFactorChallengeController.php b/core/Controller/TwoFactorChallengeController.php index 7405e66cdfc..e2a0b5423ab 100644 --- a/core/Controller/TwoFactorChallengeController.php +++ b/core/Controller/TwoFactorChallengeController.php @@ -32,6 +32,7 @@ use OC_Util; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\StandaloneTemplateResponse; +use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin; use OCP\Authentication\TwoFactorAuth\IProvider; use OCP\Authentication\TwoFactorAuth\IProvidesCustomCSP; use OCP\Authentication\TwoFactorAuth\TwoFactorException; @@ -107,6 +108,7 @@ class TwoFactorChallengeController extends Controller { $providerSet = $this->twoFactorManager->getProviderSet($user); $allProviders = $providerSet->getProviders(); list($providers, $backupProvider) = $this->splitProvidersAndBackupCodes($allProviders); + $setupProviders = $this->twoFactorManager->getLoginSetupProviders($user); $data = [ 'providers' => $providers, @@ -114,6 +116,7 @@ class TwoFactorChallengeController extends Controller { 'providerMissing' => $providerSet->isProviderMissing(), 'redirect_url' => $redirect_url, 'logout_url' => $this->getLogoutUrl(), + 'hasSetupProviders' => !empty($setupProviders), ]; return new StandaloneTemplateResponse($this->appName, 'twofactorselectchallenge', $data, 'guest'); } @@ -131,6 +134,7 @@ class TwoFactorChallengeController extends Controller { $user = $this->userSession->getUser(); $providerSet = $this->twoFactorManager->getProviderSet($user); $provider = $providerSet->getProvider($challengeProviderId); + if (is_null($provider)) { return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge')); } @@ -209,4 +213,67 @@ class TwoFactorChallengeController extends Controller { ])); } + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function setupProviders() { + $user = $this->userSession->getUser(); + $setupProviders = $this->twoFactorManager->getLoginSetupProviders($user); + + $data = [ + 'providers' => $setupProviders, + 'logout_url' => $this->getLogoutUrl(), + ]; + + $response = new StandaloneTemplateResponse($this->appName, 'twofactorsetupselection', $data, 'guest'); + return $response; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function setupProvider(string $providerId) { + $user = $this->userSession->getUser(); + $providers = $this->twoFactorManager->getLoginSetupProviders($user); + + $provider = null; + foreach ($providers as $p) { + if ($p->getId() === $providerId) { + $provider = $p; + break; + } + } + + if ($provider === null) { + return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge')); + } + + /** @var IActivatableAtLogin $provider */ + $tmpl = $provider->getLoginSetup($user)->getBody(); + $data = [ + 'provider' => $provider, + 'logout_url' => $this->getLogoutUrl(), + 'template' => $tmpl->fetchPage(), + ]; + $response = new StandaloneTemplateResponse($this->appName, 'twofactorsetupchallenge', $data, 'guest'); + return $response; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * @todo handle the extreme edge case of an invalid provider ID and redirect to the provider selection page + */ + public function confirmProviderSetup(string $providerId) { + return new RedirectResponse($this->urlGenerator->linkToRoute( + 'core.TwoFactorChallenge.showChallenge', + [ + 'challengeProviderId' => $providerId, + ] + )); + } + } diff --git a/core/Middleware/TwoFactorMiddleware.php b/core/Middleware/TwoFactorMiddleware.php index 167545b0df9..7b32c0dd895 100644 --- a/core/Middleware/TwoFactorMiddleware.php +++ b/core/Middleware/TwoFactorMiddleware.php @@ -36,6 +36,7 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Middleware; use OCP\AppFramework\Utility\IControllerMethodReflector; +use OCP\Authentication\TwoFactorAuth\ALoginSetupController; use OCP\IRequest; use OCP\ISession; use OCP\IURLGenerator; @@ -87,6 +88,12 @@ class TwoFactorMiddleware extends Middleware { return; } + if ($controller instanceof ALoginSetupController + && $this->userSession->getUser() !== null + && $this->twoFactorManager->needsSecondFactor($this->userSession->getUser())) { + return; + } + if ($controller instanceof LoginController && $methodName === 'logout') { // Don't block the logout page, to allow canceling the 2FA return; @@ -95,7 +102,6 @@ class TwoFactorMiddleware extends Middleware { if ($this->userSession->isLoggedIn()) { $user = $this->userSession->getUser(); - if ($this->session->exists('app_password') || $this->twoFactorManager->isTwoFactorAuthenticated($user)) { $this->checkTwoFactor($controller, $methodName, $user); } else if ($controller instanceof TwoFactorChallengeController) { diff --git a/core/routes.php b/core/routes.php index 1544fd67e07..79db0c6ccdf 100644 --- a/core/routes.php +++ b/core/routes.php @@ -67,6 +67,9 @@ $application->registerRoutes($this, [ ['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'], + ['name' => 'TwoFactorChallenge#setupProviders', 'url' => 'login/setupchallenge', 'verb' => 'GET'], + ['name' => 'TwoFactorChallenge#setupProvider', 'url' => 'login/setupchallenge/{providerId}', 'verb' => 'GET'], + ['name' => 'TwoFactorChallenge#confirmProviderSetup', 'url' => 'login/setupchallenge/{providerId}', 'verb' => 'POST'], ['name' => 'OCJS#getConfig', 'url' => '/core/js/oc.js', 'verb' => 'GET'], ['name' => 'Preview#getPreviewByFileId', 'url' => '/core/preview', 'verb' => 'GET'], ['name' => 'Preview#getPreview', 'url' => '/core/preview.png', 'verb' => 'GET'], diff --git a/core/templates/twofactorselectchallenge.php b/core/templates/twofactorselectchallenge.php index 65691f5857d..8508039268e 100644 --- a/core/templates/twofactorselectchallenge.php +++ b/core/templates/twofactorselectchallenge.php @@ -15,9 +15,20 @@ $noProviders = empty($_['providers']);

- t('Two-factor authentication is enforced but has not been configured on your account. Contact your admin for assistance.')) ?> + + t('Two-factor authentication is enforced but has not been configured on your account. Contact your admin for assistance.')) ?> + + t('Two-factor authentication is enforced but has not been configured on your account. Please continue to setup two-factor authentication.')) ?> + + t('Set up two-factor authentication')) ?> + + - t('Two-factor authentication is enforced but has not been configured on your account. Use one of your backup codes to log in or contact your admin for assistance.')) ?> + t('Two-factor authentication is enforced but has not been configured on your account. Use one of your backup codes to log in or contact your admin for assistance.')) ?>

diff --git a/core/templates/twofactorsetupchallenge.php b/core/templates/twofactorsetupchallenge.php new file mode 100644 index 00000000000..9c182db1715 --- /dev/null +++ b/core/templates/twofactorsetupchallenge.php @@ -0,0 +1,16 @@ + + + diff --git a/core/templates/twofactorsetupselection.php b/core/templates/twofactorsetupselection.php new file mode 100644 index 00000000000..7d689b89af7 --- /dev/null +++ b/core/templates/twofactorsetupselection.php @@ -0,0 +1,58 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +?> + diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 1d71a209dda..e8bdc89515d 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -77,8 +77,11 @@ return array( 'OCP\\Authentication\\IApacheBackend' => $baseDir . '/lib/public/Authentication/IApacheBackend.php', 'OCP\\Authentication\\LoginCredentials\\ICredentials' => $baseDir . '/lib/public/Authentication/LoginCredentials/ICredentials.php', 'OCP\\Authentication\\LoginCredentials\\IStore' => $baseDir . '/lib/public/Authentication/LoginCredentials/IStore.php', + 'OCP\\Authentication\\TwoFactorAuth\\ALoginSetupController' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/ALoginSetupController.php', + 'OCP\\Authentication\\TwoFactorAuth\\IActivatableAtLogin' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IActivatableAtLogin.php', 'OCP\\Authentication\\TwoFactorAuth\\IActivatableByAdmin' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IActivatableByAdmin.php', 'OCP\\Authentication\\TwoFactorAuth\\IDeactivatableByAdmin' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IDeactivatableByAdmin.php', + 'OCP\\Authentication\\TwoFactorAuth\\ILoginSetupProvider' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php', 'OCP\\Authentication\\TwoFactorAuth\\IPersonalProviderSettings' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IPersonalProviderSettings.php', 'OCP\\Authentication\\TwoFactorAuth\\IProvider' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IProvider.php', 'OCP\\Authentication\\TwoFactorAuth\\IProvidesCustomCSP' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IProvidesCustomCSP.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 1628c876323..d5dccfc1028 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -107,8 +107,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Authentication\\IApacheBackend' => __DIR__ . '/../../..' . '/lib/public/Authentication/IApacheBackend.php', 'OCP\\Authentication\\LoginCredentials\\ICredentials' => __DIR__ . '/../../..' . '/lib/public/Authentication/LoginCredentials/ICredentials.php', 'OCP\\Authentication\\LoginCredentials\\IStore' => __DIR__ . '/../../..' . '/lib/public/Authentication/LoginCredentials/IStore.php', + 'OCP\\Authentication\\TwoFactorAuth\\ALoginSetupController' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/ALoginSetupController.php', + 'OCP\\Authentication\\TwoFactorAuth\\IActivatableAtLogin' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IActivatableAtLogin.php', 'OCP\\Authentication\\TwoFactorAuth\\IActivatableByAdmin' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IActivatableByAdmin.php', 'OCP\\Authentication\\TwoFactorAuth\\IDeactivatableByAdmin' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IDeactivatableByAdmin.php', + 'OCP\\Authentication\\TwoFactorAuth\\ILoginSetupProvider' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php', 'OCP\\Authentication\\TwoFactorAuth\\IPersonalProviderSettings' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IPersonalProviderSettings.php', 'OCP\\Authentication\\TwoFactorAuth\\IProvider' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IProvider.php', 'OCP\\Authentication\\TwoFactorAuth\\IProvidesCustomCSP' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IProvidesCustomCSP.php', diff --git a/lib/private/Authentication/Login/TwoFactorCommand.php b/lib/private/Authentication/Login/TwoFactorCommand.php index 2825dc1763f..7a1daa0ad50 100644 --- a/lib/private/Authentication/Login/TwoFactorCommand.php +++ b/lib/private/Authentication/Login/TwoFactorCommand.php @@ -28,6 +28,7 @@ namespace OC\Authentication\Login; use function array_pop; use function count; use OC\Authentication\TwoFactorAuth\Manager; +use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; use OCP\Authentication\TwoFactorAuth\IProvider; use OCP\IURLGenerator; @@ -36,12 +37,17 @@ class TwoFactorCommand extends ALoginCommand { /** @var Manager */ private $twoFactorManager; + /** @var MandatoryTwoFactor */ + private $mandatoryTwoFactor; + /** @var IURLGenerator */ private $urlGenerator; public function __construct(Manager $twoFactorManager, + MandatoryTwoFactor $mandatoryTwoFactor, IURLGenerator $urlGenerator) { $this->twoFactorManager = $twoFactorManager; + $this->mandatoryTwoFactor = $mandatoryTwoFactor; $this->urlGenerator = $urlGenerator; } @@ -52,9 +58,18 @@ class TwoFactorCommand extends ALoginCommand { $this->twoFactorManager->prepareTwoFactorLogin($loginData->getUser(), $loginData->isRememberLogin()); - $providers = $this->twoFactorManager->getProviderSet($loginData->getUser())->getPrimaryProviders(); - if (count($providers) === 1) { - // Single provider, hence we can redirect to that provider's challenge page directly + $providerSet = $this->twoFactorManager->getProviderSet($loginData->getUser()); + $loginProviders = $this->twoFactorManager->getLoginSetupProviders($loginData->getUser()); + $providers = $providerSet->getPrimaryProviders(); + if (empty($providers) + && !$providerSet->isProviderMissing() + && !empty($loginProviders) + && $this->mandatoryTwoFactor->isEnforcedFor($loginData->getUser())) { + // No providers set up, but 2FA is enforced and setup providers are available + $url = 'core.TwoFactorChallenge.setupProviders'; + $urlParams = []; + } else if (!$providerSet->isProviderMissing() && count($providers) === 1) { + // Single provider (and no missing ones), hence we can redirect to that provider's challenge page directly /* @var $provider IProvider */ $provider = array_pop($providers); $url = 'core.TwoFactorChallenge.showChallenge'; diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php index ef95184aba7..17f8479f257 100644 --- a/lib/private/Authentication/TwoFactorAuth/Manager.php +++ b/lib/private/Authentication/TwoFactorAuth/Manager.php @@ -36,6 +36,8 @@ use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Token\IProvider as TokenProvider; use OCP\Activity\IManager; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin; +use OCP\Authentication\TwoFactorAuth\ILoginSetupProvider; use OCP\Authentication\TwoFactorAuth\IProvider; use OCP\Authentication\TwoFactorAuth\IRegistry; use OCP\IConfig; @@ -133,6 +135,18 @@ class Manager { return $providers[$challengeProviderId] ?? null; } + /** + * @param IUser $user + * @return IActivatableAtLogin[] + * @throws Exception + */ + public function getLoginSetupProviders(IUser $user): array { + $providers = $this->providerLoader->getProviders($user); + return array_filter($providers, function(IProvider $provider) { + return ($provider instanceof IActivatableAtLogin); + }); + } + /** * Check if the persistant mapping of enabled/disabled state of each available * provider is missing an entry and add it to the registry in that case. diff --git a/lib/public/Authentication/TwoFactorAuth/ALoginSetupController.php b/lib/public/Authentication/TwoFactorAuth/ALoginSetupController.php new file mode 100644 index 00000000000..8914295d615 --- /dev/null +++ b/lib/public/Authentication/TwoFactorAuth/ALoginSetupController.php @@ -0,0 +1,34 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCP\Authentication\TwoFactorAuth; + +use OCP\AppFramework\Controller; + +/** + * @since 17.0.0 + */ +abstract class ALoginSetupController extends Controller { + +} diff --git a/lib/public/Authentication/TwoFactorAuth/IActivatableAtLogin.php b/lib/public/Authentication/TwoFactorAuth/IActivatableAtLogin.php new file mode 100644 index 00000000000..22d5c6d1447 --- /dev/null +++ b/lib/public/Authentication/TwoFactorAuth/IActivatableAtLogin.php @@ -0,0 +1,43 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCP\Authentication\TwoFactorAuth; + +use OCP\IUser; + +/** + * @since 17.0.0 + */ +interface IActivatableAtLogin extends IProvider { + + /** + * @param IUser $user + * + * @return ILoginSetupProvider + * + * @since 17.0.0 + */ + public function getLoginSetup(IUser $user): ILoginSetupProvider; + +} diff --git a/lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php b/lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php new file mode 100644 index 00000000000..7815f60b66a --- /dev/null +++ b/lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php @@ -0,0 +1,41 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCP\Authentication\TwoFactorAuth; + +use OCP\Template; + +/** + * @since 17.0.0 + */ +interface ILoginSetupProvider { + + /** + * @return Template + * + * @since 17.0.0 + */ + public function getBody(): Template; + +} diff --git a/tests/Core/Controller/TwoFactorChallengeControllerTest.php b/tests/Core/Controller/TwoFactorChallengeControllerTest.php index a405914cc47..73b035a408c 100644 --- a/tests/Core/Controller/TwoFactorChallengeControllerTest.php +++ b/tests/Core/Controller/TwoFactorChallengeControllerTest.php @@ -28,6 +28,8 @@ use OC\Core\Controller\TwoFactorChallengeController; use OC_Util; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\StandaloneTemplateResponse; +use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin; +use OCP\Authentication\TwoFactorAuth\ILoginSetupProvider; use OCP\Authentication\TwoFactorAuth\IProvider; use OCP\Authentication\TwoFactorAuth\TwoFactorException; use OCP\IRequest; @@ -86,11 +88,15 @@ class TwoFactorChallengeControllerTest extends TestCase { public function testSelectChallenge() { $user = $this->getMockBuilder(IUser::class)->getMock(); - $p1 = $this->createMock(IProvider::class); + $p1 = $this->createMock(IActivatableAtLogin::class); $p1->method('getId')->willReturn('p1'); $backupProvider = $this->createMock(IProvider::class); $backupProvider->method('getId')->willReturn('backup_codes'); $providerSet = new ProviderSet([$p1, $backupProvider], true); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($user) + ->willReturn([$p1]); $this->userSession->expects($this->once()) ->method('getUser') @@ -108,7 +114,8 @@ class TwoFactorChallengeControllerTest extends TestCase { 'backupProvider' => $backupProvider, 'redirect_url' => '/some/url', 'logout_url' => 'logoutAttribute', - ], 'guest'); + 'hasSetupProviders' => true, + ], 'guest'); $this->assertEquals($expected, $this->controller->selectChallenge('/some/url')); } @@ -159,7 +166,7 @@ class TwoFactorChallengeControllerTest extends TestCase { 'template' => '', 'redirect_url' => '/re/dir/ect/url', 'error_message' => null, - ], 'guest'); + ], 'guest'); $this->assertEquals($expected, $this->controller->showChallenge('myprovider', '/re/dir/ect/url')); } @@ -323,4 +330,118 @@ class TwoFactorChallengeControllerTest extends TestCase { $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token', '/url')); } + public function testSetUpProviders() { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $provider = $this->createMock(IActivatableAtLogin::class); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($user) + ->willReturn([ + $provider, + ]); + $expected = new StandaloneTemplateResponse( + 'core', + 'twofactorsetupselection', + [ + 'providers' => [ + $provider, + ], + 'logout_url' => 'logoutAttribute', + ], + 'guest' + ); + + $response = $this->controller->setupProviders(); + + $this->assertEquals($expected, $response); + } + + public function testSetUpInvalidProvider() { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $provider = $this->createMock(IActivatableAtLogin::class); + $provider->expects($this->any()) + ->method('getId') + ->willReturn('prov1'); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($user) + ->willReturn([ + $provider, + ]); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('core.TwoFactorChallenge.selectChallenge') + ->willReturn('2fa/select/page'); + $expected = new RedirectResponse('2fa/select/page'); + + $response = $this->controller->setupProvider('prov2'); + + $this->assertEquals($expected, $response); + } + + public function testSetUpProvider() { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($user)); + $provider = $this->createMock(IActivatableAtLogin::class); + $provider->expects($this->any()) + ->method('getId') + ->willReturn('prov1'); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($user) + ->willReturn([ + $provider, + ]); + $loginSetup = $this->createMock(ILoginSetupProvider::class); + $provider->expects($this->any()) + ->method('getLoginSetup') + ->with($user) + ->willReturn($loginSetup); + $tmpl = $this->createMock(Template::class); + $loginSetup->expects($this->once()) + ->method('getBody') + ->willReturn($tmpl); + $tmpl->expects($this->once()) + ->method('fetchPage') + ->willReturn('tmpl'); + $expected = new StandaloneTemplateResponse( + 'core', + 'twofactorsetupchallenge', + [ + 'provider' => $provider, + 'logout_url' => 'logoutAttribute', + 'template' => 'tmpl', + ], + 'guest' + ); + + $response = $this->controller->setupProvider('prov1'); + + $this->assertEquals($expected, $response); + } + + public function testConfirmProviderSetup() { + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.showChallenge', + [ + 'challengeProviderId' => 'totp', + ]) + ->willReturn('2fa/select/page'); + $expected = new RedirectResponse('2fa/select/page'); + + $response = $this->controller->confirmProviderSetup('totp'); + + $this->assertEquals($expected, $response); + } + } diff --git a/tests/Core/Middleware/TwoFactorMiddlewareTest.php b/tests/Core/Middleware/TwoFactorMiddlewareTest.php index eb72b3e6796..70566760184 100644 --- a/tests/Core/Middleware/TwoFactorMiddlewareTest.php +++ b/tests/Core/Middleware/TwoFactorMiddlewareTest.php @@ -28,20 +28,35 @@ use OC\AppFramework\Http\Request; use OC\User\Session; use OCP\AppFramework\Controller; use OCP\AppFramework\Utility\IControllerMethodReflector; +use OCP\Authentication\TwoFactorAuth\ALoginSetupController; use OCP\IConfig; +use OCP\IRequest; use OCP\ISession; use OCP\IURLGenerator; use OCP\IUser; +use OCP\IUserSession; use OCP\Security\ISecureRandom; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class TwoFactorMiddlewareTest extends TestCase { + /** @var Manager|MockObject */ private $twoFactorManager; + + /** @var IUserSession|MockObject */ private $userSession; + + /** @var ISession|MockObject */ private $session; + + /** @var IURLGenerator|MockObject */ private $urlGenerator; + + /** @var IControllerMethodReflector|MockObject */ private $reflector; + + /** @var IRequest|MockObject */ private $request; /** @var TwoFactorMiddleware */ @@ -102,6 +117,25 @@ class TwoFactorMiddlewareTest extends TestCase { $this->middleware->beforeController($this->controller, 'create'); } + public function testBeforeSetupController() { + $user = $this->createMock(IUser::class); + $controller = $this->createMock(ALoginSetupController::class); + $this->reflector->expects($this->once()) + ->method('hasAnnotation') + ->with('PublicPage') + ->willReturn(false); + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $this->twoFactorManager->expects($this->once()) + ->method('needsSecondFactor') + ->willReturn(true); + $this->userSession->expects($this->never()) + ->method('isLoggedIn'); + + $this->middleware->beforeController($controller, 'create'); + } + public function testBeforeControllerNoTwoFactorCheckNeeded() { $user = $this->createMock(IUser::class); diff --git a/tests/lib/Authentication/Login/TwoFactorCommandTest.php b/tests/lib/Authentication/Login/TwoFactorCommandTest.php index a5c1c8e352b..5f91d812525 100644 --- a/tests/lib/Authentication/Login/TwoFactorCommandTest.php +++ b/tests/lib/Authentication/Login/TwoFactorCommandTest.php @@ -27,7 +27,9 @@ namespace lib\Authentication\Login; use OC\Authentication\Login\TwoFactorCommand; use OC\Authentication\TwoFactorAuth\Manager; +use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; use OC\Authentication\TwoFactorAuth\ProviderSet; +use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin; use OCP\Authentication\TwoFactorAuth\IProvider as ITwoFactorAuthProvider; use OCP\IURLGenerator; use PHPUnit\Framework\MockObject\MockObject; @@ -37,6 +39,9 @@ class TwoFactorCommandTest extends ALoginCommandTest { /** @var Manager|MockObject */ private $twoFactorManager; + /** @var MandatoryTwoFactor|MockObject */ + private $mandatoryTwoFactor; + /** @var IURLGenerator|MockObject */ private $urlGenerator; @@ -44,10 +49,12 @@ class TwoFactorCommandTest extends ALoginCommandTest { parent::setUp(); $this->twoFactorManager = $this->createMock(Manager::class); + $this->mandatoryTwoFactor = $this->createMock(MandatoryTwoFactor::class); $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->cmd = new TwoFactorCommand( $this->twoFactorManager, + $this->mandatoryTwoFactor, $this->urlGenerator ); } @@ -82,6 +89,14 @@ class TwoFactorCommandTest extends ALoginCommandTest { ->willReturn(new ProviderSet([ $provider, ], false)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(false); $provider->expects($this->once()) ->method('getId') ->willReturn('test'); @@ -101,6 +116,47 @@ class TwoFactorCommandTest extends ALoginCommandTest { $this->assertEquals('two/factor/url', $result->getRedirectUrl()); } + public function testProcessMissingProviders() { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $provider = $this->createMock(ITwoFactorAuthProvider::class); + $provider->expects($this->once()) + ->method('getId') + ->willReturn('test1'); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([ + $provider, + ], true)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(false); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.selectChallenge' + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + public function testProcessTwoActiveProviders() { $data = $this->getLoggedInLoginData(); $this->twoFactorManager->expects($this->once()) @@ -126,6 +182,122 @@ class TwoFactorCommandTest extends ALoginCommandTest { $provider1, $provider2, ], false)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(false); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.selectChallenge' + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + + public function testProcessFailingProviderAndEnforcedButNoSetupProviders() { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([], true)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(true); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.selectChallenge' + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + + public function testProcessFailingProviderAndEnforced() { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $provider = $this->createMock(IActivatableAtLogin::class); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([ + $provider, + ], true)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(true); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.selectChallenge' + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + + public function testProcessNoProvidersButEnforced() { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([], false)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(true); $this->urlGenerator->expects($this->once()) ->method('linkToRoute') ->with( @@ -156,6 +328,14 @@ class TwoFactorCommandTest extends ALoginCommandTest { ->willReturn(new ProviderSet([ $provider, ], false)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(false); $provider->expects($this->once()) ->method('getId') ->willReturn('test'); diff --git a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php index 0f09691bc1c..e836e8d316b 100644 --- a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php +++ b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php @@ -31,6 +31,7 @@ use OC\Authentication\TwoFactorAuth\ProviderLoader; use OCP\Activity\IEvent; use OCP\Activity\IManager; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin; use OCP\Authentication\TwoFactorAuth\IProvider; use OCP\Authentication\TwoFactorAuth\IRegistry; use OCP\IConfig; @@ -38,6 +39,7 @@ use OCP\ILogger; use OCP\ISession; use OCP\IUser; use PHPUnit\Framework\MockObject\MockObject; +use function reset; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Test\TestCase; @@ -297,6 +299,23 @@ class ManagerTest extends TestCase { $this->assertNull($provider); } + public function testGetLoginSetupProviders() { + $provider1 = $this->createMock(IProvider::class); + $provider2 = $this->createMock(IActivatableAtLogin::class); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($this->user) + ->willReturn([ + $provider1, + $provider2, + ]); + + $providers = $this->manager->getLoginSetupProviders($this->user); + + $this->assertCount(1, $providers); + $this->assertSame($provider2, reset($providers)); + } + public function testGetProviders() { $this->providerRegistry->expects($this->once()) ->method('getProviderStates') -- 2.39.5