Skip to content
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

Support using certificate file paths for IdP authentication #2437

Merged
merged 4 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 49 additions & 5 deletions auth/oidc/classes/form/application.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

namespace auth_oidc\form;

use auth_oidc\utils;
use html_writer;
use moodleform;
use tool_brickfield\local\areas\mod_choice\option;
Expand Down Expand Up @@ -85,20 +86,51 @@ protected function definition() {
$mform->disabledIf('clientsecret', 'clientauthmethod', 'neq', AUTH_OIDC_AUTH_METHOD_SECRET);
$mform->addElement('static', 'clientsecret_help', '', get_string('clientsecret_help', 'auth_oidc'));

// Certificate source.
$mform->addElement('select', 'clientcertsource', auth_oidc_config_name_in_form('clientcertsource'), [
AUTH_OIDC_AUTH_CERT_SOURCE_TEXT => get_string('cert_source_text', 'auth_oidc'),
AUTH_OIDC_AUTH_CERT_SOURCE_FILE => get_string('cert_source_path', 'auth_oidc')
]);
$mform->setDefault('clientcertsource', 0);
$mform->disabledIf('clientcertsource', 'clientauthmethod', 'neq', AUTH_OIDC_AUTH_METHOD_CERTIFICATE);
$mform->addElement('static', 'clientcertsource_help', '', get_string('clientcertsource_help', 'auth_oidc'));

// Certificate private key.
$mform->addElement('textarea', 'clientprivatekey', auth_oidc_config_name_in_form('clientprivatekey'),
['rows' => 10, 'cols' => 80]);
$mform->setType('clientprivatekey', PARAM_TEXT);
$mform->disabledIf('clientprivatekey', 'clientauthmethod', 'neq', AUTH_OIDC_AUTH_METHOD_CERTIFICATE);
$mform->disabledIf('clientprivatekey', 'clientcertsource', 'neq', AUTH_OIDC_AUTH_CERT_SOURCE_TEXT);
$mform->addElement('static', 'clientprivatekey_help', '', get_string('clientprivatekey_help', 'auth_oidc'));

// Certificate certificate.
$mform->addElement('textarea', 'clientcert', auth_oidc_config_name_in_form('clientcert'),
['rows' => 10, 'cols' => 80]);
$mform->setType('clientcert', PARAM_TEXT);
$mform->disabledIf('clientcert', 'clientauthmethod', 'neq', AUTH_OIDC_AUTH_METHOD_CERTIFICATE);
$mform->disabledIf('clientcert', 'clientcertsource', 'neq', AUTH_OIDC_AUTH_CERT_SOURCE_TEXT);
$mform->addElement('static', 'clientcert_help', '', get_string('clientcert_help', 'auth_oidc'));

// Certificate file of private key.
$mform->addElement('text', 'clientprivatekeyfile', auth_oidc_config_name_in_form('clientprivatekeyfile'), ['size' => 60]);
$mform->setType('clientprivatekeyfile', PARAM_FILE);
$mform->disabledIf('clientprivatekeyfile', 'clientauthmethod', 'neq', AUTH_OIDC_AUTH_METHOD_CERTIFICATE);
$mform->disabledIf('clientprivatekeyfile', 'clientcertsource', 'neq', AUTH_OIDC_AUTH_CERT_SOURCE_FILE);
$mform->addElement('static', 'clientprivatekeyfile_help', '', get_string('clientprivatekeyfile_help', 'auth_oidc'));

// Certificate file of certificate or public key.
$mform->addElement('text', 'clientcertfile', auth_oidc_config_name_in_form('clientcertfile'), ['size' => 60]);
$mform->setType('clientcertfile', PARAM_FILE);
$mform->disabledIf('clientcertfile', 'clientauthmethod', 'neq', AUTH_OIDC_AUTH_METHOD_CERTIFICATE);
$mform->disabledIf('clientcertfile', 'clientcertsource', 'neq', AUTH_OIDC_AUTH_CERT_SOURCE_FILE);
$mform->addElement('static', 'clientcertfile_help', '', get_string('clientcertfile_help', 'auth_oidc'));

// Certificate file passphrase.
$mform->addElement('text', 'clientcertpassphrase', auth_oidc_config_name_in_form('clientcertpassphrase'), ['size' => 60]);
$mform->setType('clientcertpassphrase', PARAM_TEXT);
$mform->disabledIf('clientcertpassphrase', 'clientauthmethod', 'neq', AUTH_OIDC_AUTH_METHOD_CERTIFICATE);
$mform->addElement('static', 'clientcertpassphrase_help', '', get_string('clientcertpassphrase_help', 'auth_oidc'));

// Endpoints header.
$mform->addElement('header', 'endpoints', get_string('settings_section_endpoints', 'auth_oidc'));
$mform->setExpanded('endpoints');
Expand Down Expand Up @@ -174,11 +206,23 @@ function validation($data, $files) {
}
break;
case AUTH_OIDC_AUTH_METHOD_CERTIFICATE:
if (empty(trim($data['clientprivatekey']))) {
$errors['clientprivatekey'] = get_string('error_empty_client_private_key', 'auth_oidc');
}
if (empty(trim($data['clientcert']))) {
$errors['clientcert'] = get_string('error_empty_client_cert', 'auth_oidc');
switch ($data['clientcertsource']) {
case AUTH_OIDC_AUTH_CERT_SOURCE_TEXT:
if (empty(trim($data['clientprivatekey']))) {
$errors['clientprivatekey'] = get_string('error_empty_client_private_key', 'auth_oidc');
}
if (empty(trim($data['clientcert']))) {
$errors['clientcert'] = get_string('error_empty_client_cert', 'auth_oidc');
}
break;
case AUTH_OIDC_AUTH_CERT_SOURCE_FILE:
if (empty(trim($data['clientprivatekeyfile']))) {
$errors['clientprivatekeyfile'] = get_string('error_empty_client_private_key_file', 'auth_oidc');
}
if (empty(trim($data['clientcertfile']))) {
$errors['clientcertfile'] = get_string('error_empty_client_cert_file', 'auth_oidc');
}
break;
}
break;
}
Expand Down
2 changes: 1 addition & 1 deletion auth/oidc/classes/jwt.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public function claim($claim) {
* @param string $privatekey
* @return string
*/
public function assert_token(string $privatekey) {
public function assert_token($privatekey) {
$assertion = \Firebase\JWT\JWT::encode($this->claims, $privatekey, 'RS256', null, $this->header);

return $assertion;
Expand Down
26 changes: 22 additions & 4 deletions auth/oidc/classes/oidcclient.php
Original file line number Diff line number Diff line change
Expand Up @@ -376,13 +376,31 @@ public function app_access_token_request() {
* Calculate the return the assertion used in the token request in certificate connection method.
*
* @return string
* @throws moodle_exception
*/
public static function generate_client_assertion() {
$jwt = new jwt();
public static function generate_client_assertion() : string {
$authoidcconfig = get_config('auth_oidc');
$cert = openssl_x509_read($authoidcconfig->clientcert);
$certsource = $authoidcconfig->clientcertsource;

$clientcertpassphrase = null;
if (property_exists($authoidcconfig, 'clientcertpassphrase')) {
$clientcertpassphrase = $authoidcconfig->clientcertpassphrase;
}

if ($certsource == AUTH_OIDC_AUTH_CERT_SOURCE_TEXT) {
$cert = openssl_x509_read($authoidcconfig->clientcert);
$privatekey = openssl_pkey_get_private($authoidcconfig->clientprivatekey, $clientcertpassphrase);
} else if ($certsource == AUTH_OIDC_AUTH_CERT_SOURCE_FILE) {
$cert = openssl_x509_read(utils::get_certpath());
$privatekey = openssl_pkey_get_private(utils::get_keypath(), $clientcertpassphrase);
} else {
throw new moodle_exception('errorinvalidcertificatesource', 'auth_oidc');
}

$sh1hash = openssl_x509_fingerprint($cert);
$x5t = base64_encode(hex2bin($sh1hash));

$jwt = new jwt();
$jwt->set_header(['alg' => 'RS256', 'typ' => 'JWT', 'x5t' => $x5t]);
$jwt->set_claims([
'aud' => $authoidcconfig->tokenendpoint,
Expand All @@ -394,6 +412,6 @@ public static function generate_client_assertion() {
'iat' => time(),
]);

return $jwt->assert_token($authoidcconfig->clientprivatekey);
return $jwt->assert_token($privatekey);
}
}
45 changes: 45 additions & 0 deletions auth/oidc/classes/utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,49 @@ public static function get_frontchannellogouturl() {
$logouturl = new \moodle_url('/auth/oidc/logout.php');
return $logouturl->out(false);
}

/**
* Get and check existence of OIDC client certificate path.
*
* @return string|bool cert path if exists otherwise false
*/
public static function get_certpath() {
$clientcertfile = get_config('auth_oidc', 'clientcertfile');
$certlocation = self::get_openssl_internal_path();
$certfile = "$certlocation/$clientcertfile";

if (is_file($certfile) && is_readable($certfile)) {
return "file://$certfile";
}

return false;
}

/**
* Get and check existence of OIDC client key path.
*
* @return string|bool key path if exists otherwise false
*/
public static function get_keypath() {
$clientprivatekeyfile = get_config('auth_oidc', 'clientprivatekeyfile');
$keylocation = self::get_openssl_internal_path();
$keyfile = "$keylocation/$clientprivatekeyfile";

if (is_file($keyfile) && is_readable($keyfile)) {
return "file://$keyfile";
}

return false;
}

/**
* Get openssl cert base path, which is dataroot/microsoft_certs.
*
* @return string base path to put cert files
*/
public static function get_openssl_internal_path() {
global $CFG;

return $CFG->dataroot . '/microsoft_certs';
}
}
9 changes: 9 additions & 0 deletions auth/oidc/db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -382,5 +382,14 @@ function xmldb_auth_oidc_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2022112801, 'auth', 'oidc');
}

if ($oldversion < 2023100902) {
// Set initial value for "clientcertsource" config.
if (empty(get_config('auth_oidc', 'clientcertsource'))) {
set_config('clientcertsource', AUTH_OIDC_AUTH_CERT_SOURCE_TEXT, 'auth_oidc');
}

upgrade_plugin_savepoint(true, 2023100902, 'auth', 'oidc');
}

return true;
}
28 changes: 22 additions & 6 deletions auth/oidc/lang/en/auth_oidc.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
</ul>
The differences between <b>Azure AD (v1.0)</b> and <b>Microsoft identity platform (v2.0)</b> options can be found at <a href="https://docs.microsoft.com/en-us/azure/active-directory/azuread-dev/azure-ad-endpoint-comparison">https://docs.microsoft.com/en-us/azure/active-directory/azuread-dev/azure-ad-endpoint-comparison</a>.<br/>
Notably, the configured application can use <b>certificate</b> besides <b>secret</b> for authentication when using <b>Microsoft identity platform (v2.0)</b> IdP.<br/>
Authorization and token endpoints need to be configured according to the configured IdP type.';
Authorization and token endpoints need to be configured according to the IdP type.';
$string['idp_type_azuread'] = 'Azure AD (v1.0)';
$string['idp_type_microsoft'] = 'Microsoft identity platform (v2.0)';
$string['idp_type_other'] = 'Other';
Expand All @@ -66,21 +66,34 @@
$string['cfg_autoappend_key'] = 'Auto-Append';
$string['cfg_autoappend_desc'] = 'Automatically append this string when logging in users using the "Resource Owner Password Credentials" authentication method. This is useful when your IdP requires a common domain, but don\'t want to require users to type it in when logging in. For example, if the full OpenID Connect user is "james@example.com" and you enter "@example.com" here, the user will only have to enter "james" as their username. <br /><b>Note:</b> In the case where conflicting usernames exist - i.e. a Moodle user exists wth the same name, the priority of the authentication plugin is used to determine which user wins out.';
$string['clientid'] = 'Application ID';
$string['clientid_help'] = 'Your registered Application / Client ID on the IdP.';
$string['clientid_help'] = 'The registered Application / Client ID on the IdP.';
$string['clientauthmethod'] = 'Client authentication method';
$string['clientauthmethod_help'] = '<ul>
<li>IdP in all types can use "<b>Secret</b>" authentication method.</li>
<li>IdP in <b>Microsoft identity platform (v2.0)</b> type can additionally use <b>Certificate</b> authentication method.</li>
</ul>
Note <b>Certificate</b> authentication method is not supported in <b>Resource Owner Password Credentials Grant</b> login flow.';
</ul>';
$string['auth_method_secret'] = 'Secret';
$string['auth_method_certificate'] = 'Certificate';
$string['clientsecret'] = 'Client Secret';
$string['clientsecret_help'] = 'When using <b>secret</b> authentication method, this is the client secret on the IdP. On some providers, it is also referred to as a key.';
$string['clientprivatekey'] = 'Client certificate private key';
$string['clientprivatekey_help'] = 'When using <b>certificate</b> authentication method, this is the private key of the certificate used to authenticate with IdP.';
$string['clientprivatekey_help'] = 'When using <b>certificate</b> authentication method and <b>Plain text</b> certificate source, this is the private key of the certificate used to authenticate with IdP.';
$string['clientcert'] = 'Client certificate public key';
$string['clientcert_help'] = 'When using <b>certificate</b> authentication method, this is the public key, or certificate, used in to authenticate with IdP.';
$string['clientcert_help'] = 'When using <b>certificate</b> authentication method and <b>Plain text</b> certificate source, this is the public key, or certificate, used in to authenticate with IdP.';
$string['clientcertsource'] = 'Certificate source';
$string['clientcertsource_help'] = 'When using <b>certificate</b> authentication method, this is used to define where to retrieve the certificate from.
<ul>
<li><b>Plain text</b> source requires the certificate/private key file contents to be configured in the subsequent text area settings.</li>
<li><b>File name</b> source requires the certificate/private key files exist in a folder <b>microsoft_certs</b> in the Moodle data folder.</li>
</ul>';
$string['cert_source_text'] = 'Plain text';
$string['cert_source_path'] = 'File name';
$string['clientprivatekeyfile'] = 'File name of client certificate private key';
$string['clientprivatekeyfile_help'] = 'When using <b>certificate</b> authentication method and <b>File name</b> certificate source, this is the file name of private key used to authenticate with IdP. The file needs to present in a folder <b>microsoft_certs</b> in the Moodle data folder.';
$string['clientcertfile'] = 'File name of client certificate public key';
$string['clientcertfile_help'] = 'When using <b>certificate</b> authentication method and <b>File name</b> certificate source, this is the file name of public key, or certificate, used to authenticate with IdP. The file needs to present in a folder <b>microsoft_certs</b> in the Moodle data folder.';
$string['clientcertpassphrase'] = 'Client certificate passphrase';
$string['clientcertpassphrase_help'] = 'If the client certificate private key is encrypted, this is the passphrase to decrypt it.';
$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 @@ -199,11 +212,14 @@
$string['erroroidccall'] = 'Error in OpenID Connect. Please check logs for more information.';
$string['erroroidccall_message'] = 'Error in OpenID Connect: {$a}';
$string['errorinvalidredirect_message'] = 'The URL you are trying to redirect to does not exist.';
$string['errorinvalidcertificatesource'] = 'Invalid certificate source';
$string['error_empty_tenantnameorguid'] = 'Tenant name or GUID cannot be empty when using Azure AD (v1.0) or Microsoft identity platform (v2.0) IdPs.';
$string['error_invalid_client_authentication_method'] = "Invalid client authentication method";
$string['error_empty_client_secret'] = 'Client secret cannot be empty when using "secret" authentication method';
$string['error_empty_client_private_key'] = 'Client certificate private key cannot be empty when using "certificate" authentication method';
$string['error_empty_client_cert'] = 'Client certificate public key cannot be empty when using "certificate" authentication method';
$string['error_empty_client_private_key_file'] = 'Client certificate private key file cannot be empty when using "certificate" authentication method';
$string['error_empty_client_cert_file'] = 'Client certificate public key file cannot be empty when using "certificate" authentication method';
$string['error_empty_tenantname_or_guid'] = 'Tenant name or GUID cannot be empty when using "certificate" authentication method';
$string['error_endpoint_mismatch_auth_endpoint'] = 'The configured authorization endpoint does not match configured IdP type.<br/>
<ul>
Expand Down
23 changes: 21 additions & 2 deletions auth/oidc/lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
* @copyright (C) 2014 onwards Microsoft, Inc. (http://microsoft.com/)
*/

use auth_oidc\utils;

defined('MOODLE_INTERNAL') || die();

// IdP types.
Expand All @@ -40,6 +42,10 @@
CONST AUTH_OIDC_AUTH_METHOD_SECRET = 1;
CONST AUTH_OIDC_AUTH_METHOD_CERTIFICATE = 2;

// OIDC application auth certificate source.
CONST AUTH_OIDC_AUTH_CERT_SOURCE_TEXT = 1;
CONST AUTH_OIDC_AUTH_CERT_SOURCE_FILE = 2;

/**
* Initialize custom icon.
*
Expand Down Expand Up @@ -570,8 +576,21 @@ function auth_oidc_is_setup_complete() {
}
break;
case AUTH_OIDC_AUTH_METHOD_CERTIFICATE:
if (empty($pluginconfig->clientcert) || empty($pluginconfig->clientprivatekey)) {
return false;
if (!isset($pluginconfig->clientcertsource)) {
set_config('clientcertsource', AUTH_OIDC_AUTH_CERT_SOURCE_TEXT, 'auth_oidc');
$pluginconfig->clientcertsource = AUTH_OIDC_AUTH_CERT_SOURCE_TEXT;
}
switch ($pluginconfig->clientcertsource) {
case AUTH_OIDC_AUTH_CERT_SOURCE_FILE:
if (!utils::get_certpath() || !utils::get_keypath()) {
return false;
}
break;
case AUTH_OIDC_AUTH_CERT_SOURCE_TEXT:
if (empty($pluginconfig->clientcert) || empty($pluginconfig->clientprivatekey)) {
return false;
}
break;
}
break;
}
Expand Down
15 changes: 13 additions & 2 deletions auth/oidc/manageapplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@

$formdata = [];
foreach (['idptype', 'clientid', 'clientauthmethod', 'clientsecret', 'clientprivatekey', 'clientcert',
'clientcertsource', 'clientprivatekeyfile', 'clientcertfile', 'clientcertpassphrase',
'authendpoint', 'tokenendpoint', 'oidcresource', 'oidcscope'] as $field) {
if (isset($oidcconfig->$field)) {
$formdata[$field] = $oidcconfig->$field;
Expand All @@ -82,8 +83,18 @@
$configstosave[] = 'clientsecret';
break;
case AUTH_OIDC_AUTH_METHOD_CERTIFICATE:
$configstosave[] = 'clientprivatekey';
$configstosave[] = 'clientcert';
$configstosave[] = 'clientcertsource';
$configstosave[] = 'clientcertpassphrase';
switch ($fromform->clientcertsource) {
case AUTH_OIDC_AUTH_CERT_SOURCE_TEXT:
$configstosave[] = 'clientprivatekey';
$configstosave[] = 'clientcert';
break;
case AUTH_OIDC_AUTH_CERT_SOURCE_FILE:
$configstosave[] = 'clientprivatekeyfile';
$configstosave[] = 'clientcertfile';
break;
}
break;
}

Expand Down
2 changes: 1 addition & 1 deletion auth/oidc/version.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

defined('MOODLE_INTERNAL') || die();

$plugin->version = 2023100900;
$plugin->version = 2023100902;
$plugin->requires = 2023100900;
$plugin->release = '4.3.0';
$plugin->component = 'auth_oidc';
Expand Down
Loading