Merge branch 'MDL-76985-MOODLE_400_STABLE' of https://github.com/sh-csg/moodle into...
[moodle.git] / lib / tests / oauth2_test.php
blob8b707375f9ebf1f97e9dc5d255ab079c246db0dc
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.
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/>.
17 namespace core;
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;
25 /**
26 * Tests for oauth2 apis (\core\oauth2\*).
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
33 class oauth2_test extends \advanced_testcase {
35 /**
36 * Tests the crud operations on oauth2 issuers.
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/');
46 $issuers = api::get_all_issuers();
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');
53 api::move_down_issuer($issuers[0]->get('id'));
55 $issuers = api::get_all_issuers();
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');
62 api::delete_issuer($issuers[1]->get('id'));
64 $issuers = api::get_all_issuers();
66 $this->assertEquals($issuers[0]->get('name'), 'Facebook');
67 $this->assertEquals($issuers[1]->get('name'), 'Microsoft');
68 $this->assertEquals($issuers[2]->get('name'), 'Nextcloud');
71 /**
72 * Tests the crud operations on oauth2 issuers.
74 public function test_create_nextcloud_without_url() {
75 $this->resetAfterTest();
76 $this->setAdminUser();
78 $this->expectException(\moodle_exception::class);
79 api::create_standard_issuer('nextcloud');
82 /**
83 * Tests we can list and delete each of the persistents related to an issuer.
85 public function test_getters() {
86 $this->resetAfterTest();
87 $this->setAdminUser();
88 $issuer = api::create_standard_issuer('microsoft');
90 $same = api::get_issuer($issuer->get('id'));
92 foreach ($same->properties_definition() as $name => $def) {
93 $this->assertTrue($issuer->get($name) == $same->get($name));
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'));
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'));
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'));
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'));
117 * Data provider for \core_oauth2_testcase::test_get_system_oauth_client().
119 * @return array
121 public function system_oauth_client_provider() {
122 return [
124 (object) [
125 'access_token' => 'fdas...',
126 'token_type' => 'Bearer',
127 'expires_in' => '3600',
128 'id_token' => 'llfsd..',
129 ], HOURSECS - 10
132 (object) [
133 'access_token' => 'fdas...',
134 'token_type' => 'Bearer',
135 'id_token' => 'llfsd..',
136 ], WEEKSECS
142 * Tests we can get a logged in oauth client for a system account.
144 * @dataProvider system_oauth_client_provider
145 * @param \stdClass $responsedata The response data to be mocked.
146 * @param int $expiresin The expected expiration time.
148 public function test_get_system_oauth_client($responsedata, $expiresin) {
149 $this->resetAfterTest();
150 $this->setAdminUser();
152 $issuer = api::create_standard_issuer('microsoft');
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'
163 $sys = new system_account(0, $data);
164 $sys->create();
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());
172 // Check token expiry.
173 $accesstoken = access_token::get_record(['issuerid' => $issuer->get('id')]);
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');
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);
185 * Tests we can enable and disable an issuer.
187 public function test_enable_disable_issuer() {
188 $this->resetAfterTest();
189 $this->setAdminUser();
191 $issuer = api::create_standard_issuer('microsoft');
193 $issuerid = $issuer->get('id');
195 api::enable_issuer($issuerid);
196 $check = api::get_issuer($issuer->get('id'));
197 $this->assertTrue((boolean)$check->get('enabled'));
199 api::enable_issuer($issuerid);
200 $check = api::get_issuer($issuer->get('id'));
201 $this->assertTrue((boolean)$check->get('enabled'));
203 api::disable_issuer($issuerid);
204 $check = api::get_issuer($issuer->get('id'));
205 $this->assertFalse((boolean)$check->get('enabled'));
207 api::enable_issuer($issuerid);
208 $check = api::get_issuer($issuer->get('id'));
209 $this->assertTrue((boolean)$check->get('enabled'));
213 * Test the alloweddomains for an issuer.
215 public function test_issuer_alloweddomains() {
216 $this->resetAfterTest();
217 $this->setAdminUser();
219 $issuer = api::create_standard_issuer('microsoft');
221 $issuer->set('alloweddomains', '');
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'));
228 $issuer->set('alloweddomains', 'example.com');
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'));
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'));
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'));
255 * Test endpoints creation for issuers.
256 * @dataProvider create_endpoints_for_standard_issuer_provider
258 * @covers ::create_endpoints_for_standard_issuer
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.
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 {
269 $this->resetAfterTest();
271 // Mark test as long because it connects with external services.
272 if (!PHPUNIT_LONGTEST) {
273 $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
276 $this->setAdminUser();
278 // Method create_endpoints_for_standard_issuer is called internally from create_standard_issuer.
279 if ($expectedexception) {
280 $this->expectException($expectedexception);
282 $issuer = api::create_standard_issuer($type, $baseurl);
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'));
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);
304 * Data provider for test_create_endpoints_for_standard_issuer.
306 * @return array
308 public function create_endpoints_for_standard_issuer_provider(): array {
309 return [
310 'Google' => [
311 'type' => 'google',
312 'discoveryurl' => '.well-known/openid-configuration',
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/',
320 'IMS OBv2.1' => [
321 'type' => 'imsobv2p1',
322 'discoveryurl' => '.well-known/badgeconnect.json',
323 'hasmappingfields' => false,
324 'baseurl' => 'https://dc.imsglobal.org/',
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',
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,
339 'Microsoft' => [
340 'type' => 'microsoft',
342 'Facebook' => [
343 'type' => 'facebook',
345 'NextCloud' => [
346 'type' => 'nextcloud',
347 'discoveryurl' => null,
348 'hasmappingfields' => true,
349 'baseurl' => 'https://dummy.local/nextcloud/',
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,
358 'Invalid type should return an exception' => [
359 'type' => 'fictitious',
360 'discoveryurl' => null,
361 'hasmappingfields' => true,
362 'baseurl' => null,
363 'expectedexception' => \moodle_exception::class,
369 * Test for get all issuers.
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');
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);
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()]);
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()]);
396 * Test for is available for login.
398 public function test_is_available_for_login() {
399 $this->resetAfterTest();
400 $this->setAdminUser();
401 $googleissuer = api::create_standard_issuer('google');
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);
408 $this->assertFalse($googleissuer->is_available_for_login());
410 // Set a clientid and clientsecret.
411 $googleissuer->set('clientid', 'clientid');
412 $googleissuer->set('clientsecret', 'secret');
413 $googleissuer->update();
415 $this->assertTrue($googleissuer->is_available_for_login());
417 // Set showonloginpage to service only.
418 $googleissuer->set('showonloginpage', issuer::SERVICEONLY);
419 $googleissuer->update();
421 $this->assertFalse($googleissuer->is_available_for_login());
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();
428 $this->assertFalse($googleissuer->is_available_for_login());
430 // Enable issuer.
431 $googleissuer->set('enabled', 1);
432 $googleissuer->update();
434 $this->assertTrue($googleissuer->is_available_for_login());
436 // Remove userinfo endpoint from issuer.
437 $endpoint = endpoint::get_record([
438 'issuerid' => $googleissuer->get('id'),
439 'name' => 'userinfo_endpoint'
441 api::delete_endpoint($endpoint->get('id'));
443 $this->assertFalse($googleissuer->is_available_for_login());