Merge pull request #15304 from nextcloud/enh/2fa_setup_at_login
2FA setup during login
This commit is contained in:
commit
528eb1b223
17 changed files with 677 additions and 9 deletions
|
@ -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,
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -15,9 +15,20 @@ $noProviders = empty($_['providers']);
|
|||
<img class="two-factor-icon" src="<?php p(image_path('core', 'actions/password-white.svg')) ?>" alt="" />
|
||||
<p>
|
||||
<?php if (is_null($_['backupProvider'])): ?>
|
||||
<strong><?php p($l->t('Two-factor authentication is enforced but has not been configured on your account. Contact your admin for assistance.')) ?></strong>
|
||||
<?php if (!$_['hasSetupProviders']) { ?>
|
||||
<strong><?php p($l->t('Two-factor authentication is enforced but has not been configured on your account. Contact your admin for assistance.')) ?></strong>
|
||||
<?php } else { ?>
|
||||
<strong><?php p($l->t('Two-factor authentication is enforced but has not been configured on your account. Please continue to setup two-factor authentication.')) ?></strong>
|
||||
<a class="button primary two-factor-primary" href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.setupProviders',
|
||||
[
|
||||
'redirect_url' => $_['redirect_url'],
|
||||
]
|
||||
)) ?>">
|
||||
<?php p($l->t('Set up two-factor authentication')) ?>
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php else: ?>
|
||||
<strong><?php p($l->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.')) ?></strong>
|
||||
<strong><?php p($l->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.')) ?></strong>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
|
|
16
core/templates/twofactorsetupchallenge.php
Normal file
16
core/templates/twofactorsetupchallenge.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
/** @var $l \OCP\IL10N */
|
||||
/** @var $_ array */
|
||||
/* @var $provider OCP\Authentication\TwoFactorAuth\IProvider */
|
||||
$provider = $_['provider'];
|
||||
/* @var $template string */
|
||||
$template = $_['template'];
|
||||
?>
|
||||
|
||||
<div class="body-login-container update">
|
||||
<h2 class="two-factor-header"><?php p($provider->getDisplayName()); ?></h2>
|
||||
<?php print_unescaped($template); ?>
|
||||
<p><a class="two-factor-secondary" href="<?php print_unescaped($_['logout_url']); ?>">
|
||||
<?php p($l->t('Cancel log in')) ?>
|
||||
</a></p>
|
||||
</div>
|
58
core/templates/twofactorsetupselection.php
Normal file
58
core/templates/twofactorsetupselection.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
?>
|
||||
<div class="body-login-container update">
|
||||
<h2 class="two-factor-header"><?php p($l->t('Setup two-factor authentication')) ?></h2>
|
||||
<?php p($l->t('Enhanced security is enforced for your account. Choose wich provider to set up:')) ?>
|
||||
<ul>
|
||||
<?php foreach ($_['providers'] as $provider): ?>
|
||||
<li>
|
||||
<a class="two-factor-provider"
|
||||
href="<?php p(\OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.setupProvider',
|
||||
[
|
||||
'providerId' => $provider->getId(),
|
||||
'redirect_url' => $_['redirect_url'],
|
||||
]
|
||||
)) ?>">
|
||||
<?php
|
||||
if ($provider instanceof \OCP\Authentication\TwoFactorAuth\IProvidesIcons) {
|
||||
$icon = $provider->getLightIcon();
|
||||
} else {
|
||||
$icon = image_path('core', 'actions/password-white.svg');
|
||||
}
|
||||
?>
|
||||
<img src="<?php p($icon) ?>" alt="" />
|
||||
<div>
|
||||
<h3><?php p($provider->getDisplayName()) ?></h3>
|
||||
<p><?php p($provider->getDescription()) ?></p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p><a class="two-factor-secondary" href="<?php print_unescaped($_['logout_url']); ?>">
|
||||
<?php p($l->t('Cancel log in')) ?>
|
||||
</a></p>
|
||||
</div>
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCP\Authentication\TwoFactorAuth;
|
||||
|
||||
use OCP\AppFramework\Controller;
|
||||
|
||||
/**
|
||||
* @since 17.0.0
|
||||
*/
|
||||
abstract class ALoginSetupController extends Controller {
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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;
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCP\Authentication\TwoFactorAuth;
|
||||
|
||||
use OCP\Template;
|
||||
|
||||
/**
|
||||
* @since 17.0.0
|
||||
*/
|
||||
interface ILoginSetupProvider {
|
||||
|
||||
/**
|
||||
* @return Template
|
||||
*
|
||||
* @since 17.0.0
|
||||
*/
|
||||
public function getBody(): Template;
|
||||
|
||||
}
|
|
@ -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' => '<html/>',
|
||||
'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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue