Add enhanced OIDC binding ID token claim options

This commit is contained in:
Patryk Mroczko 2023-12-07 18:08:14 +01:00 коммит произвёл Lai Wei
Родитель e5b25b3a3a
Коммит 44ba8aeb66
6 изменённых файлов: 131 добавлений и 85 удалений

Просмотреть файл

@ -64,6 +64,28 @@ class application extends moodleform {
$mform->addElement('static', 'clientid_help', '', get_string('clientid_help', 'auth_oidc'));
$mform->addRule('clientid', null, 'required', null, 'client');
// Add the new selector for "Binding username claim"
$bindingusernameoptions = [
'auto' => get_string('binding_username_auto', 'auth_oidc'), // Use current logic
'preferred_username' => get_string('binding_username_preferred_username', 'auth_oidc'),
'email' => get_string('binding_username_email', 'auth_oidc'),
'upn' => get_string('binding_username_upn', 'auth_oidc'),
'unique_name' => get_string('binding_username_unique_name', 'auth_oidc'),
'sub' => get_string('binding_username_sub', 'auth_oidc'),
'custom' => get_string('binding_username_custom', 'auth_oidc'), // Custom value
];
$mform->addElement('select', 'bindingusernameclaim', auth_oidc_config_name_in_form('bindingusernameclaim'), $bindingusernameoptions);
$mform->setDefault('bindingusernameclaim', 'auto');
$mform->addElement('static', 'bindingusernameclaim_help', '', get_string('bindingusernameclaim_help', 'auth_oidc'));
// Add a text field for custom claim name
$mform->addElement('text', 'customclaimname', auth_oidc_config_name_in_form('customclaimname'), ['size' => 40]);
$mform->setType('customclaimname', PARAM_TEXT);
$mform->disabledIf('customclaimname', 'bindingusernameclaim', 'neq', 'custom'); // Enable only if "Custom" is selected
$mform->addElement('static', 'customclaimname_description', '', get_string('customclaimname_description', 'auth_oidc'));
$mform->disabledIf('customclaimname_description', 'bindingusernameclaim', 'neq', 'custom');
// Authentication header.
$mform->addElement('header', 'authentication', get_string('settings_section_authentication', 'auth_oidc'));
$mform->setExpanded('authentication');

Просмотреть файл

@ -362,22 +362,13 @@ class authcode extends base {
if (isloggedin() && !isguestuser() && (empty($tokenrec) || (isset($USER->auth) && $USER->auth !== 'oidc'))) {
// If user is already logged in and trying to link Microsoft 365 account or use it for OIDC.
// Check if that Microsoft 365 account already exists in moodle.
if (get_config('auth_oidc', 'idptype') == AUTH_OIDC_IDP_TYPE_MICROSOFT_IDENTITY_PLATFORM) {
$upn = $idtoken->claim('preferred_username');
if (empty($upn)) {
$upn = $idtoken->claim('email');
}
} else {
$upn = $idtoken->claim('upn');
if (empty($upn)) {
$upn = $idtoken->claim('unique_name');
}
}
$oidcusername = $this->get_oidc_username_from_token_claim($idtoken);
$userrec = $DB->count_records_sql('SELECT COUNT(*)
FROM {user}
WHERE username = ?
AND id != ?',
[$upn, $USER->id]);
[$oidcusername, $USER->id]);
if (!empty($userrec)) {
if (empty($additionaldata['redirect'])) {
@ -562,20 +553,7 @@ class authcode extends base {
// Find the latest real Microsoft username.
// Determine remote username depending on IdP type, or fall back to standard 'sub'.
if (get_config('auth_oidc', 'idptype') == AUTH_OIDC_IDP_TYPE_MICROSOFT_IDENTITY_PLATFORM) {
$oidcusername = $idtoken->claim('preferred_username');
if (empty($oidcusername)) {
$oidcusername = $idtoken->claim('email');
}
} else {
$oidcusername = $idtoken->claim('upn');
if (empty($oidcusername)) {
$oidcusername = $idtoken->claim('unique_name');
}
}
if (empty($oidcusername)) {
$oidcusername = $idtoken->claim('sub');
}
$oidcusername = $this->get_oidc_username_from_token_claim($idtoken);
$usernamechanged = false;
if ($oidcusername && $tokenrec && strtolower($oidcusername) !== strtolower($tokenrec->oidcusername)) {
@ -695,17 +673,8 @@ class authcode extends base {
$existinguser = core_user::get_user($existingmatching->moodleid);
if (get_config('auth_oidc', 'idptype') == AUTH_OIDC_IDP_TYPE_MICROSOFT_IDENTITY_PLATFORM) {
$username = $idtoken->claim('preferred_username');
if (empty($username)) {
$username = $idtoken->claim('email');
}
} else {
$username = $idtoken->claim('upn');
if (empty($username)) {
$username = $idtoken->claim('unique_name');
}
}
$username = $this->get_oidc_username_from_token_claim($idtoken);
$originalupn = null;
if (empty($username)) {
@ -764,18 +733,9 @@ class authcode extends base {
*/
// Generate a Moodle username.
// Use 'upn' if available for username (Microsoft-specific), or fall back to lower-case oidcuniqid.
if (get_config('auth_oidc', 'idptype') == AUTH_OIDC_IDP_TYPE_MICROSOFT_IDENTITY_PLATFORM) {
$username = $idtoken->claim('preferred_username');
if (empty($username)) {
$username = $idtoken->claim('email');
}
} else {
$username = $idtoken->claim('upn');
if (empty($username)) {
$username = $idtoken->claim('unique_name');
}
}
// Use 'upn' if available for username (Azure-specific), or fall back to lower-case oidcuniqid.
$username = $this->get_oidc_username_from_token_claim($idtoken);
$originalupn = null;
if (empty($username)) {

Просмотреть файл

@ -173,6 +173,7 @@ class base {
$upn = $token->claim('unique_name');
}
}
if (!empty($upn)) {
$userdata['userPrincipalName'] = $upn;
}
@ -252,6 +253,7 @@ class base {
$upn = $token->claim('unique_name');
}
}
if (!empty($upn)) {
$userdata['userPrincipalName'] = $upn;
}
@ -572,21 +574,7 @@ class base {
if ($restrictions !== '') {
$restrictions = explode("\n", $restrictions);
// Check main user identifier claim based on IdP type, and falls back to oidc-standard "sub" if still empty.
if (get_config('auth_oidc', 'idptype') == AUTH_OIDC_IDP_TYPE_MICROSOFT_IDENTITY_PLATFORM) {
$tomatch = $idtoken->claim('preferred_username');
if (empty($tomatch)) {
$tomatch = $idtoken->claim('email');
}
} else {
$tomatch = $idtoken->claim('upn');
if (empty($tomatch)) {
$tomatch = $idtoken->claim('unique_name');
}
}
if (empty($tomatch)) {
$tomatch = $idtoken->claim('sub');
}
$oidcusername = $this->get_oidc_username_from_token_claim($idtoken);
foreach ($restrictions as $restriction) {
$restriction = trim($restriction);
if ($restriction !== '') {
@ -597,7 +585,7 @@ class base {
if (isset($this->config->userrestrictionscasesensitive) && !$this->config->userrestrictionscasesensitive) {
$pattern .= 'i';
}
$count = @preg_match($pattern, $tomatch, $matches);
$count = @preg_match($pattern, $oidcusername, $matches);
if (!empty($count)) {
$userpassed = true;
break;
@ -606,7 +594,7 @@ class base {
$debugdata = [
'exception' => $e,
'restriction' => $restriction,
'tomatch' => $tomatch,
'tomatch' => $oidcusername,
];
utils::debug('Error running user restrictions.', __METHOD__, $debugdata);
}
@ -616,7 +604,7 @@ class base {
$debugdata = [
'contents' => $contents,
'restriction' => $restriction,
'tomatch' => $tomatch,
'tomatch' => $oidcusername,
];
utils::debug('Output while running user restrictions.', __METHOD__, $debugdata);
}
@ -646,21 +634,7 @@ class base {
$oidcusername = $originalupn;
} else {
// Determine remote username depending on IdP type, or fall back to standard 'sub'.
if (get_config('auth_oidc', 'idptype') == AUTH_OIDC_IDP_TYPE_MICROSOFT_IDENTITY_PLATFORM) {
$oidcusername = $idtoken->claim('preferred_username');
if (empty($oidcusername)) {
$oidcusername = $idtoken->claim('email');
}
} else {
$oidcusername = $idtoken->claim('upn');
if (empty($oidcusername)) {
$oidcusername = $idtoken->claim('unique_name');
}
}
if (empty($oidcusername)) {
$oidcusername = $idtoken->claim('sub');
}
$oidcusername = $this->get_oidc_username_from_token_claim($idtoken);
}
// We should not fail here (idtoken was verified earlier to at least contain 'sub', but just in case...).
@ -726,4 +700,43 @@ class base {
$tokenrec->idtoken = $tokenparams['id_token'];
$DB->update_record('auth_oidc_token', $tokenrec);
}
/**
* Get OIDC username from token claims based on configured claim.
*
* @param jwt $idtoken The OIDC ID token.
* @return string|null The OIDC username if found, null otherwise.
*/
protected function get_oidc_username_from_token_claim($idtoken) {
if (empty($idtoken)) {
return null;
}
$bindingclaim = get_config('auth_oidc', 'bindingusernameclaim');
if ($bindingclaim === 'custom') {
$bindingclaim = get_config('auth_oidc', 'custombindingclaim');
}
$oidcusername = $idtoken->claim($bindingclaim);
if (empty($oidcusername) || $bindingclaim === 'auto') {
if (get_config('auth_oidc', 'idptype') == AUTH_OIDC_IDP_TYPE_MICROSOFT) {
$oidcusername = $idtoken->claim('preferred_username');
if (empty($oidcusername)) {
$oidcusername = $idtoken->claim('email');
}
} else {
$oidcusername = $idtoken->claim('upn');
if (empty($oidcusername)) {
$oidcusername = $idtoken->claim('unique_name');
}
}
if (empty($oidcusername)) {
$oidcusername = $idtoken->claim('sub');
}
}
return $oidcusername;
}
}

Просмотреть файл

@ -335,6 +335,7 @@ $string['update_onlogin_and_usersync'] = 'On every login and every user sync tas
// Remote fields.
$string['settings_fieldmap_feild_not_mapped'] = '(not mapped)';
$string['settings_fieldmap_field_bindingusernameclaim'] = 'Binding Username Claim';
$string['settings_fieldmap_field_city'] = 'City';
$string['settings_fieldmap_field_companyName'] = 'Company Name';
$string['settings_fieldmap_field_objectId'] = 'Object ID';
@ -352,7 +353,7 @@ $string['settings_fieldmap_field_postalCode'] = 'Postal Code';
$string['settings_fieldmap_field_preferredLanguage'] = 'Language';
$string['settings_fieldmap_field_state'] = 'State';
$string['settings_fieldmap_field_streetAddress'] = 'Street Address';
$string['settings_fieldmap_field_userPrincipalName'] = 'Username (UPN)';
$string['settings_fieldmap_field_userPrincipalName'] = 'User Principal Name';
$string['settings_fieldmap_field_employeeId'] = 'Employee ID';
$string['settings_fieldmap_field_businessPhones'] = 'Office phone';
$string['settings_fieldmap_field_mobilePhone'] = 'Mobile phone';
@ -375,3 +376,25 @@ $string['settings_fieldmap_field_sds_student_graduationYear'] = 'SDS student gra
$string['settings_fieldmap_field_sds_student_studentNumber'] = 'SDS student number';
$string['settings_fieldmap_field_sds_teacher_externalId'] = 'SDS teacher external ID';
$string['settings_fieldmap_field_sds_teacher_teacherNumber'] = 'SDS teacher number';
// Binding username claim options.
$string['binding_username_auto'] = 'Choose automatically';
$string['binding_username_preferred_username'] = 'preferred_username';
$string['binding_username_email'] = 'email';
$string['binding_username_upn'] = 'upn';
$string['binding_username_unique_name'] = 'unique_name';
$string['binding_username_sub'] = 'sub';
$string['binding_username_custom'] = 'Custom';
$string['bindingusernameclaim'] = 'Binding Username Claim';
$string['customclaimname'] = 'Custom claim name';
$string['customclaimname_description'] = 'This field is used only when <b>Binding Username Claim</b> is set to <b>Custom</b>.';
$string['bindingusernameclaim_help'] = 'This is an advanced feature. Select the ID token claim to be used for binding the username. The options include:<br/>
- <b>Choose automatically</b>: Uses current logic, determining the token by IdP type and falling back to <b>sub</b> if no claim is found.<br/>
- <b>preferred_username</b>: Default for Microsoft identity platform (v2.0) IdP type.<br/>
- <b>email</b>: Fallback for Microsoft identity platform (v2.0).<br/>
- <b>upn</b>: Default for Microsoft Entra ID (v1.0) and other IdP types.<br/>
- <b>unique_name</b>: Fallback for Microsoft Entra ID (v1.0) and other IdP types.<br/>
- <b>sub</b>: Fallback if no other claims are present.<br/>
- <b>Custom</b>: Allows the site admin to enter a custom value.';

12
lib.php
Просмотреть файл

@ -239,9 +239,20 @@ function auth_oidc_delete_token(int $tokenid): void {
* @return array
*/
function auth_oidc_get_remote_fields() {
$bindingusernameclaim = get_config('auth_oidc', 'bindingusernameclaim');
if (empty($bindingusernameclaim) || $bindingusernameclaim === 'auto') {
if (get_config('auth_oidc', 'idptype') == AUTH_OIDC_IDP_TYPE_MICROSOFT) {
$bindingusernameclaim = 'preferred_username';
} else {
$bindingusernameclaim = 'upn';
}
}
if (auth_oidc_is_local_365_installed()) {
$remotefields = [
'' => get_string('settings_fieldmap_feild_not_mapped', 'auth_oidc'),
$bindingusernameclaim => get_string('settings_fieldmap_field_bindingusernameclaim', 'auth_oidc'),
'objectId' => get_string('settings_fieldmap_field_objectId', 'auth_oidc'),
'userPrincipalName' => get_string('settings_fieldmap_field_userPrincipalName', 'auth_oidc'),
'displayName' => get_string('settings_fieldmap_field_displayName', 'auth_oidc'),
@ -299,6 +310,7 @@ function auth_oidc_get_remote_fields() {
} else {
$remotefields = [
'' => get_string('settings_fieldmap_feild_not_mapped', 'auth_oidc'),
$bindingusernameclaim => get_string('settings_fieldmap_field_bindingusernameclaim', 'auth_oidc'),
'objectId' => get_string('settings_fieldmap_field_objectId', 'auth_oidc'),
'userPrincipalName' => get_string('settings_fieldmap_field_userPrincipalName', 'auth_oidc'),
'givenName' => get_string('settings_fieldmap_field_givenName', 'auth_oidc'),

Просмотреть файл

@ -57,12 +57,24 @@ $form = new application(null, ['oidcconfig' => $oidcconfig]);
$formdata = [];
foreach (['idptype', 'clientid', 'clientauthmethod', 'clientsecret', 'clientprivatekey', 'clientcert',
'clientcertsource', 'clientprivatekeyfile', 'clientcertfile', 'clientcertpassphrase',
'authendpoint', 'tokenendpoint', 'oidcresource', 'oidcscope', 'secretexpiryrecipients'] as $field) {
'authendpoint', 'tokenendpoint', 'oidcresource', 'oidcscope', 'secretexpiryrecipients',
'bindingusernameclaim', 'customclaimname'] as $field) {
if (isset($oidcconfig->$field)) {
$formdata[$field] = $oidcconfig->$field;
}
}
$bindingusernameclaim = get_config('auth_oidc', 'bindingusernameclaim');
$predefinedoptions = ['auto', 'preferred_username', 'email', 'upn', 'unique_name', 'sub'];
if (!in_array($bindingusernameclaim, $predefinedoptions)) {
$formdata['bindingusernameclaim'] = 'custom';
$formdata['customclaimname'] = $bindingusernameclaim;
} else {
$formdata['bindingusernameclaim'] = $bindingusernameclaim;
}
$form->set_data($formdata);
if ($form->is_cancelled()) {
@ -73,9 +85,13 @@ if ($form->is_cancelled()) {
$fromform->clientauthmethod = optional_param('clientauthmethod', AUTH_OIDC_AUTH_METHOD_SECRET, PARAM_INT);
}
if ($fromform->bindingusernameclaim === 'custom') {
$fromform->bindingusernameclaim = $fromform->customclaimname;
}
// Prepare config settings to save.
$configstosave = ['idptype', 'clientid', 'clientauthmethod', 'authendpoint', 'tokenendpoint',
'oidcresource', 'oidcscope'];
'oidcresource', 'oidcscope', 'bindingusernameclaim', 'customclaimname'];
// Depending on the value of clientauthmethod, save clientsecret or (clientprivatekey and clientcert).
switch ($fromform->clientauthmethod) {