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 * Authentication Plugin: CAS Authentication
19 *
20 * Authentication using CAS (Central Authentication Server).
21 *
22 * @author Martin Dougiamas
23 * @author Jerome GUTIERREZ
24 * @author Iñaki Arenaza
25 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
26 * @package auth_cas
27 */
28
29defined('MOODLE_INTERNAL') || die();
30
31require_once($CFG->dirroot.'/auth/ldap/auth.php');
32require_once($CFG->dirroot.'/auth/cas/CAS/CAS.php');
33
34/**
35 * CAS authentication plugin.
36 */
37class auth_plugin_cas extends auth_plugin_ldap {
38
39    /**
40     * Constructor.
41     */
42    public function __construct() {
43        $this->authtype = 'cas';
44        $this->roleauth = 'auth_cas';
45        $this->errorlogtag = '[AUTH CAS] ';
46        $this->init_plugin($this->authtype);
47    }
48
49    /**
50     * Old syntax of class constructor. Deprecated in PHP7.
51     *
52     * @deprecated since Moodle 3.1
53     */
54    public function auth_plugin_cas() {
55        debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
56        self::__construct();
57    }
58
59    function prevent_local_passwords() {
60        return true;
61    }
62
63    /**
64     * Authenticates user against CAS
65     * Returns true if the username and password work and false if they are
66     * wrong or don't exist.
67     *
68     * @param string $username The username (with system magic quotes)
69     * @param string $password The password (with system magic quotes)
70     * @return bool Authentication success or failure.
71     */
72    function user_login ($username, $password) {
73        $this->connectCAS();
74        return phpCAS::isAuthenticated() && (trim(core_text::strtolower(phpCAS::getUser())) == $username);
75    }
76
77    /**
78     * Returns true if this authentication plugin is 'internal'.
79     *
80     * @return bool
81     */
82    function is_internal() {
83        return false;
84    }
85
86    /**
87     * Returns true if this authentication plugin can change the user's
88     * password.
89     *
90     * @return bool
91     */
92    function can_change_password() {
93        return false;
94    }
95
96    /**
97     * Authentication choice (CAS or other)
98     * Redirection to the CAS form or to login/index.php
99     * for other authentication
100     */
101    function loginpage_hook() {
102        global $frm;
103        global $CFG;
104        global $SESSION, $OUTPUT, $PAGE;
105
106        $site = get_site();
107        $CASform = get_string('CASform', 'auth_cas');
108        $username = optional_param('username', '', PARAM_RAW);
109        $courseid = optional_param('courseid', 0, PARAM_INT);
110
111        if (!empty($username)) {
112            if (isset($SESSION->wantsurl) && (strstr($SESSION->wantsurl, 'ticket') ||
113                                              strstr($SESSION->wantsurl, 'NOCAS'))) {
114                unset($SESSION->wantsurl);
115            }
116            return;
117        }
118
119        // Return if CAS enabled and settings not specified yet
120        if (empty($this->config->hostname)) {
121            return;
122        }
123
124        // If the multi-authentication setting is used, check for the param before connecting to CAS.
125        if ($this->config->multiauth) {
126
127            // If there is an authentication error, stay on the default authentication page.
128            if (!empty($SESSION->loginerrormsg)) {
129                return;
130            }
131
132            $authCAS = optional_param('authCAS', '', PARAM_RAW);
133            if ($authCAS != 'CAS') {
134                return;
135            }
136
137        }
138
139        // Connection to CAS server
140        $this->connectCAS();
141
142        if (phpCAS::checkAuthentication()) {
143            $frm = new stdClass();
144            $frm->username = phpCAS::getUser();
145            $frm->password = 'passwdCas';
146            $frm->logintoken = \core\session\manager::get_login_token();
147
148            // Redirect to a course if multi-auth is activated, authCAS is set to CAS and the courseid is specified.
149            if ($this->config->multiauth && !empty($courseid)) {
150                redirect(new moodle_url('/course/view.php', array('id'=>$courseid)));
151            }
152
153            return;
154        }
155
156        if (isset($_GET['loginguest']) && ($_GET['loginguest'] == true)) {
157            $frm = new stdClass();
158            $frm->username = 'guest';
159            $frm->password = 'guest';
160            $frm->logintoken = \core\session\manager::get_login_token();
161            return;
162        }
163
164        // Force CAS authentication (if needed).
165        if (!phpCAS::isAuthenticated()) {
166            phpCAS::setLang($this->config->language);
167            phpCAS::forceAuthentication();
168        }
169    }
170
171
172    /**
173     * Connect to the CAS (clientcas connection or proxycas connection)
174     *
175     */
176    function connectCAS() {
177        global $CFG;
178        static $connected = false;
179
180        if (!$connected) {
181            // Make sure phpCAS doesn't try to start a new PHP session when connecting to the CAS server.
182            if ($this->config->proxycas) {
183                phpCAS::proxy($this->config->casversion, $this->config->hostname, (int) $this->config->port, $this->config->baseuri, false);
184            } else {
185                phpCAS::client($this->config->casversion, $this->config->hostname, (int) $this->config->port, $this->config->baseuri, false);
186            }
187            // Some CAS installs require SSLv3 that should be explicitly set.
188            if (!empty($this->config->curl_ssl_version)) {
189                phpCAS::setExtraCurlOption(CURLOPT_SSLVERSION, $this->config->curl_ssl_version);
190            }
191
192            $connected = true;
193        }
194
195        // If Moodle is configured to use a proxy, phpCAS needs some curl options set.
196        if (!empty($CFG->proxyhost) && !is_proxybypass(phpCAS::getServerLoginURL())) {
197            phpCAS::setExtraCurlOption(CURLOPT_PROXY, $CFG->proxyhost);
198            if (!empty($CFG->proxyport)) {
199                phpCAS::setExtraCurlOption(CURLOPT_PROXYPORT, $CFG->proxyport);
200            }
201            if (!empty($CFG->proxytype)) {
202                // Only set CURLOPT_PROXYTYPE if it's something other than the curl-default http
203                if ($CFG->proxytype == 'SOCKS5') {
204                    phpCAS::setExtraCurlOption(CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
205                }
206            }
207            if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
208                phpCAS::setExtraCurlOption(CURLOPT_PROXYUSERPWD, $CFG->proxyuser.':'.$CFG->proxypassword);
209                if (defined('CURLOPT_PROXYAUTH')) {
210                    // any proxy authentication if PHP 5.1
211                    phpCAS::setExtraCurlOption(CURLOPT_PROXYAUTH, CURLAUTH_BASIC | CURLAUTH_NTLM);
212                }
213            }
214        }
215
216        if ($this->config->certificate_check && $this->config->certificate_path){
217            phpCAS::setCasServerCACert($this->config->certificate_path);
218        } else {
219            // Don't try to validate the server SSL credentials
220            phpCAS::setNoCasServerValidation();
221        }
222    }
223
224    /**
225     * Returns the URL for changing the user's pw, or empty if the default can
226     * be used.
227     *
228     * @return moodle_url
229     */
230    function change_password_url() {
231        return null;
232    }
233
234    /**
235     * Returns true if user should be coursecreator.
236     *
237     * @param mixed $username    username (without system magic quotes)
238     * @return boolean result
239     */
240    function iscreator($username) {
241        if (empty($this->config->host_url) or (empty($this->config->attrcreators) && empty($this->config->groupecreators)) or empty($this->config->memberattribute)) {
242            return false;
243        }
244
245        $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding);
246
247        // Test for group creator
248        if (!empty($this->config->groupecreators)) {
249            $ldapconnection = $this->ldap_connect();
250            if ($this->config->memberattribute_isdn) {
251                if(!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) {
252                    return false;
253                }
254            } else {
255                $userid = $extusername;
256            }
257
258            $group_dns = explode(';', $this->config->groupecreators);
259            if (ldap_isgroupmember($ldapconnection, $userid, $group_dns, $this->config->memberattribute)) {
260                return true;
261            }
262        }
263
264        // Build filter for attrcreator
265        if (!empty($this->config->attrcreators)) {
266            $attrs = explode(';', $this->config->attrcreators);
267            $filter = '(& ('.$this->config->user_attribute."=$username)(|";
268            foreach ($attrs as $attr){
269                if(strpos($attr, '=')) {
270                    $filter .= "($attr)";
271                } else {
272                    $filter .= '('.$this->config->memberattribute."=$attr)";
273                }
274            }
275            $filter .= '))';
276
277            // Search
278            $result = $this->ldap_get_userlist($filter);
279            if (count($result) != 0) {
280                return true;
281            }
282        }
283
284        return false;
285    }
286
287    /**
288     * Reads user information from LDAP and returns it as array()
289     *
290     * If no LDAP servers are configured, user information has to be
291     * provided via other methods (CSV file, manually, etc.). Return
292     * an empty array so existing user info is not lost. Otherwise,
293     * calls parent class method to get user info.
294     *
295     * @param string $username username
296     * @return mixed array with no magic quotes or false on error
297     */
298    function get_userinfo($username) {
299        if (empty($this->config->host_url)) {
300            return array();
301        }
302        return parent::get_userinfo($username);
303    }
304
305    /**
306     * Syncronizes users from LDAP server to moodle user table.
307     *
308     * If no LDAP servers are configured, simply return. Otherwise,
309     * call parent class method to do the work.
310     *
311     * @param bool $do_updates will do pull in data updates from LDAP if relevant
312     * @return nothing
313     */
314    function sync_users($do_updates=true) {
315        if (empty($this->config->host_url)) {
316            error_log('[AUTH CAS] '.get_string('noldapserver', 'auth_cas'));
317            return;
318        }
319        parent::sync_users($do_updates);
320    }
321
322    /**
323    * Hook for logout page
324    */
325    function logoutpage_hook() {
326        global $USER, $redirect;
327
328        // Only do this if the user is actually logged in via CAS
329        if ($USER->auth === $this->authtype) {
330            // Check if there is an alternative logout return url defined
331            if (isset($this->config->logout_return_url) && !empty($this->config->logout_return_url)) {
332                // Set redirect to alternative return url
333                $redirect = $this->config->logout_return_url;
334            }
335        }
336    }
337
338    /**
339     * Post logout hook.
340     *
341     * Note: this method replace the prelogout_hook method to avoid redirect to CAS logout
342     * before the event userlogout being triggered.
343     *
344     * @param stdClass $user clone of USER object object before the user session was terminated
345     */
346    public function postlogout_hook($user) {
347        global $CFG;
348        // Only redirect to CAS logout if the user is logged as a CAS user.
349        if (!empty($this->config->logoutcas) && $user->auth == $this->authtype) {
350            $backurl = !empty($this->config->logout_return_url) ? $this->config->logout_return_url : $CFG->wwwroot;
351            $this->connectCAS();
352            phpCAS::logoutWithRedirectService($backurl);
353        }
354    }
355
356    /**
357     * Return a list of identity providers to display on the login page.
358     *
359     * @param string|moodle_url $wantsurl The requested URL.
360     * @return array List of arrays with keys url, iconurl and name.
361     */
362    public function loginpage_idp_list($wantsurl) {
363        if (empty($this->config->hostname)) {
364            // CAS is not configured.
365            return [];
366        }
367
368        if ($this->config->auth_logo) {
369            $iconurl = moodle_url::make_pluginfile_url(
370                context_system::instance()->id,
371                'auth_cas',
372                'logo',
373                null,
374                null,
375                $this->config->auth_logo);
376        } else {
377            $iconurl = null;
378        }
379
380        return [
381            [
382                'url' => new moodle_url(get_login_url(), [
383                        'authCAS' => 'CAS',
384                    ]),
385                'iconurl' => $iconurl,
386                'name' => format_string($this->config->auth_name),
387            ],
388        ];
389    }
390}
391