From 41e516eac1ab694f73cf891f81289e8230159ca9 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Tue, 20 May 2025 08:34:56 +0700 Subject: [PATCH 1/2] MDL-80380 admin: Support Microsoft XOAUTH2 for outgoing email --- admin/tool/oauth2/classes/form/issuer.php | 8 ++++ admin/tool/oauth2/classes/output/renderer.php | 3 +- admin/tool/oauth2/lang/en/tool_oauth2.php | 5 ++- lib/classes/oauth2/api.php | 16 ++++++- lib/classes/oauth2/client/microsoft.php | 42 +++++++++++++++++++ lib/classes/oauth2/issuer.php | 17 ++++++++ lib/db/install.xml | 3 +- lib/db/upgrade.php | 15 +++++++ lib/oauthlib.php | 14 +++++++ lib/tests/oauth2_test.php | 6 +++ version.php | 2 +- 11 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 lib/classes/oauth2/client/microsoft.php diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php index 93ab537bd48..1f79e649c33 100644 --- a/admin/tool/oauth2/classes/form/issuer.php +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -129,6 +129,7 @@ class issuer extends persistent { \core\oauth2\issuer::EVERYWHERE => get_string('issueruseineverywhere', 'tool_oauth2'), \core\oauth2\issuer::LOGINONLY => get_string('issueruseinloginonly', 'tool_oauth2'), \core\oauth2\issuer::SERVICEONLY => get_string('issueruseininternalonly', 'tool_oauth2'), + \core\oauth2\issuer::SMTPWITHXOAUTH2 => get_string('issueruseinsmtpwithoauth', 'tool_oauth2'), ]; $mform->addElement('select', 'showonloginpage', get_string('issuerusein', 'tool_oauth2'), $options); $mform->addHelpButton('showonloginpage', 'issuerusein', 'tool_oauth2'); @@ -138,6 +139,13 @@ class issuer extends persistent { $mform->addRule('loginpagename', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $mform->addHelpButton('loginpagename', 'issuerloginpagename', 'tool_oauth2'); $mform->hideIf('loginpagename', 'showonloginpage', 'eq', \core\oauth2\issuer::SERVICEONLY); + $mform->hideIf('loginpagename', 'showonloginpage', 'eq', \core\oauth2\issuer::SMTPWITHXOAUTH2); + + // Connected email for XOAUTH2. + $mform->addElement('text', 'systememail', get_string('issuersmtpsystememail', 'tool_oauth2')); + $mform->setType('systememail', PARAM_EMAIL); + $mform->addHelpButton('systememail', 'issuersmtpsystememail', 'tool_oauth2'); + $mform->hideIf('systememail', 'showonloginpage', 'ne', \core\oauth2\issuer::SMTPWITHXOAUTH2); // Login scopes. $mform->addElement('text', 'loginscopes', get_string('issuerloginscopes', 'tool_oauth2')); diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php index f42598b8db6..b97684f77c8 100644 --- a/admin/tool/oauth2/classes/output/renderer.php +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -86,7 +86,8 @@ class renderer extends plugin_renderer_base { $namecell->header = true; // Login issuer. - if ((int)$issuer->get('showonloginpage') == issuer::SERVICEONLY) { + if ((int)$issuer->get('showonloginpage') == issuer::SERVICEONLY || + (int)$issuer->get('showonloginpage') == issuer::SMTPWITHXOAUTH2) { $loginissuer = $this->pix_icon('no', get_string('notloginissuer', 'tool_oauth2'), 'tool_oauth2'); $logindisplayas = ''; } else { diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php index cf6faf81499..6a244cf766b 100644 --- a/admin/tool/oauth2/lang/en/tool_oauth2.php +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -89,13 +89,16 @@ $string['issuershowonloginpage'] = 'Show on login page'; $string['issuerrequireconfirmation_help'] = 'Require that all users verify their email address before they can log in with OAuth. This applies to newly created accounts as part of the login process, or when an existing Moodle account is connected to an OAuth login via matching email addresses.'; $string['issuerrequireconfirmation'] = 'Require email verification'; $string['issuers'] = 'Issuers'; +$string['issuersmtpsystememail'] = 'SMTP email'; +$string['issuersmtpsystememail_help'] = 'If specified, this email will be used to connect a system account for sending email via SMTP. This is required for some OAuth 2 services (e.g. Microsoft). Please check the documentation for your OAuth 2 service to see if this is required.'; $string['issuersservicesallow'] = 'Allow services'; $string['issuersservicesnotallow'] = 'Do not allow services'; $string['issuerusein'] = 'This service will be used'; -$string['issuerusein_help'] = 'OAuth 2 services can be used for internal services, on the login page, or both, if required.'; +$string['issuerusein_help'] = 'OAuth 2 services can be used for internal services, on the login page, SMTP with XOAUTH2, or both login page and internal services, if required.'; $string['issueruseineverywhere'] = 'Login page and internal services'; $string['issueruseininternalonly'] = 'Internal services only'; $string['issueruseinloginonly'] = 'Login page only'; +$string['issueruseinsmtpwithoauth'] = 'SMTP with XOAUTH2 only'; $string['issuerusedforlogin'] = 'Login'; $string['issuerusedforinternal'] = 'Internal services'; $string['linkedin_service'] = 'LinkedIn'; diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php index f012a258551..8c7966f1a8b 100644 --- a/lib/classes/oauth2/api.php +++ b/lib/classes/oauth2/api.php @@ -641,8 +641,20 @@ class api { $record->issuerid = $issuer->get('id'); $record->refreshtoken = $refreshtoken; $record->grantedscopes = $scopes; - $record->email = isset($userinfo['email']) ? $userinfo['email'] : ''; - $record->username = $userinfo['username']; + // Get email. + if (isset($userinfo['email'])) { + $record->email = $userinfo['email']; + } else if ($issuer->get_system_email()) { + $record->email = $issuer->get_system_email(); + } else { + $record->email = ''; + } + // Get username. + if (isset($userinfo['username'])) { + $record->username = $userinfo['username']; + } else if ($issuer->get_system_email()) { + $record->username = $issuer->get_system_email(); + } $systemaccount = new system_account(0, $record); diff --git a/lib/classes/oauth2/client/microsoft.php b/lib/classes/oauth2/client/microsoft.php new file mode 100644 index 00000000000..98e0b83cbb0 --- /dev/null +++ b/lib/classes/oauth2/client/microsoft.php @@ -0,0 +1,42 @@ +. + +namespace core\oauth2\client; + +use core\oauth2\client; + +/** + * Custom oauth2 client for Microsoft to handle specific requirements. + * + * @package core + * @copyright 2025 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class microsoft extends client { + + #[\Override] + public function get_additional_upgrade_token_parameters(): array { + $issuer = $this->get_issuer(); + if ($issuer->get('showonloginpage') == $issuer::SMTPWITHXOAUTH2) { + // We are using this issuer for SMTP with XOAUTH2. + // We need to add the SMTP scope to the token request. + return [ + 'scope' => 'https://outlook.office.com/SMTP.Send', + ]; + } + return parent::get_additional_upgrade_token_parameters(); + } +} diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php index 89beafefb94..5d424ac98b7 100644 --- a/lib/classes/oauth2/issuer.php +++ b/lib/classes/oauth2/issuer.php @@ -40,6 +40,8 @@ class issuer extends persistent { const EVERYWHERE = 1; /** @var int Issuer is displayed on the login page only */ const LOGINONLY = 2; + /** @var int Issuer is used for sending email using SMTP with XOAUTH2 */ + const SMTPWITHXOAUTH2 = 3; /** @var int Issuer is displayed only in the services lists and can not be used for login */ const SERVICEONLY = 0; @@ -127,6 +129,11 @@ class issuer extends persistent { 'null' => NULL_ALLOWED, 'default' => null, ), + 'systememail' => [ + 'type' => PARAM_EMAIL, + 'null' => NULL_ALLOWED, + 'default' => null, + ], ); } @@ -200,6 +207,7 @@ class issuer extends persistent { return $this->get('id') && $this->is_configured() && $this->get('showonloginpage') != self::SERVICEONLY && + $this->get('showonloginpage') != self::SMTPWITHXOAUTH2 && $this->get('enabled') && !empty($this->get_endpoint_url('userinfo')); } @@ -268,4 +276,13 @@ class issuer extends persistent { public function get_display_name(): string { return $this->get('loginpagename') ? $this->get('loginpagename') : $this->get('name'); } + + /** + * Get the system email address for this issuer. + * + * @return string|null The system email address or null if not set. + */ + public function get_system_email(): ?string { + return $this->get('systememail') ? $this->get('systememail') : null; + } } diff --git a/lib/db/install.xml b/lib/db/install.xml index 1f518c4f9b3..5408fecccc9 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3982,6 +3982,7 @@ + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index bc261c25f0a..cd653ef458f 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1827,5 +1827,20 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2025053000.01); } + if ($oldversion < 2025053000.02) { + + // Define field systememail to be added to oauth2_issuer. + $table = new xmldb_table('oauth2_issuer'); + $field = new xmldb_field('systememail', XMLDB_TYPE_CHAR, '100', null, null, null, null, 'loginpagename'); + + // Conditionally launch add field systememail. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2025053000.02); + } + return true; } diff --git a/lib/oauthlib.php b/lib/oauthlib.php index 9a8bc8f150d..c93815bd6de 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -581,6 +581,11 @@ abstract class oauth2_client extends curl { $params['client_secret'] = $this->clientsecret; } + // If we have additional parameters, add them to the request. + if ($this->get_additional_upgrade_token_parameters()) { + $params = array_merge($params, $this->get_additional_upgrade_token_parameters()); + } + // Requests can either use http GET or POST. if ($this->use_http_get()) { $response = $this->get($this->token_url(), $params); @@ -781,4 +786,13 @@ abstract class oauth2_client extends curl { protected function use_http_get() { return false; } + + /** + * An additional array of url params to pass with upgrade token request. + * + * @return array of name value pairs. + */ + public function get_additional_upgrade_token_parameters(): array { + return []; + } } diff --git a/lib/tests/oauth2_test.php b/lib/tests/oauth2_test.php index 5292b7338e3..06ca7350973 100644 --- a/lib/tests/oauth2_test.php +++ b/lib/tests/oauth2_test.php @@ -420,6 +420,12 @@ final class oauth2_test extends \advanced_testcase { $this->assertFalse($googleissuer->is_available_for_login()); + // Set showonloginpage to SMTP with XOAUTH2 only. + $googleissuer->set('showonloginpage', issuer::SMTPWITHXOAUTH2); + $googleissuer->update(); + + $this->assertFalse($googleissuer->is_available_for_login()); + // Set showonloginpage to everywhere (service and login) and disable issuer. $googleissuer->set('showonloginpage', issuer::EVERYWHERE); $googleissuer->set('enabled', 0); diff --git a/version.php b/version.php index fc95b5fe38a..a3f9f019a3b 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2025053000.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2025053000.02; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '5.1dev (Build: 20250530)'; // Human-friendly version name From 9647c3fd1b121a602a52bd70efbfec284eb97b62 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Wed, 21 May 2025 11:14:40 +0700 Subject: [PATCH 2/2] MDL-80380 core_auth: Add upgrade note --- .upgradenotes/MDL-80380-2025052104135989.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .upgradenotes/MDL-80380-2025052104135989.yml diff --git a/.upgradenotes/MDL-80380-2025052104135989.yml b/.upgradenotes/MDL-80380-2025052104135989.yml new file mode 100644 index 00000000000..e604009777c --- /dev/null +++ b/.upgradenotes/MDL-80380-2025052104135989.yml @@ -0,0 +1,9 @@ +issueNumber: MDL-80380 +notes: + core_auth: + - message: > + A new method called `get_additional_upgrade_token_parameters` has been + added to `oauth2_client` class. This will allow custom clients to + override this one and add their extra parameters for upgrade token + request. + type: improved