Skip to content

Commit 753ba84

Browse files
authored
Merge pull request #65 from microsoft/MOODLE_501_STABLE
Release 5.1.1 of Microsoft plugins for Moodle 5.1
2 parents 0d578f2 + 744954a commit 753ba84

17 files changed

Lines changed: 734 additions & 20 deletions

auth.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ public function __construct($forceloginflow = null) {
5353

5454
if (
5555
isset($SESSION->stateadditionaldata) && !empty($SESSION->stateadditionaldata) &&
56-
isset($SESSION->stateadditoinaldata['forceflow'])
56+
isset($SESSION->stateadditionaldata['forceflow'])
5757
) {
58-
$loginflow = $SESSION->stateadditoinaldata['forceflow'];
58+
$loginflow = $SESSION->stateadditionaldata['forceflow'];
5959
} else {
6060
if (!empty($forceloginflow) && is_string($forceloginflow)) {
6161
$loginflow = $forceloginflow;

classes/event/user_created.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static function get_name() {
4444
* @return string
4545
*/
4646
public function get_description() {
47-
return "A user (user id '{$this->userid}') was creatd using the OpenID Connect authentication plugin.";
47+
return "A user (user id '{$this->userid}') was created using the OpenID Connect authentication plugin.";
4848
}
4949

5050
/**

classes/form/application.php

Lines changed: 155 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,39 @@ protected function definition() {
7979
$mform->setDefault('clientauthmethod', AUTH_OIDC_AUTH_METHOD_SECRET);
8080
$mform->addElement('static', 'clientauthmethod_help', '', get_string('clientauthmethod_help', 'auth_oidc'));
8181

82-
// Secret.
83-
$mform->addElement('text', 'clientsecret', auth_oidc_config_name_in_form('clientsecret'), ['size' => 60]);
82+
// Secret - Check if there's an existing secret to determine if we should add a "change" checkbox.
83+
$hasexistingsecret = isset($this->_customdata['oidcconfig']->clientsecret) &&
84+
!empty($this->_customdata['oidcconfig']->clientsecret);
85+
86+
if ($hasexistingsecret) {
87+
$mform->addElement(
88+
'advcheckbox',
89+
'changesecret',
90+
get_string('change_client_secret', 'auth_oidc'),
91+
get_string('change_client_secret_desc', 'auth_oidc')
92+
);
93+
$mform->setType('changesecret', PARAM_BOOL);
94+
$mform->disabledIf('changesecret', 'clientauthmethod', 'eq', AUTH_OIDC_AUTH_METHOD_CERTIFICATE);
95+
96+
// Store the original masked value in a hidden field AND as a data attribute for JavaScript to use.
97+
$maskedsecret = auth_oidc_mask_secret($this->_customdata['oidcconfig']->clientsecret);
98+
$mform->addElement('hidden', 'originalsecretmasked', $maskedsecret);
99+
$mform->setType('originalsecretmasked', PARAM_TEXT);
100+
}
101+
102+
$attributes = ['size' => 60, 'autocomplete' => 'off', 'class' => 'secret-field'];
103+
if ($hasexistingsecret) {
104+
$maskedsecret = auth_oidc_mask_secret($this->_customdata['oidcconfig']->clientsecret);
105+
$attributes['data-original-masked'] = $maskedsecret;
106+
}
107+
$mform->addElement('text', 'clientsecret', auth_oidc_config_name_in_form('clientsecret'), $attributes);
84108
$mform->setType('clientsecret', PARAM_TEXT);
85109
$mform->disabledIf('clientsecret', 'clientauthmethod', 'neq', AUTH_OIDC_AUTH_METHOD_SECRET);
110+
111+
if ($hasexistingsecret) {
112+
$mform->disabledIf('clientsecret', 'changesecret', 'notchecked');
113+
}
114+
86115
$mform->addElement('static', 'clientsecret_help', '', get_string('clientsecret_help', 'auth_oidc'));
87116

88117
// Certificate source.
@@ -132,10 +161,38 @@ protected function definition() {
132161
$mform->disabledIf('clientcertfile', 'clientcertsource', 'neq', AUTH_OIDC_AUTH_CERT_SOURCE_FILE);
133162
$mform->addElement('static', 'clientcertfile_help', '', get_string('clientcertfile_help', 'auth_oidc'));
134163

135-
// Certificate file passphrase.
136-
$mform->addElement('text', 'clientcertpassphrase', auth_oidc_config_name_in_form('clientcertpassphrase'), ['size' => 60]);
164+
// Certificate file passphrase - Check if there's an existing passphrase.
165+
$hasexistingpassphrase = isset($this->_customdata['oidcconfig']->clientcertpassphrase) &&
166+
!empty($this->_customdata['oidcconfig']->clientcertpassphrase);
167+
168+
if ($hasexistingpassphrase) {
169+
$mform->addElement(
170+
'advcheckbox',
171+
'changecertpassphrase',
172+
get_string('change_cert_passphrase', 'auth_oidc'),
173+
get_string('change_cert_passphrase_desc', 'auth_oidc')
174+
);
175+
$mform->setType('changecertpassphrase', PARAM_BOOL);
176+
177+
// Store the original masked value in a hidden field AND as a data attribute for JavaScript to use.
178+
$maskedpassphrase = auth_oidc_mask_secret($this->_customdata['oidcconfig']->clientcertpassphrase);
179+
$mform->addElement('hidden', 'originalpassphrasemasked', $maskedpassphrase);
180+
$mform->setType('originalpassphrasemasked', PARAM_TEXT);
181+
}
182+
183+
$attributes = ['size' => 60, 'autocomplete' => 'off', 'class' => 'secret-field'];
184+
if ($hasexistingpassphrase) {
185+
$maskedpassphrase = auth_oidc_mask_secret($this->_customdata['oidcconfig']->clientcertpassphrase);
186+
$attributes['data-original-masked'] = $maskedpassphrase;
187+
}
188+
$mform->addElement('text', 'clientcertpassphrase', auth_oidc_config_name_in_form('clientcertpassphrase'), $attributes);
137189
$mform->setType('clientcertpassphrase', PARAM_TEXT);
138190
$mform->disabledIf('clientcertpassphrase', 'clientauthmethod', 'neq', AUTH_OIDC_AUTH_METHOD_CERTIFICATE);
191+
192+
if ($hasexistingpassphrase) {
193+
$mform->disabledIf('clientcertpassphrase', 'changecertpassphrase', 'notchecked');
194+
}
195+
139196
$mform->addElement('static', 'clientcertpassphrase_help', '', get_string('clientcertpassphrase_help', 'auth_oidc'));
140197

141198
// Endpoints header.
@@ -172,6 +229,11 @@ protected function definition() {
172229
$mform->setDefault('oidcscope', 'openid profile email');
173230
$mform->addElement('static', 'oidcscope_help', '', get_string('oidcscope_help', 'auth_oidc'));
174231

232+
// Custom Claim.
233+
$mform->addElement('text', 'customclaims', auth_oidc_config_name_in_form('customclaims'), ['size' => 120]);
234+
$mform->setType('customclaims', PARAM_TEXT);
235+
$mform->setDefault('customclaims', '');
236+
$mform->addElement('static', 'customclaims_help', '', get_string('customclaims_help', 'auth_oidc'));
175237
// Secret expiry notifications recipients.
176238
if (auth_oidc_is_local_365_installed()) {
177239
$mform->addElement(
@@ -230,7 +292,21 @@ public function validation($data, $files) {
230292
// Validate authentication variables.
231293
switch ($data['clientauthmethod']) {
232294
case AUTH_OIDC_AUTH_METHOD_SECRET:
233-
if (empty(trim($data['clientsecret']))) {
295+
// Check if user is attempting to change the secret.
296+
$changesecret = isset($data['changesecret']) ? $data['changesecret'] : true;
297+
$existingsecret = get_config('auth_oidc', 'clientsecret');
298+
299+
if ($changesecret) {
300+
// User wants to change the secret, validate the new value.
301+
if (empty(trim($data['clientsecret']))) {
302+
$errors['clientsecret'] = get_string('error_empty_client_secret', 'auth_oidc');
303+
} else if (auth_oidc_is_masked_secret($data['clientsecret'])) {
304+
// User checked "change secret" but didn't enter a new value.
305+
$errors['clientsecret'] = get_string('error_masked_secret_not_changed', 'auth_oidc');
306+
}
307+
} else if (empty($existingsecret) && empty(trim($data['clientsecret']))) {
308+
// No existing secret and field is empty - this is invalid.
309+
// This handles edge cases where checkbox logic fails.
234310
$errors['clientsecret'] = get_string('error_empty_client_secret', 'auth_oidc');
235311
}
236312
break;
@@ -253,6 +329,16 @@ public function validation($data, $files) {
253329
}
254330
break;
255331
}
332+
333+
// Validate certificate passphrase if user is attempting to change it.
334+
$changecertpassphrase = isset($data['changecertpassphrase']) ? $data['changecertpassphrase'] : true;
335+
336+
if ($changecertpassphrase && !empty($data['clientcertpassphrase'])) {
337+
if (auth_oidc_is_masked_secret($data['clientcertpassphrase'])) {
338+
// User checked "change passphrase" but didn't enter a new value.
339+
$errors['clientcertpassphrase'] = get_string('error_masked_secret_not_changed', 'auth_oidc');
340+
}
341+
}
256342
break;
257343
}
258344

@@ -299,6 +385,70 @@ public function validation($data, $files) {
299385
}
300386
}
301387

388+
// Validate custom claims.
389+
if (!empty($data['customclaims'])) {
390+
$claims = explode(' ', $data['customclaims']);
391+
foreach ($claims as $claim) {
392+
$claim = trim($claim);
393+
if (!empty($claim) && !preg_match('/^[a-zA-Z0-9_-]+$/', $claim)) {
394+
$errors['customclaims'] = get_string('error_invalid_custom_claim', 'auth_oidc');
395+
break;
396+
}
397+
}
398+
}
399+
302400
return $errors;
303401
}
402+
403+
/**
404+
* Process data after form definition and data loading.
405+
* This is called after set_data() and after validation errors, allowing us to override submitted values.
406+
*
407+
* @return void
408+
*/
409+
public function definition_after_data() {
410+
parent::definition_after_data();
411+
412+
$mform =& $this->_form;
413+
414+
// Get the current checkbox states using getSubmitValue (works for submitted data).
415+
$changesecret = $mform->getSubmitValue('changesecret');
416+
$changecertpassphrase = $mform->getSubmitValue('changecertpassphrase');
417+
418+
// If the "change secret" checkbox is NOT checked and there's an existing secret,
419+
// ensure the field shows the masked value (especially important after validation errors).
420+
if (isset($this->_customdata['oidcconfig']->clientsecret) && !empty($this->_customdata['oidcconfig']->clientsecret)) {
421+
$currentvalue = $mform->getSubmitValue('clientsecret');
422+
423+
// If checkbox is not checked and field is empty or doesn't match masked value, restore it.
424+
if (empty($changesecret) && (empty($currentvalue) || !auth_oidc_is_masked_secret($currentvalue))) {
425+
$maskedsecret = auth_oidc_mask_secret($this->_customdata['oidcconfig']->clientsecret);
426+
// Force the element to use the masked value.
427+
$element = $mform->getElement('clientsecret');
428+
$element->setValue($maskedsecret);
429+
430+
// Also update the data attribute for JavaScript reliability.
431+
$element->updateAttributes(['data-original-masked' => $maskedsecret]);
432+
}
433+
}
434+
435+
// Same logic for certificate passphrase.
436+
if (
437+
isset($this->_customdata['oidcconfig']->clientcertpassphrase) &&
438+
!empty($this->_customdata['oidcconfig']->clientcertpassphrase)
439+
) {
440+
$currentvalue = $mform->getSubmitValue('clientcertpassphrase');
441+
442+
// If checkbox is not checked and field is empty or doesn't match masked value, restore it.
443+
if (empty($changecertpassphrase) && (empty($currentvalue) || !auth_oidc_is_masked_secret($currentvalue))) {
444+
$maskedpassphrase = auth_oidc_mask_secret($this->_customdata['oidcconfig']->clientcertpassphrase);
445+
// Force the element to use the masked value.
446+
$element = $mform->getElement('clientcertpassphrase');
447+
$element->setValue($maskedpassphrase);
448+
449+
// Also update the data attribute for JavaScript reliability.
450+
$element->updateAttributes(['data-original-masked' => $maskedpassphrase]);
451+
}
452+
}
453+
}
304454
}

classes/loginflow/authcode.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
namespace auth_oidc\loginflow;
2828

2929
use auth_oidc\event\user_authed;
30+
use auth_oidc\event\user_created;
3031
use auth_oidc\event\user_rename_attempt;
3132
use auth_oidc\jwt;
3233
use auth_oidc\utils;
@@ -801,6 +802,15 @@ protected function handlelogin(string $oidcuniqid, array $authparams, array $tok
801802
}
802803
}
803804
$user = create_user_record($username, '', 'oidc');
805+
806+
// Trigger user_created event.
807+
$eventdata = [
808+
'objectid' => $user->id,
809+
'userid' => $user->id,
810+
'relateduserid' => $user->id,
811+
];
812+
$event = user_created::create($eventdata);
813+
$event->trigger();
804814
} else {
805815
// Trigger login failed event.
806816
$failurereason = AUTH_LOGIN_NOUSER;

classes/loginflow/base.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,9 @@ protected function createtoken(
674674
// Cleanup old invalid token with the same oidcusername.
675675
$DB->delete_records('auth_oidc_token', ['oidcusername' => $oidcusername]);
676676

677+
// Cleanup old token with the same Moodle username to prevent duplicates.
678+
$DB->delete_records('auth_oidc_token', ['username' => $username]);
679+
677680
// Handle "The existing token for this user does not contain a valid user ID" error.
678681
if ($userid == 0) {
679682
$userrec = $DB->get_record('user', ['username' => $username]);
@@ -752,7 +755,7 @@ protected function get_oidc_username_from_token_claim(jwt $idtoken, string $bind
752755

753756
switch ($bindingusernameclaim) {
754757
case 'custom':
755-
$bindingusernameclaim = get_config('auth_oidc', 'custombindingclaim');
758+
$bindingusernameclaim = get_config('auth_oidc', 'customclaimname');
756759
// No break.
757760
case 'preferred_username':
758761
case 'email':

classes/loginflow/rocreds.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
namespace auth_oidc\loginflow;
2727

28+
use auth_oidc\event\user_created;
2829
use auth_oidc\utils;
2930

3031
defined('MOODLE_INTERNAL') || die();
@@ -137,6 +138,16 @@ public function loginpage_hook(&$frm, &$user) {
137138
}
138139

139140
$user = create_user_record($username, $password, $auth);
141+
142+
// Trigger user_created event.
143+
$eventdata = [
144+
'objectid' => $user->id,
145+
'userid' => $user->id,
146+
'relateduserid' => $user->id,
147+
];
148+
$event = user_created::create($eventdata);
149+
$event->trigger();
150+
140151
return true;
141152
}
142153

classes/observers.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class observers {
4545
public static function handle_user_deleted(user_deleted $event) {
4646
global $DB;
4747
$userid = $event->objectid;
48-
$DB->delete_records('auth_oidc_token', ['userid' => $userid]);
49-
return true;
48+
return $DB->delete_records('auth_oidc_token', ['userid' => $userid]);
5049
}
5150
}

db/install.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
<INDEX NAME="oidcuniqid" UNIQUE="false" FIELDS="oidcuniqid"/>
5959
<INDEX NAME="userid" UNIQUE="false" FIELDS="userid"/>
6060
<INDEX NAME="username" UNIQUE="false" FIELDS="username"/>
61+
<INDEX NAME="oidcusername" UNIQUE="false" FIELDS="oidcusername"/>
6162
</INDEXES>
6263
</TABLE>
6364
<TABLE NAME="auth_oidc_sid" COMMENT="Stores sid from IdP, to be used to find connected Moodle users when SLO is triggered from IdP.">

db/upgrade.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,5 +567,19 @@ function xmldb_auth_oidc_upgrade($oldversion) {
567567
upgrade_plugin_savepoint(true, 2024100702, 'auth', 'oidc');
568568
}
569569

570+
if ($oldversion < 2025100600.01) {
571+
// Define index to be added to auth_oidc_token.
572+
$table = new xmldb_table('auth_oidc_token');
573+
$index = new xmldb_index('oidcusername', XMLDB_INDEX_NOTUNIQUE, ['oidcusername']);
574+
575+
// Conditionally launch add index oidcusername.
576+
if (!$dbman->index_exists($table, $index)) {
577+
$dbman->add_index($table, $index);
578+
}
579+
580+
// Oidc savepoint reached.
581+
upgrade_plugin_savepoint(true, 2025100600.01, 'auth', 'oidc');
582+
}
583+
570584
return true;
571585
}

index.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,38 @@
2929

3030
$auth = new \auth_plugin_oidc('authcode');
3131
$auth->set_httpclient(new \auth_oidc\httpclient());
32-
$auth->handleredirect();
32+
33+
try {
34+
$auth->handleredirect();
35+
} catch (moodle_exception $e) {
36+
// If debugging is off, re-throw to let Moodle handle it with generic message.
37+
if (empty($CFG->debug) || $CFG->debug < DEBUG_MINIMAL) {
38+
throw $e;
39+
}
40+
41+
// Only display detailed debug information if debug display is enabled.
42+
// This prevents leaking sensitive internal details to unauthenticated users.
43+
$showdetails = !empty($CFG->debugdisplay);
44+
45+
if ($showdetails) {
46+
// Display error details when debug display is enabled.
47+
$errormessage = $e->getMessage();
48+
if (!empty($e->debuginfo)) {
49+
$errormessage .= ' (' . $e->debuginfo . ')';
50+
}
51+
} else {
52+
// Show generic error message to prevent information disclosure.
53+
$errormessage = get_string('errorauthgeneral', 'auth_oidc');
54+
}
55+
56+
$PAGE->set_url('/auth/oidc/');
57+
$PAGE->set_context(context_system::instance());
58+
$PAGE->set_pagelayout('login');
59+
$PAGE->set_title(get_string('error'));
60+
61+
echo $OUTPUT->header();
62+
echo $OUTPUT->notification($errormessage, 'error');
63+
echo $OUTPUT->single_button(new moodle_url('/login/index.php'), get_string('login'), 'get');
64+
echo $OUTPUT->footer();
65+
exit;
66+
}

0 commit comments

Comments
 (0)