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  
  19  /**
  20   * Tests for MFA secret manager class.
  21   *
  22   * @package     tool_mfa
  23   * @author      Peter Burnett <peterburnett@catalyst-au.net>
  24   * @copyright   Catalyst IT
  25   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26   */
  27  class secret_manager_test extends \advanced_testcase {
  28  
  29      /**
  30       * Tests create factor's secret
  31       *
  32       * @covers ::create_secret
  33       */
  34      public function test_create_secret() {
  35          global $DB;
  36  
  37          $this->resetAfterTest(true);
  38          $this->setUser($this->getDataGenerator()->create_user());
  39  
  40          // Test adding secret to DB.
  41          $secman = new \tool_mfa\local\secret_manager('mock');
  42  
  43          // Mutate the sessionid using reflection.
  44          $reflectedsessionid = new \ReflectionProperty($secman, 'sessionid');
  45          $reflectedsessionid->setAccessible(true);
  46          $reflectedsessionid->setValue($secman, 'fakesession');
  47  
  48          $sec1 = $secman->create_secret(1800, false);
  49          $count1 = $DB->count_records('tool_mfa_secrets', ['factor' => 'mock']);
  50          $record1 = $DB->get_record('tool_mfa_secrets', []);
  51          $this->assertEquals(1, $count1);
  52          $this->assertNotEquals('', $sec1);
  53          $this->assertTrue(empty($record1->sessionid));
  54          $sec2 = $secman->create_secret(1800, false);
  55          $count2 = $DB->count_records('tool_mfa_secrets', ['factor' => 'mock']);
  56          $this->assertEquals(1, $count2);
  57          $this->assertEquals('', $sec2);
  58          $DB->delete_records('tool_mfa_secrets', []);
  59  
  60          // Now adding secret to session.
  61          $sec1 = $secman->create_secret(1800, true);
  62          $count1 = $DB->count_records('tool_mfa_secrets', ['factor' => 'mock']);
  63          $record1 = $DB->get_record('tool_mfa_secrets', []);
  64          $this->assertEquals(1, $count1);
  65          $this->assertNotEquals('', $sec1);
  66          $this->assertEquals('fakesession', $record1->sessionid);
  67          $sec2 = $secman->create_secret(1800, true);
  68          $this->assertEquals('', $sec2);
  69          $DB->delete_records('tool_mfa_secrets', []);
  70  
  71          // Now test adding a forced code.
  72          $sec1 = $secman->create_secret(1800, false);
  73          $count1 = $DB->count_records('tool_mfa_secrets', ['factor' => 'mock']);
  74          $this->assertEquals(1, $count1);
  75          $this->assertNotEquals('', $sec1);
  76          $sec2 = $secman->create_secret(1800, false, 'code');
  77          $count2 = $DB->count_records('tool_mfa_secrets', ['factor' => 'mock']);
  78          $this->assertEquals(2, $count2);
  79          $this->assertEquals('code', $sec2);
  80          $DB->delete_records('tool_mfa_secrets', []);
  81      }
  82  
  83      /**
  84       * Tests add factor's secret to database
  85       *
  86       * @covers ::get_record
  87       * @covers ::delete_records
  88       */
  89      public function test_add_secret_to_db() {
  90          global $DB, $USER;
  91  
  92          $this->resetAfterTest(true);
  93          $secman = new \tool_mfa\local\secret_manager('mock');
  94          $this->setUser($this->getDataGenerator()->create_user());
  95          $sid = 'fakeid';
  96  
  97          // Let's make stuff public using reflection.
  98          $reflectedscanner = new \ReflectionClass($secman);
  99          $reflectedmethod = $reflectedscanner->getMethod('add_secret_to_db');
 100          $reflectedmethod->setAccessible(true);
 101  
 102          // Now add a secret and confirm it creates the correct record.
 103          $reflectedmethod->invoke($secman, 'code', 1800);
 104          $record = $DB->get_record('tool_mfa_secrets', []);
 105          $this->assertEquals('code', $record->secret);
 106          $this->assertEquals($USER->id, $record->userid);
 107          $this->assertEquals('mock', $record->factor);
 108          $this->assertGreaterThanOrEqual(time(), (int) $record->expiry);
 109          $this->assertEquals(0, $record->revoked);
 110          $DB->delete_records('tool_mfa_secrets', []);
 111  
 112          // Now add a sessionid and confirm it is added correctly.
 113          $reflectedmethod->invoke($secman, 'code', 1800, $sid);
 114          $record = $DB->get_record('tool_mfa_secrets', []);
 115          $this->assertEquals('code', $record->secret);
 116          $this->assertGreaterThanOrEqual(time(), (int) $record->expiry);
 117          $this->assertEquals(0, $record->revoked);
 118          $this->assertEquals($sid, $record->sessionid);
 119      }
 120  
 121      /**
 122       * Tests validating factor's secret
 123       *
 124       * @covers ::validate_secret
 125       * @covers ::create_secret
 126       */
 127      public function test_validate_secret() {
 128          global $DB;
 129  
 130          // Test adding a code and getting it returned, then validated.
 131          $this->resetAfterTest(true);
 132          $this->setUser($this->getDataGenerator()->create_user());
 133          $secman = new \tool_mfa\local\secret_manager('mock');
 134  
 135          $secret = $secman->create_secret(1800, false);
 136          $this->assertEquals(\tool_mfa\local\secret_manager::VALID, $secman->validate_secret($secret));
 137          $DB->delete_records('tool_mfa_secrets', []);
 138  
 139          // Test a manual forced code.
 140          $secret = $secman->create_secret(1800, false, 'code');
 141          $this->assertEquals(\tool_mfa\local\secret_manager::VALID, $secman->validate_secret($secret));
 142          $this->assertEquals('code', $secret);
 143          $DB->delete_records('tool_mfa_secrets', []);
 144  
 145          // Test bad codes.
 146          $secret = $secman->create_secret(1800, false);
 147          $this->assertEquals(\tool_mfa\local\secret_manager::NONVALID, $secman->validate_secret('nonvalid'));
 148          $DB->delete_records('tool_mfa_secrets', []);
 149  
 150          // Test validate when no secrets present.
 151          $this->assertEquals(\tool_mfa\local\secret_manager::NONVALID, $secman->validate_secret('nonvalid'));
 152  
 153          // Test revoked secrets.
 154          $secret = $secman->create_secret(1800, false);
 155          $DB->set_field('tool_mfa_secrets', 'revoked', 1, []);
 156          $this->assertEquals(\tool_mfa\local\secret_manager::REVOKED, $secman->validate_secret($secret));
 157          $DB->delete_records('tool_mfa_secrets', []);
 158  
 159          // Test expired secrets.
 160          $secret = $secman->create_secret(-1, false);
 161          $this->assertEquals(\tool_mfa\local\secret_manager::NONVALID, $secman->validate_secret($secret));
 162          $DB->delete_records('tool_mfa_secrets', []);
 163  
 164          // Session locked code from the same session id.
 165          // Mutate the sessionid using reflection.
 166          $reflectedsessionid = new \ReflectionProperty($secman, 'sessionid');
 167          $reflectedsessionid->setAccessible(true);
 168          $reflectedsessionid->setValue($secman, 'fakesession');
 169  
 170          $secret = $secman->create_secret(1800, true);
 171          $this->assertEquals(\tool_mfa\local\secret_manager::VALID, $secman->validate_secret($secret));
 172          $DB->delete_records('tool_mfa_secrets', []);
 173  
 174          // Now test a session locked code from a different sessionid.
 175          $secret = $secman->create_secret(1800, true);
 176          $reflectedsessionid->setValue($secman, 'diffsession');
 177          $this->assertEquals(\tool_mfa\local\secret_manager::NONVALID, $secman->validate_secret($secret));
 178          $DB->delete_records('tool_mfa_secrets', []);
 179      }
 180  
 181      /**
 182       * Tests revoking factor's secret
 183       *
 184       * @covers ::validate_secret
 185       * @covers ::create_secret
 186       * @covers ::revoke_secret
 187       */
 188      public function test_revoke_secret() {
 189          global $DB, $SESSION;
 190  
 191          $this->resetAfterTest(true);
 192          $secman = new \tool_mfa\local\secret_manager('mock');
 193          $this->setUser($this->getDataGenerator()->create_user());
 194  
 195          // Session secrets.
 196          $secret = $secman->create_secret(1800, true);
 197          $secman->revoke_secret($secret);
 198          $this->assertEquals(\tool_mfa\local\secret_manager::REVOKED, $secman->validate_secret($secret));
 199          unset($SESSION->tool_mfa_secrets_mock);
 200  
 201          // DB secrets.
 202          $secret = $secman->create_secret(1800, false);
 203          $secman->revoke_secret($secret);
 204          $this->assertEquals(\tool_mfa\local\secret_manager::REVOKED, $secman->validate_secret($secret));
 205          $DB->delete_records('tool_mfa_secrets', []);
 206  
 207          // Revoke a non-valid secret.
 208          $secret = $secman->create_secret(1800, false);
 209          $secman->revoke_secret('nonvalid');
 210          $this->assertEquals(\tool_mfa\local\secret_manager::NONVALID, $secman->validate_secret('nonvalid'));
 211      }
 212  
 213      /**
 214       * Tests checking if factor has an active secret
 215       *
 216       * @covers ::create_secret
 217       * @covers ::revoke_secret
 218       */
 219      public function test_has_active_secret() {
 220          global $DB;
 221  
 222          $this->resetAfterTest(true);
 223          $secman = new \tool_mfa\local\secret_manager('mock');
 224          $this->setUser($this->getDataGenerator()->create_user());
 225  
 226          // Let's make stuff public using reflection.
 227          $reflectedscanner = new \ReflectionClass($secman);
 228  
 229          $reflectedmethod = $reflectedscanner->getMethod('has_active_secret');
 230          $reflectedmethod->setAccessible(true);
 231  
 232          // DB secrets.
 233          $this->assertFalse($reflectedmethod->invoke($secman));
 234          $secman->create_secret(1800, false);
 235          $this->assertTrue($reflectedmethod->invoke($secman));
 236          $DB->delete_records('tool_mfa_secrets', []);
 237          $secman->create_secret(-1, false);
 238          $this->assertFalse($reflectedmethod->invoke($secman));
 239          $DB->delete_records('tool_mfa_secrets', []);
 240          $secret = $secman->create_secret(1800, false);
 241          $secman->revoke_secret($secret);
 242          $this->assertFalse($reflectedmethod->invoke($secman));
 243  
 244          // Now check a secret with session involvement.
 245          // Mutate the sessionid using reflection.
 246          $reflectedsessionid = new \ReflectionProperty($secman, 'sessionid');
 247          $reflectedsessionid->setAccessible(true);
 248          $reflectedsessionid->setValue($secman, 'fakesession');
 249  
 250          $this->assertFalse($reflectedmethod->invoke($secman, true));
 251          $secman->create_secret(1800, true);
 252          $this->assertTrue($reflectedmethod->invoke($secman, true));
 253          $DB->delete_records('tool_mfa_secrets', []);
 254          $secman->create_secret(-1, true);
 255          $this->assertFalse($reflectedmethod->invoke($secman, true));
 256          $DB->delete_records('tool_mfa_secrets', []);
 257          $secret = $secman->create_secret(1800, true);
 258          $secman->revoke_secret($secret);
 259          $this->assertFalse($reflectedmethod->invoke($secman, true));
 260          $DB->delete_records('tool_mfa_secrets', []);
 261          $secret = $secman->create_secret(1800, true);
 262           $reflectedsessionid->setValue($secman, 'diffsession');
 263          $this->assertFalse($reflectedmethod->invoke($secman, true));
 264      }
 265  }