Skip to content

Commit

Permalink
WIP - allow Moodle to function as an IDP.
Browse files Browse the repository at this point in the history
  • Loading branch information
danmarsden committed Jul 3, 2024
1 parent 974796e commit da2d734
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 0 deletions.
55 changes: 55 additions & 0 deletions idp/metadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?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();

$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);
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);
162 changes: 162 additions & 0 deletions idp/sso.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?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.
// 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 .= '<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;

0 comments on commit da2d734

Please sign in to comment.