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/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); + } + } + } + } +}