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

Allow Moodle to function as an IDP. #820

Open
wants to merge 3 commits into
base: MOODLE_39_STABLE
Choose a base branch
from
Open
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
64 changes: 64 additions & 0 deletions idp/metadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* 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();

if ($saml2auth->config->moodleidpenabled) {
$download = optional_param('download', '', PARAM_RAW);
if ($download) {
header('Content-Disposition: attachment; filename=' . $saml2auth->spname . '.xml');
}

$cert = file_get_contents($saml2auth->certcrt);
$cert = preg_replace('~(-----(BEGIN|END) CERTIFICATE-----)|\n~', '', $cert);
$baseurl = $CFG->wwwroot . '/auth/saml2/idp';

$xml = <<<EOF
<md:EntityDescriptor entityID="{$baseurl}/metadata.php" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" WantAuthnRequestsSigned="false">
<md:KeyDescriptor>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data><X509Certificate>{$cert}</X509Certificate></X509Data>
</KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="{$baseurl}/slo.php" />
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="{$baseurl}/sso.php" />
</md:IDPSSODescriptor>
</md:EntityDescriptor>
EOF;

header('Content-Type: text/xml');
echo($xml);
} else {
throw new saml2_exception('idp_enabled_error', get_string('moodleidpenabled_error', 'auth_saml2'));
}
31 changes: 31 additions & 0 deletions idp/slo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* 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);
177 changes: 177 additions & 0 deletions idp/sso.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* 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.
throw new saml2_exception('guest_error', get_string('moodleidpguest_error', 'auth_saml2'));
}

// 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);

// Load profile fields into attributes.
$authplugin = get_auth_plugin('saml2');
$userfields = array_merge($authplugin->userfields, $authplugin->get_custom_user_profile_fields());
profile_load_data($USER);
// Add username as `uid` as many services look for `uid` by default.
$attributes = ['uid' => $USER->username];
foreach ($userfields as $field) {
$attributes[$field] = $USER->$field ? $USER->$field : '';
}

// 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"])');

// Confirm we know about this SP.
$knownsps = [];
foreach (explode(PHP_EOL, $saml2auth->config->moodleidpsplist) as $ksp) {
$ksp = trim($ksp);
if (empty($ksp)) {
continue;
}
$knownsps[] = $ksp;
}

if (!in_array($sp, $knownsps)) {
throw new saml2_exception('unknown_sp_error', get_string('moodleidpsplist_error', 'auth_saml2', $sp));
}

// 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 .= '<saml:Attribute Name="' . $name .
'" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">' .
'<saml:AttributeValue>' . htmlspecialchars($value) . '</saml:AttributeValue>' .
'</saml:Attribute>' . "\n";
}
$email = htmlspecialchars($USER->email);
// Construct XML without signature.
$responsexml = <<<EOF
<samlp:Response
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="{$id}_2" InResponseTo="{$id}" Version="2.0" IssueInstant="{$instant}" Destination="{$destination}">
<saml:Issuer>{$issuer}</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{$id}_3" Version="2.0"
IssueInstant="{$instant}">
<saml:Issuer>{$issuer}</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
{$email}
</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData InResponseTo="{$id}"
Recipient="{$destination}"
NotOnOrAfter="{$after}"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions
NotBefore="{$before}"
NotOnOrAfter="{$after}">
<saml:AudienceRestriction>
<saml:Audience>{$sp}</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="{$instant}" SessionIndex="{$session}">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
{$attributexml}
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
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(['id' => 'ID']);

$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 '<!doctype html>';
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;
11 changes: 11 additions & 0 deletions lang/en/auth_saml2.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@
$string['metadatafetchfailed'] = 'Metadata fetch failed: {$a}';
$string['metadatafetchfailedstatus'] = 'Metadata fetch failed: Status code {$a}';
$string['metadatafetchfailedunknown'] = 'Metadata fetch failed: Unknown cURL error';
$string['moodleidpdescription'] = 'Settings for Moodle as an Identity Provider for other services.';
$string['moodleidpenabled'] = 'Enable IDP';
$string['moodleidpenabled_error'] = 'Moodle IDP is not enabled. Check Settings.';
$string['moodleidpenabled_help'] = 'Allow Moodle to act as an IDP for external services.';
$string['moodleidpguest_error'] = 'Guest users cannot log in via SAML.';
$string['moodleidpheading'] = 'Moodle IDP Settings';
$string['moodleidpmetadata'] = 'IDP Metadata';
$string['moodleidpmetadata_help'] = '<a href=\'{$a}\'>View Identity Provider Metadata</a> | <a href=\'{$a}?download=1\'>Download IDP Metadata</a>';
$string['moodleidpsplist'] = 'Valid Issuers';
$string['moodleidpsplist_error'] = 'Unknown service attempting to authenticate: {$a}. Check config.';
$string['moodleidpsplist_help'] = 'List of services allowed to use this moodle as an IDP identified by the <code>saml:Issuer</code> tag in the SAML request. One per line. {$a->example}';
$string['multiidp:label:displayname'] = 'Display name';
$string['multiidp:label:alias'] = 'Alias';
$string['multiidp:label:active'] = 'Active';
Expand Down
28 changes: 28 additions & 0 deletions settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -444,4 +444,32 @@
',',
PARAM_TEXT,
5));

// Moodle as an IDP feature setting section.
$settings->add(new admin_setting_heading('auth_saml2/moodleidpheading', get_string('moodleidpheading', 'auth_saml2'),
new lang_string('moodleidpdescription', 'auth_saml2')));

// Enable Moodle IDP.
$settings->add(new admin_setting_configselect(
'auth_saml2/moodleidpenabled',
get_string('moodleidpenabled', 'auth_saml2'),
get_string('moodleidpenabled_help', 'auth_saml2'),
0, $yesno));

// IDP Metadata.
$settings->add(new setting_textonly(
'auth_saml2/moodleidpmetadata',
get_string('moodleidpmetadata', 'auth_saml2'),
get_string('moodleidpmetadata_help', 'auth_saml2', $CFG->wwwroot . '/auth/saml2/idp/metadata.php')
));

// List valid SPs.
$settings->add(new admin_setting_configtextarea(
'auth_saml2/moodleidpsplist',
get_string('moodleidpsplist', 'auth_saml2'),
get_string('moodleidpsplist_help', 'auth_saml2', ['example' => "<pre>
https://www.someothermoodle.com/auth/saml2/sp/metadata.php
</pre>"]),
'',
PARAM_TEXT));
}
Loading