Skip to content

Commit ed66116

Browse files
Allow Moodle to function as an IDP. (#820)
* Allow Moodle to function as an IDP. * Adds IDP settings * en/disable moodle as an IDP. * Whitelist services by their <saml:Issuer> * Populate IDP saml response with user profile fields. * fix line ending --------- Co-authored-by: Mark Webster <mark.webster@catalyst-eu.net>
1 parent d64ee5d commit ed66116

File tree

5 files changed

+311
-0
lines changed

5 files changed

+311
-0
lines changed

idp/metadata.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
/**
18+
* Identity provider metadata
19+
*
20+
* @package auth_saml2
21+
* @copyright Catalyst IT
22+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23+
*/
24+
25+
// @codingStandardsIgnoreStart
26+
require_once(__DIR__ . '/../../../config.php');
27+
// @codingStandardsIgnoreEnd
28+
require_once('../setup.php');
29+
require_once('../locallib.php');
30+
31+
$saml2auth = new \auth_saml2\auth();
32+
33+
if ($saml2auth->config->moodleidpenabled) {
34+
$download = optional_param('download', '', PARAM_RAW);
35+
if ($download) {
36+
header('Content-Disposition: attachment; filename=' . $saml2auth->spname . '.xml');
37+
}
38+
39+
$cert = file_get_contents($saml2auth->certcrt);
40+
$cert = preg_replace('~(-----(BEGIN|END) CERTIFICATE-----)|\n~', '', $cert);
41+
$baseurl = $CFG->wwwroot . '/auth/saml2/idp';
42+
43+
$xml = <<<EOF
44+
<md:EntityDescriptor entityID="{$baseurl}/metadata.php" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
45+
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" WantAuthnRequestsSigned="false">
46+
<md:KeyDescriptor>
47+
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
48+
<X509Data><X509Certificate>{$cert}</X509Certificate></X509Data>
49+
</KeyInfo>
50+
</md:KeyDescriptor>
51+
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
52+
Location="{$baseurl}/slo.php" />
53+
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
54+
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
55+
Location="{$baseurl}/sso.php" />
56+
</md:IDPSSODescriptor>
57+
</md:EntityDescriptor>
58+
EOF;
59+
60+
header('Content-Type: text/xml');
61+
echo($xml);
62+
} else {
63+
throw new saml2_exception('idp_enabled_error', get_string('moodleidpenabled_error', 'auth_saml2'));
64+
}

idp/slo.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
/**
18+
* This file handles the login process when Moodle is acting as an IDP.
19+
*
20+
* @package auth_saml2
21+
* @copyright Catalyst IT
22+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23+
*/
24+
25+
26+
require_once(__DIR__ . '/../../../config.php');
27+
require_once($CFG->dirroot.'/auth/saml2/setup.php');
28+
29+
require_logout();
30+
31+
redirect($CFG->wwwroot);

idp/sso.php

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
/**
18+
* This file handles the login process when Moodle is acting as an IDP.
19+
*
20+
* @package auth_saml2
21+
* @copyright Catalyst IT
22+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23+
*/
24+
25+
26+
require_once(__DIR__ . '/../../../config.php');
27+
require_once($CFG->dirroot.'/auth/saml2/setup.php');
28+
29+
require_login(null, false);
30+
$relaystate = optional_param('RelayState', '', PARAM_RAW);
31+
32+
if (isguestuser()) {
33+
// Guest user not allowed here.
34+
throw new saml2_exception('guest_error', get_string('moodleidpguest_error', 'auth_saml2'));
35+
}
36+
37+
// Get the request data.
38+
$requestparam = required_param('SAMLRequest', PARAM_RAW);
39+
$request = gzinflate(base64_decode($requestparam));
40+
$domxml = new DOMDocument();
41+
$domxml->loadXML($request);
42+
$xpath = new DOMXPath($domxml);
43+
44+
// Load profile fields into attributes.
45+
$authplugin = get_auth_plugin('saml2');
46+
$userfields = array_merge($authplugin->userfields, $authplugin->get_custom_user_profile_fields());
47+
profile_load_data($USER);
48+
// Add username as `uid` as many services look for `uid` by default.
49+
$attributes = ['uid' => $USER->username];
50+
foreach ($userfields as $field) {
51+
$attributes[$field] = $USER->$field ? $USER->$field : '';
52+
}
53+
54+
// Get data from input request.
55+
$id = $xpath->evaluate('normalize-space(/*/@ID)');
56+
$destination = htmlspecialchars($xpath->evaluate('normalize-space(/*/@AssertionConsumerServiceURL)'));
57+
$sp = $xpath->evaluate('normalize-space(/*/*[local-name() = "Issuer"])');
58+
59+
// Confirm we know about this SP.
60+
$knownsps = [];
61+
foreach (explode(PHP_EOL, $saml2auth->config->moodleidpsplist) as $ksp) {
62+
$ksp = trim($ksp);
63+
if (empty($ksp)) {
64+
continue;
65+
}
66+
$knownsps[] = $ksp;
67+
}
68+
69+
if (!in_array($sp, $knownsps)) {
70+
throw new saml2_exception('unknown_sp_error', get_string('moodleidpsplist_error', 'auth_saml2', $sp));
71+
}
72+
73+
// Get time in UTC.
74+
$datetime = new DateTime();
75+
$datetime->setTimezone(new DatetimeZone('UTC'));
76+
$instant = $datetime->format('Y-m-d') . 'T' . $datetime->format('H:i:s') . 'Z';
77+
$datetime->sub(new DateInterval('P1D'));
78+
$before = $datetime->format('Y-m-d') . 'T' . $datetime->format('H:i:s') . 'Z';
79+
$datetime->add(new DateInterval('P1M'));
80+
$after = $datetime->format('Y-m-d') . 'T' . $datetime->format('H:i:s') . 'Z';
81+
82+
// Get our own IdP URL.
83+
$baseurl = $CFG->wwwroot . '/auth/saml2/idp';
84+
$issuer = $baseurl . '/metadata.php';
85+
86+
// Make up a session.
87+
$session = 'session' . mt_rand(100000, 999999);
88+
89+
// Construct attributes in XML.
90+
$attributexml = '';
91+
foreach ((array)$attributes as $name => $value) {
92+
$attributexml .= '<saml:Attribute Name="' . $name .
93+
'" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">' .
94+
'<saml:AttributeValue>' . htmlspecialchars($value) . '</saml:AttributeValue>' .
95+
'</saml:Attribute>' . "\n";
96+
}
97+
$email = htmlspecialchars($USER->email);
98+
// Construct XML without signature.
99+
$responsexml = <<<EOF
100+
<samlp:Response
101+
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
102+
ID="{$id}_2" InResponseTo="{$id}" Version="2.0" IssueInstant="{$instant}" Destination="{$destination}">
103+
<saml:Issuer>{$issuer}</saml:Issuer>
104+
<samlp:Status>
105+
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
106+
</samlp:Status>
107+
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{$id}_3" Version="2.0"
108+
IssueInstant="{$instant}">
109+
<saml:Issuer>{$issuer}</saml:Issuer>
110+
<saml:Subject>
111+
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
112+
{$email}
113+
</saml:NameID>
114+
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
115+
<saml:SubjectConfirmationData InResponseTo="{$id}"
116+
Recipient="{$destination}"
117+
NotOnOrAfter="{$after}"/>
118+
</saml:SubjectConfirmation>
119+
</saml:Subject>
120+
<saml:Conditions
121+
NotBefore="{$before}"
122+
NotOnOrAfter="{$after}">
123+
<saml:AudienceRestriction>
124+
<saml:Audience>{$sp}</saml:Audience>
125+
</saml:AudienceRestriction>
126+
</saml:Conditions>
127+
<saml:AuthnStatement AuthnInstant="{$instant}" SessionIndex="{$session}">
128+
<saml:AuthnContext>
129+
<saml:AuthnContextClassRef>
130+
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
131+
</saml:AuthnContextClassRef>
132+
</saml:AuthnContext>
133+
</saml:AuthnStatement>
134+
<saml:AttributeStatement>
135+
{$attributexml}
136+
</saml:AttributeStatement>
137+
</saml:Assertion>
138+
</samlp:Response>
139+
EOF;
140+
// Load it into a DOM.
141+
$outdoc = new \DOMDocument();
142+
$outdoc->loadXML($responsexml);
143+
144+
// Find the relevant elements.
145+
$xpath = new DOMXPath($outdoc);
146+
$assertion = $xpath->query('//*[local-name()="Assertion"]')[0];
147+
$subject = $xpath->query('child::*[local-name()="Subject"]', $assertion)[0];
148+
149+
// Sign it using the fixture key/cert.
150+
$signer = new \SimpleSAML\XML\Signer(['id' => 'ID']);
151+
152+
$signer->loadPrivateKey($saml2auth->certpem, $saml2auth->config->privatekeypass, true);
153+
$signer->loadCertificate($saml2auth->certcrt, true);
154+
$signer->sign($assertion, $assertion, $subject);
155+
156+
// Don't send as a referer or the login form might end up coming back here.
157+
header('Referrer-Policy: no-referrer');
158+
159+
// Output an HTML form that automatically submits this.
160+
echo '<!doctype html>';
161+
echo html_writer::start_tag('html');
162+
echo html_writer::tag('head', html_writer::tag('title', 'SSO redirect back'));
163+
echo html_writer::start_tag('body');
164+
echo html_writer::start_tag('form', ['id' => 'frog', 'method' => 'post', 'action' => htmlspecialchars_decode($destination)]);
165+
echo html_writer::empty_tag(
166+
'input',
167+
['type' => 'hidden', 'name' => 'SAMLResponse', 'value' => base64_encode($outdoc->saveXML())]
168+
);
169+
echo html_writer::empty_tag(
170+
'input',
171+
['type' => 'hidden', 'name' => 'RelayState', 'value' => $relaystate]
172+
);
173+
echo html_writer::end_tag('form');
174+
echo html_writer::tag('script', 'document.getElementById("frog").submit();');
175+
echo html_writer::end_tag('form');
176+
echo html_writer::end_tag('body');
177+
exit;

lang/en/auth_saml2.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,17 @@
142142
$string['metadatafetchfailed'] = 'Metadata fetch failed: {$a}';
143143
$string['metadatafetchfailedstatus'] = 'Metadata fetch failed: Status code {$a}';
144144
$string['metadatafetchfailedunknown'] = 'Metadata fetch failed: Unknown cURL error';
145+
$string['moodleidpdescription'] = 'Settings for Moodle as an Identity Provider for other services.';
146+
$string['moodleidpenabled'] = 'Enable IDP';
147+
$string['moodleidpenabled_error'] = 'Moodle IDP is not enabled. Check Settings.';
148+
$string['moodleidpenabled_help'] = 'Allow Moodle to act as an IDP for external services.';
149+
$string['moodleidpguest_error'] = 'Guest users cannot log in via SAML.';
150+
$string['moodleidpheading'] = 'Moodle IDP Settings';
151+
$string['moodleidpmetadata'] = 'IDP Metadata';
152+
$string['moodleidpmetadata_help'] = '<a href=\'{$a}\'>View Identity Provider Metadata</a> | <a href=\'{$a}?download=1\'>Download IDP Metadata</a>';
153+
$string['moodleidpsplist'] = 'Valid Issuers';
154+
$string['moodleidpsplist_error'] = 'Unknown service attempting to authenticate: {$a}. Check config.';
155+
$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}';
145156
$string['multiidp:label:active'] = 'Active';
146157
$string['multiidp:label:admin'] = 'For admin users only';
147158
$string['multiidp:label:admin_help'] = 'Any users that log in using this IdP will automatically be made an site administrator';

settings.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,4 +444,32 @@
444444
',',
445445
PARAM_TEXT,
446446
5));
447+
448+
// Moodle as an IDP feature setting section.
449+
$settings->add(new admin_setting_heading('auth_saml2/moodleidpheading', get_string('moodleidpheading', 'auth_saml2'),
450+
new lang_string('moodleidpdescription', 'auth_saml2')));
451+
452+
// Enable Moodle IDP.
453+
$settings->add(new admin_setting_configselect(
454+
'auth_saml2/moodleidpenabled',
455+
get_string('moodleidpenabled', 'auth_saml2'),
456+
get_string('moodleidpenabled_help', 'auth_saml2'),
457+
0, $yesno));
458+
459+
// IDP Metadata.
460+
$settings->add(new setting_textonly(
461+
'auth_saml2/moodleidpmetadata',
462+
get_string('moodleidpmetadata', 'auth_saml2'),
463+
get_string('moodleidpmetadata_help', 'auth_saml2', $CFG->wwwroot . '/auth/saml2/idp/metadata.php')
464+
));
465+
466+
// List valid SPs.
467+
$settings->add(new admin_setting_configtextarea(
468+
'auth_saml2/moodleidpsplist',
469+
get_string('moodleidpsplist', 'auth_saml2'),
470+
get_string('moodleidpsplist_help', 'auth_saml2', ['example' => "<pre>
471+
https://www.someothermoodle.com/auth/saml2/sp/metadata.php
472+
</pre>"]),
473+
'',
474+
PARAM_TEXT));
447475
}

0 commit comments

Comments
 (0)