diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index 2a8dd4e08ea..cb838da60a3 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -179,7 +179,8 @@ class issuer extends persistent { $mform->hideIf('acceptrisk', 'requireconfirmation', 'checked'); - if ($this->type == 'imsobv2p1' || $issuer->get('servicetype') == 'imsobv2p1') { + if ($this->type == 'imsobv2p1' || $issuer->get('servicetype') == 'imsobv2p1' + || $this->type == 'moodlenet' || $issuer->get('servicetype') == 'moodlenet') { $mform->addRule('baseurl', null, 'required', null, 'client'); } else { $mform->addRule('clientid', null, 'required', null, 'client'); diff --git a/admin/tool/oauth2/issuers.php b/admin/tool/oauth2/issuers.php index dff41e06602..79d04c87f98 100644 --- a/admin/tool/oauth2/issuers.php +++ b/admin/tool/oauth2/issuers.php @@ -228,6 +228,12 @@ if ($mform && $mform->is_cancelled()) { $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); echo $renderer->single_button($addurl, get_string('clever_service', 'tool_oauth2')); + // MoodleNet template. + $docs = 'admin/tool/oauth2/issuers/moodlenet'; + $params = ['action' => 'edittemplate', 'type' => 'moodlenet', 'sesskey' => sesskey(), 'docslink' => $docs]; + $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + echo $renderer->single_button($addurl, get_string('moodlenet_service', 'tool_oauth2')); + // Generic issuer. $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']); echo $renderer->single_button($addurl, get_string('custom_service', 'tool_oauth2')); diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index c5e5e2b54a8..3315a42ced4 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -103,6 +103,7 @@ $string['linkedin_service'] = 'LinkedIn'; $string['logindisplay'] = 'Display on login page as'; $string['loginissuer'] = 'Allow login'; $string['microsoft_service'] = 'Microsoft'; +$string['moodlenet_service'] = 'MoodleNet'; $string['nextcloud_service'] = 'Nextcloud'; $string['notconfigured'] = 'Not configured'; $string['notdiscovered'] = 'Service discovery not successful'; diff --git a/lib/classes/oauth2/discovery/auth_server_config_reader.php b/lib/classes/oauth2/discovery/auth_server_config_reader.php new file mode 100644 index 00000000000..ed7c58350ea --- /dev/null +++ b/lib/classes/oauth2/discovery/auth_server_config_reader.php @@ -0,0 +1,109 @@ +. + +namespace core\oauth2\discovery; + +use core\http_client; +use GuzzleHttp\Exception\ClientException; + +/** + * Simple reader class, allowing OAuth 2 Authorization Server Metadata to be read from an auth server's well-known. + * + * {@link https://www.rfc-editor.org/rfc/rfc8414} + * + * @package core + * @copyright 2023 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class auth_server_config_reader { + + /** @var \stdClass the config object read from the discovery document. */ + protected \stdClass $metadata; + + /** @var \moodle_url the base URL for the auth server which was last used during a read.*/ + protected \moodle_url $issuerurl; + + /** + * Constructor. + * + * @param http_client $httpclient an http client instance. + * @param string $wellknownsuffix the well-known suffix, defaulting to 'oauth-authorization-server'. + */ + public function __construct(protected http_client $httpclient, + protected string $wellknownsuffix = 'oauth-authorization-server') { + } + + /** + * Read the metadata from the remote host. + * + * @param \moodle_url $issuerurl the auth server issuer URL. + * @return \stdClass the configuration data object. + * @throws ClientException|\GuzzleHttp\Exception\GuzzleException if the http client experiences any problems. + */ + public function read_configuration(\moodle_url $issuerurl): \stdClass { + $this->issuerurl = $issuerurl; + $this->validate_uri(); + + $url = $this->get_configuration_url()->out(false); + $response = $this->httpclient->request('GET', $url); + $this->metadata = json_decode($response->getBody()); + return $this->metadata; + } + + /** + * Make sure the base URI is suitable for use in discovery. + * + * @return void + * @throws \moodle_exception if the URI fails validation. + */ + protected function validate_uri() { + if (!empty($this->issuerurl->get_query_string())) { + throw new \moodle_exception('Error: '.__METHOD__.': Auth server base URL cannot contain a query component.'); + } + if (strtolower($this->issuerurl->get_scheme()) !== 'https') { + throw new \moodle_exception('Error: '.__METHOD__.': Auth server base URL must use HTTPS scheme.'); + } + // This catches URL fragments. Since a query string is ruled out above, out_omit_querystring(false) returns only fragments. + if ($this->issuerurl->out_omit_querystring() != $this->issuerurl->out(false)) { + throw new \moodle_exception('Error: '.__METHOD__.': Auth server base URL must not contain fragments.'); + } + } + + /** + * Get the Auth server metadata URL. + * + * Per {@link https://www.rfc-editor.org/rfc/rfc8414#section-3}, if the issuer URL contains a path component, + * the well known suffix is added between the host and path components. + * + * @return \moodle_url the full URL to the auth server metadata. + */ + protected function get_configuration_url(): \moodle_url { + $path = $this->issuerurl->get_path(); + if (!empty($path) && $path !== '/') { + // Insert the well known suffix between the host and path components. + $port = $this->issuerurl->get_port() ? ':'.$this->issuerurl->get_port() : ''; + $uri = $this->issuerurl->get_scheme() . "://" . $this->issuerurl->get_host() . $port ."/". + ".well-known/" . $this->wellknownsuffix . $path; + } else { + // No path, just append the well known suffix. + $uri = $this->issuerurl->out(false); + $uri .= (substr($uri, -1) == '/' ? '' : '/'); + $uri .= ".well-known/$this->wellknownsuffix"; + } + + return new \moodle_url($uri); + } +} diff --git a/lib/classes/oauth2/service/moodlenet.php b/lib/classes/oauth2/service/moodlenet.php new file mode 100644 index 00000000000..03900d6ee6e --- /dev/null +++ b/lib/classes/oauth2/service/moodlenet.php @@ -0,0 +1,181 @@ +. + +namespace core\oauth2\service; + +use core\http_client; +use core\oauth2\discovery\auth_server_config_reader; +use core\oauth2\endpoint; +use core\oauth2\issuer; +use GuzzleHttp\Psr7\Request; + +/** + * MoodleNet OAuth 2 configuration. + * + * @package core + * @copyright 2023 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class moodlenet implements issuer_interface { + + /** + * Get the issuer template to display in the form. + * + * @return issuer the issuer. + */ + public static function init(): ?issuer { + $record = (object) [ + 'name' => 'MoodleNet', + 'image' => 'https://moodle.net/favicon.ico', + 'baseurl' => 'https://moodle.net', + 'loginscopes' => '', + 'loginscopesoffline' => '', + 'loginparamsoffline' => '', + 'showonloginpage' => issuer::SERVICEONLY, + 'servicetype' => 'moodlenet', + ]; + $issuer = new issuer(0, $record); + + return $issuer; + } + + /** + * Create the endpoints for the issuer. + * + * @param issuer $issuer the issuer instance. + * @return issuer the issuer instance. + */ + public static function create_endpoints(issuer $issuer): issuer { + self::discover_endpoints($issuer); + return $issuer; + } + + /** + * Read the OAuth 2 Auth Server Metadata. + * + * @param issuer $issuer the issuer instance. + * @return int the number of endpoints created. + */ + public static function discover_endpoints($issuer): int { + $baseurl = $issuer->get('baseurl'); + if (empty($baseurl)) { + return 0; + } + + $endpointscreated = 0; + $configreader = new auth_server_config_reader(new http_client()); + try { + $config = $configreader->read_configuration(new \moodle_url($baseurl)); + + foreach ($config as $key => $value) { + if (substr_compare($key, '_endpoint', -strlen('_endpoint')) === 0) { + $record = new \stdClass(); + $record->issuerid = $issuer->get('id'); + $record->name = $key; + $record->url = $value; + + $endpoint = new endpoint(0, $record); + $endpoint->create(); + $endpointscreated++; + } + + if ($key == 'scopes_supported') { + $issuer->set('scopessupported', implode(' ', $value)); + $issuer->update(); + } + } + } catch (\Exception $e) { + throw new \moodle_exception('Could not read service configuration for issuer: ' . $issuer->get('name')); + } + + try { + self::client_registration($issuer); + } catch (\Exception $e) { + throw new \moodle_exception('Could not register client for issuer: ' . $issuer->get('name')); + } + + return $endpointscreated; + } + + /** + * Perform (open) OAuth 2 Dynamic Client Registration with the MoodleNet application. + * + * @param issuer $issuer the issuer instance containing the service baseurl. + * @return void + */ + protected static function client_registration(issuer $issuer): void { + global $CFG, $SITE; + + $clientid = $issuer->get('clientid'); + $clientsecret = $issuer->get('clientsecret'); + + if (empty($clientid) && empty($clientsecret)) { + $url = $issuer->get_endpoint_url('registration'); + if ($url) { + $scopes = str_replace("\r", " ", $issuer->get('scopessupported')); + $hosturl = $CFG->wwwroot; + + $request = [ + 'client_name' => $SITE->fullname, + 'client_uri' => $hosturl, + 'logo_uri' => $hosturl . '/pix/f/moodle-256.png', + 'tos_uri' => $hosturl, + 'policy_uri' => $hosturl, + 'software_id' => 'moodle', + 'software_version' => $CFG->version, + 'redirect_uris' => [ + $hosturl . '/admin/oauth2callback.php' + ], + 'token_endpoint_auth_method' => 'client_secret_basic', + 'grant_types' => [ + 'authorization_code', + 'refresh_token' + ], + 'response_types' => [ + 'code' + ], + 'scope' => $scopes + ]; + + $client = new http_client(); + $request = new Request( + 'POST', + $url, + [ + 'Content-type' => 'application/json', + 'Accept' => 'application/json', + ], + json_encode($request) + ); + + try { + $response = $client->send($request); + $responsebody = $response->getBody()->getContents(); + $decodedbody = json_decode($responsebody, true); + if (is_null($decodedbody)) { + throw new \moodle_exception('Error: ' . __METHOD__ . ': Failed to decode response body. Invalid JSON.'); + } + $issuer->set('clientid', $decodedbody['client_id']); + $issuer->set('clientsecret', $decodedbody['client_secret']); + $issuer->update(); + } catch (\Exception $e) { + $msg = "Could not self-register {$issuer->get('name')}. Wrong URL or JSON data [URL: $url]"; + throw new \moodle_exception($msg); + } + } + } + } +} diff --git a/lib/tests/oauth2/discovery/auth_server_config_reader_test.php b/lib/tests/oauth2/discovery/auth_server_config_reader_test.php new file mode 100644 index 00000000000..fb5671b8870 --- /dev/null +++ b/lib/tests/oauth2/discovery/auth_server_config_reader_test.php @@ -0,0 +1,674 @@ +. + +namespace core\oauth2\discovery; + +use core\http_client; +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use GuzzleHttp\Psr7\Response; +use Psr\Http\Message\ResponseInterface; + +/** + * Unit tests for {@see auth_server_config_reader}. + * + * @coversDefaultClass \core\oauth2\discovery\auth_server_config_reader + * @package core + * @copyright 2023 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class auth_server_config_reader_test extends \advanced_testcase { + + /** + * Test reading the config for an auth server. + * + * @covers ::read_configuration + * @dataProvider config_provider + * @param string $issuerurl the auth server issuer URL. + * @param ResponseInterface $httpresponse a stub HTTP response. + * @param null|string $altwellknownsuffix an alternate value for the well known suffix to use in the reader. + * @param array $expected test expectations. + * @return void + */ + public function test_read_configuration(string $issuerurl, ResponseInterface $httpresponse, ?string $altwellknownsuffix = null, + array $expected = []) { + + $mock = new MockHandler([$httpresponse]); + $handlerstack = HandlerStack::create($mock); + if (!empty($expected['request'])) { + // Request history tracking to allow asserting that request was sent as expected below (to the stub client). + $container = []; + $history = Middleware::history($container); + $handlerstack->push($history); + } + + $args = [ + new http_client(['handler' => $handlerstack]), + ]; + if (!is_null($altwellknownsuffix)) { + $args[] = $altwellknownsuffix; + } + + if (!empty($expected['exception'])) { + $this->expectException($expected['exception']); + } + $configreader = new auth_server_config_reader(...$args); + $config = $configreader->read_configuration(new \moodle_url($issuerurl)); + + if (!empty($expected['request'])) { + // Verify the request goes to the correct URL (i.e. the well known suffix is correctly positioned). + $this->assertEquals($expected['request']['url'], $container[0]['request']->getUri()); + } + + $this->assertEquals($expected['metadata'], (array) $config); + } + + /** + * Provider for testing read_configuration(). + * + * @return array test data. + */ + public function config_provider(): array { + return [ + 'Valid, good issuer URL, good config' => [ + 'issuer_url' => 'https://app.example.com', + 'http_response' => new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode([ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ]) + ), + 'well_known_suffix' => null, + 'expected' => [ + 'request' => [ + 'url' => 'https://app.example.com/.well-known/oauth-authorization-server' + ], + 'metadata' => [ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ] + ] + ], + 'Valid, issuer URL with path component confirming well known suffix placement' => [ + 'issuer_url' => 'https://app.example.com/some/path', + 'http_response' => new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode([ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ]) + ), + 'well_known_suffix' => null, + 'expected' => [ + 'request' => [ + 'url' => 'https://app.example.com/.well-known/oauth-authorization-server/some/path' + ], + 'metadata' => [ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ] + ] + ], + 'Valid, single trailing / path only' => [ + 'issuer_url' => 'https://app.example.com/', + 'http_response' => new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode([ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ]) + ), + 'well_known_suffix' => null, + 'expected' => [ + 'request' => [ + 'url' => 'https://app.example.com/.well-known/oauth-authorization-server' + ], + 'metadata' => [ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ] + ] + ], + 'Invalid, non HTTPS issuer URL' => [ + 'issuer_url' => 'http://app.example.com', + 'http_response' => new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode([ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ]) + ), + 'well_known_suffix' => null, + 'expected' => [ + 'exception' => \moodle_exception::class + ] + ], + 'Invalid, query string in issuer URL' => [ + 'issuer_url' => 'https://app.example.com?test=cat', + 'http_response' => new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode([ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ]) + ), + 'well_known_suffix' => null, + 'expected' => [ + 'exception' => \moodle_exception::class + ] + ], + 'Invalid, fragment in issuer URL' => [ + 'issuer_url' => 'https://app.example.com/#cat', + 'http_response' => new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode([ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ]) + ), + 'well_known_suffix' => null, + 'expected' => [ + 'exception' => \moodle_exception::class + ] + ], + 'Valid, port in issuer URL' => [ + 'issuer_url' => 'https://app.example.com:8080/some/path', + 'http_response' => new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode([ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ]) + ), + 'well_known_suffix' => null, + 'expected' => [ + 'request' => [ + 'url' => 'https://app.example.com:8080/.well-known/oauth-authorization-server/some/path' + ], + 'metadata' => [ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ] + ] + ], + 'Valid, alternate well known suffix, no path' => [ + 'issuer_url' => 'https://app.example.com', + 'http_response' => new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode([ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ]) + ), + 'well_known_suffix' => 'openid-configuration', // An application using the openid well known, which is valid. + 'expected' => [ + 'request' => [ + 'url' => 'https://app.example.com/.well-known/openid-configuration' + ], + 'metadata' => [ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ] + ] + ], + 'Valid, alternate well known suffix, with path' => [ + 'issuer_url' => 'https://app.example.com/some/path/', + 'http_response' => new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode([ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ]) + ), + 'well_known_suffix' => 'openid-configuration', // An application using the openid well known, which is valid. + 'expected' => [ + 'request' => [ + 'url' => 'https://app.example.com/.well-known/openid-configuration/some/path/' + ], + 'metadata' => [ + "issuer" => "https://app.example.com", + "authorization_endpoint" => "https://app.example.com/authorize", + "token_endpoint" => "https://app.example.com/token", + "token_endpoint_auth_methods_supported" => [ + "client_secret_basic", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported" => [ + "RS256", + "ES256" + ], + "userinfo_endpoint" => "https://app.example.com/userinfo", + "jwks_uri" => "https://app.example.com/jwks.json", + "registration_endpoint" => "https://app.example.com/register", + "scopes_supported" => [ + "openid", + "profile", + "email", + ], + "response_types_supported" => [ + "code", + "code token" + ], + "service_documentation" => "http://app.example.com/service_documentation.html", + "ui_locales_supported" => [ + "en-US", + "en-GB", + "fr-FR", + ] + ] + ] + ], + 'Invalid, bad response' => [ + 'issuer_url' => 'https://app.example.com', + 'http_response' => new Response(404), + 'well_known_suffix' => null, + 'expected' => [ + 'exception' => ClientException::class + ] + ] + ]; + } +}