lib.php 49.6 KB
Newer Older
1
2
3
4
5
6
<?php
/**
 *
 * @package    mahara
 * @subpackage auth-saml
 * @author     Piers Harding <piers@catalyst.net.nz>
7
8
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
 * @copyright  For copyright information on Mahara, please see the README file distributed with this software.
9
10
11
12
13
14
15
16
17
18
19
 * @copyright  (C) portions from Moodle, (C) Martin Dougiamas http://dougiamas.com
 */

defined('INTERNAL') || die();
require_once(get_config('docroot') . 'auth/lib.php');

/**
 * Authenticates users with SAML 2.0
 */
class AuthSaml extends Auth {

20
21
22
23
24
    public static function get_metadata_path() {
        check_dir_exists(get_config('dataroot') . 'metadata/');
        return get_config('dataroot') . 'metadata/';
    }

25
26
27
28
29
    public static function prepare_metadata_path($idp) {
        $path = self::get_metadata_path() . preg_replace('/[\/:\.]/', '_', $idp) . '.xml';
        return $path;
    }

30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
    /**
    * Loads and merges in a file with an attribute map.
    *
    * @param string $filepath  Path of attribute map file.
    * @param array $mapping Array where the attributes from the file should be added
    */
    private static function custom_loadmapfile($filepath, $mapping = array()) {
        if (!is_readable($filepath)) {
            throw new Exception(get_string('attributemapfilenotfound', 'auth.saml', $filepath));
        }
        $attributemap = NULL;
        include($filepath);
        if (!is_array($attributemap)) {
            throw new Exception(get_string('attributemapfilenotamap', 'auth.saml', $filepath));
        }

        $mapping = array_merge_recursive($mapping, $attributemap);
        return $mapping;
    }

/*
* Loads all mappings in the files into an array with 'class' => 'core:AttributeMap'
*
* @param filepaths array Paths to files that contain a mapping array
*/
    public static function get_attributemappings($filepaths= array()) {

          $configparameter = array(
              'class' => 'core:AttributeMap',
          );

          $attributemap = array();
          foreach ($filepaths as $key => $filepath) {
                //get the $attributemap array in the file
                $attributemap = self::custom_loadmapfile($filepath, $attributemap);
          }

          return array_merge($attributemap, $configparameter);
    }

70
71
72
73
74
    public static function get_certificate_path() {
        check_dir_exists(get_config('dataroot') . 'certificate/');
        return get_config('dataroot') . 'certificate/';
    }

75
76
77
    public function __construct($id = null) {
        $this->type = 'saml';
        $this->has_instance_config = true;
78

79
80
81
82
83
        $this->config['user_attribute'] = '';
        $this->config['weautocreateusers'] = 1;
        $this->config['firstnamefield' ] = '';
        $this->config['surnamefield'] = '';
        $this->config['emailfield'] = '';
84
        $this->config['studentidfield'] = '';
85
86
87
88
        $this->config['institutionattribute'] = '';
        $this->config['institutionregex'] = 0;
        $this->config['institutionvalue'] = '';
        $this->config['updateuserinfoonlogin'] = 1;
89
        $this->config['remoteuser'] = true;
90
        $this->config['loginlink'] = false;
91
        $this->config['institutionidp'] = '';
92
        $this->config['institutionidpentityid'] = '';
93
        $this->instanceid = $id;
94

95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
        if (!empty($id)) {
            return $this->init($id);
        }
        return true;
    }

    public function init($id = null) {
        $this->ready = parent::init($id);

        // Check that required fields are set
        if ( empty($this->config['user_attribute']) ||
             empty($this->config['institutionattribute'])
              ) {
            $this->ready = false;
        }

        return $this->ready;
    }


    /**
     * We can autocreate users if the admin has said we can
     * in weautocreateusers
     */
    public function can_auto_create_users() {
120
        return (bool)$this->config['weautocreateusers'];
121
122
123
124
125
126
127
128
129
    }


    /**
     * Grab a delegate object for auth stuff
     */
    public function request_user_authorise($attributes) {
        global $USER, $SESSION;
        $this->must_be_ready();
130

131
132
133
134
        if (empty($attributes) or !array_key_exists($this->config['user_attribute'], $attributes)
                               or !array_key_exists($this->config['institutionattribute'], $attributes)) {
            throw new AccessDeniedException();
        }
135

136
        $remoteuser      = $attributes[$this->config['user_attribute']][0];
137
138
139
        $firstname       = isset($attributes[$this->config['firstnamefield']][0]) ? $attributes[$this->config['firstnamefield']][0] : null;
        $lastname        = isset($attributes[$this->config['surnamefield']][0]) ? $attributes[$this->config['surnamefield']][0] : null;
        $email           = isset($attributes[$this->config['emailfield']][0]) ? $attributes[$this->config['emailfield']][0] : null;
140
        $studentid       = isset($attributes[$this->config['studentidfield']][0]) ? $attributes[$this->config['studentidfield']][0] : null;
141
        $institutionname = $this->institution;
142

143
144
145
146
147
        $create = false;
        $update = false;

        // Retrieve a $user object. If that fails, create a blank one.
        try {
148
            $isremote = $this->config['remoteuser'] ? true : false;
149
150
            $user = new User;
            if (get_config('usersuniquebyusername')) {
151
152
                // When turned on, this setting means that it doesn't matter
                // which other application the user SSOs from, they will be
153
154
                // given the same account in Mahara.
                //
155
156
157
158
159
                // This setting is one that has security implications unless
                // only turned on by people who know what they're doing. In
                // particular, every system linked to Mahara should be making
                // sure that same username == same person.  This happens for
                // example if two Moodles are using the same LDAP server for
160
161
                // authentication.
                //
162
163
164
                // If this setting is on, it must NOT be possible to self
                // register on the site for ANY institution - otherwise users
                // could simply pick usernames of people's accounts they wished
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
                // to steal.
                if ($institutions = get_column('institution', 'name', 'registerallowed', '1')) {
                    log_warn("usersuniquebyusername is turned on but registration is allowed for an institution. "
                        . "No institution can have registration allowed for it, for security reasons.\n"
                        . "The following institutions have registration enabled:\n  " . join("\n  ", $institutions));
                    throw new AccessDeniedException();
                }

                if (!get_config('usersallowedmultipleinstitutions')) {
                    log_warn("usersuniquebyusername is turned on but usersallowedmultipleinstitutions is off. "
                        . "This makes no sense, as users will then change institution every time they log in from "
                        . "somewhere else. Please turn this setting on in Site Options");
                    throw new AccessDeniedException();
                }
            }
180
181
182
183
184
185
186
187
            else {
                if (!$isremote){
                    log_warn("usersuniquebyusername is turned off but remoteuser has not been set on for this institution: $institutionname. "
                        . "This is a security risk as users from different institutions with different IdPs can hijack "
                        . "each others accounts.  Fix this in the institution level auth/saml settings.");
                    throw new AccessDeniedException();
                }
            }
188
189
190
191
            if ($isremote && !empty($email) && $this->config['loginlink']) {
                $user->find_by_email_address($email);
            }
            else if ($isremote) {
192
193
194
195
196
                $user->find_by_instanceid_username($this->instanceid, $remoteuser, $isremote);
            }
            else {
                $user->find_by_username($remoteuser);
            }
197
198
199
200
201
202
203
204
205
206
207
208

            if ($user->get('suspendedcusr')) {
                die_info(get_string('accountsuspended', 'mahara', strftime(get_string('strftimedaydate'), $user->get('suspendedctime')), $user->get('suspendedreason')));
            }

            if ('1' == $this->config['updateuserinfoonlogin']) {
                $update = true;
            }
        } catch (AuthUnknownUserException $e) {
            if (!empty($this->config['weautocreateusers'])) {
                $institution = new Institution($this->institution);
                if ($institution->isFull()) {
209
                    $institution->send_admin_institution_is_full_message();
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
                    throw new XmlrpcClientException('SSO attempt from ' . $institution->displayname . ' failed - institution is full');
                }
                $user = new User;
                $create = true;
            }
            else {
                log_debug("User authorisation request from SAML failed - "
                    . "remote user '$remoteuser' is unknown to us and auto creation of users is turned off");
                return false;
            }
        }

        /*******************************************/

        if ($create) {

            $user->passwordchange     = 1;
            $user->active             = 1;
            $user->deleted            = 0;

            $user->expiry             = null;
            $user->expirymailsent     = 0;
            $user->lastlogin          = time();
233

234
235
236
            $user->firstname          = $firstname;
            $user->lastname           = $lastname;
            $user->email              = $email;
237
            $user->studentid          = $studentid;
238

239
240
            // must have these values
            if (empty($firstname) || empty($lastname) || empty($email)) {
241
                throw new AccessDeniedException(get_string('errormissinguserattributes1', 'auth.saml', get_config('sitename')));
242
243
            }

244
245
246
            $user->authinstance       = empty($this->config['parent']) ? $this->instanceid : $this->parent;

            db_begin();
247
            $user->username           = get_new_username($remoteuser, 40);
248
249
250
251
252
253
254
255
256
257
258
259
260

            $user->id = create_user($user, array(), $institutionname, $this, $remoteuser);

            /*
             * We need to convert the object to a stdclass with its own
             * custom method because it uses overloaders in its implementation
             * and its properties wouldn't be visible to a simple cast operation
             * like (array)$user
             */
            $userobj = $user->to_stdclass();
            $userarray = (array)$userobj;
            db_commit();

261
            // Now we have fired the create event, we need to re-get the data
262
263
264
265
            // for this user
            $user = new User;
            $user->find_by_id($userobj->id);

266
267
268
269
270
            if (get_config('usersuniquebyusername')) {
                // Add them to the institution they have SSOed in by
                $user->join_institution($institutionname);
            }

271
        } elseif ($update) {
272
273
274
275
276
277
278
279
280
281
282
            if (! empty($firstname)) {
                set_profile_field($user->id, 'firstname', $firstname);
                $user->firstname = $firstname;
            }
            if (! empty($lastname)) {
                set_profile_field($user->id, 'lastname', $lastname);
                $user->lastname = $lastname;
            }
            if (! empty($email)) {
                set_profile_field($user->id, 'email', $email);
                $user->email = $email;
283
            }
284
285
286
287
288
            if (! empty($studentid)) {
                set_profile_field($user->id, 'studentid', $studentid);
                $user->studentid = $studentid;
            }

289
290
291
            $user->lastlastlogin      = $user->lastlogin;
            $user->lastlogin          = time();
        }
292
        $user->commit();
293
294
295
296
297
298


        /*******************************************/

        // We know who our user is now. Bring em back to life.
        $result = $USER->reanimate($user->id, $this->instanceid);
299
        log_debug("remote user '$remoteuser' is now reanimated as '{$USER->username}' ");
300
301
302
303
        $SESSION->set('authinstance', $this->instanceid);

        return true;
    }
304

305
306
307
    // ensure that a user is logged out of mahara and the SAML 2.0 IdP
    public function logout() {
        global $CFG, $USER, $SESSION;
308

309
310
        // logout of mahara
        $USER->logout();
311

312
313
        // tidy up the session for retries
        $SESSION->set('messages', array());
314
        $SESSION->set('wantsurl', null);
Aaron Wells's avatar
Aaron Wells committed
315

316
        // redirect for logout of SAML 2.0 IdP
317
        redirect($CFG->wwwroot.'/auth/saml/index.php?logout=1');
318
    }
319

320
321
322
    public function after_auth_setup_page_hook() {
        return;
    }
323
324
325
326

    public function needs_remote_username() {
        return $this->config['remoteuser'] || parent::needs_remote_username();
    }
327
328
329
330
331
332
333
334
}

/**
 * Plugin configuration class
 */
class PluginAuthSaml extends PluginAuth {

    private static $default_config = array(
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
        'user_attribute'         => '',
        'weautocreateusers'      => 0,
        'firstnamefield'         => '',
        'surnamefield'           => '',
        'emailfield'             => '',
        'studentidfield'         => '',
        'updateuserinfoonlogin'  => 1,
        'institution'            => '',
        'institutionattribute'   => '',
        'institutionvalue'       => '',
        'institutionregex'       => 0,
        'remoteuser'             => 1,
        'loginlink'              => 0,
        'institutionidpentityid' => '',
        'active'                 => 1
    );
351

352
353
354
355
    public static function can_be_disabled() {
        return true;
    }

356
357
358
359
    public static function is_active() {
        return get_field('auth_installed', 'active', 'name', 'saml');
    }

360
361
362
363
    public static function has_config() {
        return true;
    }

364
365
366
367
368
    public static function install_auth_default() {
        // Set library version to download
        set_config_plugin('auth', 'saml', 'version', '1.14.16');
    }

369
370
371
    private static function create_certificates($numberofdays = 3650) {
        global $CFG;
        // Get the details of the first site admin and use it for setting up the certificate
372
        $userid = get_record_sql('SELECT id FROM {usr} WHERE admin = 1 AND deleted = 0 ORDER BY id LIMIT 1', array());
373
374
375
376
377
378
379
380
381
382
383
        $id = $userid->id;
        $user = new User;
        $user->find_by_id($id);

        $country = get_profile_field($id, 'country');
        $town = get_profile_field($id, 'town');
        $city = get_profile_field($id, 'city');
        $industry = get_profile_field($id, 'industry');
        $occupation = get_profile_field($id, 'occupation');

        $dn = array(
384
            'commonName' => ($user->get('username') ? substr($user->get('username'), 0, 64) : 'Mahara'),
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
            'countryName' => ($country ? strtoupper($country) : 'NZ'),
            'localityName' => ($town ? $town : 'Wellington'),
            'emailAddress' => ($user->get('email') ? $user->get('email') : $CFG->noreplyaddress),
            'organizationName' => ($industry ? $industry : get_config('sitename')),
            'stateOrProvinceName' => ($city ? $city : 'Wellington'),
            'organizationalUnitName' => ($occupation ? $occupation : 'Mahara'),
        );

        $privkeypass = get_config('sitename');
        $privkey = openssl_pkey_new();
        $csr     = openssl_csr_new($dn, $privkey);
        $sscert  = openssl_csr_sign($csr, null, $privkey, $numberofdays);
        openssl_x509_export($sscert, $publickey);
        openssl_pkey_export($privkey, $privatekey, $privkeypass);

        // Write Private Key and Certificate files to disk.
        // If there was a generation error with either explode.
        if (empty($privatekey)) {
            throw new Exception(get_string('nullprivatecert', 'auth.saml'), 1);
        }
        if (empty($publickey)) {
            throw new Exception(get_string('nullpubliccert', 'auth.saml'), 1);
        }

        if ( !file_put_contents(AuthSaml::get_certificate_path() . 'server.pem', $privatekey) ) {
            throw new Exception(get_string('nullprivatecert', 'auth.saml'), 1);
        }
        if ( !file_put_contents(AuthSaml::get_certificate_path() . 'server.crt', $publickey) ) {
            throw new Exception(get_string('nullpubliccert', 'auth.saml'), 1);
        }
    }

417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
    /*
     * Return an array of signature algorithms in a form suitable for feeding into a dropdown form
     */
    public static function get_valid_saml_signature_algorithms() {
        $return = array();
        $return['http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'] = get_string('sha256', 'auth.saml');
        $return['http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'] = get_string('sha384', 'auth.saml');
        $return['http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'] = get_string('sha512', 'auth.saml');
        $return['http://www.w3.org/2000/09/xmldsig#rsa-sha1'] = get_string('sha1', 'auth.saml');

        return $return;
    }

    /*
     * Return a sensible default signature algorithm for simplesamlphp config
     */
    public static function get_default_saml_signature_algorithm() {
        //Sha1 is deprecated so we default to something more sensible
        return 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
    }

    /*
     * Check if a given value is a valid signature algorithm for configuration
     * in simplesamlphp
     */
    public static function is_valid_saml_signature_algorithm($value) {
        $valids = self::get_valid_saml_signature_algorithms();
        return array_key_exists($value, $valids);
    }

    /*
     * Get the configured signature algorithm, falling back to the default if
     * no valid value can be found or no value is set
     */
    public static function get_config_saml_signature_algorithm() {
        $signaturealgo = get_config_plugin('auth', 'saml', 'sigalgo');
        if (empty($signaturealgo) || !self::is_valid_saml_signature_algorithm($signaturealgo)) {
                $signaturealgo = self::get_default_saml_signature_algorithm();
        }

        return $signaturealgo;
    }

460
461
    public static function get_config_options() {

462
463
464
465
466
        $spentityid = get_config_plugin('auth', 'saml', 'spentityid');
        if (empty($spentityid)) {
            $spentityid = $_SERVER['HTTP_HOST'] . '/mahara';
        }

467
468
469
        $signaturealgo = self::get_config_saml_signature_algorithm();
        $possiblealgos = self::get_valid_saml_signature_algorithms();

470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
        // first time - create it
        if (!file_exists(AuthSaml::get_certificate_path() . 'server.crt')) {
            error_log("auth/saml: Creating the certificate for the first time");
            self::create_certificates();
        }
        $cert = file_get_contents(AuthSaml::get_certificate_path() . 'server.crt');
        $pem = file_get_contents(AuthSaml::get_certificate_path() . 'server.pem');
        if (empty($cert) || empty($pem)) {
            // bad cert - get rid of it
            unlink(AuthSaml::get_certificate_path() . 'server.crt');
            unlink(AuthSaml::get_certificate_path() . 'server.pem');
        }
        else {
            $privatekey = openssl_pkey_get_private($pem);
            $publickey  = openssl_pkey_get_public($cert);
            $data = openssl_pkey_get_details($publickey);
            // Load data from the current certificate.
            $data = openssl_x509_parse($cert);
        }

        // Calculate date expirey interval.
        $date1 = date("Y-m-d\TH:i:s\Z", str_replace ('Z', '', $data['validFrom_time_t']));
        $date2 = date("Y-m-d\TH:i:s\Z", str_replace ('Z', '', $data['validTo_time_t']));
        $datetime1 = new DateTime($date1);
        $datetime2 = new DateTime($date2);
        $interval = $datetime1->diff($datetime2);
        $expirydays = $interval->format('%a');

498
499
500
501
502
503
504
505
506
        $elements = array(
            'authname' => array(
                'type'  => 'hidden',
                'value' => 'saml',
            ),
            'authglobalconfig' => array(
                'type'  => 'hidden',
                'value' => 'saml',
            ),
507
            'spentityid' => array(
508
509
                'type'  => 'text',
                'size' => 50,
510
                'title' => get_string('spentityid', 'auth.saml'),
511
512
513
                'rules' => array(
                    'required' => true,
                ),
514
                'defaultvalue' => $spentityid,
515
516
                'help'  => true,
            ),
517
518
519
520
521
522
523
            'sigalgo' => array(
                'type' => 'select',
                'title' => get_string('sigalgo', 'auth.saml'),
                'options' => $possiblealgos,
                'defaultvalue' => $signaturealgo,
                'help' => true,
            ),
524
525
526
            'makereallysure' => array(
                'type'         => 'html',
                'value'        => "<script>jQuery('document').ready(function() {     jQuery('#pluginconfig_save').on('click', function() {
527
                return confirm('" . get_string('reallyreallysure1', 'auth.saml') . "');
528
            });});</script>",
529
            ),
530
531
            'certificate' => array(
                                'type' => 'fieldset',
532
                                'legend' => get_string('certificate1', 'auth.saml'),
533
534
535
                                'elements' =>  array(
                                                'protos_help' =>  array(
                                                'type' => 'html',
536
                                                'value' => '<div><p>' . get_string('manage_certificate1', 'auth.saml', get_config('wwwroot') . 'auth/saml/sp/metadata.php?output=xhtml') . '</p></div>',
537
538
539
540
541
542
543
544
545
                                                ),

                                                'pubkey' => array(
                                                    'type'         => 'html',
                                                    'value'        => '<h3 class="title">' . get_string('publickey','admin') . '</h3>' .
                                                      '<pre style="font-size: 0.7em; white-space: pre;">' . $cert . '</pre>'
                                                ),
                                                'sha1fingerprint' => array(
                                                    'type'         => 'html',
546
                                                    'value'        => '<div><p>' . get_string('sha1fingerprint', 'auth.webservice', auth_saml_openssl_x509_fingerprint($cert, "sha1")) . '</p></div>',
547
548
549
                                                ),
                                                'md5fingerprint' => array(
                                                    'type'         => 'html',
550
                                                    'value'        => '<div><p>' . get_string('md5fingerprint', 'auth.webservice', auth_saml_openssl_x509_fingerprint($cert, "md5")) . '</p></div>',
551
552
553
554
555
556
557
558
559
560
561
                                                ),
                                                'expires' => array(
                                                    'type'         => 'html',
                                                    'value'        => '<div><p>' . get_string('publickeyexpireson', 'auth.webservice',
                                                    format_date($data['validTo_time_t']) . " (" . $expirydays . " days)") . '</p></div>'
                                                ),
                                            ),
                                'collapsible' => false,
                                'collapsed'   => false,
                                'name' => 'activate_webservices_networking',
                            ),
562
563
        );

564
565
        // check extensions are loaded
        $libchecks = '';
566
        // Make sure mcrypt exists
567
        if (!extension_loaded('mcrypt')) {
568
            $libchecks .= '<li>' . get_string_php_version('errornomcrypt', 'auth.saml') . '</li>';
569
        }
570
        // Make sure the simplesamlphp files have been installed via 'make ssphp'
571
        if (!self::is_simplesamlphp_installed()) {
572
573
            $libchecks .= '<li>' . get_string('errorbadlib', 'auth.saml', get_config('docroot') .'auth/saml/extlib/simplesamlphp/vendor/autoload.php') . '</li>';
        }
574
575
576
577
578
579
580
581
582
583
584
        else {
            require(get_config('docroot') .'auth/saml/extlib/simplesamlphp/vendor/autoload.php');
            $config = SimpleSAML_Configuration::getInstance();

            //simplesaml version we install with 'make ssphp'
            $libversion = get_config_plugin('auth', 'saml', 'version');

            if (!empty($libversion) && $config->getVersion() != $libversion) {
                $libchecks .= '<li>' . get_string('errorupdatelib', 'auth.saml') . '</li>';
            }
        }
585
        // Make sure we can use 'memcache' with simplesamlphp as 'phpsession' doesn't work correctly in many situations
586
        if (!self::is_memcache_configured()) {
587
            $libchecks .= '<li>' . get_string_php_version('errornomemcache', 'auth.saml') . '</li>';
588
589
590
591
592
593
594
595
596
        }
        if (!empty($libchecks)) {
            $libcheckstr = '<div class="alert alert-danger"><ul class="unstyled">' . $libchecks . '</ul></div>';
            $elements = array_merge(array('libchecks' => array(
                                                'type' => 'html',
                                                'value' => $libcheckstr,
                                     )), $elements);
        }

597
598
599
600
601
        return array(
            'elements' => $elements,
        );
    }

602
603
604
605
606
607
608
    public static function validate_config_options(Pieform $form, $values) {
        if (empty($values['spentityid'])) {
            $form->set_error('spentityid', get_string('errorbadspentityid', 'auth.saml', $values['spentityid']));
        }
    }

    public static function save_config_options(Pieform $form, $values) {
609
        delete_records_select('auth_config', 'plugin = ? AND field NOT LIKE ?', array('saml', 'version'));
610
        $configs = array('spentityid', 'sigalgo');
611
612
613
614
615
616
617
618
619
620
        foreach ($configs as $config) {
            set_config_plugin('auth', 'saml', $config, $values[$config]);
        }

        // generate new certificates
        error_log("auth/saml: Creating new certificates");
        self::create_certificates();

    }

621
622
623
624
625
    public static function has_instance_config() {
        return true;
    }

    public static function is_usable() {
626
627
628
629
630
631
632
633
        if (!self::is_simplesamlphp_installed()) {
            return false;
        }

        if (empty(get_config('ssphpsessionhandler'))) {
            return self::is_memcache_configured();
        }

634
635
636
        return true;
    }

637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
    public static function is_simplesamlphp_installed() {
        return file_exists(get_config('docroot') . 'auth/saml/extlib/simplesamlphp/vendor/autoload.php');
    }

    public static function init_simplesamlphp() {
        if (!self::is_simplesamlphp_installed()) {
            throw new AuthInstanceException(get_string('errorbadlib', 'auth.saml', get_config('docroot') . 'auth/saml/extlib/simplesamlphp/vendor/autoload.php'));
        }

        require_once(get_config('docroot') . 'auth/saml/extlib/simplesamlphp/vendor/autoload.php');
        require_once(get_config('docroot') . 'auth/saml/extlib/_autoload.php');

        SimpleSAML_Configuration::init(get_config('docroot') . 'auth/saml/config');
    }

    public static function is_memcache_configured() {
        $is_configured = false;

        if (extension_loaded('memcache')) {
            foreach (self::get_memcache_servers() as $server) {
                $memcache = new Memcache;

                if (!empty($server['hostname']) && !empty($server['port'])) {
                    if ($memcache->connect($server['hostname'], $server['port'])) {
                        $is_configured = true;
                        break;
                    }
                }
            }
        }

        return $is_configured;
    }

    public static function get_memcache_servers() {
        $memcache_servers = array();

        $servers = get_config('memcacheservers');

        if (empty($servers)) {
            $servers = 'localhost';
        }

        $servers = explode(',', $servers);

        foreach ($servers as $server) {
            $url = parse_url($server);
            $host = !empty($url['host']) ? $url['host'] : $url['path'];
            $port = !empty($url['port']) ? $url['port'] : 11211;

            $memcache_servers[] = array('hostname' => $host, 'port' => $port);
        }

        return $memcache_servers;
    }

693
694
695
696
697
698
699
700
701
702
703
704
705
    public static function get_idps($xml) {
        $xml = new SimpleXMLElement($xml);
        $xml->registerXPathNamespace('md',   'urn:oasis:names:tc:SAML:2.0:metadata');
        $xml->registerXPathNamespace('mdui', 'urn:oasis:names:tc:SAML:metadata:ui');
        // Find all IDPSSODescriptor elements and then work back up to the entityID.
        $idps = $xml->xpath('//md:EntityDescriptor[//md:IDPSSODescriptor]');
        $entityid = null;
        if ($idps && isset($idps[0])) {
            $entityid = (string)$idps[0]->attributes('', true)->entityID[0];
        }
        return array($entityid, $idps);
    }

706
707
708
709
    public static function get_disco_list($lang = null, $entityidps = array()) {
        if (empty($lang)) {
            $lang = current_language();
        }
710
        PluginAuthSaml::init_simplesamlphp();
711
712
713
714
715
716
717
718
719
720
721
722
723
724
        $discoHandler = new PluginAuthSaml_IdPDisco(array('saml20-idp-remote', 'shib13-idp-remote'), 'saml');
        $disco = $discoHandler->getTheIdPs();
        if (count($disco['list']) > 0) {
            $lang = explode('.', $lang);
            $lang = strtolower(array_shift($lang));
            foreach($disco['list'] as $idp) {
                $idpname = (isset($idp['name'][$lang])) ? $idp['name'][$lang] : $idp['entityid'];
                $entityidps[$idp['entityid']] = $idpname;
            }
            return $entityidps;
        }
        return false;
    }

725
    public static function get_instance_config_options($institution, $instance = 0) {
726
727
728
729
730
        if (!class_exists('SimpleSAML_XHTML_IdPDisco')) {
            global $SESSION;
            $SESSION->add_error_msg(get_string('errorssphpsetup', 'auth.saml'));
            redirect(get_config('wwwroot') . 'admin/users/institutions.php?i=' . $institution);
        }
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746

        if ($instance > 0) {
            $default = get_record('auth_instance', 'id', $instance);
            if ($default == false) {
                throw new SystemException('Could not find data for auth instance ' . $instance);
            }
            $current_config = get_records_menu('auth_instance_config', 'instance', $instance, '', 'field, value');

            if ($current_config == false) {
                $current_config = array();
            }
            foreach (self::$default_config as $key => $value) {
                if (array_key_exists($key, $current_config)) {
                    self::$default_config[$key] = $current_config[$key];
                }
            }
747
            if (empty(self::$default_config['institutionvalue'])) {
748
749
                self::$default_config['institutionvalue'] = $institution;
            }
750
751
752
            self::$default_config['active'] = $default->active;
        }
        else {
753
754
            $default = new stdClass();
            $default->instancename = '';
755
            $default->active = 1;
756
757
        }

758
759
760
        // lookup the institution metadata
        $entityid = "";
        self::$default_config['institutionidp'] = "";
761
762
763
764
765
766
767
        if (!empty(self::$default_config['institutionidpentityid'])) {
            $idpfile = AuthSaml::prepare_metadata_path(self::$default_config['institutionidpentityid']);
            if (file_exists($idpfile)) {
                $rawxml = file_get_contents($idpfile);
                if (empty($rawxml)) {
                    // bad metadata - get rid of it
                    unlink($idpfile);
768
769
                }
                else {
770
771
772
773
774
775
776
777
                    list ($entityid, $idps) = self::get_idps($rawxml);
                    if ($entityid) {
                        self::$default_config['institutionidp'] = $rawxml;
                    }
                    else {
                        // bad metadata - get rid of it
                        unlink($idpfile);
                    }
778
779
780
781
782
783
784
785
                }
            }
        }

        $idp_title = get_string('institutionidp', 'auth.saml', $institution);
        if ($entityid) {
            $idp_title .= " (" . $entityid . ")";
        }
786
787
788
        $entityidps = array();
        $entityidp_hiddenlabel = true;
        // Fetch the idp info via disco
789
790
791
        $discolist = self::get_disco_list();
        if ($discolist) {
            $entityidps += $discolist;
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
            $entityidp_hiddenlabel = false;
        }
        asort($entityidps);
        // add the 'New' option to the top of the list
        $entityidps = array('new' => get_string('newidpentity', 'auth.saml')) + $entityidps;

        $idpselectjs = <<< EOF
<script type="application/javascript">
jQuery('document').ready(function($) {

    function update_idp_label(idp) {
        var idplabel = $('label[for="auth_config_institutionidp"]').html();
        // remove the idp entity from string
        if (idplabel.lastIndexOf('(') != -1) {
            idplabel = idplabel.substring(0, idplabel.lastIndexOf('('));
        }
        // add in new one
        if (idp) {
            idplabel = idplabel.trim() + ' (' + idp + ')';
        }
        $('label[for="auth_config_institutionidp"]').html(idplabel);
    }

    function update_idp_info(idp) {

        if (idp == 'new') {
            // clear the metadata box
            $('#auth_config_institutionidp').val('');
            update_idp_label(false);
        }
        else {
            // fetch the metadata info and update the textarea
            idpsafe = idp.replace(/[\/:\.]/g, '_'); // change dots to underscores as that is how we save file
            sendjsonrequest(config.wwwroot + 'auth/saml/idpmetadata.json.php', {'idp': idpsafe}, 'POST', function (data) {
                if (!data.error) {
                    $('#auth_config_institutionidp').val(data.data.metadata);
                }
            });
            update_idp_label(idp);
        }
    }

    // On change
    $('#auth_config_institutionidpentityid').on('change', function() {
        update_idp_info($(this).val());
    });
    // On load
    update_idp_info($('#auth_config_institutionidpentityid').val());
});
</script>
EOF;

844
845
846
847
848
        $elements = array(
            'instance' => array(
                'type'  => 'hidden',
                'value' => $instance,
            ),
849
850
851
852
            'instancename' => array(
                'type'  => 'hidden',
                'value' => 'SAML',
            ),
853
854
855
856
857
858
859
860
            'institution' => array(
                'type'  => 'hidden',
                'value' => $institution,
            ),
            'authname' => array(
                'type'  => 'hidden',
                'value' => 'saml',
            ),
861
862
863
864
865
            'active' => array(
                'type'  => 'switchbox',
                'title' => get_string('active', 'auth'),
                'defaultvalue' => (int) self::$default_config['active'],
            ),
866
867
868
869
870
871
872
            'institutionidpentityid' => array(
                'type'  => 'select',
                'title' => get_string('institutionidpentity', 'auth.saml'),
                'options' => $entityidps,
                'defaultvalue' => ($entityid ? $entityid : 'new'),
                'hiddenlabel' => $entityidp_hiddenlabel,
            ),
873
874
875
876
877
878
879
            'institutionidp' => array(
                'type'  => 'textarea',
                'title' => $idp_title,
                'rows' => 10,
                'cols' => 80,
                'defaultvalue' => self::$default_config['institutionidp'],
                'help' => true,
880
                'class' => 'under-label',
881
            ),
882
883
884
885
            'idpselectjs' => array(
                'type'         => 'html',
                'value'        => $idpselectjs,
            ),
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
            'institutionattribute' => array(
                'type'  => 'text',
                'title' => get_string('institutionattribute', 'auth.saml', $institution),
                'rules' => array(
                    'required' => true,
                ),
                'defaultvalue' => self::$default_config['institutionattribute'],
                'help' => true,
            ),
            'institutionvalue' => array(
                'type'  => 'text',
                'title' => get_string('institutionvalue', 'auth.saml'),
                'rules' => array(
                'required' => true,
                ),
                'defaultvalue' => self::$default_config['institutionvalue'],
                'help' => true,
            ),
            'institutionregex' => array(
905
                'type'         => 'switchbox',
906
907
908
909
910
911
912
913
914
915
916
917
918
                'title' => get_string('institutionregex', 'auth.saml'),
                'defaultvalue' => self::$default_config['institutionregex'],
                'help' => true,
            ),
            'user_attribute' => array(
                'type'  => 'text',
                'title' => get_string('userattribute', 'auth.saml'),
                'rules' => array(
                    'required' => true,
                ),
                'defaultvalue' => self::$default_config['user_attribute'],
                'help' => true,
            ),
919
            'remoteuser' => array(
920
                'type'         => 'switchbox',
921
922
923
924
                'title' => get_string('remoteuser', 'auth.saml'),
                'defaultvalue' => self::$default_config['remoteuser'],
                'help'  => true,
            ),
925
            'loginlink' => array(
926
                'type'         => 'switchbox',
927
928
929
930
931
                'title' => get_string('loginlink', 'auth.saml'),
                'defaultvalue' => self::$default_config['loginlink'],
                'disabled' => (self::$default_config['remoteuser'] ? false : true),
                'help'  => true,
            ),
932
            'updateuserinfoonlogin' => array(
933
                'type'         => 'switchbox',
934
935
936
937
938
                'title' => get_string('updateuserinfoonlogin', 'auth.saml'),
                'defaultvalue' => self::$default_config['updateuserinfoonlogin'],
                'help'  => true,
            ),
            'weautocreateusers' => array(
939
                'type'         => 'switchbox',
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
                'title' => get_string('weautocreateusers', 'auth.saml'),
                'defaultvalue' => self::$default_config['weautocreateusers'],
                'help'  => true,
            ),
            'firstnamefield' => array(
                'type'  => 'text',
                'title' => get_string('samlfieldforfirstname', 'auth.saml'),
                'defaultvalue' => self::$default_config['firstnamefield'],
                'help'  => true,
            ),
            'surnamefield' => array(
                'type'  => 'text',
                'title' => get_string('samlfieldforsurname', 'auth.saml'),
                'defaultvalue' => self::$default_config['surnamefield'],
                'help'  => true,
            ),
            'emailfield' => array(
                'type'  => 'text',
                'title' => get_string('samlfieldforemail', 'auth.saml'),
                'defaultvalue' => self::$default_config['emailfield'],
                'help' => true,
            ),
962
963
964
965
966
967
            'studentidfield' => array(
                'type' => 'text',
                'title' => get_string('samlfieldforstudentid', 'auth.saml'),
                'defaultvalue' => self::$default_config['studentidfield'],
                'help' => true,
            ),
968
969
970
971
        );

        return array(
            'elements' => $elements,
972
            'renderer' => 'div'
973
974
975
        );
    }

Son Nguyen's avatar
Son Nguyen committed
976
    public static function validate_instance_config_options($values, Pieform $form) {
977

978
        // only allow remoteuser to be unset if usersuniquebyusername is NOT set
979
        if (!get_config('usersuniquebyusername') && !$values['remoteuser']) {
980
            $form->set_error('remoteuser', get_string('errorremoteuser1', 'auth.saml'));
981
        }
982

983
984
        if (!empty($values['institutionidp'])) {
            try {
985
986
                list ($entityid, $idps) = self::get_idps($values['institutionidp']);
                if (!$entityid) {
987
988
989
990
991
992
993
                    throw new Exception("Could not find entityId", 1);
                }
            }
            catch (Exception $e) {
                $form->set_error('institutionidp', get_string('errorbadmetadata', 'auth.saml'));
            }
        }
994
995
996
        else {
            $form->set_error('institutionidpentityid', get_string('errormissingmetadata', 'auth.saml'));
        }
997

998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
        // If we're using Mahara usernames (usr.username) instead of remote usernames
        // (auth_remote_user.remoteusername), then autocreation cannot be enabled if any
        // institutions have registration enabled.
        //
        // This is because a user self-registering with another institution might pick
        // a username that matches the username from this SAML service, allowing them
        // to highjack someone else's account.
        //
        // (see the comments in the request_user_authorise function above).
        if ((!$values['remoteuser']) && ($values['weautocreateusers']) && ($institutions = get_column('institution', 'name', 'registerallowed', '1'))) {
            $form->set_error('weautocreateusers', get_string('errorregistrationenabledwithautocreate1', 'auth.saml'));
1009
        }
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020

        // If enabled "We auto-create users" check that all required fields for that are set.
        if ($values['weautocreateusers']) {
            $required= array('firstnamefield', 'surnamefield', 'emailfield');
            foreach ($required as $required_field) {
                if (empty($values[$required_field])) {
                    $form->set_error($required_field, get_string('errorextrarequiredfield', 'auth.saml'));
                }
            }
        }

1021
1022
1023
1024
1025
1026
1027
        $dup = get_records_sql_array('SELECT COUNT(instance) AS instance FROM {auth_instance_config}
                                          WHERE ((field = \'institutionattribute\' AND value = ?) OR
                                                 (field = \'institutionvalue\' AND value = ?)) AND
                                                 instance IN (SELECT id FROM {auth_instance} WHERE authname = \'saml\' AND id != ?)
                                          GROUP BY instance
                                          ORDER BY instance',
                                      array($values['institutionattribute'], $values['institutionvalue'], $values['instance']));
1028
1029
1030
1031
1032
1033
1034
        if (is_array($dup)) {
            foreach ($dup as $instance) {
                if ($instance->instance >= 2) {
                    // we already have an authinstance with these same values
                    $form->set_error('institutionattribute', get_string('errorbadinstitutioncombo', 'auth.saml'));
                    break;
                }
1035
1036
            }
        }
1037
    }
1038

Son Nguyen's avatar
Son Nguyen committed
1039
    public static function save_instance_config_options($values, Pieform $form) {
1040
        global $SESSION;
1041

1042
        $authinstance = new stdClass();
1043

1044
1045
1046
1047
        if ($values['instance'] > 0) {
            $values['create'] = false;
            $current = get_records_assoc('auth_instance_config', 'instance', $values['instance'], '', 'field, value');
            $authinstance->id = $values['instance'];
1048
1049
        }
        else {
1050
1051
            $values['create'] = true;
            $lastinstance = get_records_array('auth_instance', 'institution', $values['institution'], 'priority DESC', '*', '0', '1');
1052

1053
1054
            if ($lastinstance == false) {
                $authinstance->priority = 0;
1055
1056
            }
            else {
1057
                $authinstance->priority = $lastinstance[0]->priority + 1;
1058
            }
1059
        }
1060

1061
1062
        $authinstance->institution  = $values['institution'];
        $authinstance->authname     = $values['authname'];
1063
        $authinstance->active       = (int) $values['active'];
1064
        $authinstance->instancename = $values['authname'];
1065

1066
1067
1068
1069
1070
1071
        if ($values['create']) {
            $values['instance'] = insert_record('auth_instance', $authinstance, 'id', true);
        }
        else {
            update_record('auth_instance', $authinstance, array('id' => $values['instance']));
        }
1072

1073
1074
1075
        if (empty($current)) {
            $current = array();
        }
1076

1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
        // grab the entityId from the metadata
        list ($entityid, $idps) = self::get_idps($values['institutionidp']);

        $changedxml = false;
        if ($values['institutionidpentityid'] != 'new') {
            $existingidpfile = AuthSaml::prepare_metadata_path($values['institutionidpentityid']);
            if (file_exists($existingidpfile)) {
                $rawxml = file_get_contents($existingidpfile);
                if ($rawxml != $values['institutionidp']) {
                    $changedxml = true;
                    // find out which institutions are using it
                    $duplicates = get_records_sql_array("
                        SELECT COUNT(aic.instance) AS instances
                        FROM {auth_instance_config} aic
                        JOIN {auth_instance} ai ON (ai.authname = 'saml' AND ai.id = aic.instance)
                        WHERE aic.field = 'institutionidpentityid' AND aic.value = ? AND aic.instance != ?",
                        array($values['institutionidpentityid'], $values['instance']));
                    if ($duplicates[0]->instances > 0) {
                        $SESSION->add_ok_msg(get_string('idpentityupdatedduplicates', 'auth.saml', $duplicates[0]->instances));
                    }
                    else {
                        $SESSION->add_ok_msg(get_string('idpentityupdated', 'auth.saml'));
                    }
                }
                else {
                    $SESSION->add_ok_msg(get_string('idpentityadded', 'auth.saml'));
                }
            }
            else {
                // existing idpfile not found so just save it
                $changedxml = true;
            }
1109
1110
        }
        else {
1111
1112
1113
           $values['institutionidpentityid'] = $entityid;
           $changedxml = true;
           $SESSION->add_ok_msg(get_string('idpentityadded', 'auth.saml'));
1114
1115
        }

1116
1117
1118
1119
1120
1121
1122
1123
        self::$default_config = array(
            'user_attribute' => $values['user_attribute'],
            'weautocreateusers' => $values['weautocreateusers'],
            'loginlink' => $values['loginlink'],
            'remoteuser' => $values['remoteuser'],
            'firstnamefield' => $values['firstnamefield'],
            'surnamefield' => $values['surnamefield'],
            'emailfield' => $values['emailfield'],
1124
            'studentidfield' => $values['studentidfield'],
1125
1126
1127
1128
            'updateuserinfoonlogin' => $values['updateuserinfoonlogin'],
            'institutionattribute' => $values['institutionattribute'],
            'institutionvalue' => $values['institutionvalue'],
            'institutionregex' => $values['institutionregex'],
1129
            'institutionidpentityid' => $entityid,
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
        );

        foreach(self::$default_config as $field => $value) {
            $record = new stdClass();
            $record->instance = $values['instance'];
            $record->field    = $field;
            $record->value    = $value;

            if ($values['create'] || !array_key_exists($field, $current)) {
                insert_record('auth_instance_config', $record);
            }
            else {
                update_record('auth_instance_config', $record, array('instance' => $values['instance'], 'field' => $field));
1143
1144
            }
        }
1145

1146
        // save the institution config
1147
1148
1149
        if ($changedxml) {
            $idpfile = AuthSaml::prepare_metadata_path($values['institutionidpentityid']);
            file_put_contents($idpfile, $values['institutionidp']);
1150
        }
1151

1152
1153
        return $values;
    }
1154
1155
1156
1157
1158

    /**
     * Add "SSO Login" link below the normal login form.
     */
    public static function login_form_elements() {
1159
1160
1161
1162
1163
        $url = get_config('wwwroot') . 'auth/saml/index.php';
        if (isset($_GET['login'])) {
            // We're on the transient login page. Redirect back to original page once we're done.
            $url .= '?wantsurl=' . urlencode(get_full_script_path());
        }
1164
1165
        $elements = array(
            'loginsaml' => array(
1166
                'value' => '<div class="login-externallink"><a class="btn btn-primary btn-xs" href="' . $url . '">' . get_string('login', 'auth.saml') . '</a></div>'
1167
1168
1169
1170
            )
        );
        return $elements;
    }
1171
1172
1173
1174

    public static function need_basic_login_form() {
        return false;
    }
1175
}
1176

1177
1178
1179
1180
1181
1182
1183
1184
1185
/**
 * Work around for missing function in 5.5 - is in 5.6
 */
function auth_saml_openssl_x509_fingerprint($cert, $hash) {
   $cert = preg_replace('#-.*-|\r|\n#', '', $cert);
   $bin = base64_decode($cert);
   return hash($hash, $bin);
}

1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
if (file_exists(get_config('docroot') . 'auth/saml/extlib/simplesamlphp/lib/SimpleSAML/XHTML/IdPDisco.php')) {
    require_once(get_config('docroot') . 'auth/saml/extlib/simplesamlphp/lib/SimpleSAML/XHTML/IdPDisco.php');

    class PluginAuthSaml_IdPDisco extends SimpleSAML_XHTML_IdPDisco
    {

        /**
         * Initializes this discovery service.
         *
         * The constructor does the parsing of the request. If this is an invalid request, it will throw an exception.
         *
         * @param array  $metadataSets Array with metadata sets we find remote entities in.
         * @param string $instance The name of this instance of the discovery service.
         *
         * @throws Exception If the request is invalid.
         */
        public function __construct(array $metadataSets, $instance) {
            assert('is_string($instance)');

            // initialize standard classes
            $this->config = SimpleSAML_Configuration::getInstance();
            $this->metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
            $this->instance = $instance;
            $this->metadataSets = $metadataSets;
            $this->isPassive = false;
        }

        public function getTheIdPs() {
            $idpList = $this->getIdPList();
            $idpList = $this->filterList($idpList);
            $preferredIdP = $this->getRecommendedIdP();
            return array('list' => $idpList, 'preferred' => $preferredIdP);
        }
    }
}