From a8479c76fa43643f1fd24bfc3512c2eff2d23d2a Mon Sep 17 00:00:00 2001 From: Dan Marsden Date: Wed, 26 Jun 2024 13:32:26 +1200 Subject: [PATCH] WIP - allow Moodle to function as an IDP. --- idp/metadata.php | 55 ++++++++++++++++ idp/slo.php | 31 +++++++++ idp/sso.php | 162 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100644 idp/metadata.php create mode 100644 idp/slo.php create mode 100644 idp/sso.php diff --git a/idp/metadata.php b/idp/metadata.php new file mode 100644 index 000000000..cf91b7644 --- /dev/null +++ b/idp/metadata.php @@ -0,0 +1,55 @@ +. + +/** + * Identity provider metadata + * + * @package auth_saml2 + * @copyright Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// @codingStandardsIgnoreStart +require_once(__DIR__ . '/../../../config.php'); +// @codingStandardsIgnoreEnd +require_once('../setup.php'); +require_once('../locallib.php'); + +$saml2auth = new \auth_saml2\auth(); + +$cert = file_get_contents($saml2auth->certcrt); +$cert = preg_replace('~(-----(BEGIN|END) CERTIFICATE-----)|\n~', '', $cert); +$baseurl = $CFG->wwwroot . '/auth/saml2/idp'; + +$xml = << + + + + {$cert} + + + +urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + + + +EOF; + +header('Content-Type: text/xml'); +echo($xml); \ No newline at end of file diff --git a/idp/slo.php b/idp/slo.php new file mode 100644 index 000000000..7de43ac9a --- /dev/null +++ b/idp/slo.php @@ -0,0 +1,31 @@ +. + +/** + * This file handles the login process when Moodle is acting as an IDP. + * + * @package auth_saml2 + * @copyright Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->dirroot.'/auth/saml2/setup.php'); + +require_logout(); + +redirect($CFG->wwwroot); \ No newline at end of file diff --git a/idp/sso.php b/idp/sso.php new file mode 100644 index 000000000..89f1e7a67 --- /dev/null +++ b/idp/sso.php @@ -0,0 +1,162 @@ +. + +/** + * This file handles the login process when Moodle is acting as an IDP. + * + * @package auth_saml2 + * @copyright Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->dirroot.'/auth/saml2/setup.php'); + +require_login(null, false); +$relaystate = optional_param('RelayState', '', PARAM_RAW); + +if (isguestuser()) { + // Guest user not allowed here. + // TODO: add exception. + die; +} + +// Get the request data. +$requestparam = required_param('SAMLRequest', PARAM_RAW); +$request = gzinflate(base64_decode($requestparam)); +$domxml = new DOMDocument(); +$domxml->loadXML($request); +$xpath = new DOMXPath($domxml); + +// Attributes provided by the Behat step. +$attributes = [ + 'uid' => $USER->username, + 'email' => $USER->email, + 'firstname' => $USER->firstname, + 'lastname' => $USER->lastname +]; + +// Get data from input request. +$id = $xpath->evaluate('normalize-space(/*/@ID)'); +$destination = htmlspecialchars($xpath->evaluate('normalize-space(/*/@AssertionConsumerServiceURL)')); +$sp = $xpath->evaluate('normalize-space(/*/*[local-name() = "Issuer"])'); + +// Get time in UTC. +$datetime = new DateTime(); +$datetime->setTimezone(new DatetimeZone('UTC')); +$instant = $datetime->format('Y-m-d') . 'T' . $datetime->format('H:i:s') . 'Z'; +$datetime->sub(new DateInterval('P1D')); +$before = $datetime->format('Y-m-d') . 'T' . $datetime->format('H:i:s') . 'Z'; +$datetime->add(new DateInterval('P1M')); +$after = $datetime->format('Y-m-d') . 'T' . $datetime->format('H:i:s') . 'Z'; + +// Get our own IdP URL. +$baseurl = $CFG->wwwroot . '/auth/saml2/idp'; +$issuer = $baseurl . '/metadata.php'; + +// Make up a session. +$session = 'session' . mt_rand(100000, 999999); + +// Construct attributes in XML. +$attributexml = ''; +foreach ((array)$attributes as $name => $value) { + $attributexml .= '' . + '' . htmlspecialchars($value) . '' . + '' . "\n"; +} +$email = htmlspecialchars($USER->email); +// Construct XML without signature. +$responsexml = << + {$issuer} + + + + + {$issuer} + + + {$email} + + + + + + + + {$sp} + + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + + {$attributexml} + + + +EOF; +// Load it into a DOM. +$outdoc = new \DOMDocument(); +$outdoc->loadXML($responsexml); + +// Find the relevant elements. +$xpath = new DOMXPath($outdoc); +$assertion = $xpath->query('//*[local-name()="Assertion"]')[0]; +$subject = $xpath->query('child::*[local-name()="Subject"]', $assertion)[0]; + +// Sign it using the fixture key/cert. +$signer = new \SimpleSAML\XML\Signer([]); + +$signer->loadPrivateKey($saml2auth->certpem, $saml2auth->config->privatekeypass, true); +$signer->loadCertificate($saml2auth->certcrt, true); +$signer->sign($assertion, $assertion, $subject); + +// Don't send as a referer or the login form might end up coming back here. +header('Referrer-Policy: no-referrer'); + +// Output an HTML form that automatically submits this. +echo ''; +echo html_writer::start_tag('html'); +echo html_writer::tag('head', html_writer::tag('title', 'SSO redirect back')); +echo html_writer::start_tag('body'); +echo html_writer::start_tag('form', ['id' => 'frog', 'method' => 'post', 'action' => htmlspecialchars_decode($destination)]); +echo html_writer::empty_tag( + 'input', + ['type' => 'hidden', 'name' => 'SAMLResponse', 'value' => base64_encode($outdoc->saveXML())] +); +echo html_writer::empty_tag( + 'input', + ['type' => 'hidden', 'name' => 'RelayState', 'value' => $relaystate] +); +echo html_writer::end_tag('form'); +echo html_writer::tag('script', 'document.getElementById("frog").submit();'); +echo html_writer::end_tag('form'); +echo html_writer::end_tag('body'); +exit;