Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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 core;
  18  
  19  use core\oauth2\access_token;
  20  use core\oauth2\api;
  21  use core\oauth2\endpoint;
  22  use core\oauth2\issuer;
  23  use core\oauth2\system_account;
  24  
  25  /**
  26   * Tests for oauth2 apis (\core\oauth2\*).
  27   *
  28   * @package    core
  29   * @copyright  2017 Damyon Wiese
  30   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
  31   * @coversDefaultClass \core\oauth2\api
  32   */
  33  class oauth2_test extends \advanced_testcase {
  34  
  35      /**
  36       * Tests the crud operations on oauth2 issuers.
  37       */
  38      public function test_create_and_delete_standard_issuers() {
  39          $this->resetAfterTest();
  40          $this->setAdminUser();
  41          api::create_standard_issuer('google');
  42          api::create_standard_issuer('facebook');
  43          api::create_standard_issuer('microsoft');
  44          api::create_standard_issuer('nextcloud', 'https://dummy.local/nextcloud/');
  45  
  46          $issuers = api::get_all_issuers();
  47  
  48          $this->assertEquals($issuers[0]->get('name'), 'Google');
  49          $this->assertEquals($issuers[1]->get('name'), 'Facebook');
  50          $this->assertEquals($issuers[2]->get('name'), 'Microsoft');
  51          $this->assertEquals($issuers[3]->get('name'), 'Nextcloud');
  52  
  53          api::move_down_issuer($issuers[0]->get('id'));
  54  
  55          $issuers = api::get_all_issuers();
  56  
  57          $this->assertEquals($issuers[0]->get('name'), 'Facebook');
  58          $this->assertEquals($issuers[1]->get('name'), 'Google');
  59          $this->assertEquals($issuers[2]->get('name'), 'Microsoft');
  60          $this->assertEquals($issuers[3]->get('name'), 'Nextcloud');
  61  
  62          api::delete_issuer($issuers[1]->get('id'));
  63  
  64          $issuers = api::get_all_issuers();
  65  
  66          $this->assertEquals($issuers[0]->get('name'), 'Facebook');
  67          $this->assertEquals($issuers[1]->get('name'), 'Microsoft');
  68          $this->assertEquals($issuers[2]->get('name'), 'Nextcloud');
  69      }
  70  
  71      /**
  72       * Tests the crud operations on oauth2 issuers.
  73       */
  74      public function test_create_nextcloud_without_url() {
  75          $this->resetAfterTest();
  76          $this->setAdminUser();
  77  
  78          $this->expectException(\moodle_exception::class);
  79          api::create_standard_issuer('nextcloud');
  80      }
  81  
  82      /**
  83       * Tests we can list and delete each of the persistents related to an issuer.
  84       */
  85      public function test_getters() {
  86          $this->resetAfterTest();
  87          $this->setAdminUser();
  88          $issuer = api::create_standard_issuer('microsoft');
  89  
  90          $same = api::get_issuer($issuer->get('id'));
  91  
  92          foreach ($same->properties_definition() as $name => $def) {
  93              $this->assertTrue($issuer->get($name) == $same->get($name));
  94          }
  95  
  96          $endpoints = api::get_endpoints($issuer);
  97          $same = api::get_endpoint($endpoints[0]->get('id'));
  98          $this->assertEquals($endpoints[0]->get('id'), $same->get('id'));
  99          $this->assertEquals($endpoints[0]->get('name'), $same->get('name'));
 100  
 101          $todelete = $endpoints[0];
 102          api::delete_endpoint($todelete->get('id'));
 103          $endpoints = api::get_endpoints($issuer);
 104          $this->assertNotEquals($endpoints[0]->get('id'), $todelete->get('id'));
 105  
 106          $userfields = api::get_user_field_mappings($issuer);
 107          $same = api::get_user_field_mapping($userfields[0]->get('id'));
 108          $this->assertEquals($userfields[0]->get('id'), $same->get('id'));
 109  
 110          $todelete = $userfields[0];
 111          api::delete_user_field_mapping($todelete->get('id'));
 112          $userfields = api::get_user_field_mappings($issuer);
 113          $this->assertNotEquals($userfields[0]->get('id'), $todelete->get('id'));
 114      }
 115  
 116      /**
 117       * Data provider for \core_oauth2_testcase::test_get_system_oauth_client().
 118       *
 119       * @return array
 120       */
 121      public function system_oauth_client_provider() {
 122          return [
 123              [
 124                  (object) [
 125                      'access_token' => 'fdas...',
 126                      'token_type' => 'Bearer',
 127                      'expires_in' => '3600',
 128                      'id_token' => 'llfsd..',
 129                  ], HOURSECS - 10
 130              ],
 131              [
 132                  (object) [
 133                      'access_token' => 'fdas...',
 134                      'token_type' => 'Bearer',
 135                      'id_token' => 'llfsd..',
 136                  ], WEEKSECS
 137              ],
 138          ];
 139      }
 140  
 141      /**
 142       * Tests we can get a logged in oauth client for a system account.
 143       *
 144       * @dataProvider system_oauth_client_provider
 145       * @param \stdClass $responsedata The response data to be mocked.
 146       * @param int $expiresin The expected expiration time.
 147       */
 148      public function test_get_system_oauth_client($responsedata, $expiresin) {
 149          $this->resetAfterTest();
 150          $this->setAdminUser();
 151  
 152          $issuer = api::create_standard_issuer('microsoft');
 153  
 154          $requiredscopes = api::get_system_scopes_for_issuer($issuer);
 155          // Fake a system account.
 156          $data = (object) [
 157              'issuerid' => $issuer->get('id'),
 158              'refreshtoken' => 'abc',
 159              'grantedscopes' => $requiredscopes,
 160              'email' => 'sys@example.com',
 161              'username' => 'sys'
 162          ];
 163          $sys = new system_account(0, $data);
 164          $sys->create();
 165  
 166          // Fake a response with an access token.
 167          $response = json_encode($responsedata);
 168          \curl::mock_response($response);
 169          $client = api::get_system_oauth_client($issuer);
 170          $this->assertTrue($client->is_logged_in());
 171  
 172          // Check token expiry.
 173          $accesstoken = access_token::get_record(['issuerid' => $issuer->get('id')]);
 174  
 175          // Get the difference between the actual and expected expiry times.
 176          // They might differ by a couple of seconds depending on the timing when the token gets actually processed.
 177          $expiresdifference = time() + $expiresin - $accesstoken->get('expires');
 178  
 179          // Assert that the actual token expiration is more or less the same as the expected.
 180          $this->assertGreaterThanOrEqual(0, $expiresdifference);
 181          $this->assertLessThanOrEqual(3, $expiresdifference);
 182      }
 183  
 184      /**
 185       * Tests we can enable and disable an issuer.
 186       */
 187      public function test_enable_disable_issuer() {
 188          $this->resetAfterTest();
 189          $this->setAdminUser();
 190  
 191          $issuer = api::create_standard_issuer('microsoft');
 192  
 193          $issuerid = $issuer->get('id');
 194  
 195          api::enable_issuer($issuerid);
 196          $check = api::get_issuer($issuer->get('id'));
 197          $this->assertTrue((boolean)$check->get('enabled'));
 198  
 199          api::enable_issuer($issuerid);
 200          $check = api::get_issuer($issuer->get('id'));
 201          $this->assertTrue((boolean)$check->get('enabled'));
 202  
 203          api::disable_issuer($issuerid);
 204          $check = api::get_issuer($issuer->get('id'));
 205          $this->assertFalse((boolean)$check->get('enabled'));
 206  
 207          api::enable_issuer($issuerid);
 208          $check = api::get_issuer($issuer->get('id'));
 209          $this->assertTrue((boolean)$check->get('enabled'));
 210      }
 211  
 212      /**
 213       * Test the alloweddomains for an issuer.
 214       */
 215      public function test_issuer_alloweddomains() {
 216          $this->resetAfterTest();
 217          $this->setAdminUser();
 218  
 219          $issuer = api::create_standard_issuer('microsoft');
 220  
 221          $issuer->set('alloweddomains', '');
 222  
 223          // Anything is allowed when domain is empty.
 224          $this->assertTrue($issuer->is_valid_login_domain(''));
 225          $this->assertTrue($issuer->is_valid_login_domain('a@b'));
 226          $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com'));
 227  
 228          $issuer->set('alloweddomains', 'example.com');
 229  
 230          // One domain - must match exactly - no substrings etc.
 231          $this->assertFalse($issuer->is_valid_login_domain(''));
 232          $this->assertFalse($issuer->is_valid_login_domain('a@b'));
 233          $this->assertFalse($issuer->is_valid_login_domain('longer.example@example'));
 234          $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com'));
 235  
 236          $issuer->set('alloweddomains', 'example.com,example.net');
 237          // Multiple domains - must match any exactly - no substrings etc.
 238          $this->assertFalse($issuer->is_valid_login_domain(''));
 239          $this->assertFalse($issuer->is_valid_login_domain('a@b'));
 240          $this->assertFalse($issuer->is_valid_login_domain('longer.example@example'));
 241          $this->assertFalse($issuer->is_valid_login_domain('invalid@email@example.net'));
 242          $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.net'));
 243          $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com'));
 244  
 245          $issuer->set('alloweddomains', '*.example.com');
 246          // Wildcard.
 247          $this->assertFalse($issuer->is_valid_login_domain(''));
 248          $this->assertFalse($issuer->is_valid_login_domain('a@b'));
 249          $this->assertFalse($issuer->is_valid_login_domain('longer.example@example'));
 250          $this->assertFalse($issuer->is_valid_login_domain('longer.example@example.com'));
 251          $this->assertTrue($issuer->is_valid_login_domain('longer.example@sub.example.com'));
 252      }
 253  
 254      /**
 255       * Test endpoints creation for issuers.
 256       * @dataProvider create_endpoints_for_standard_issuer_provider
 257       *
 258       * @covers ::create_endpoints_for_standard_issuer
 259       *
 260       * @param string $type Issuer type to create.
 261       * @param string|null $discoveryurl Expected discovery URL or null if this endpoint doesn't exist.
 262       * @param bool $hasmappingfields True if it's expected the issuer to create has mapping fields.
 263       * @param string|null $baseurl The service URL (mandatory parameter for some issuers, such as NextCloud or IMS OBv2.1).
 264       * @param string|null $expectedexception Name of the expected expection or null if no exception will be thrown.
 265       */
 266      public function test_create_endpoints_for_standard_issuer(string $type, ?string $discoveryurl = null,
 267          bool $hasmappingfields = true, ?string $baseurl = null, ?string $expectedexception = null): void {
 268  
 269          $this->resetAfterTest();
 270  
 271          // Mark test as long because it connects with external services.
 272          if (!PHPUNIT_LONGTEST) {
 273              $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
 274          }
 275  
 276          $this->setAdminUser();
 277  
 278          // Method create_endpoints_for_standard_issuer is called internally from create_standard_issuer.
 279          if ($expectedexception) {
 280              $this->expectException($expectedexception);
 281          }
 282          $issuer = api::create_standard_issuer($type, $baseurl);
 283  
 284          // Check endpoints have been created.
 285          $endpoints = api::get_endpoints($issuer);
 286          $this->assertNotEmpty($endpoints);
 287          $this->assertNotEmpty($issuer->get('image'));
 288          // Check discovery URL.
 289          if ($discoveryurl) {
 290              $this->assertStringContainsString($discoveryurl, $issuer->get_endpoint_url('discovery'));
 291          } else {
 292              $this->assertFalse($issuer->get_endpoint_url('discovery'));
 293          }
 294          // Check userfield mappings.
 295          $userfieldmappings =api::get_user_field_mappings($issuer);
 296          if ($hasmappingfields) {
 297              $this->assertNotEmpty($userfieldmappings);
 298          } else {
 299              $this->assertEmpty($userfieldmappings);
 300          }
 301      }
 302  
 303      /**
 304       * Data provider for test_create_endpoints_for_standard_issuer.
 305       *
 306       * @return array
 307       */
 308      public function create_endpoints_for_standard_issuer_provider(): array {
 309          return [
 310              'Google' => [
 311                  'type' => 'google',
 312                  'discoveryurl' => '.well-known/openid-configuration',
 313              ],
 314              'Google will work too with a valid baseurl parameter' => [
 315                  'type' => 'google',
 316                  'discoveryurl' => '.well-known/openid-configuration',
 317                  'hasmappingfields' => true,
 318                  'baseurl' => 'https://accounts.google.com/',
 319              ],
 320              'IMS OBv2.1' => [
 321                  'type' => 'imsobv2p1',
 322                  'discoveryurl' => '.well-known/badgeconnect.json',
 323                  'hasmappingfields' => false,
 324                  'baseurl' => 'https://dc.imsglobal.org/',
 325              ],
 326              'IMS OBv2.1 without slash in baseurl should work too' => [
 327                  'type' => 'imsobv2p1',
 328                  'discoveryurl' => '.well-known/badgeconnect.json',
 329                  'hasmappingfields' => false,
 330                  'baseurl' => 'https://dc.imsglobal.org',
 331              ],
 332              'IMS OBv2.1 with empty baseurl should return an exception' => [
 333                  'type' => 'imsobv2p1',
 334                  'discoveryurl' => null,
 335                  'hasmappingfields' => false,
 336                  'baseurl' => null,
 337                  'expectedexception' => \moodle_exception::class,
 338              ],
 339              'Microsoft' => [
 340                  'type' => 'microsoft',
 341              ],
 342              'Facebook' => [
 343                  'type' => 'facebook',
 344              ],
 345              'NextCloud' => [
 346                  'type' => 'nextcloud',
 347                  'discoveryurl' => null,
 348                  'hasmappingfields' => true,
 349                  'baseurl' => 'https://dummy.local/nextcloud/',
 350              ],
 351              'NextCloud with empty baseurl should return an exception' => [
 352                  'type' => 'nextcloud',
 353                  'discoveryurl' => null,
 354                  'hasmappingfields' => true,
 355                  'baseurl' => null,
 356                  'expectedexception' => \moodle_exception::class,
 357              ],
 358              'Invalid type should return an exception' => [
 359                  'type' => 'fictitious',
 360                  'discoveryurl' => null,
 361                  'hasmappingfields' => true,
 362                  'baseurl' => null,
 363                  'expectedexception' => \moodle_exception::class,
 364              ],
 365          ];
 366      }
 367  
 368      /**
 369       * Test for get all issuers.
 370       */
 371      public function test_get_all_issuers() {
 372          $this->resetAfterTest();
 373          $this->setAdminUser();
 374          $googleissuer = api::create_standard_issuer('google');
 375          api::create_standard_issuer('facebook');
 376          api::create_standard_issuer('microsoft');
 377  
 378          // Set Google issuer to be shown only on login page.
 379          $record = $googleissuer->to_record();
 380          $record->showonloginpage = $googleissuer::LOGINONLY;
 381          api::update_issuer($record);
 382  
 383          $issuers = api::get_all_issuers();
 384          $this->assertCount(2, $issuers);
 385          $expected = ['Microsoft', 'Facebook'];
 386          $this->assertEqualsCanonicalizing($expected, [$issuers[0]->get_display_name(), $issuers[1]->get_display_name()]);
 387  
 388          $issuers = api::get_all_issuers(true);
 389          $this->assertCount(3, $issuers);
 390          $expected = ['Google', 'Microsoft', 'Facebook'];
 391          $this->assertEqualsCanonicalizing($expected,
 392              [$issuers[0]->get_display_name(), $issuers[1]->get_display_name(), $issuers[2]->get_display_name()]);
 393      }
 394  
 395      /**
 396       * Test for is available for login.
 397       */
 398      public function test_is_available_for_login() {
 399          $this->resetAfterTest();
 400          $this->setAdminUser();
 401          $googleissuer = api::create_standard_issuer('google');
 402  
 403          // Set Google issuer to be shown only on login page.
 404          $record = $googleissuer->to_record();
 405          $record->showonloginpage = $googleissuer::LOGINONLY;
 406          api::update_issuer($record);
 407  
 408          $this->assertFalse($googleissuer->is_available_for_login());
 409  
 410          // Set a clientid and clientsecret.
 411          $googleissuer->set('clientid', 'clientid');
 412          $googleissuer->set('clientsecret', 'secret');
 413          $googleissuer->update();
 414  
 415          $this->assertTrue($googleissuer->is_available_for_login());
 416  
 417          // Set showonloginpage to service only.
 418          $googleissuer->set('showonloginpage', issuer::SERVICEONLY);
 419          $googleissuer->update();
 420  
 421          $this->assertFalse($googleissuer->is_available_for_login());
 422  
 423          // Set showonloginpage to everywhere (service and login) and disable issuer.
 424          $googleissuer->set('showonloginpage', issuer::EVERYWHERE);
 425          $googleissuer->set('enabled', 0);
 426          $googleissuer->update();
 427  
 428          $this->assertFalse($googleissuer->is_available_for_login());
 429  
 430          // Enable issuer.
 431          $googleissuer->set('enabled', 1);
 432          $googleissuer->update();
 433  
 434          $this->assertTrue($googleissuer->is_available_for_login());
 435  
 436          // Remove userinfo endpoint from issuer.
 437          $endpoint = endpoint::get_record([
 438              'issuerid' => $googleissuer->get('id'),
 439              'name' => 'userinfo_endpoint'
 440          ]);
 441          api::delete_endpoint($endpoint->get('id'));
 442  
 443          $this->assertFalse($googleissuer->is_available_for_login());
 444      }
 445  }