From 13d93f5b25aa3e663146349583a0a8e01b216f7a Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Tue, 22 May 2018 08:52:16 +0200 Subject: [PATCH 1/2] Make 2FA providers stateful This adds persistence to the Nextcloud server 2FA logic so that the server knows which 2FA providers are enabled for a specific user at any time, even when the provider is not available. The `IStatefulProvider` interface was added as tagging interface for providers that are compatible with this new API. Signed-off-by: Christoph Wurst --- core/Command/TwoFactorAuth/State.php | 110 ++++++++ core/Controller/LoginController.php | 2 +- .../TwoFactorChallengeController.php | 29 +- .../Version14000Date20180522074438.php | 62 +++++ core/register_command.php | 1 + core/templates/twofactorselectchallenge.php | 5 + lib/composer/composer/autoload_classmap.php | 7 + lib/composer/composer/autoload_static.php | 7 + .../Db/ProviderUserAssignmentDao.php | 86 ++++++ .../Authentication/TwoFactorAuth/Manager.php | 163 +++++++----- .../TwoFactorAuth/ProviderLoader.php | 88 +++++++ .../TwoFactorAuth/ProviderSet.php | 71 +++++ .../Authentication/TwoFactorAuth/Registry.php | 55 ++++ lib/private/Server.php | 13 +- .../TwoFactorAuth/IRegistry.php | 65 +++++ tests/Core/Controller/LoginControllerTest.php | 32 +-- .../TwoFactorChallengeControllerTest.php | 43 +-- .../Db/ProviderUserAssignmentDaoTest.php | 95 +++++++ .../TwoFactorAuth/ManagerTest.php | 247 ++++++++---------- .../TwoFactorAuth/ProviderLoaderTest.php | 86 ++++++ .../TwoFactorAuth/ProviderSetTest.php | 74 ++++++ .../TwoFactorAuth/RegistryTest.php | 85 ++++++ version.php | 2 +- 23 files changed, 1166 insertions(+), 262 deletions(-) create mode 100644 core/Command/TwoFactorAuth/State.php create mode 100644 core/Migrations/Version14000Date20180522074438.php create mode 100644 lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php create mode 100644 lib/private/Authentication/TwoFactorAuth/ProviderLoader.php create mode 100644 lib/private/Authentication/TwoFactorAuth/ProviderSet.php create mode 100644 lib/private/Authentication/TwoFactorAuth/Registry.php create mode 100644 lib/public/Authentication/TwoFactorAuth/IRegistry.php create mode 100644 tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php create mode 100644 tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php create mode 100644 tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php create mode 100644 tests/lib/Authentication/TwoFactorAuth/RegistryTest.php diff --git a/core/Command/TwoFactorAuth/State.php b/core/Command/TwoFactorAuth/State.php new file mode 100644 index 00000000000..076e2211a12 --- /dev/null +++ b/core/Command/TwoFactorAuth/State.php @@ -0,0 +1,110 @@ + + * + * @author 2018 Christoph Wurst + * + * @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 OC\Core\Command\TwoFactorAuth; + +use OC\Core\Command\Base; +use OCP\Authentication\TwoFactorAuth\IRegistry; +use OCP\IUserManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class State extends Base { + + /** @var IRegistry */ + private $registry; + + /** @var IUserManager */ + private $userManager; + + public function __construct(IRegistry $registry, IUserManager $userManager) { + parent::__construct('twofactorauth:state'); + + $this->registry = $registry; + $this->userManager = $userManager; + } + + protected function configure() { + parent::configure(); + + $this->setName('twofactorauth:state'); + $this->setDescription('Get the two-factor authentication (2FA) state of a user'); + $this->addArgument('uid', InputArgument::REQUIRED); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + $uid = $input->getArgument('uid'); + $user = $this->userManager->get($uid); + if (is_null($user)) { + $output->writeln("Invalid UID"); + return; + } + + $providerStates = $this->registry->getProviderStates($user); + $filtered = $this->filterEnabledDisabledUnknownProviders($providerStates); + list ($enabled, $disabled) = $filtered; + + if (!empty($enabled)) { + $output->writeln("Two-factor authentication is enabled for user $uid"); + } else { + $output->writeln("Two-factor authentication is not enabled for user $uid"); + } + + $output->writeln(""); + $this->printProviders("Enabled providers", $enabled, $output); + $this->printProviders("Disabled providers", $disabled, $output); + } + + private function filterEnabledDisabledUnknownProviders(array $providerStates): array { + $enabled = []; + $disabled = []; + + foreach ($providerStates as $providerId => $isEnabled) { + if ($isEnabled) { + $enabled[] = $providerId; + } else { + $disabled[] = $providerId; + } + } + + return [$enabled, $disabled]; + } + + private function printProviders(string $title, array $providers, + OutputInterface $output) { + if (empty($providers)) { + // Ignore and don't print anything + return; + } + + $output->writeln($title . ":"); + foreach ($providers as $provider) { + $output->writeln("- " . $provider); + } + } + +} diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 2235439d956..7bf2555819d 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -302,7 +302,7 @@ class LoginController extends Controller { if ($this->twoFactorManager->isTwoFactorAuthenticated($loginResult)) { $this->twoFactorManager->prepareTwoFactorLogin($loginResult, $remember_login); - $providers = $this->twoFactorManager->getProviders($loginResult); + $providers = $this->twoFactorManager->getProviderSet($loginResult)->getProviders(); if (count($providers) === 1) { // Single provider, hence we can redirect to that provider's challenge page directly /* @var $provider IProvider */ diff --git a/core/Controller/TwoFactorChallengeController.php b/core/Controller/TwoFactorChallengeController.php index a5d7d14f367..3d14b157f77 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\TemplateResponse; +use OCP\Authentication\TwoFactorAuth\IProvider; use OCP\Authentication\TwoFactorAuth\IProvidesCustomCSP; use OCP\Authentication\TwoFactorAuth\TwoFactorException; use OCP\IRequest; @@ -76,6 +77,23 @@ class TwoFactorChallengeController extends Controller { protected function getLogoutUrl() { return OC_User::getLogoutUrl($this->urlGenerator); } + + /** + * @param IProvider[] $providers + */ + private function splitProvidersAndBackupCodes(array $providers): array { + $regular = []; + $backup = null; + foreach ($providers as $provider) { + if ($provider->getId() === 'backup_codes') { + $backup = $provider; + } else { + $regular[] = $provider; + } + } + + return [$regular, $backup]; + } /** * @NoAdminRequired @@ -86,12 +104,14 @@ class TwoFactorChallengeController extends Controller { */ public function selectChallenge($redirect_url) { $user = $this->userSession->getUser(); - $providers = $this->twoFactorManager->getProviders($user); - $backupProvider = $this->twoFactorManager->getBackupProvider($user); + $providerSet = $this->twoFactorManager->getProviderSet($user); + $allProviders = $providerSet->getProviders(); + list($providers, $backupProvider) = $this->splitProvidersAndBackupCodes($allProviders); $data = [ 'providers' => $providers, 'backupProvider' => $backupProvider, + 'providerMissing' => $providerSet->isProviderMissing(), 'redirect_url' => $redirect_url, 'logout_url' => $this->getLogoutUrl(), ]; @@ -109,12 +129,13 @@ class TwoFactorChallengeController extends Controller { */ public function showChallenge($challengeProviderId, $redirect_url) { $user = $this->userSession->getUser(); - $provider = $this->twoFactorManager->getProvider($user, $challengeProviderId); + $providerSet = $this->twoFactorManager->getProviderSet($user); + $provider = $providerSet->getProvider($challengeProviderId); if (is_null($provider)) { return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge')); } - $backupProvider = $this->twoFactorManager->getBackupProvider($user); + $backupProvider = $providerSet->getProvider('backup_codes'); if (!is_null($backupProvider) && $backupProvider->getId() === $provider->getId()) { // Don't show the backup provider link if we're already showing that provider's challenge $backupProvider = null; diff --git a/core/Migrations/Version14000Date20180522074438.php b/core/Migrations/Version14000Date20180522074438.php new file mode 100644 index 00000000000..bbb805a9cda --- /dev/null +++ b/core/Migrations/Version14000Date20180522074438.php @@ -0,0 +1,62 @@ + + * + * @author 2018 Christoph Wurst + * + * @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 OC\Core\Migrations; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version14000Date20180522074438 extends SimpleMigrationStep { + + public function changeSchema(IOutput $output, Closure $schemaClosure, + array $options): ISchemaWrapper { + + $schema = $schemaClosure(); + + if (!$schema->hasTable('twofactor_providers')) { + $table = $schema->createTable('twofactor_providers'); + $table->addColumn('provider_id', 'string', + [ + 'notnull' => true, + 'length' => 32, + ]); + $table->addColumn('uid', 'string', + [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('enabled', 'smallint', + [ + 'notnull' => true, + 'length' => 1, + ]); + $table->setPrimaryKey(['provider_id', 'uid']); + } + + return $schema; + } + +} diff --git a/core/register_command.php b/core/register_command.php index 9df51e517a0..0115c179bf9 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -72,6 +72,7 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { $application->add(new OC\Core\Command\TwoFactorAuth\Disable( \OC::$server->getTwoFactorAuthManager(), \OC::$server->getUserManager() )); + $application->add(\OC::$server->query(\OC\Core\Command\TwoFactorAuth\State::class)); $application->add(new OC\Core\Command\Background\Cron(\OC::$server->getConfig())); $application->add(new OC\Core\Command\Background\WebCron(\OC::$server->getConfig())); diff --git a/core/templates/twofactorselectchallenge.php b/core/templates/twofactorselectchallenge.php index a1e626567e7..55d315d904d 100644 --- a/core/templates/twofactorselectchallenge.php +++ b/core/templates/twofactorselectchallenge.php @@ -1,6 +1,11 @@

t('Two-factor authentication')) ?>

t('Enhanced security is enabled for your account. Please authenticate using a second factor.')) ?>

+ +

+ t('Could not load at least one of your enabled two-factor auth methods. Please contact your admin.')) ?> +

+

    diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 77729886601..687128833b7 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -69,6 +69,7 @@ return array( 'OCP\\Authentication\\LoginCredentials\\IStore' => $baseDir . '/lib/public/Authentication/LoginCredentials/IStore.php', 'OCP\\Authentication\\TwoFactorAuth\\IProvider' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IProvider.php', 'OCP\\Authentication\\TwoFactorAuth\\IProvidesCustomCSP' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IProvidesCustomCSP.php', + 'OCP\\Authentication\\TwoFactorAuth\\IRegistry' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IRegistry.php', 'OCP\\Authentication\\TwoFactorAuth\\TwoFactorException' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/TwoFactorException.php', 'OCP\\AutoloadNotAllowedException' => $baseDir . '/lib/public/AutoloadNotAllowedException.php', 'OCP\\BackgroundJob' => $baseDir . '/lib/public/BackgroundJob.php', @@ -425,7 +426,11 @@ return array( 'OC\\Authentication\\Token\\PublicKeyToken' => $baseDir . '/lib/private/Authentication/Token/PublicKeyToken.php', 'OC\\Authentication\\Token\\PublicKeyTokenMapper' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php', 'OC\\Authentication\\Token\\PublicKeyTokenProvider' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php', + 'OC\\Authentication\\TwoFactorAuth\\Db\\ProviderUserAssignmentDao' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Manager.php', + 'OC\\Authentication\\TwoFactorAuth\\ProviderLoader' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php', + 'OC\\Authentication\\TwoFactorAuth\\ProviderSet' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/ProviderSet.php', + 'OC\\Authentication\\TwoFactorAuth\\Registry' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Registry.php', 'OC\\Avatar' => $baseDir . '/lib/private/Avatar.php', 'OC\\AvatarManager' => $baseDir . '/lib/private/AvatarManager.php', 'OC\\BackgroundJob\\Job' => $baseDir . '/lib/private/BackgroundJob/Job.php', @@ -536,6 +541,7 @@ return array( 'OC\\Core\\Command\\TwoFactorAuth\\Base' => $baseDir . '/core/Command/TwoFactorAuth/Base.php', 'OC\\Core\\Command\\TwoFactorAuth\\Disable' => $baseDir . '/core/Command/TwoFactorAuth/Disable.php', 'OC\\Core\\Command\\TwoFactorAuth\\Enable' => $baseDir . '/core/Command/TwoFactorAuth/Enable.php', + 'OC\\Core\\Command\\TwoFactorAuth\\State' => $baseDir . '/core/Command/TwoFactorAuth/State.php', 'OC\\Core\\Command\\Upgrade' => $baseDir . '/core/Command/Upgrade.php', 'OC\\Core\\Command\\User\\Add' => $baseDir . '/core/Command/User/Add.php', 'OC\\Core\\Command\\User\\Delete' => $baseDir . '/core/Command/User/Delete.php', @@ -575,6 +581,7 @@ return array( 'OC\\Core\\Migrations\\Version14000Date20180404140050' => $baseDir . '/core/Migrations/Version14000Date20180404140050.php', 'OC\\Core\\Migrations\\Version14000Date20180516101403' => $baseDir . '/core/Migrations/Version14000Date20180516101403.php', 'OC\\Core\\Migrations\\Version14000Date20180518120534' => $baseDir . '/core/Migrations/Version14000Date20180518120534.php', + 'OC\\Core\\Migrations\\Version14000Date20180522074438' => $baseDir . '/core/Migrations/Version14000Date20180522074438.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => $baseDir . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => $baseDir . '/lib/private/DB/AdapterOCI8.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index be9c71d8246..673f7b5c49b 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -99,6 +99,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Authentication\\LoginCredentials\\IStore' => __DIR__ . '/../../..' . '/lib/public/Authentication/LoginCredentials/IStore.php', 'OCP\\Authentication\\TwoFactorAuth\\IProvider' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IProvider.php', 'OCP\\Authentication\\TwoFactorAuth\\IProvidesCustomCSP' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IProvidesCustomCSP.php', + 'OCP\\Authentication\\TwoFactorAuth\\IRegistry' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IRegistry.php', 'OCP\\Authentication\\TwoFactorAuth\\TwoFactorException' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/TwoFactorException.php', 'OCP\\AutoloadNotAllowedException' => __DIR__ . '/../../..' . '/lib/public/AutoloadNotAllowedException.php', 'OCP\\BackgroundJob' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob.php', @@ -455,7 +456,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Authentication\\Token\\PublicKeyToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyToken.php', 'OC\\Authentication\\Token\\PublicKeyTokenMapper' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php', 'OC\\Authentication\\Token\\PublicKeyTokenProvider' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php', + 'OC\\Authentication\\TwoFactorAuth\\Db\\ProviderUserAssignmentDao' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Manager.php', + 'OC\\Authentication\\TwoFactorAuth\\ProviderLoader' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php', + 'OC\\Authentication\\TwoFactorAuth\\ProviderSet' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/ProviderSet.php', + 'OC\\Authentication\\TwoFactorAuth\\Registry' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Registry.php', 'OC\\Avatar' => __DIR__ . '/../../..' . '/lib/private/Avatar.php', 'OC\\AvatarManager' => __DIR__ . '/../../..' . '/lib/private/AvatarManager.php', 'OC\\BackgroundJob\\Job' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/Job.php', @@ -566,6 +571,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Command\\TwoFactorAuth\\Base' => __DIR__ . '/../../..' . '/core/Command/TwoFactorAuth/Base.php', 'OC\\Core\\Command\\TwoFactorAuth\\Disable' => __DIR__ . '/../../..' . '/core/Command/TwoFactorAuth/Disable.php', 'OC\\Core\\Command\\TwoFactorAuth\\Enable' => __DIR__ . '/../../..' . '/core/Command/TwoFactorAuth/Enable.php', + 'OC\\Core\\Command\\TwoFactorAuth\\State' => __DIR__ . '/../../..' . '/core/Command/TwoFactorAuth/State.php', 'OC\\Core\\Command\\Upgrade' => __DIR__ . '/../../..' . '/core/Command/Upgrade.php', 'OC\\Core\\Command\\User\\Add' => __DIR__ . '/../../..' . '/core/Command/User/Add.php', 'OC\\Core\\Command\\User\\Delete' => __DIR__ . '/../../..' . '/core/Command/User/Delete.php', @@ -605,6 +611,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Migrations\\Version14000Date20180404140050' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180404140050.php', 'OC\\Core\\Migrations\\Version14000Date20180516101403' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180516101403.php', 'OC\\Core\\Migrations\\Version14000Date20180518120534' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180518120534.php', + 'OC\\Core\\Migrations\\Version14000Date20180522074438' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180522074438.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterOCI8.php', diff --git a/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php new file mode 100644 index 00000000000..5b3f2849433 --- /dev/null +++ b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php @@ -0,0 +1,86 @@ + + * + * @author 2018 Christoph Wurst + * + * @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 OC\Authentication\TwoFactorAuth\Db; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Data access object to query and assign (provider_id, uid, enabled) tuples of + * 2FA providers + */ +class ProviderUserAssignmentDao { + + const TABLE_NAME = 'twofactor_providers'; + + /** @var IDBConnection */ + private $conn; + + public function __construct(IDBConnection $dbConn) { + $this->conn = $dbConn; + } + + /** + * Get all assigned provider IDs for the given user ID + * + * @return string[] where the array key is the provider ID (string) and the + * value is the enabled state (bool) + */ + public function getState(string $uid): array { + $qb = $this->conn->getQueryBuilder(); + + $query = $qb->select('provider_id', 'enabled') + ->from(self::TABLE_NAME) + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))); + $result = $query->execute(); + $providers = []; + foreach ($result->fetchAll() as $row) { + $providers[$row['provider_id']] = 1 === (int) $row['enabled']; + } + $result->closeCursor(); + + return $providers; + } + + /** + * Persist a new/updated (provider_id, uid, enabled) tuple + */ + public function persist(string $providerId, string $uid, int $enabled) { + $qb = $this->conn->getQueryBuilder(); + + // TODO: concurrency? What if (providerId, uid) private key is inserted + // twice at the same time? + $query = $qb->insert(self::TABLE_NAME)->values([ + 'provider_id' => $qb->createNamedParameter($providerId), + 'uid' => $qb->createNamedParameter($uid), + 'enabled' => $qb->createNamedParameter($enabled, IQueryBuilder::PARAM_INT), + ]); + + $query->execute(); + } + +} diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php index 933b86ffda5..0837ec339a5 100644 --- a/lib/private/Authentication/TwoFactorAuth/Manager.php +++ b/lib/private/Authentication/TwoFactorAuth/Manager.php @@ -1,5 +1,6 @@ appManager = $appManager; + public function __construct(ProviderLoader $providerLoader, + IRegistry $providerRegistry, ISession $session, IConfig $config, + IManager $activityManager, ILogger $logger, TokenProvider $tokenProvider, + ITimeFactory $timeFactory, EventDispatcherInterface $eventDispatcher) { + $this->providerLoader = $providerLoader; $this->session = $session; $this->config = $config; $this->activityManager = $activityManager; @@ -102,6 +87,7 @@ class Manager { $this->tokenProvider = $tokenProvider; $this->timeFactory = $timeFactory; $this->dispatcher = $eventDispatcher; + $this->providerRegistry = $providerRegistry; } /** @@ -112,7 +98,15 @@ class Manager { */ public function isTwoFactorAuthenticated(IUser $user): bool { $twoFactorEnabled = ((int) $this->config->getUserValue($user->getUID(), 'core', 'two_factor_auth_disabled', 0)) === 0; - return $twoFactorEnabled && \count($this->getProviders($user)) > 0; + + if (!$twoFactorEnabled) { + return false; + } + + $providerStates = $this->providerRegistry->getProviderStates($user); + $enabled = array_filter($providerStates); + + return $twoFactorEnabled && !empty($enabled); } /** @@ -141,71 +135,96 @@ class Manager { * @return IProvider|null */ public function getProvider(IUser $user, string $challengeProviderId) { - $providers = $this->getProviders($user, true); + $providers = $this->getProviderSet($user)->getProviders(); return $providers[$challengeProviderId] ?? null; } /** + * 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. + * + * @todo remove in Nextcloud 17 as by then all providers should have been updated + * + * @param string[] $providerStates + * @param IProvider[] $providers * @param IUser $user - * @return IProvider|null the backup provider, if enabled for the given user + * @return string[] the updated $providerStates variable */ - public function getBackupProvider(IUser $user) { - $providers = $this->getProviders($user, true); - if (!isset($providers[self::BACKUP_CODES_PROVIDER_ID])) { - return null; + private function fixMissingProviderStates(array $providerStates, + array $providers, IUser $user): array { + + foreach ($providers as $provider) { + if (isset($providerStates[$provider->getId()])) { + // All good + continue; + } + + $enabled = $provider->isTwoFactorAuthEnabledForUser($user); + if ($enabled) { + $this->providerRegistry->enableProviderFor($provider, $user); + } else { + $this->providerRegistry->disableProviderFor($provider, $user); + } + $providerStates[$provider->getId()] = $enabled; } - return $providers[self::BACKUP_CODES_PROVIDER_ID]; + + return $providerStates; } /** - * Get the list of 2FA providers for the given user - * - * @param IUser $user - * @param bool $includeBackupApp - * @return IProvider[] - * @throws Exception + * @param array $states + * @param IProvider $providers */ - public function getProviders(IUser $user, bool $includeBackupApp = false): array { - $allApps = $this->appManager->getEnabledAppsForUser($user); - $providers = []; + private function isProviderMissing(array $states, array $providers): bool { + $indexed = []; + foreach ($providers as $provider) { + $indexed[$provider->getId()] = $provider; + } - foreach ($allApps as $appId) { - if (!$includeBackupApp && $appId === self::BACKUP_CODES_APP_ID) { + $missing = []; + foreach ($states as $providerId => $enabled) { + if (!$enabled) { + // Don't care continue; } - $info = $this->appManager->getAppInfo($appId); - if (isset($info['two-factor-providers'])) { - /** @var string[] $providerClasses */ - $providerClasses = $info['two-factor-providers']; - foreach ($providerClasses as $class) { - try { - $this->loadTwoFactorApp($appId); - $provider = OC::$server->query($class); - $providers[$provider->getId()] = $provider; - } catch (QueryException $exc) { - // Provider class can not be resolved - throw new Exception("Could not load two-factor auth provider $class"); - } - } + if (!isset($indexed[$providerId])) { + $missing[] = $providerId; + $this->logger->alert("two-factor auth provider '$providerId' failed to load", + [ + 'app' => 'core', + ]); } } - return array_filter($providers, function ($provider) use ($user) { - /* @var $provider IProvider */ - return $provider->isTwoFactorAuthEnabledForUser($user); - }); + if (!empty($missing)) { + // There was at least one provider missing + $this->logger->alert(count($missing) . " two-factor auth providers failed to load", ['app' => 'core']); + + return true; + } + + // If we reach this, there was not a single provider missing + return false; } /** - * Load an app by ID if it has not been loaded yet + * Get the list of 2FA providers for the given user * - * @param string $appId + * @param IUser $user + * @throws Exception */ - protected function loadTwoFactorApp(string $appId) { - if (!OC_App::isAppLoaded($appId)) { - OC_App::loadApp($appId); - } + public function getProviderSet(IUser $user): ProviderSet { + $providerStates = $this->providerRegistry->getProviderStates($user); + $providers = $this->providerLoader->getProviders($user); + + $fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user); + $isProviderMissing = $this->isProviderMissing($fixedStates, $providers); + + $enabled = array_filter($providers, function (IProvider $provider) use ($fixedStates) { + return $fixedStates[$provider->getId()]; + }); + return new ProviderSet($enabled, $isProviderMissing); } /** @@ -272,7 +291,7 @@ class Manager { try { $this->activityManager->publish($activity); } catch (BadMethodCallException $e) { - $this->logger->warning('could not publish backup code creation activity', ['app' => 'core']); + $this->logger->warning('could not publish activity', ['app' => 'core']); $this->logger->logException($e, ['app' => 'core']); } } diff --git a/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php b/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php new file mode 100644 index 00000000000..bd20cd12949 --- /dev/null +++ b/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php @@ -0,0 +1,88 @@ + + * + * @author 2018 Christoph Wurst + * + * @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 OC\Authentication\TwoFactorAuth; + +use Exception; +use OC; +use OC_App; +use OCP\App\IAppManager; +use OCP\AppFramework\QueryException; +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\IUser; + +class ProviderLoader { + + const BACKUP_CODES_APP_ID = 'twofactor_backupcodes'; + + /** @var IAppManager */ + private $appManager; + + public function __construct(IAppManager $appManager) { + $this->appManager = $appManager; + } + + /** + * Get the list of 2FA providers for the given user + * + * @return IProvider[] + * @throws Exception + */ + public function getProviders(IUser $user): array { + $allApps = $this->appManager->getEnabledAppsForUser($user); + $providers = []; + + foreach ($allApps as $appId) { + $info = $this->appManager->getAppInfo($appId); + if (isset($info['two-factor-providers'])) { + /** @var string[] $providerClasses */ + $providerClasses = $info['two-factor-providers']; + foreach ($providerClasses as $class) { + try { + $this->loadTwoFactorApp($appId); + $provider = OC::$server->query($class); + $providers[$provider->getId()] = $provider; + } catch (QueryException $exc) { + // Provider class can not be resolved + throw new Exception("Could not load two-factor auth provider $class"); + } + } + } + } + + return $providers; + } + + /** + * Load an app by ID if it has not been loaded yet + * + * @param string $appId + */ + protected function loadTwoFactorApp(string $appId) { + if (!OC_App::isAppLoaded($appId)) { + OC_App::loadApp($appId); + } + } + +} diff --git a/lib/private/Authentication/TwoFactorAuth/ProviderSet.php b/lib/private/Authentication/TwoFactorAuth/ProviderSet.php new file mode 100644 index 00000000000..60e967ba546 --- /dev/null +++ b/lib/private/Authentication/TwoFactorAuth/ProviderSet.php @@ -0,0 +1,71 @@ + + * + * @author 2018 Christoph Wurst + * + * @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 OC\Authentication\TwoFactorAuth; + +use OCP\Authentication\TwoFactorAuth\IProvider; + +/** + * Contains all two-factor provider information for the two-factor login challenge + */ +class ProviderSet { + + /** @var IProvider */ + private $providers; + + /** @var bool */ + private $providerMissing; + + /** + * @param IProvider[] $providers + * @param bool $providerMissing + */ + public function __construct(array $providers, bool $providerMissing) { + $this->providers = []; + foreach ($providers as $provider) { + $this->providers[$provider->getId()] = $provider; + } + $this->providerMissing = $providerMissing; + } + + /** + * @param string $providerId + * @return IProvider|null + */ + public function getProvider(string $providerId) { + return $this->providers[$providerId] ?? null; + } + + /** + * @return IProvider[] + */ + public function getProviders(): array { + return $this->providers; + } + + public function isProviderMissing(): bool { + return $this->providerMissing; + } + +} diff --git a/lib/private/Authentication/TwoFactorAuth/Registry.php b/lib/private/Authentication/TwoFactorAuth/Registry.php new file mode 100644 index 00000000000..0cfb052440e --- /dev/null +++ b/lib/private/Authentication/TwoFactorAuth/Registry.php @@ -0,0 +1,55 @@ + + * + * @author 2018 Christoph Wurst + * + * @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 OC\Authentication\TwoFactorAuth; + +use OC\Authentication\TwoFactorAuth\Db\ProviderUserAssignmentDao; +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\Authentication\TwoFactorAuth\IRegistry; +use OCP\IUser; + +class Registry implements IRegistry { + + /** @var ProviderUserAssignmentDao */ + private $assignmentDao; + + public function __construct(ProviderUserAssignmentDao $assignmentDao) { + $this->assignmentDao = $assignmentDao; + } + + public function getProviderStates(IUser $user): array { + return $this->assignmentDao->getState($user->getUID()); + } + + public function enableProviderFor(IProvider $provider, IUser $user) { + $this->assignmentDao->persist($provider->getId(), $user->getUID(), 1); + } + + public function disableProviderFor(IProvider $provider, IUser $user) { + $this->assignmentDao->persist($provider->getId(), $user->getUID(), 0); + } + +} diff --git a/lib/private/Server.php b/lib/private/Server.php index 31f088ea718..7824638b212 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -412,18 +412,7 @@ class Server extends ServerContainer implements IServerContainer { }); $this->registerAlias('UserSession', \OCP\IUserSession::class); - $this->registerService(\OC\Authentication\TwoFactorAuth\Manager::class, function (Server $c) { - return new \OC\Authentication\TwoFactorAuth\Manager( - $c->getAppManager(), - $c->getSession(), - $c->getConfig(), - $c->getActivityManager(), - $c->getLogger(), - $c->query(IProvider::class), - $c->query(ITimeFactory::class), - $c->query(EventDispatcherInterface::class) - ); - }); + $this->registerAlias(\OCP\Authentication\TwoFactorAuth\IRegistry::class, \OC\Authentication\TwoFactorAuth\Registry::class); $this->registerAlias(\OCP\INavigationManager::class, \OC\NavigationManager::class); $this->registerAlias('NavigationManager', \OCP\INavigationManager::class); diff --git a/lib/public/Authentication/TwoFactorAuth/IRegistry.php b/lib/public/Authentication/TwoFactorAuth/IRegistry.php new file mode 100644 index 00000000000..5013892d402 --- /dev/null +++ b/lib/public/Authentication/TwoFactorAuth/IRegistry.php @@ -0,0 +1,65 @@ + + * + * @author 2018 Christoph Wurst + * + * @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; + +/** + * Nextcloud 2FA provider registry for stateful 2FA providers + * + * This service keeps track of which providers are currently active for a specific + * user. Stateful 2FA providers (IStatefulProvider) must use this service to save + * their enabled/disabled state. + * + * @since 14.0.0 + */ +interface IRegistry { + + /** + * Get a key-value map of providers and their enabled/disabled state for + * the given user. + * + * @since 14.0.0 + * @return string[] where the array key is the provider ID (string) and the + * value is the enabled state (bool) + */ + public function getProviderStates(IUser $user): array; + + /** + * Enable the given 2FA provider for the given user + * + * @since 14.0.0 + */ + public function enableProviderFor(IProvider $provider, IUser $user); + + /** + * Disable the given 2FA provider for the given user + * + * @since 14.0.0 + */ + public function disableProviderFor(IProvider $provider, IUser $user); +} diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php index ccd70111ae5..1e26d86a039 100644 --- a/tests/Core/Controller/LoginControllerTest.php +++ b/tests/Core/Controller/LoginControllerTest.php @@ -23,11 +23,13 @@ namespace Tests\Core\Controller; use OC\Authentication\Token\IToken; use OC\Authentication\TwoFactorAuth\Manager; +use OC\Authentication\TwoFactorAuth\ProviderSet; use OC\Core\Controller\LoginController; use OC\Security\Bruteforce\Throttler; use OC\User\Session; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; +use OCP\Authentication\TwoFactorAuth\IProvider; use OCP\Defaults; use OCP\IConfig; use OCP\ILogger; @@ -414,7 +416,7 @@ class LoginControllerTest extends TestCase { $user->expects($this->any()) ->method('getUID') ->will($this->returnValue('uid')); - $loginName = 'loginli'; + $loginName = 'loginli'; $password = 'secret'; $indexPageUrl = \OC_Util::getDefaultPageUrl(); @@ -539,7 +541,7 @@ class LoginControllerTest extends TestCase { $expected = new \OCP\AppFramework\Http\RedirectResponse(urldecode($redirectUrl)); $this->assertEquals($expected, $this->loginController->tryLogin('Jane', $password, $originalUrl)); } - + public function testLoginWithOneTwoFactorProvider() { /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ $user = $this->createMock(IUser::class); @@ -548,7 +550,7 @@ class LoginControllerTest extends TestCase { ->will($this->returnValue('john')); $password = 'secret'; $challengeUrl = 'challenge/url'; - $provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock(); + $provider = $this->createMock(IProvider::class); $this->request ->expects($this->once()) @@ -570,10 +572,11 @@ class LoginControllerTest extends TestCase { $this->twoFactorManager->expects($this->once()) ->method('prepareTwoFactorLogin') ->with($user); + $providerSet = new ProviderSet([$provider], false); $this->twoFactorManager->expects($this->once()) - ->method('getProviders') + ->method('getProviderSet') ->with($user) - ->will($this->returnValue([$provider])); + ->willReturn($providerSet); $provider->expects($this->once()) ->method('getId') ->will($this->returnValue('u2f')); @@ -593,7 +596,7 @@ class LoginControllerTest extends TestCase { $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', $password, null)); } - public function testLoginWithMultpleTwoFactorProviders() { + public function testLoginWithMultipleTwoFactorProviders() { /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ $user = $this->createMock(IUser::class); $user->expects($this->any()) @@ -601,8 +604,10 @@ class LoginControllerTest extends TestCase { ->will($this->returnValue('john')); $password = 'secret'; $challengeUrl = 'challenge/url'; - $provider1 = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock(); - $provider2 = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock(); + $provider1 = $this->createMock(IProvider::class); + $provider2 = $this->createMock(IProvider::class); + $provider1->method('getId')->willReturn('prov1'); + $provider2->method('getId')->willReturn('prov2'); $this->request ->expects($this->once()) @@ -624,14 +629,11 @@ class LoginControllerTest extends TestCase { $this->twoFactorManager->expects($this->once()) ->method('prepareTwoFactorLogin') ->with($user); + $providerSet = new ProviderSet([$provider1, $provider2], false); $this->twoFactorManager->expects($this->once()) - ->method('getProviders') + ->method('getProviderSet') ->with($user) - ->will($this->returnValue([$provider1, $provider2])); - $provider1->expects($this->never()) - ->method('getId'); - $provider2->expects($this->never()) - ->method('getId'); + ->willReturn($providerSet); $this->urlGenerator->expects($this->once()) ->method('linkToRoute') ->with('core.TwoFactorChallenge.selectChallenge') @@ -661,7 +663,7 @@ class LoginControllerTest extends TestCase { ->method('checkPassword') ->with('john', 'just wrong') ->willReturn(false); - + $this->userManager->expects($this->once()) ->method('getByEmail') ->with('john@doe.com') diff --git a/tests/Core/Controller/TwoFactorChallengeControllerTest.php b/tests/Core/Controller/TwoFactorChallengeControllerTest.php index ed6452316ff..6a01c510ed2 100644 --- a/tests/Core/Controller/TwoFactorChallengeControllerTest.php +++ b/tests/Core/Controller/TwoFactorChallengeControllerTest.php @@ -23,6 +23,7 @@ namespace Test\Core\Controller; use OC\Authentication\TwoFactorAuth\Manager; +use OC\Authentication\TwoFactorAuth\ProviderSet; use OC\Core\Controller\TwoFactorChallengeController; use OC_Util; use OCP\AppFramework\Http\RedirectResponse; @@ -85,26 +86,26 @@ class TwoFactorChallengeControllerTest extends TestCase { public function testSelectChallenge() { $user = $this->getMockBuilder(IUser::class)->getMock(); - $providers = [ - 'prov1', - 'prov2', - ]; + $p1 = $this->createMock(IProvider::class); + $p1->method('getId')->willReturn('p1'); + $backupProvider = $this->createMock(IProvider::class); + $backupProvider->method('getId')->willReturn('backup_codes'); + $providerSet = new ProviderSet([$p1, $backupProvider], true); $this->userSession->expects($this->once()) ->method('getUser') ->will($this->returnValue($user)); $this->twoFactorManager->expects($this->once()) - ->method('getProviders') - ->with($user) - ->will($this->returnValue($providers)); - $this->twoFactorManager->expects($this->once()) - ->method('getBackupProvider') + ->method('getProviderSet') ->with($user) - ->will($this->returnValue('backup')); + ->will($this->returnValue($providerSet)); $expected = new TemplateResponse('core', 'twofactorselectchallenge', [ - 'providers' => $providers, - 'backupProvider' => 'backup', + 'providers' => [ + $p1, + ], + 'providerMissing' => true, + 'backupProvider' => $backupProvider, 'redirect_url' => '/some/url', 'logout_url' => 'logoutAttribute', ], 'guest'); @@ -115,20 +116,19 @@ class TwoFactorChallengeControllerTest extends TestCase { public function testShowChallenge() { $user = $this->createMock(IUser::class); $provider = $this->createMock(IProvider::class); + $provider->method('getId')->willReturn('myprovider'); $backupProvider = $this->createMock(IProvider::class); + $backupProvider->method('getId')->willReturn('backup_codes'); $tmpl = $this->createMock(Template::class); + $providerSet = new ProviderSet([$provider, $backupProvider], true); $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('getBackupProvider') + ->method('getProviderSet') ->with($user) - ->will($this->returnValue($backupProvider)); + ->will($this->returnValue($providerSet)); $provider->expects($this->once()) ->method('getId') ->will($this->returnValue('u2f')); @@ -166,14 +166,15 @@ class TwoFactorChallengeControllerTest extends TestCase { public function testShowInvalidChallenge() { $user = $this->createMock(IUser::class); + $providerSet = new ProviderSet([], false); $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)); + ->method('getProviderSet') + ->with($user) + ->will($this->returnValue($providerSet)); $this->urlGenerator->expects($this->once()) ->method('linkToRoute') ->with('core.TwoFactorChallenge.selectChallenge') diff --git a/tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php b/tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php new file mode 100644 index 00000000000..adce55e11e3 --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php @@ -0,0 +1,95 @@ + + * + * @author 2018 Christoph Wurst + * + * @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 Test\Authentication\TwoFactorAuth\Db; + +use OC; +use OC\Authentication\TwoFactorAuth\Db\ProviderUserAssignmentDao; +use OCP\IDBConnection; +use Test\TestCase; + +/** + * @group DB + */ +class ProviderUserAssignmentDaoTest extends TestCase { + + /** @var IDBConnection */ + private $dbConn; + + /** @var ProviderUserAssignmentDao */ + private $dao; + + protected function setUp() { + parent::setUp(); + + $this->dbConn = OC::$server->getDatabaseConnection(); + $qb = $this->dbConn->getQueryBuilder(); + $q = $qb->delete(ProviderUserAssignmentDao::TABLE_NAME); + $q->execute(); + + $this->dao = new ProviderUserAssignmentDao($this->dbConn); + } + + public function testGetState() { + $qb = $this->dbConn->getQueryBuilder(); + $q1 = $qb->insert(ProviderUserAssignmentDao::TABLE_NAME)->values([ + 'provider_id' => $qb->createNamedParameter('twofactor_u2f'), + 'uid' => $qb->createNamedParameter('user123'), + 'enabled' => $qb->createNamedParameter(1), + ]); + $q1->execute(); + $q2 = $qb->insert(ProviderUserAssignmentDao::TABLE_NAME)->values([ + 'provider_id' => $qb->createNamedParameter('twofactor_totp'), + 'uid' => $qb->createNamedParameter('user123'), + 'enabled' => $qb->createNamedParameter(0), + ]); + $q2->execute(); + $expected = [ + 'twofactor_u2f' => true, + 'twofactor_totp' => false, + ]; + + $state = $this->dao->getState('user123'); + + $this->assertEquals($expected, $state); + } + + public function testPersist() { + $qb = $this->dbConn->getQueryBuilder(); + + $this->dao->persist('twofactor_totp', 'user123', 0); + + $q = $qb + ->select('*') + ->from(ProviderUserAssignmentDao::TABLE_NAME) + ->where($qb->expr()->eq('provider_id', $qb->createNamedParameter('twofactor_totp'))) + ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter('user123'))) + ->andWhere($qb->expr()->eq('enabled', $qb->createNamedParameter(0))); + $res = $q->execute(); + $data = $res->fetchAll(); + $res->closeCursor(); + $this->assertCount(1, $data); + } + +} diff --git a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php index 3c617fe8442..e54e4353404 100644 --- a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php +++ b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php @@ -24,13 +24,14 @@ namespace Test\Authentication\TwoFactorAuth; use Exception; use OC; -use OC\App\AppManager; use OC\Authentication\Token\IProvider as TokenProvider; use OC\Authentication\TwoFactorAuth\Manager; +use OC\Authentication\TwoFactorAuth\ProviderLoader; use OCP\Activity\IEvent; use OCP\Activity\IManager; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\Authentication\TwoFactorAuth\IRegistry; use OCP\IConfig; use OCP\ILogger; use OCP\ISession; @@ -43,8 +44,11 @@ class ManagerTest extends TestCase { /** @var IUser|\PHPUnit_Framework_MockObject_MockObject */ private $user; - /** @var AppManager|\PHPUnit_Framework_MockObject_MockObject */ - private $appManager; + /** @var ProviderLoader|\PHPUnit_Framework_MockObject_MockObject */ + private $providerLoader; + + /** @var IRegistry|\PHPUnit_Framework_MockObject_MockObject */ + private $providerRegistry; /** @var ISession|\PHPUnit_Framework_MockObject_MockObject */ private $session; @@ -80,7 +84,8 @@ class ManagerTest extends TestCase { parent::setUp(); $this->user = $this->createMock(IUser::class); - $this->appManager = $this->createMock(AppManager::class); + $this->providerLoader = $this->createMock(\OC\Authentication\TwoFactorAuth\ProviderLoader::class); + $this->providerRegistry = $this->createMock(IRegistry::class); $this->session = $this->createMock(ISession::class); $this->config = $this->createMock(IConfig::class); $this->activityManager = $this->createMock(IManager::class); @@ -89,172 +94,145 @@ class ManagerTest extends TestCase { $this->timeFactory = $this->createMock(ITimeFactory::class); $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); - $this->manager = $this->getMockBuilder(Manager::class) - ->setConstructorArgs([ - $this->appManager, - $this->session, - $this->config, - $this->activityManager, - $this->logger, - $this->tokenProvider, - $this->timeFactory, - $this->eventDispatcher - ]) - ->setMethods(['loadTwoFactorApp']) // Do not actually load the apps - ->getMock(); + $this->manager = new Manager( + $this->providerLoader, + $this->providerRegistry, + $this->session, + $this->config, + $this->activityManager, + $this->logger, + $this->tokenProvider, + $this->timeFactory, + $this->eventDispatcher + ); $this->fakeProvider = $this->createMock(IProvider::class); - $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; - }); + $this->fakeProvider->method('getId')->willReturn('email'); + $this->fakeProvider->method('isTwoFactorAuthEnabledForUser')->willReturn(true); $this->backupProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock(); - $this->backupProvider->expects($this->any()) - ->method('getId') - ->will($this->returnValue('backup_codes')); - $this->backupProvider->expects($this->any()) - ->method('isTwoFactorAuthEnabledForUser') - ->will($this->returnValue(true)); - OC::$server->registerService('\OCA\TwoFactorBackupCodes\Provider\FakeBackupCodesProvider', function () { - return $this->backupProvider; - }); + $this->backupProvider->method('getId')->willReturn('backup_codes'); + $this->backupProvider->method('isTwoFactorAuthEnabledForUser')->willReturn(true); } private function prepareNoProviders() { - $this->appManager->expects($this->any()) - ->method('getEnabledAppsForUser') + $this->providerLoader->method('getProviders') ->with($this->user) ->will($this->returnValue([])); - - $this->appManager->expects($this->never()) - ->method('getAppInfo'); - - $this->manager->expects($this->never()) - ->method('loadTwoFactorApp'); } private function prepareProviders() { - $this->appManager->expects($this->any()) - ->method('getEnabledAppsForUser') + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') ->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', - ], - ])); - - $this->manager->expects($this->once()) - ->method('loadTwoFactorApp') - ->with('mycustom2faapp'); - } - - private function prepareProvidersWitBackupProvider() { - $this->appManager->expects($this->any()) - ->method('getEnabledAppsForUser') + ->willReturn([ + $this->fakeProvider->getId() => true, + ]); + $this->providerLoader->expects($this->once()) + ->method('getProviders') ->with($this->user) - ->will($this->returnValue([ - 'mycustom2faapp', - 'twofactor_backupcodes', - ])); - - $this->appManager->expects($this->exactly(2)) - ->method('getAppInfo') - ->will($this->returnValueMap([ - [ - 'mycustom2faapp', false, null, - ['two-factor-providers' => [ - '\OCA\MyCustom2faApp\FakeProvider', - ] - ] - ], - [ - 'twofactor_backupcodes', false, null, - ['two-factor-providers' => [ - '\OCA\TwoFactorBackupCodes\Provider\FakeBackupCodesProvider', - ] - ] - ], - ])); - - $this->manager->expects($this->exactly(2)) - ->method('loadTwoFactorApp'); + ->willReturn([$this->fakeProvider]); } - /** - * @expectedException Exception - * @expectedExceptionMessage Could not load two-factor auth provider \OCA\MyFaulty2faApp\DoesNotExist - */ - public function testFailHardIfProviderCanNotBeLoaded() { - $this->appManager->expects($this->once()) - ->method('getEnabledAppsForUser') + private function prepareProvidersWitBackupProvider() { + $this->providerLoader->method('getProviders') ->with($this->user) - ->will($this->returnValue(['faulty2faapp'])); - $this->manager->expects($this->once()) - ->method('loadTwoFactorApp') - ->with('faulty2faapp'); - - $this->appManager->expects($this->once()) - ->method('getAppInfo') - ->with('faulty2faapp') - ->will($this->returnValue([ - 'two-factor-providers' => [ - '\OCA\MyFaulty2faApp\DoesNotExist', - ], - ])); - - $this->manager->getProviders($this->user); + ->willReturn([ + $this->fakeProvider, + $this->backupProvider, + ]); } public function testIsTwoFactorAuthenticated() { - $this->prepareProviders(); - $this->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)); + ->willReturn(0); + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->willReturn([ + 'twofactor_totp' => true, + 'twofactor_u2f' => false, + ]); $this->assertTrue($this->manager->isTwoFactorAuthenticated($this->user)); } public function testGetProvider() { - $this->prepareProviders(); - - $this->assertSame($this->fakeProvider, $this->manager->getProvider($this->user, 'email')); - } + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->with($this->user) + ->willReturn([ + $this->fakeProvider->getId() => true, + ]); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($this->user) + ->willReturn([$this->fakeProvider]); - public function testGetBackupProvider() { - $this->prepareProvidersWitBackupProvider(); + $provider = $this->manager->getProvider($this->user, $this->fakeProvider->getId()); - $this->assertSame($this->backupProvider, $this->manager->getBackupProvider($this->user)); + $this->assertSame($this->fakeProvider, $provider); } public function testGetInvalidProvider() { - $this->prepareProviders(); + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->with($this->user) + ->willReturn([]); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($this->user) + ->willReturn([]); - $this->assertSame(null, $this->manager->getProvider($this->user, 'nonexistent')); + $provider = $this->manager->getProvider($this->user, 'nonexistent'); + + $this->assertNull($provider); } public function testGetProviders() { - $this->prepareProviders(); + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->with($this->user) + ->willReturn([ + $this->fakeProvider->getId() => true, + ]); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($this->user) + ->willReturn([$this->fakeProvider]); $expectedProviders = [ 'email' => $this->fakeProvider, ]; - $this->assertEquals($expectedProviders, $this->manager->getProviders($this->user)); + $providerSet = $this->manager->getProviderSet($this->user); + $providers = $providerSet->getProviders(); + + $this->assertEquals($expectedProviders, $providers); + $this->assertFalse($providerSet->isProviderMissing()); + } + + public function testGetProvidersOneMissing() { + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->with($this->user) + ->willReturn([ + $this->fakeProvider->getId() => true, + ]); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($this->user) + ->willReturn([]); + $expectedProviders = [ + 'email' => $this->fakeProvider, + ]; + + $providerSet = $this->manager->getProviderSet($this->user); + + $this->assertTrue($providerSet->isProviderMissing()); } public function testVerifyChallenge() { @@ -266,7 +244,6 @@ class ManagerTest extends TestCase { ->method('verifyChallenge') ->with($this->user, $challenge) ->will($this->returnValue(true)); - $this->session->expects($this->once()) ->method('get') ->with('two_factor_remember_login') @@ -282,15 +259,12 @@ class ManagerTest extends TestCase { ->with(Manager::SESSION_UID_DONE, 'jos'); $this->session->method('getId') ->willReturn('mysessionid'); - $this->activityManager->expects($this->once()) ->method('generateEvent') ->willReturn($event); - $this->user->expects($this->any()) ->method('getUID') ->willReturn('jos'); - $event->expects($this->once()) ->method('setApp') ->with($this->equalTo('core')) @@ -316,19 +290,19 @@ class ManagerTest extends TestCase { 'provider' => 'Fake 2FA', ])) ->willReturnSelf(); - $token = $this->createMock(OC\Authentication\Token\IToken::class); $this->tokenProvider->method('getToken') ->with('mysessionid') ->willReturn($token); $token->method('getId') ->willReturn(42); - $this->config->expects($this->once()) ->method('deleteUserValue') ->with('jos', 'login_token_2fa', 42); - $this->assertTrue($this->manager->verifyChallenge('email', $this->user, $challenge)); + $result = $this->manager->verifyChallenge('email', $this->user, $challenge); + + $this->assertTrue($result); } public function testVerifyChallengeInvalidProviderId() { @@ -424,7 +398,8 @@ class ManagerTest extends TestCase { $manager = $this->getMockBuilder(Manager::class) ->setConstructorArgs([ - $this->appManager, + $this->providerLoader, + $this->providerRegistry, $this->session, $this->config, $this->activityManager, @@ -433,7 +408,7 @@ class ManagerTest extends TestCase { $this->timeFactory, $this->eventDispatcher ]) - ->setMethods(['loadTwoFactorApp','isTwoFactorAuthenticated']) // Do not actually load the apps + ->setMethods(['loadTwoFactorApp', 'isTwoFactorAuthenticated'])// Do not actually load the apps ->getMock(); $manager->method('isTwoFactorAuthenticated') @@ -531,7 +506,7 @@ class ManagerTest extends TestCase { ->willReturn('user'); $this->session->method('exists') - ->will($this->returnCallback(function($var) { + ->will($this->returnCallback(function ($var) { if ($var === Manager::SESSION_UID_KEY) { return false; } else if ($var === 'app_password') { diff --git a/tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php b/tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php new file mode 100644 index 00000000000..6479e41fffb --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php @@ -0,0 +1,86 @@ +appManager = $this->createMock(IAppManager::class); + $this->user = $this->createMock(\OCP\IUser::class); + + $this->loader = new ProviderLoader($this->appManager); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage Could not load two-factor auth provider \OCA\MyFaulty2faApp\DoesNotExist + */ + public function testFailHardIfProviderCanNotBeLoaded() { + $this->appManager->expects($this->once()) + ->method('getEnabledAppsForUser') + ->with($this->user) + ->willReturn(['mail', 'twofactor_totp']); + $this->appManager + ->method('getAppInfo') + ->will($this->returnValueMap([ + ['mail', false, null, []], + ['twofactor_totp', false, null, [ + 'two-factor-providers' => [ + '\\OCA\\MyFaulty2faApp\\DoesNotExist', + ], + ]], + ])); + + $this->loader->getProviders($this->user); + } + + public function testGetProviders() { + $provider = $this->createMock(IProvider::class); + $provider->method('getId')->willReturn('test'); + \OC::$server->registerService('\\OCA\\TwoFactorTest\\Provider', function () use ($provider) { + return $provider; + }); + $this->appManager->expects($this->once()) + ->method('getEnabledAppsForUser') + ->with($this->user) + ->willReturn(['twofactor_test']); + $this->appManager + ->method('getAppInfo') + ->with('twofactor_test') + ->willReturn(['two-factor-providers' => [ + '\\OCA\\TwoFactorTest\\Provider', + ]]); + + $providers = $this->loader->getProviders($this->user); + + $this->assertCount(1, $providers); + $this->assertArrayHasKey('test', $providers); + $this->assertSame($provider, $providers['test']); + } + +} diff --git a/tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php b/tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php new file mode 100644 index 00000000000..46191780f81 --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php @@ -0,0 +1,74 @@ + + * + * @author 2018 Christoph Wurst + * + * @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 Test\Authentication\TwoFactorAuth; + +use OC\Authentication\TwoFactorAuth\ProviderSet; +use OCP\Authentication\TwoFactorAuth\IProvider; +use Test\TestCase; + +class ProviderSetTest extends TestCase { + + /** @var ProviderSet */ + private $providerSet; + + public function testIndexesProviders() { + $p1 = $this->createMock(IProvider::class); + $p1->method('getId')->willReturn('p1'); + $p2 = $this->createMock(IProvider::class); + $p2->method('getId')->willReturn('p2'); + $expected = [ + 'p1' => $p1, + 'p2' => $p2, + ]; + + $set = new ProviderSet([$p2, $p1], false); + + $this->assertEquals($expected, $set->getProviders()); + } + + public function testGetProvider() { + $p1 = $this->createMock(IProvider::class); + $p1->method('getId')->willReturn('p1'); + + $set = new ProviderSet([$p1], false); + $provider = $set->getProvider('p1'); + + $this->assertEquals($p1, $provider); + } + + public function testGetProviderNotFound() { + $set = new ProviderSet([], false); + $provider = $set->getProvider('p1'); + + $this->assertNull($provider); + } + + public function testIsProviderMissing() { + $set = new ProviderSet([], true); + + $this->assertTrue($set->isProviderMissing()); + } + +} diff --git a/tests/lib/Authentication/TwoFactorAuth/RegistryTest.php b/tests/lib/Authentication/TwoFactorAuth/RegistryTest.php new file mode 100644 index 00000000000..71f104ca429 --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/RegistryTest.php @@ -0,0 +1,85 @@ + + * + * @author 2018 Christoph Wurst + * + * @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 Test\Authentication\TwoFactorAuth; + +use OC\Authentication\TwoFactorAuth\Db\ProviderUserAssignmentDao; +use OC\Authentication\TwoFactorAuth\Registry; +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\IUser; +use PHPUnit_Framework_MockObject_MockObject; +use Test\TestCase; + +class RegistryTest extends TestCase { + + /** @var ProviderUserAssignmentDao|PHPUnit_Framework_MockObject_MockObject */ + private $dao; + + /** @var Registry */ + private $registry; + + protected function setUp() { + parent::setUp(); + + $this->dao = $this->createMock(ProviderUserAssignmentDao::class); + + $this->registry = new Registry($this->dao); + } + + public function testGetProviderStates() { + $user = $this->createMock(IUser::class); + $user->expects($this->once())->method('getUID')->willReturn('user123'); + $state = [ + 'twofactor_totp' => true, + ]; + $this->dao->expects($this->once())->method('getState')->willReturn($state); + + $actual = $this->registry->getProviderStates($user); + + $this->assertEquals($state, $actual); + } + + public function testEnableProvider() { + $user = $this->createMock(IUser::class); + $provider = $this->createMock(IProvider::class); + $user->expects($this->once())->method('getUID')->willReturn('user123'); + $provider->expects($this->once())->method('getId')->willReturn('p1'); + $this->dao->expects($this->once())->method('persist')->with('p1', 'user123', + true); + + $this->registry->enableProviderFor($provider, $user); + } + + public function testDisableProvider() { + $user = $this->createMock(IUser::class); + $provider = $this->createMock(IProvider::class); + $user->expects($this->once())->method('getUID')->willReturn('user123'); + $provider->expects($this->once())->method('getId')->willReturn('p1'); + $this->dao->expects($this->once())->method('persist')->with('p1', 'user123', + false); + + $this->registry->disableProviderFor($provider, $user); + } + +} diff --git a/version.php b/version.php index 0a9aee66adc..4a0aa45a894 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = array(14, 0, 0, 5); +$OC_Version = array(14, 0, 0, 6); // The human readable string $OC_VersionString = '14.0.0 alpha'; From 7be465febe2b6f8b5cee8ad95f45028b4bcc8d35 Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Mon, 25 Jun 2018 07:38:52 +0200 Subject: [PATCH 2/2] Make new classes strict and fix license header Signed-off-by: Christoph Wurst --- .../Version14000Date20180522074438.php | 1 + .../TwoFactorAuth/ProviderLoader.php | 1 + .../TwoFactorAuth/ProviderSet.php | 1 + .../Db/ProviderUserAssignmentDaoTest.php | 1 + .../TwoFactorAuth/ProviderLoaderTest.php | 24 +++++++++++++++---- .../TwoFactorAuth/ProviderSetTest.php | 1 + 6 files changed, 25 insertions(+), 4 deletions(-) diff --git a/core/Migrations/Version14000Date20180522074438.php b/core/Migrations/Version14000Date20180522074438.php index bbb805a9cda..28207d0b900 100644 --- a/core/Migrations/Version14000Date20180522074438.php +++ b/core/Migrations/Version14000Date20180522074438.php @@ -1,4 +1,5 @@ diff --git a/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php b/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php index bd20cd12949..f735bad1695 100644 --- a/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php +++ b/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php @@ -1,4 +1,5 @@ diff --git a/lib/private/Authentication/TwoFactorAuth/ProviderSet.php b/lib/private/Authentication/TwoFactorAuth/ProviderSet.php index 60e967ba546..bbb9467798b 100644 --- a/lib/private/Authentication/TwoFactorAuth/ProviderSet.php +++ b/lib/private/Authentication/TwoFactorAuth/ProviderSet.php @@ -1,4 +1,5 @@ diff --git a/tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php b/tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php index adce55e11e3..d1fed0c3a60 100644 --- a/tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php +++ b/tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php @@ -1,4 +1,5 @@ diff --git a/tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php b/tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php index 6479e41fffb..55a1e173928 100644 --- a/tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php +++ b/tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php @@ -1,10 +1,26 @@ + * + * @author 2018 Christoph Wurst + * + * @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 lib\Authentication\TwoFactorAuth; diff --git a/tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php b/tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php index 46191780f81..a6f0a703d5e 100644 --- a/tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php +++ b/tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php @@ -1,4 +1,5 @@