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\oauth2\discovery; 18 19 use core\http_client; 20 use GuzzleHttp\Exception\ClientException; 21 use GuzzleHttp\Handler\MockHandler; 22 use GuzzleHttp\HandlerStack; 23 use GuzzleHttp\Middleware; 24 use GuzzleHttp\Psr7\Response; 25 use Psr\Http\Message\ResponseInterface; 26 27 /** 28 * Unit tests for {@see auth_server_config_reader}. 29 * 30 * @coversDefaultClass \core\oauth2\discovery\auth_server_config_reader 31 * @package core 32 * @copyright 2023 Jake Dallimore <jrhdallimore@gmail.com> 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 */ 35 class auth_server_config_reader_test extends \advanced_testcase { 36 37 /** 38 * Test reading the config for an auth server. 39 * 40 * @covers ::read_configuration 41 * @dataProvider config_provider 42 * @param string $issuerurl the auth server issuer URL. 43 * @param ResponseInterface $httpresponse a stub HTTP response. 44 * @param null|string $altwellknownsuffix an alternate value for the well known suffix to use in the reader. 45 * @param array $expected test expectations. 46 * @return void 47 */ 48 public function test_read_configuration(string $issuerurl, ResponseInterface $httpresponse, ?string $altwellknownsuffix = null, 49 array $expected = []) { 50 51 $mock = new MockHandler([$httpresponse]); 52 $handlerstack = HandlerStack::create($mock); 53 if (!empty($expected['request'])) { 54 // Request history tracking to allow asserting that request was sent as expected below (to the stub client). 55 $container = []; 56 $history = Middleware::history($container); 57 $handlerstack->push($history); 58 } 59 60 $args = [ 61 new http_client(['handler' => $handlerstack]), 62 ]; 63 if (!is_null($altwellknownsuffix)) { 64 $args[] = $altwellknownsuffix; 65 } 66 67 if (!empty($expected['exception'])) { 68 $this->expectException($expected['exception']); 69 } 70 $configreader = new auth_server_config_reader(...$args); 71 $config = $configreader->read_configuration(new \moodle_url($issuerurl)); 72 73 if (!empty($expected['request'])) { 74 // Verify the request goes to the correct URL (i.e. the well known suffix is correctly positioned). 75 $this->assertEquals($expected['request']['url'], $container[0]['request']->getUri()); 76 } 77 78 $this->assertEquals($expected['metadata'], (array) $config); 79 } 80 81 /** 82 * Provider for testing read_configuration(). 83 * 84 * @return array test data. 85 */ 86 public function config_provider(): array { 87 return [ 88 'Valid, good issuer URL, good config' => [ 89 'issuer_url' => 'https://app.example.com', 90 'http_response' => new Response( 91 200, 92 ['Content-Type' => 'application/json'], 93 json_encode([ 94 "issuer" => "https://app.example.com", 95 "authorization_endpoint" => "https://app.example.com/authorize", 96 "token_endpoint" => "https://app.example.com/token", 97 "token_endpoint_auth_methods_supported" => [ 98 "client_secret_basic", 99 "private_key_jwt" 100 ], 101 "token_endpoint_auth_signing_alg_values_supported" => [ 102 "RS256", 103 "ES256" 104 ], 105 "userinfo_endpoint" => "https://app.example.com/userinfo", 106 "jwks_uri" => "https://app.example.com/jwks.json", 107 "registration_endpoint" => "https://app.example.com/register", 108 "scopes_supported" => [ 109 "openid", 110 "profile", 111 "email", 112 ], 113 "response_types_supported" => [ 114 "code", 115 "code token" 116 ], 117 "service_documentation" => "http://app.example.com/service_documentation.html", 118 "ui_locales_supported" => [ 119 "en-US", 120 "en-GB", 121 "fr-FR", 122 ] 123 ]) 124 ), 125 'well_known_suffix' => null, 126 'expected' => [ 127 'request' => [ 128 'url' => 'https://app.example.com/.well-known/oauth-authorization-server' 129 ], 130 'metadata' => [ 131 "issuer" => "https://app.example.com", 132 "authorization_endpoint" => "https://app.example.com/authorize", 133 "token_endpoint" => "https://app.example.com/token", 134 "token_endpoint_auth_methods_supported" => [ 135 "client_secret_basic", 136 "private_key_jwt" 137 ], 138 "token_endpoint_auth_signing_alg_values_supported" => [ 139 "RS256", 140 "ES256" 141 ], 142 "userinfo_endpoint" => "https://app.example.com/userinfo", 143 "jwks_uri" => "https://app.example.com/jwks.json", 144 "registration_endpoint" => "https://app.example.com/register", 145 "scopes_supported" => [ 146 "openid", 147 "profile", 148 "email", 149 ], 150 "response_types_supported" => [ 151 "code", 152 "code token" 153 ], 154 "service_documentation" => "http://app.example.com/service_documentation.html", 155 "ui_locales_supported" => [ 156 "en-US", 157 "en-GB", 158 "fr-FR", 159 ] 160 ] 161 ] 162 ], 163 'Valid, issuer URL with path component confirming well known suffix placement' => [ 164 'issuer_url' => 'https://app.example.com/some/path', 165 'http_response' => new Response( 166 200, 167 ['Content-Type' => 'application/json'], 168 json_encode([ 169 "issuer" => "https://app.example.com", 170 "authorization_endpoint" => "https://app.example.com/authorize", 171 "token_endpoint" => "https://app.example.com/token", 172 "token_endpoint_auth_methods_supported" => [ 173 "client_secret_basic", 174 "private_key_jwt" 175 ], 176 "token_endpoint_auth_signing_alg_values_supported" => [ 177 "RS256", 178 "ES256" 179 ], 180 "userinfo_endpoint" => "https://app.example.com/userinfo", 181 "jwks_uri" => "https://app.example.com/jwks.json", 182 "registration_endpoint" => "https://app.example.com/register", 183 "scopes_supported" => [ 184 "openid", 185 "profile", 186 "email", 187 ], 188 "response_types_supported" => [ 189 "code", 190 "code token" 191 ], 192 "service_documentation" => "http://app.example.com/service_documentation.html", 193 "ui_locales_supported" => [ 194 "en-US", 195 "en-GB", 196 "fr-FR", 197 ] 198 ]) 199 ), 200 'well_known_suffix' => null, 201 'expected' => [ 202 'request' => [ 203 'url' => 'https://app.example.com/.well-known/oauth-authorization-server/some/path' 204 ], 205 'metadata' => [ 206 "issuer" => "https://app.example.com", 207 "authorization_endpoint" => "https://app.example.com/authorize", 208 "token_endpoint" => "https://app.example.com/token", 209 "token_endpoint_auth_methods_supported" => [ 210 "client_secret_basic", 211 "private_key_jwt" 212 ], 213 "token_endpoint_auth_signing_alg_values_supported" => [ 214 "RS256", 215 "ES256" 216 ], 217 "userinfo_endpoint" => "https://app.example.com/userinfo", 218 "jwks_uri" => "https://app.example.com/jwks.json", 219 "registration_endpoint" => "https://app.example.com/register", 220 "scopes_supported" => [ 221 "openid", 222 "profile", 223 "email", 224 ], 225 "response_types_supported" => [ 226 "code", 227 "code token" 228 ], 229 "service_documentation" => "http://app.example.com/service_documentation.html", 230 "ui_locales_supported" => [ 231 "en-US", 232 "en-GB", 233 "fr-FR", 234 ] 235 ] 236 ] 237 ], 238 'Valid, single trailing / path only' => [ 239 'issuer_url' => 'https://app.example.com/', 240 'http_response' => new Response( 241 200, 242 ['Content-Type' => 'application/json'], 243 json_encode([ 244 "issuer" => "https://app.example.com", 245 "authorization_endpoint" => "https://app.example.com/authorize", 246 "token_endpoint" => "https://app.example.com/token", 247 "token_endpoint_auth_methods_supported" => [ 248 "client_secret_basic", 249 "private_key_jwt" 250 ], 251 "token_endpoint_auth_signing_alg_values_supported" => [ 252 "RS256", 253 "ES256" 254 ], 255 "userinfo_endpoint" => "https://app.example.com/userinfo", 256 "jwks_uri" => "https://app.example.com/jwks.json", 257 "registration_endpoint" => "https://app.example.com/register", 258 "scopes_supported" => [ 259 "openid", 260 "profile", 261 "email", 262 ], 263 "response_types_supported" => [ 264 "code", 265 "code token" 266 ], 267 "service_documentation" => "http://app.example.com/service_documentation.html", 268 "ui_locales_supported" => [ 269 "en-US", 270 "en-GB", 271 "fr-FR", 272 ] 273 ]) 274 ), 275 'well_known_suffix' => null, 276 'expected' => [ 277 'request' => [ 278 'url' => 'https://app.example.com/.well-known/oauth-authorization-server' 279 ], 280 'metadata' => [ 281 "issuer" => "https://app.example.com", 282 "authorization_endpoint" => "https://app.example.com/authorize", 283 "token_endpoint" => "https://app.example.com/token", 284 "token_endpoint_auth_methods_supported" => [ 285 "client_secret_basic", 286 "private_key_jwt" 287 ], 288 "token_endpoint_auth_signing_alg_values_supported" => [ 289 "RS256", 290 "ES256" 291 ], 292 "userinfo_endpoint" => "https://app.example.com/userinfo", 293 "jwks_uri" => "https://app.example.com/jwks.json", 294 "registration_endpoint" => "https://app.example.com/register", 295 "scopes_supported" => [ 296 "openid", 297 "profile", 298 "email", 299 ], 300 "response_types_supported" => [ 301 "code", 302 "code token" 303 ], 304 "service_documentation" => "http://app.example.com/service_documentation.html", 305 "ui_locales_supported" => [ 306 "en-US", 307 "en-GB", 308 "fr-FR", 309 ] 310 ] 311 ] 312 ], 313 'Invalid, non HTTPS issuer URL' => [ 314 'issuer_url' => 'http://app.example.com', 315 'http_response' => new Response( 316 200, 317 ['Content-Type' => 'application/json'], 318 json_encode([ 319 "issuer" => "https://app.example.com", 320 "authorization_endpoint" => "https://app.example.com/authorize", 321 "token_endpoint" => "https://app.example.com/token", 322 "token_endpoint_auth_methods_supported" => [ 323 "client_secret_basic", 324 "private_key_jwt" 325 ], 326 "token_endpoint_auth_signing_alg_values_supported" => [ 327 "RS256", 328 "ES256" 329 ], 330 "userinfo_endpoint" => "https://app.example.com/userinfo", 331 "jwks_uri" => "https://app.example.com/jwks.json", 332 "registration_endpoint" => "https://app.example.com/register", 333 "scopes_supported" => [ 334 "openid", 335 "profile", 336 "email", 337 ], 338 "response_types_supported" => [ 339 "code", 340 "code token" 341 ], 342 "service_documentation" => "http://app.example.com/service_documentation.html", 343 "ui_locales_supported" => [ 344 "en-US", 345 "en-GB", 346 "fr-FR", 347 ] 348 ]) 349 ), 350 'well_known_suffix' => null, 351 'expected' => [ 352 'exception' => \moodle_exception::class 353 ] 354 ], 355 'Invalid, query string in issuer URL' => [ 356 'issuer_url' => 'https://app.example.com?test=cat', 357 'http_response' => new Response( 358 200, 359 ['Content-Type' => 'application/json'], 360 json_encode([ 361 "issuer" => "https://app.example.com", 362 "authorization_endpoint" => "https://app.example.com/authorize", 363 "token_endpoint" => "https://app.example.com/token", 364 "token_endpoint_auth_methods_supported" => [ 365 "client_secret_basic", 366 "private_key_jwt" 367 ], 368 "token_endpoint_auth_signing_alg_values_supported" => [ 369 "RS256", 370 "ES256" 371 ], 372 "userinfo_endpoint" => "https://app.example.com/userinfo", 373 "jwks_uri" => "https://app.example.com/jwks.json", 374 "registration_endpoint" => "https://app.example.com/register", 375 "scopes_supported" => [ 376 "openid", 377 "profile", 378 "email", 379 ], 380 "response_types_supported" => [ 381 "code", 382 "code token" 383 ], 384 "service_documentation" => "http://app.example.com/service_documentation.html", 385 "ui_locales_supported" => [ 386 "en-US", 387 "en-GB", 388 "fr-FR", 389 ] 390 ]) 391 ), 392 'well_known_suffix' => null, 393 'expected' => [ 394 'exception' => \moodle_exception::class 395 ] 396 ], 397 'Invalid, fragment in issuer URL' => [ 398 'issuer_url' => 'https://app.example.com/#cat', 399 'http_response' => new Response( 400 200, 401 ['Content-Type' => 'application/json'], 402 json_encode([ 403 "issuer" => "https://app.example.com", 404 "authorization_endpoint" => "https://app.example.com/authorize", 405 "token_endpoint" => "https://app.example.com/token", 406 "token_endpoint_auth_methods_supported" => [ 407 "client_secret_basic", 408 "private_key_jwt" 409 ], 410 "token_endpoint_auth_signing_alg_values_supported" => [ 411 "RS256", 412 "ES256" 413 ], 414 "userinfo_endpoint" => "https://app.example.com/userinfo", 415 "jwks_uri" => "https://app.example.com/jwks.json", 416 "registration_endpoint" => "https://app.example.com/register", 417 "scopes_supported" => [ 418 "openid", 419 "profile", 420 "email", 421 ], 422 "response_types_supported" => [ 423 "code", 424 "code token" 425 ], 426 "service_documentation" => "http://app.example.com/service_documentation.html", 427 "ui_locales_supported" => [ 428 "en-US", 429 "en-GB", 430 "fr-FR", 431 ] 432 ]) 433 ), 434 'well_known_suffix' => null, 435 'expected' => [ 436 'exception' => \moodle_exception::class 437 ] 438 ], 439 'Valid, port in issuer URL' => [ 440 'issuer_url' => 'https://app.example.com:8080/some/path', 441 'http_response' => new Response( 442 200, 443 ['Content-Type' => 'application/json'], 444 json_encode([ 445 "issuer" => "https://app.example.com", 446 "authorization_endpoint" => "https://app.example.com/authorize", 447 "token_endpoint" => "https://app.example.com/token", 448 "token_endpoint_auth_methods_supported" => [ 449 "client_secret_basic", 450 "private_key_jwt" 451 ], 452 "token_endpoint_auth_signing_alg_values_supported" => [ 453 "RS256", 454 "ES256" 455 ], 456 "userinfo_endpoint" => "https://app.example.com/userinfo", 457 "jwks_uri" => "https://app.example.com/jwks.json", 458 "registration_endpoint" => "https://app.example.com/register", 459 "scopes_supported" => [ 460 "openid", 461 "profile", 462 "email", 463 ], 464 "response_types_supported" => [ 465 "code", 466 "code token" 467 ], 468 "service_documentation" => "http://app.example.com/service_documentation.html", 469 "ui_locales_supported" => [ 470 "en-US", 471 "en-GB", 472 "fr-FR", 473 ] 474 ]) 475 ), 476 'well_known_suffix' => null, 477 'expected' => [ 478 'request' => [ 479 'url' => 'https://app.example.com:8080/.well-known/oauth-authorization-server/some/path' 480 ], 481 'metadata' => [ 482 "issuer" => "https://app.example.com", 483 "authorization_endpoint" => "https://app.example.com/authorize", 484 "token_endpoint" => "https://app.example.com/token", 485 "token_endpoint_auth_methods_supported" => [ 486 "client_secret_basic", 487 "private_key_jwt" 488 ], 489 "token_endpoint_auth_signing_alg_values_supported" => [ 490 "RS256", 491 "ES256" 492 ], 493 "userinfo_endpoint" => "https://app.example.com/userinfo", 494 "jwks_uri" => "https://app.example.com/jwks.json", 495 "registration_endpoint" => "https://app.example.com/register", 496 "scopes_supported" => [ 497 "openid", 498 "profile", 499 "email", 500 ], 501 "response_types_supported" => [ 502 "code", 503 "code token" 504 ], 505 "service_documentation" => "http://app.example.com/service_documentation.html", 506 "ui_locales_supported" => [ 507 "en-US", 508 "en-GB", 509 "fr-FR", 510 ] 511 ] 512 ] 513 ], 514 'Valid, alternate well known suffix, no path' => [ 515 'issuer_url' => 'https://app.example.com', 516 'http_response' => new Response( 517 200, 518 ['Content-Type' => 'application/json'], 519 json_encode([ 520 "issuer" => "https://app.example.com", 521 "authorization_endpoint" => "https://app.example.com/authorize", 522 "token_endpoint" => "https://app.example.com/token", 523 "token_endpoint_auth_methods_supported" => [ 524 "client_secret_basic", 525 "private_key_jwt" 526 ], 527 "token_endpoint_auth_signing_alg_values_supported" => [ 528 "RS256", 529 "ES256" 530 ], 531 "userinfo_endpoint" => "https://app.example.com/userinfo", 532 "jwks_uri" => "https://app.example.com/jwks.json", 533 "registration_endpoint" => "https://app.example.com/register", 534 "scopes_supported" => [ 535 "openid", 536 "profile", 537 "email", 538 ], 539 "response_types_supported" => [ 540 "code", 541 "code token" 542 ], 543 "service_documentation" => "http://app.example.com/service_documentation.html", 544 "ui_locales_supported" => [ 545 "en-US", 546 "en-GB", 547 "fr-FR", 548 ] 549 ]) 550 ), 551 'well_known_suffix' => 'openid-configuration', // An application using the openid well known, which is valid. 552 'expected' => [ 553 'request' => [ 554 'url' => 'https://app.example.com/.well-known/openid-configuration' 555 ], 556 'metadata' => [ 557 "issuer" => "https://app.example.com", 558 "authorization_endpoint" => "https://app.example.com/authorize", 559 "token_endpoint" => "https://app.example.com/token", 560 "token_endpoint_auth_methods_supported" => [ 561 "client_secret_basic", 562 "private_key_jwt" 563 ], 564 "token_endpoint_auth_signing_alg_values_supported" => [ 565 "RS256", 566 "ES256" 567 ], 568 "userinfo_endpoint" => "https://app.example.com/userinfo", 569 "jwks_uri" => "https://app.example.com/jwks.json", 570 "registration_endpoint" => "https://app.example.com/register", 571 "scopes_supported" => [ 572 "openid", 573 "profile", 574 "email", 575 ], 576 "response_types_supported" => [ 577 "code", 578 "code token" 579 ], 580 "service_documentation" => "http://app.example.com/service_documentation.html", 581 "ui_locales_supported" => [ 582 "en-US", 583 "en-GB", 584 "fr-FR", 585 ] 586 ] 587 ] 588 ], 589 'Valid, alternate well known suffix, with path' => [ 590 'issuer_url' => 'https://app.example.com/some/path/', 591 'http_response' => new Response( 592 200, 593 ['Content-Type' => 'application/json'], 594 json_encode([ 595 "issuer" => "https://app.example.com", 596 "authorization_endpoint" => "https://app.example.com/authorize", 597 "token_endpoint" => "https://app.example.com/token", 598 "token_endpoint_auth_methods_supported" => [ 599 "client_secret_basic", 600 "private_key_jwt" 601 ], 602 "token_endpoint_auth_signing_alg_values_supported" => [ 603 "RS256", 604 "ES256" 605 ], 606 "userinfo_endpoint" => "https://app.example.com/userinfo", 607 "jwks_uri" => "https://app.example.com/jwks.json", 608 "registration_endpoint" => "https://app.example.com/register", 609 "scopes_supported" => [ 610 "openid", 611 "profile", 612 "email", 613 ], 614 "response_types_supported" => [ 615 "code", 616 "code token" 617 ], 618 "service_documentation" => "http://app.example.com/service_documentation.html", 619 "ui_locales_supported" => [ 620 "en-US", 621 "en-GB", 622 "fr-FR", 623 ] 624 ]) 625 ), 626 'well_known_suffix' => 'openid-configuration', // An application using the openid well known, which is valid. 627 'expected' => [ 628 'request' => [ 629 'url' => 'https://app.example.com/.well-known/openid-configuration/some/path/' 630 ], 631 'metadata' => [ 632 "issuer" => "https://app.example.com", 633 "authorization_endpoint" => "https://app.example.com/authorize", 634 "token_endpoint" => "https://app.example.com/token", 635 "token_endpoint_auth_methods_supported" => [ 636 "client_secret_basic", 637 "private_key_jwt" 638 ], 639 "token_endpoint_auth_signing_alg_values_supported" => [ 640 "RS256", 641 "ES256" 642 ], 643 "userinfo_endpoint" => "https://app.example.com/userinfo", 644 "jwks_uri" => "https://app.example.com/jwks.json", 645 "registration_endpoint" => "https://app.example.com/register", 646 "scopes_supported" => [ 647 "openid", 648 "profile", 649 "email", 650 ], 651 "response_types_supported" => [ 652 "code", 653 "code token" 654 ], 655 "service_documentation" => "http://app.example.com/service_documentation.html", 656 "ui_locales_supported" => [ 657 "en-US", 658 "en-GB", 659 "fr-FR", 660 ] 661 ] 662 ] 663 ], 664 'Invalid, bad response' => [ 665 'issuer_url' => 'https://app.example.com', 666 'http_response' => new Response(404), 667 'well_known_suffix' => null, 668 'expected' => [ 669 'exception' => ClientException::class 670 ] 671 ] 672 ]; 673 } 674 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body