|
| 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; |
0 commit comments