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