Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace tool_mfa;
  18  use tool_mfa\tool_mfa_trait;
  19  
  20  defined('MOODLE_INTERNAL') || die();
  21  require_once (__DIR__ . '/tool_mfa_trait.php');
  22  
  23  /**
  24   * Tests for MFA manager class.
  25   *
  26   * @package     tool_mfa
  27   * @author      Peter Burnett <peterburnett@catalyst-au.net>
  28   * @copyright   Catalyst IT
  29   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  class manager_test extends \advanced_testcase {
  32  
  33      use tool_mfa_trait;
  34  
  35      /**
  36       * Tests getting the factor total weight
  37       *
  38       * @covers ::get_total_weight
  39       * @covers ::setup_user_factor
  40       */
  41      public function test_get_total_weight() {
  42          $this->resetAfterTest(true);
  43  
  44          // Create and login a user.
  45          $user = $this->getDataGenerator()->create_user();
  46          $this->setUser($user);
  47  
  48          // First get weight with no active factors.
  49          $this->assertEquals(0, \tool_mfa\manager::get_total_weight());
  50  
  51          // Now setup a couple of input based factors.
  52          $this->set_factor_state('totp', 1, 100);
  53  
  54          $this->set_factor_state('email', 1, 100);
  55  
  56          // Check weight is still 0 with no passes.
  57          $this->assertEquals(0, \tool_mfa\manager::get_total_weight());
  58  
  59          // Manually pass 1 .
  60          $factor = \tool_mfa\plugininfo\factor::get_factor('totp');
  61          $totpdata = [
  62              'secret' => 'fakekey',
  63              'devicename' => 'fakedevice',
  64          ];
  65          $this->assertNotEmpty($factor->setup_user_factor((object) $totpdata));
  66          $factor->set_state(\tool_mfa\plugininfo\factor::STATE_PASS);
  67          $this->assertEquals(100, \tool_mfa\manager::get_total_weight());
  68  
  69          // Now both.
  70          $factor2 = \tool_mfa\plugininfo\factor::get_factor('email');
  71          $factor2->set_state(\tool_mfa\plugininfo\factor::STATE_PASS);
  72          $this->assertEquals(200, \tool_mfa\manager::get_total_weight());
  73  
  74          // Now setup a no input factor, and check that weight is automatically added without input.
  75          $this->set_factor_state('auth', 1, 100);
  76          set_config('goodauth', 'manual', 'factor_auth');
  77  
  78          $this->assertEquals(300, \tool_mfa\manager::get_total_weight());
  79      }
  80  
  81      /**
  82       * Tests getting the factor status
  83       *
  84       * @covers ::get_status
  85       */
  86      public function test_get_status() {
  87          $this->resetAfterTest(true);
  88  
  89          // Create and login a user.
  90          $user = $this->getDataGenerator()->create_user();
  91          $this->setUser($user);
  92  
  93          // Check for fail status with no factors.
  94          $this->assertEquals(\tool_mfa\plugininfo\factor::STATE_FAIL, \tool_mfa\manager::get_status());
  95  
  96          // Now add a no input factor.
  97          $this->set_factor_state('auth', 1, 100);
  98          set_config('goodauth', 'manual', 'factor_auth');
  99  
 100          // Check state is now passing.
 101          $this->assertEquals(\tool_mfa\plugininfo\factor::STATE_PASS, \tool_mfa\manager::get_status());
 102  
 103          // Now add a failure state factor, and ensure that fail takes precedent.
 104          $this->set_factor_state('email', 1, 100);
 105          $factoremail = \tool_mfa\plugininfo\factor::get_factor('email');
 106          $factoremail->set_state(\tool_mfa\plugininfo\factor::STATE_FAIL);
 107  
 108          $this->assertEquals(\tool_mfa\plugininfo\factor::STATE_FAIL, \tool_mfa\manager::get_status());
 109  
 110          // Remove no input factor, and remove fail state by logging in/out. Simulates no data entered yet.
 111          $this->setUser(null);
 112          $this->setUser($user);
 113          $this->set_factor_state('auth', 0, 100);
 114          $factoremail->set_state(\tool_mfa\plugininfo\factor::STATE_UNKNOWN);
 115  
 116          $this->assertEquals(\tool_mfa\plugininfo\factor::STATE_NEUTRAL, \tool_mfa\manager::get_status());
 117      }
 118  
 119      /**
 120       * Tests checking if passed enough factors
 121       *
 122       * @covers ::passed_enough_factors
 123       */
 124      public function test_passed_enough_factors() {
 125          $this->resetAfterTest(true);
 126  
 127          // Create and login a user.
 128          $user = $this->getDataGenerator()->create_user();
 129          $this->setUser($user);
 130  
 131          // Check when no factors are setup.
 132          $this->assertEquals(false, \tool_mfa\manager::passed_enough_factors());
 133  
 134          // Setup a no input factor.
 135          $this->set_factor_state('auth', 1, 100);
 136          set_config('goodauth', 'manual', 'factor_auth');
 137  
 138          // Check that is enough to pass.
 139          $this->assertEquals(true, \tool_mfa\manager::passed_enough_factors());
 140  
 141          // Lower the weight of the factor.
 142          $this->set_factor_state('auth', 1, 75);
 143          $this->assertEquals(false, \tool_mfa\manager::passed_enough_factors());
 144  
 145          // Add another factor to get enough weight to pass, but dont set pass state yet.
 146          $this->set_factor_state('email', 1, 100);
 147          $factoremail = \tool_mfa\plugininfo\factor::get_factor('email');
 148          $this->assertEquals(false, \tool_mfa\manager::passed_enough_factors());
 149  
 150          // Now pass the factor and check weight.
 151          $factoremail->set_state(\tool_mfa\plugininfo\factor::STATE_PASS);
 152          $this->assertEquals(true, \tool_mfa\manager::passed_enough_factors());
 153      }
 154  
 155      /**
 156       * The data provider for whether urls should be redirected or not
 157       *
 158       * @return  array
 159       */
 160      public static function should_redirect_urls_provider() {
 161          $badurl1 = new \moodle_url('/');
 162          $badparam1 = $badurl1->out();
 163          $badurl2 = new \moodle_url('admin/tool/mfa/auth.php');
 164          $badparam2 = $badurl2->out();
 165          return [
 166              ['/', 'http://test.server', true],
 167              ['/admin/tool/mfa/action.php', 'http://test.server', true],
 168              ['/admin/tool/mfa/factor/totp/settings.php', 'http://test.server', true],
 169              ['/', 'http://test.server', true, ['url' => $badparam1]],
 170              ['/', 'http://test.server', true, ['url' => $badparam2]],
 171              ['/admin/tool/mfa/auth.php', 'http://test.server', false],
 172              ['/admin/tool/mfa/auth.php', 'http://test.server/parent/directory', false],
 173              ['/admin/tool/mfa/action.php', 'http://test.server/parent/directory', true],
 174              ['/', 'http://test.server/parent/directory', true, ['url' => $badparam1]],
 175              ['/', 'http://test.server/parent/directory', true, ['url' => $badparam2]],
 176          ];
 177      }
 178  
 179      /**
 180       * Tests whether it should require mfa
 181       *
 182       * @covers ::should_require_mfa
 183       * @param string $urlstring
 184       * @param string $webroot
 185       * @param bool $status
 186       * @param array|null $params
 187       * @dataProvider should_redirect_urls_provider
 188       */
 189      public function test_should_require_mfa_urls($urlstring, $webroot, $status, $params = null) {
 190          $this->resetAfterTest(true);
 191          global $CFG;
 192          $user = $this->getDataGenerator()->create_user();
 193          $this->setUser($user);
 194          $CFG->wwwroot = $webroot;
 195          $url = new \moodle_url($urlstring, $params);
 196          $this->assertEquals($status, \tool_mfa\manager::should_require_mfa($url, false));
 197      }
 198  
 199      /**
 200       * Tests whether it should require the mfa checks
 201       *
 202       * @covers ::should_require_mfa
 203       */
 204      public function test_should_require_mfa_checks() {
 205          // Setup test and user.
 206          global $CFG;
 207          $this->resetAfterTest(true);
 208          $user = $this->getDataGenerator()->create_user();
 209  
 210          $badurl = new \moodle_url('/');
 211  
 212          // Upgrade checks.
 213          $this->setAdminUser();
 214          // Mark the site as upgraded so it will not fail when running the unittest as a whole.
 215          $CFG->allversionshash = \core_component::get_all_versions_hash();
 216          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 217          $oldhash = $CFG->allversionshash;
 218          $CFG->allversionshash = 'abc';
 219          $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 220          $CFG->allversionshash = $oldhash;
 221          $upgradesettings = new \moodle_url('/admin/upgradesettings.php');
 222          $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($upgradesettings, false));
 223          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 224  
 225          // Admin not setup.
 226          $this->setUser($user);
 227          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 228          $CFG->adminsetuppending = 1;
 229          $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 230          $CFG->adminsetuppending = 0;
 231  
 232          // Check prevent_redirect.
 233          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 234          $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, true));
 235  
 236          // User not setup properly.
 237          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 238          $notsetup = clone($user);
 239          unset($notsetup->firstname);
 240          $this->setUser($notsetup);
 241          $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 242          $this->setUser($user);
 243  
 244          // Enrolment.
 245          $enrolurl = new \moodle_url('/enrol/index.php');
 246          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 247          $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($enrolurl, false));
 248  
 249          // Guest User.
 250          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 251          $this->setGuestUser();
 252          $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 253          $this->setUser($user);
 254  
 255          // Forced password changes.
 256          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 257          set_user_preference('auth_forcepasswordchange', true);
 258          $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 259          set_user_preference('auth_forcepasswordchange', false);
 260  
 261          // Login as check.
 262          $user2 = $this->getDataGenerator()->create_user();
 263          $syscontext = \context_system::instance();
 264          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 265          $this->setAdminUser();
 266          \core\session\manager::loginas($user2->id, $syscontext, false);
 267          $this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
 268          $this->setUser($user);
 269      }
 270  
 271      /**
 272       * Tests should require the mfa redirection loop
 273       *
 274       * @covers ::should_require_mfa
 275       */
 276      public function test_should_require_mfa_redirection_loop() {
 277          // Setup test and user.
 278          global $CFG, $SESSION;
 279          $CFG->wwwroot = 'http://phpunit.test';
 280          $this->resetAfterTest(true);
 281          $user = $this->getDataGenerator()->create_user();
 282          $this->setUser($user);
 283  
 284          // Set first referer url.
 285          $_SERVER['HTTP_REFERER'] = 'http://phpunit.test';
 286          $url = new \moodle_url('/');
 287  
 288          // Test you get three redirs then exception.
 289          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 290          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 291          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 292          // Set count to threshold.
 293          $SESSION->mfa_redir_count = 5;
 294          $this->assertEquals(\tool_mfa\manager::REDIRECT_EXCEPTION, \tool_mfa\manager::should_require_mfa($url, false));
 295          // Reset session vars.
 296          unset($SESSION->mfa_redir_referer);
 297          unset($SESSION->mfa_redir_count);
 298  
 299          // Test 4 different redir urls.
 300          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 301          $_SERVER['HTTP_REFERER'] = 'http://phpunit.test/2';
 302          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 303          $_SERVER['HTTP_REFERER'] = 'http://phpunit3.test/3';
 304          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 305          $_SERVER['HTTP_REFERER'] = 'http://phpunit4.test/4';
 306          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 307          // Reset session vars.
 308          unset($SESSION->mfa_redir_referer);
 309          unset($SESSION->mfa_redir_count);
 310  
 311          // Test 6 then jump to new referer (5 + 1 to set the first time).
 312          $_SERVER['HTTP_REFERER'] = 'http://phpunit.test';
 313          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 314          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 315          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 316          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 317          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 318          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 319  
 320          $_SERVER['HTTP_REFERER'] = 'http://phpunit.test/2';
 321          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 322          // Now test that going back to original URL doesnt cause exception.
 323          $_SERVER['HTTP_REFERER'] = 'http://phpunit.test';
 324          $this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($url, false));
 325      }
 326  
 327      /**
 328       * Tests checking for possible setup factor
 329       *
 330       * @covers ::possible_factor_setup
 331       * @covers ::setup_user_factor
 332       */
 333      public function test_possible_factor_setup() {
 334          // Setup test and user.
 335          $this->resetAfterTest(true);
 336          $user = $this->getDataGenerator()->create_user();
 337          $this->setUser($user);
 338  
 339          // Test for totp is able to be setup.
 340          set_config('enabled', 1, 'factor_totp');
 341          $this->assertTrue(\tool_mfa\manager::possible_factor_setup());
 342          set_config('enabled', 0, 'factor_totp');
 343  
 344          // Test TOTP is already setup and can be managed.
 345          $totp = \tool_mfa\plugininfo\factor::get_factor('totp');
 346          set_config('enabled', 1, 'factor_totp');
 347          $totpdata = [
 348              'secret' => 'fakekey',
 349              'devicename' => 'fakedevice',
 350          ];
 351          $this->assertNotEmpty($totp->setup_user_factor((object) $totpdata));
 352          $this->assertTrue(\tool_mfa\manager::possible_factor_setup());
 353          set_config('enabled', 0, 'factor_totp');
 354  
 355          // Test no factors can be setup.
 356          set_config('enabled', 1, 'factor_email');
 357          set_config('enabled', 1, 'factor_admin');
 358          $this->assertFalse(\tool_mfa\manager::possible_factor_setup());
 359          set_config('enabled', 0, 'factor_email');
 360          set_config('enabled', 0, 'factor_admin');
 361      }
 362  
 363      /**
 364       * Tests checking if a factor is ready
 365       *
 366       * @covers ::is_ready
 367       */
 368      public function test_is_ready() {
 369          // Setup test and user.
 370          global $CFG;
 371          $this->resetAfterTest(true);
 372          $user = $this->getDataGenerator()->create_user();
 373          $this->setUser($user);
 374          set_config('enabled', 1, 'factor_nosetup');
 375          set_config('enabled', 1, 'tool_mfa');
 376  
 377          // Capability Check.
 378          $this->assertTrue(\tool_mfa\manager::is_ready());
 379          // Swap to role without capability.
 380          $this->setGuestUser();
 381          $this->assertFalse(\tool_mfa\manager::is_ready());
 382          $this->setUser($user);
 383  
 384          // Enabled check.
 385          $this->assertTrue(\tool_mfa\manager::is_ready());
 386          set_config('enabled', 0, 'tool_mfa');
 387          $this->assertFalse(\tool_mfa\manager::is_ready());
 388          set_config('enabled', 1, 'tool_mfa');
 389  
 390          // Upgrade check.
 391          $this->assertTrue(\tool_mfa\manager::is_ready());
 392          $CFG->upgraderunning = true;
 393          $this->assertFalse(\tool_mfa\manager::is_ready());
 394          unset($CFG->upgraderunning);
 395  
 396          // No factors check.
 397          $this->assertTrue(\tool_mfa\manager::is_ready());
 398          set_config('enabled', 0, 'factor_nosetup');
 399          $this->assertFalse(\tool_mfa\manager::is_ready());
 400          set_config('enabled', 1, 'factor_nosetup');
 401      }
 402  
 403      /**
 404       * Tests core hooks
 405       *
 406       * @covers ::mfa_config_hook_test
 407       * @covers ::mfa_login_hook_test
 408       */
 409      public function test_core_hooks() {
 410          // Setup test and user.
 411          global $CFG, $SESSION;
 412          $this->resetAfterTest(true);
 413          $user = $this->getDataGenerator()->create_user();
 414          $this->setUser($user);
 415  
 416          // Require login to fire hooks. Config we get for free.
 417          require_login();
 418  
 419          $this->assertTrue($CFG->mfa_config_hook_test);
 420          $this->assertTrue($SESSION->mfa_login_hook_test);
 421      }
 422  
 423      /**
 424       * Tests circular redirect auth
 425       *
 426       * @covers ::should_require_mfa
 427       */
 428      public function test_circular_redirect_auth() {
 429          // Setup test and user.
 430          $this->resetAfterTest(true);
 431          $user = $this->getDataGenerator()->create_user();
 432          $this->setUser($user);
 433  
 434          // Spoof the referrer for the redirect check.
 435          $_SERVER['HTTP_REFERER'] = '/admin/tool/mfa/auth.php';
 436          $baseurl = new \moodle_url('/my/naughty/page.php');
 437  
 438          // After a single check, we should redirect.
 439          $this->assertEquals(\tool_mfa\manager::REDIRECT,
 440              \tool_mfa\manager::should_require_mfa($baseurl, false));
 441  
 442          // Now hammer it up to the threshold to emulate a repeated force browse from auth.php.
 443          for ($i = 0; $i < \tool_mfa\manager::REDIR_LOOP_THRESHOLD; $i++) {
 444              \tool_mfa\manager::should_require_mfa($baseurl, false);
 445          }
 446  
 447          // Now finally confirm that a 6th access attempt (after loop safety trigger) still redirects.
 448          $this->assertEquals(\tool_mfa\manager::REDIRECT,
 449              \tool_mfa\manager::should_require_mfa($baseurl, false));
 450      }
 451  }