Skip to content

Allow OIDC with Azure B2C #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: TOTARA_160_STABLE-catalyst
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
78223d6
Allow OIDC with Azure B2C
Peterburnett Feb 23, 2023
bff9c59
Bugfix
Peterburnett Mar 7, 2023
541853c
Bugfix #2
Peterburnett Mar 7, 2023
d5fe782
Fix warning for user creation before fields initialised
Peterburnett Apr 12, 2023
c1cce0c
Removed totara incompatible test
Peterburnett Apr 27, 2023
c11a20d
Use email as primary key for user matching
Peterburnett Jul 6, 2023
3726921
Merge pull request #12 from catalyst/azureb2cemail
Peterburnett Jul 6, 2023
79fa4a1
Debugging
Peterburnett Jul 13, 2023
55866ee
Corrected field mapping logic for custom mapped attributes
Peterburnett Jul 14, 2023
110bdeb
Update auth method for matched accounts
Peterburnett Jul 14, 2023
2c3d7f0
Coerce user to OIDC correctly
Peterburnett Jul 17, 2023
947b0a5
Uncache password when coercing accounts
Peterburnett Jul 17, 2023
73a14b7
Update password update mechanism for account rollover
Peterburnett Jul 17, 2023
df676f9
Update token to match found username on auth
Peterburnett Jul 18, 2023
3641b5e
Lowercase comparisons of email before matching user
Peterburnett Aug 9, 2023
80fe3c4
Second instance corrected
Peterburnett Aug 9, 2023
bafdcb8
Bugfix for user flow order
Peterburnett Aug 10, 2023
0c04f08
Transform email at deserialise
Peterburnett Aug 10, 2023
8ed183a
Revert "Bugfix for user flow order"
Peterburnett Aug 10, 2023
01fd71a
Revert "Second instance corrected"
Peterburnett Aug 10, 2023
f6e7d23
Revert "Lowercase comparisons of email before matching user"
Peterburnett Aug 10, 2023
0845064
Apply lowercasing after token management
Peterburnett Aug 10, 2023
e400fd9
Bugfix for lowercase loading from token map
Peterburnett Aug 10, 2023
104401a
Allow for configurable matching
Peterburnett Oct 18, 2023
577871c
Merge pull request #13 from catalyst/config-matching
matthewhilton Oct 19, 2023
d3781c3
Allow for email and idnumber based match routing
Peterburnett Nov 7, 2023
2fd65c6
Fix null condition for user updating
Peterburnett Nov 14, 2023
c15b700
Another bugfix for user matching
Peterburnett Nov 14, 2023
ff604df
Set token to correct username for idnumber matching
Peterburnett Nov 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions classes/form/application.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ protected function definition() {
$mform->disabledIf('clientsecret', 'clientauthmethod', 'neq', AUTH_OIDC_AUTH_METHOD_SECRET);
$mform->addElement('static', 'clientsecret_help', '', get_string('clientsecret_help', 'auth_oidc'));

// Auth token secret toggle.
$mform->addElement('advcheckbox', 'accesstokenclientsecret', auth_oidc_config_name_in_form('accesstokenclientsecret'), '');
$mform->setType('accesstokenclientsecret', PARAM_BOOL);
$mform->addElement('static', 'accesstokenclientsecret_help', '', get_string('accesstokenclientsecret_help', 'auth_oidc'));

// Certificate private key.
$mform->addElement('textarea', 'clientprivatekey', auth_oidc_config_name_in_form('clientprivatekey'),
['rows' => 10, 'cols' => 80]);
Expand Down
63 changes: 59 additions & 4 deletions classes/loginflow/authcode.php
Original file line number Diff line number Diff line change
Expand Up @@ -617,14 +617,44 @@ protected function handlelogin(string $oidcuniqid, array $authparams, array $tok
}
$username = trim(\core_text::strtolower($username));
$tokenrec = $this->createtoken($oidcuniqid, $username, $authparams, $tokenparams, $idtoken, 0, $originalupn);
$userinfo = $this->get_userinfo($username);

$matchingmethod = get_config('auth_oidc', 'subjectmapping');
// Email can only be used when allowaccountssameemail is off.
if ($matchingmethod === 'email' && ($CFG->allowaccountssameemail || !array_key_exists('email', $userinfo))) {
// Fall back to username in this case.
$matchingmethod = 'username';
}

switch ($matchingmethod) {
case 'idnumberemail':
$isemail = filter_var($username, FILTER_VALIDATE_EMAIL);
$existinguserparams = ['mnethostid' => $CFG->mnet_localhost_id];
if ($isemail === false) {
$existinguserparams['idnumber'] = $username;
} else {
$existinguserparams['email'] = $username;
}
break;
case 'idnumber':
// In this case, we are treating the "username" from OIDC as the idnumber
$existinguserparams = ['idnumber' => $username, 'mnethostid' => $CFG->mnet_localhost_id];
break;
case 'email':
$existinguserparams = ['email' => $userinfo['email'], 'mnethostid' => $CFG->mnet_localhost_id];
break;
case 'username':
default:
$existinguserparams = ['username' => $username, 'mnethostid' => $CFG->mnet_localhost_id];
break;
}

$existinguserparams = ['username' => $username, 'mnethostid' => $CFG->mnet_localhost_id];
if ($DB->record_exists('user', $existinguserparams) !== true) {
// User does not exist. Create user if site allows, otherwise fail.
if (empty($CFG->authpreventaccountcreation)) {
if (!$CFG->allowaccountssameemail) {
$userinfo = $this->get_userinfo($username);
if ($DB->count_records('user', array('email' => $userinfo['email'], 'deleted' => 0)) > 0) {
if (array_key_exists('email', $userinfo)
&& ($DB->count_records('user', array('email' => $userinfo['email'], 'deleted' => 0)) > 0)) {
throw new moodle_exception('errorauthloginfaileddupemail', 'auth_oidc', null, null, '1');
}
}
Expand All @@ -638,7 +668,32 @@ protected function handlelogin(string $oidcuniqid, array $authparams, array $tok
throw new moodle_exception('errorauthloginfailednouser', 'auth_oidc', null, null, '1');
}
}

// Ensure user objects are in the right shape for future matching before proceeding.
if (array_key_exists('idnumber', $existinguserparams)) {
if (isset($user)) {
// The user has been created, but we need to make the idnumber bound as well.
$user->auth = 'oidc';
$user->idnumber = $username;
user_update_user($user, false);
}
$founduser = $DB->get_record('user', ['idnumber' => $username]);
// Now update the token to match the found username.
$DB->set_field('auth_oidc_token', 'username', $founduser->username, ['oidcuniqid' => $username]);
$username = $founduser->username;
} else if (array_key_exists('email', $existinguserparams)) {
// We can only match on email if allowaccountssameemail is off.
if (!$CFG->allowaccountssameemail && array_key_exists('email', $userinfo)) {
// Here we know there is atleast 1 account with that email.
// We can just get the first match which is safe as long as allowaccountssameemail is off.
$founduser = $DB->get_record('user', ['email' => $userinfo['email']]);
// Before proceeding, set the user to OIDC if they aren't already.
$founduser->auth = 'oidc';
user_update_user($founduser, false);
// Now update the token to match the found username.
$DB->set_field('auth_oidc_token', 'username', $founduser->username, ['oidcuniqid' => $username]);
$username = $founduser->username;
}
}
$user = authenticate_user_login($username, null, true);

if (!empty($user)) {
Expand Down
20 changes: 15 additions & 5 deletions classes/loginflow/base.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,20 +257,23 @@ public function get_userinfo($username) {
if (!isset($userdata['email'])) {
$email = $token->claim('email');
if (!empty($email)) {
$userdata['mail'] = $email;
$userdata['email'] = $email;
} else {
if (!empty($upn)) {
$aademailvalidateresult = filter_var($upn, FILTER_VALIDATE_EMAIL);
if (!empty($aademailvalidateresult)) {
$userdata['mail'] = $aademailvalidateresult;
$userdata['email'] = $aademailvalidateresult;
}
}
}
}
}

$updateduser = static::apply_configured_fieldmap_from_token($userdata, $eventtype);
$updateduser = static::apply_configured_fieldmap_from_token($userdata, $eventtype, $token);
$userinfo = (array)$updateduser;
if (!empty($userinfo['email'])) {
$userinfo['email'] = strtolower($userinfo['email']);
}
}

return $userinfo;
Expand All @@ -281,9 +284,10 @@ public function get_userinfo($username) {
*
* @param array $userdata
* @param string $eventtype
* @param jwt $token
* @return stdClass
*/
public static function apply_configured_fieldmap_from_token(array $userdata, string $eventtype) {
public static function apply_configured_fieldmap_from_token(array $userdata, string $eventtype, jwt $token) {
$user = new stdClass();

$fieldmappings = auth_oidc_get_field_mappings();
Expand All @@ -292,13 +296,19 @@ public static function apply_configured_fieldmap_from_token(array $userdata, str
$remotefield = $fieldmapping['field_map'];
$behavior = $fieldmapping['update_local'];

if ($behavior !== 'on' . $eventtype && $behavior !== 'always') {
if ($behavior === 'on_create' && $eventtype !== 'create') {
// Field mapping doesn't apply to this event type.
continue;
}

if (isset($userdata[$remotefield])) {
$user->$localfield = $userdata[$remotefield];
} else {
// Try a manual token claim on the value provided.
$tokenval = $token->claim($remotefield);
if (!is_null($tokenval)) {
$user->$localfield = $tokenval;
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion classes/oidcclient.php
Original file line number Diff line number Diff line change
Expand Up @@ -333,14 +333,15 @@ public function tokenrequest($code) {
'redirect_uri' => $this->redirecturi,
];

$sendsecret = get_config('auth_oidc', 'accesstokenclientsecret');
switch (get_config('auth_oidc', 'clientauthmethod')) {
case AUTH_OIDC_AUTH_METHOD_CERTIFICATE:
$params['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
$params['client_assertion'] = static::generate_client_assertion();
$params['tenant'] = 'common';
break;
default:
$params['client_secret'] = $this->clientsecret;
$params['client_secret'] = $sendsecret ? $this->clientsecret : null;
}
$returned = $this->httpclient->post($this->endpoints['token'], $params);
return utils::process_json_response($returned, ['id_token' => null]);
Expand All @@ -365,6 +366,7 @@ public function app_access_token_request() {
break;
default:
$params['client_secret'] = $this->clientsecret;
$params = [];
}

$tokenendpoint = $this->endpoints['token'];
Expand Down
9 changes: 9 additions & 0 deletions lang/en/auth_oidc.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
$string['idp_type_microsoft'] = 'Microsoft identity platform (v2.0)';
$string['idp_type_other'] = 'Other';
$string['cfg_authenticationlink_desc'] = '<a href="{$a}" target="_blank">Link to IdP and authentication configuration</a>';
$string['accesstokenclientsecret'] = 'Send client secret in access token requests.';
$string['accesstokenclientsecret_help'] = 'Some IdPs like Azure B2C do not allow the client secret to be sent in token exchange requests. Disable this config to remove the client secret from the request.';
$string['authendpoint'] = 'Authorization Endpoint';
$string['authendpoint_help'] = 'The URI of the Authorization endpoint from your IdP to use.<br/>
Note if the site is to be configured to allow users from other tenants to access, tenant specific authorization endpoint cannot be used.';
Expand All @@ -83,6 +85,8 @@
$string['clientcert_help'] = 'When using <b>certificate</b> authentication method, this is the public key, or certificate, used in to authenticate with IdP.';
$string['tenantnameorguid'] = 'Tenant name or GUID';
$string['tenantnameorguid_help'] = 'Don\'t include https:// if use tenant name.';
$string['cfg_custom_mapping'] = 'Use custom data mappings';
$string['cfg_custom_mapping_desc'] = 'Allow custom attribute entry for data mappings.';
$string['cfg_domainhint_key'] = 'Domain Hint';
$string['cfg_domainhint_desc'] = 'When using the <b>Authorization Code</b> login flow, pass this value as the "domain_hint" parameter. "domain_hint" is used by some OpenID Connect IdP to make the login process easier for users. Check with your provider to see whether they support this parameter.';
$string['cfg_err_invalidauthendpoint'] = 'Invalid Authorization Endpoint';
Expand Down Expand Up @@ -325,3 +329,8 @@
$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';

// Custom strings
$string['subjectmapping'] = 'User matching attribute';
$string['subjectmapping_desc'] = 'Select the user attribute in the platform to match against the incoming claim. Email will be matched to the \'email\' field in the claim. Username and ID Number will be matched to the subject field in the claim. The IDNumber & Email setting will try to infer the shape of the subject field: If an email address is supplied, it will match against the platform email field, otherwise against the user IDNumber.';
$string['idnumberemail'] = 'IDNumber & Email';
5 changes: 2 additions & 3 deletions manageapplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
$form = new application(null, ['oidcconfig' => $oidcconfig]);

$formdata = [];
foreach (['idptype', 'clientid', 'clientauthmethod', 'clientsecret', 'clientprivatekey', 'clientcert', 'tenantnameorguid',
foreach (['idptype', 'clientid', 'clientauthmethod', 'clientsecret', 'accesstokenclientsecret', 'clientprivatekey', 'clientcert', 'tenantnameorguid',
'authendpoint', 'tokenendpoint', 'oidcresource', 'oidcscope'] as $field) {
if (isset($oidcconfig->$field)) {
$formdata[$field] = $oidcconfig->$field;
Expand All @@ -73,7 +73,7 @@
}

// Prepare config settings to save.
$configstosave = ['idptype', 'clientid', 'tenantnameorguid', 'clientauthmethod', 'authendpoint', 'tokenendpoint',
$configstosave = ['idptype', 'clientid', 'tenantnameorguid', 'clientauthmethod', 'accesstokenclientsecret', 'authendpoint', 'tokenendpoint',
'oidcresource', 'oidcscope'];

// Depending on the value of clientauthmethod, save clientsecret or (clientprivatekey and clientcert).
Expand Down Expand Up @@ -110,4 +110,3 @@
$form->display();

echo $OUTPUT->footer();

21 changes: 19 additions & 2 deletions settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,29 @@

// Other settings page and its settings.
$fieldmappingspage = new admin_settingpage('auth_oidc_field_mapping', get_string('settings_page_field_mapping', 'auth_oidc'));
$fieldmappingspage->add(new admin_setting_configcheckbox('auth_oidc/custom_field_mapping',
get_string('cfg_custom_mapping', 'auth_oidc'), get_string('cfg_custom_mapping_desc', 'auth_oidc'), 0));
$options = [
'username' => get_string('username'),
'email' => get_string('email'),
'idnumber' => get_string('idnumber'),
'idnumberemail' => get_string('idnumberemail', 'auth_oidc'),
];
$fieldmappingspage->add(new admin_setting_configselect('auth_oidc/subjectmapping',
get_string('subjectmapping', 'auth_oidc'),
get_string('subjectmapping_desc', 'auth_oidc'), 'username', $options));
$ADMIN->add('oidcfolder', $fieldmappingspage);

// Display locking / mapping of profile fields.
$authplugin = get_auth_plugin('oidc');
auth_oidc_display_auth_lock_options($fieldmappingspage, $authplugin->authtype, $authplugin->userfields,
get_string('cfg_field_mapping_desc', 'auth_oidc'), true, false, $authplugin->get_custom_user_profile_fields());
if (get_config('auth_oidc', 'custom_field_mapping')) {
display_auth_lock_options($fieldmappingspage, $authplugin->authtype, $authplugin->userfields,
get_string('cfg_field_mapping_desc', 'auth_oidc'), true, false, $authplugin->get_custom_user_profile_fields());
} else {
auth_oidc_display_auth_lock_options($fieldmappingspage, $authplugin->authtype, $authplugin->userfields,
get_string('cfg_field_mapping_desc', 'auth_oidc'), true, false, $authplugin->get_custom_user_profile_fields());
}

}

$settings = null;
Loading