Skip to content

Commit da2d734

Browse files
committed
WIP - allow Moodle to function as an IDP.
1 parent 974796e commit da2d734

File tree

3 files changed

+248
-0
lines changed

3 files changed

+248
-0
lines changed

idp/metadata.php

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
$cert = file_get_contents($saml2auth->certcrt);
34+
$cert = preg_replace('~(-----(BEGIN|END) CERTIFICATE-----)|\n~', '', $cert);
35+
$baseurl = $CFG->wwwroot . '/auth/saml2/idp';
36+
37+
$xml = <<<EOF
38+
<md:EntityDescriptor entityID="{$baseurl}/metadata.php" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
39+
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" WantAuthnRequestsSigned="false">
40+
<md:KeyDescriptor>
41+
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
42+
<X509Data><X509Certificate>{$cert}</X509Certificate></X509Data>
43+
</KeyInfo>
44+
</md:KeyDescriptor>
45+
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
46+
Location="{$baseurl}/slo.php" />
47+
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
48+
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
49+
Location="{$baseurl}/sso.php" />
50+
</md:IDPSSODescriptor>
51+
</md:EntityDescriptor>
52+
EOF;
53+
54+
header('Content-Type: text/xml');
55+
echo($xml);

idp/slo.php

+31
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

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
// TODO: add exception.
35+
die;
36+
}
37+
38+
// Get the request data.
39+
$requestparam = required_param('SAMLRequest', PARAM_RAW);
40+
$request = gzinflate(base64_decode($requestparam));
41+
$domxml = new DOMDocument();
42+
$domxml->loadXML($request);
43+
$xpath = new DOMXPath($domxml);
44+
45+
// Attributes provided by the Behat step.
46+
$attributes = [
47+
'uid' => $USER->username,
48+
'email' => $USER->email,
49+
'firstname' => $USER->firstname,
50+
'lastname' => $USER->lastname
51+
];
52+
53+
// Get data from input request.
54+
$id = $xpath->evaluate('normalize-space(/*/@ID)');
55+
$destination = htmlspecialchars($xpath->evaluate('normalize-space(/*/@AssertionConsumerServiceURL)'));
56+
$sp = $xpath->evaluate('normalize-space(/*/*[local-name() = "Issuer"])');
57+
58+
// Get time in UTC.
59+
$datetime = new DateTime();
60+
$datetime->setTimezone(new DatetimeZone('UTC'));
61+
$instant = $datetime->format('Y-m-d') . 'T' . $datetime->format('H:i:s') . 'Z';
62+
$datetime->sub(new DateInterval('P1D'));
63+
$before = $datetime->format('Y-m-d') . 'T' . $datetime->format('H:i:s') . 'Z';
64+
$datetime->add(new DateInterval('P1M'));
65+
$after = $datetime->format('Y-m-d') . 'T' . $datetime->format('H:i:s') . 'Z';
66+
67+
// Get our own IdP URL.
68+
$baseurl = $CFG->wwwroot . '/auth/saml2/idp';
69+
$issuer = $baseurl . '/metadata.php';
70+
71+
// Make up a session.
72+
$session = 'session' . mt_rand(100000, 999999);
73+
74+
// Construct attributes in XML.
75+
$attributexml = '';
76+
foreach ((array)$attributes as $name => $value) {
77+
$attributexml .= '<saml:Attribute Name="' . $name .
78+
'" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">' .
79+
'<saml:AttributeValue>' . htmlspecialchars($value) . '</saml:AttributeValue>' .
80+
'</saml:Attribute>' . "\n";
81+
}
82+
$email = htmlspecialchars($USER->email);
83+
// Construct XML without signature.
84+
$responsexml = <<<EOF
85+
<samlp:Response
86+
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
87+
ID="{$id}_2" InResponseTo="{$id}" Version="2.0" IssueInstant="{$instant}" Destination="{$destination}">
88+
<saml:Issuer>{$issuer}</saml:Issuer>
89+
<samlp:Status>
90+
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
91+
</samlp:Status>
92+
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{$id}_3" Version="2.0"
93+
IssueInstant="{$instant}">
94+
<saml:Issuer>{$issuer}</saml:Issuer>
95+
<saml:Subject>
96+
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
97+
{$email}
98+
</saml:NameID>
99+
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
100+
<saml:SubjectConfirmationData InResponseTo="{$id}"
101+
Recipient="{$destination}"
102+
NotOnOrAfter="{$after}"/>
103+
</saml:SubjectConfirmation>
104+
</saml:Subject>
105+
<saml:Conditions
106+
NotBefore="{$before}"
107+
NotOnOrAfter="{$after}">
108+
<saml:AudienceRestriction>
109+
<saml:Audience>{$sp}</saml:Audience>
110+
</saml:AudienceRestriction>
111+
</saml:Conditions>
112+
<saml:AuthnStatement AuthnInstant="{$instant}" SessionIndex="{$session}">
113+
<saml:AuthnContext>
114+
<saml:AuthnContextClassRef>
115+
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
116+
</saml:AuthnContextClassRef>
117+
</saml:AuthnContext>
118+
</saml:AuthnStatement>
119+
<saml:AttributeStatement>
120+
{$attributexml}
121+
</saml:AttributeStatement>
122+
</saml:Assertion>
123+
</samlp:Response>
124+
EOF;
125+
// Load it into a DOM.
126+
$outdoc = new \DOMDocument();
127+
$outdoc->loadXML($responsexml);
128+
129+
// Find the relevant elements.
130+
$xpath = new DOMXPath($outdoc);
131+
$assertion = $xpath->query('//*[local-name()="Assertion"]')[0];
132+
$subject = $xpath->query('child::*[local-name()="Subject"]', $assertion)[0];
133+
134+
// Sign it using the fixture key/cert.
135+
$signer = new \SimpleSAML\XML\Signer(['id' => 'ID']);
136+
137+
$signer->loadPrivateKey($saml2auth->certpem, $saml2auth->config->privatekeypass, true);
138+
$signer->loadCertificate($saml2auth->certcrt, true);
139+
$signer->sign($assertion, $assertion, $subject);
140+
141+
// Don't send as a referer or the login form might end up coming back here.
142+
header('Referrer-Policy: no-referrer');
143+
144+
// Output an HTML form that automatically submits this.
145+
echo '<!doctype html>';
146+
echo html_writer::start_tag('html');
147+
echo html_writer::tag('head', html_writer::tag('title', 'SSO redirect back'));
148+
echo html_writer::start_tag('body');
149+
echo html_writer::start_tag('form', ['id' => 'frog', 'method' => 'post', 'action' => htmlspecialchars_decode($destination)]);
150+
echo html_writer::empty_tag(
151+
'input',
152+
['type' => 'hidden', 'name' => 'SAMLResponse', 'value' => base64_encode($outdoc->saveXML())]
153+
);
154+
echo html_writer::empty_tag(
155+
'input',
156+
['type' => 'hidden', 'name' => 'RelayState', 'value' => $relaystate]
157+
);
158+
echo html_writer::end_tag('form');
159+
echo html_writer::tag('script', 'document.getElementById("frog").submit();');
160+
echo html_writer::end_tag('form');
161+
echo html_writer::end_tag('body');
162+
exit;

0 commit comments

Comments
 (0)