Skip to content

Commit

Permalink
Merge pull request #315 from catalyst/multiple-idp-improvements
Browse files Browse the repository at this point in the history
Multiple idp improvements
  • Loading branch information
rhell4 authored Mar 10, 2019
2 parents c35ea2c + 525fd63 commit c82ca47
Show file tree
Hide file tree
Showing 20 changed files with 526 additions and 295 deletions.
192 changes: 122 additions & 70 deletions auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

global $CFG;
require_once($CFG->libdir.'/authlib.php');
require_once(__DIR__.'/locallib.php');

/**
* Plugin for Saml2 authentication.
Expand All @@ -42,9 +43,9 @@ class auth_plugin_saml2 extends auth_plugin_base {
'idpname' => '',
'idpdefaultname' => '', // Set in constructor.
'idpmetadata' => '',
'idpmduinames' => '',
'idpmduilogos' => '',
'idpentityids' => '',
'multiidp' => false,
'defaultidp' => null,
'metadataentities' => '',
'debug' => 0,
'duallogin' => saml2_settings::OPTION_DUAL_LOGIN_YES,
'anyauth' => 1,
Expand All @@ -65,7 +66,7 @@ class auth_plugin_saml2 extends auth_plugin_base {
* Constructor.
*/
public function __construct() {
global $CFG;
global $CFG, $DB;
$this->defaults['idpdefaultname'] = get_string('idpnamedefault', 'auth_saml2');
$this->authtype = 'saml2';
$mdl = new moodle_url($CFG->wwwroot);
Expand All @@ -76,16 +77,27 @@ public function __construct() {

// Parsed IdP metadata, either a list of IdP metadata urls or a single XML blob.
$parser = new \auth_saml2\idp_parser();
$this->idplist = $parser->parse($this->config->idpmetadata);
$this->metadatalist = $parser->parse($this->config->idpmetadata);

// MDUINames provided by the metadata.
$this->idpmduinames = (array) json_decode($this->config->idpmduinames);
// EntitiyIDs provded by the metadata.
$this->metadataentities = auth_saml2_get_idps(true);

// MDUILogos provided by the metadata.
$this->idpmduilogos = (array) json_decode($this->config->idpmduilogos);
// Check if we have mutiple IdPs configured.
// If we have mutliple metadata entries set multiidp to true.
$this->multiidp = false;

// EntitiyIDs provded by the metadata.
$this->idpentityids = (array) json_decode($this->config->idpentityids);
if (count($this->metadataentities) > 1) {
$this->multiidp = true;
} else {
// If we have mutliple IdP entries for a metadata set multiidp to true.
foreach ($this->metadataentities as $idpentities) {
if (count($idpentities) > 1) {
$this->multiidp = true;
}
}
}

$this->defaultidp = auth_saml2_get_default_idp();
}

public function get_saml2_directory() {
Expand Down Expand Up @@ -160,27 +172,20 @@ public function loginpage_idp_list($wantsurl) {
// The array of IdPs to return.
$idplist = [];

foreach ($this->idplist as $idp) {
if (!array_key_exists($idp->idpurl, $this->idpentityids)) {
$message = "Missing identity configuration for '{$idp->idpurl}': " .
foreach ($this->metadatalist as $metadata) {
if (!array_key_exists($metadata->idpurl, $this->metadataentities)) {
$message = "Missing identity configuration for '{$metadata->idpurl}': " .
'Please check/save SAML2 configuration or if able to inspect the database, check: ' .
"SELECT value FROM {config_plugins} WHERE plugin='auth_saml2' AND name='idpentityids' " .
"SELECT * FROM {auth_saml2_idps} WHERE metadataurl='{$metadata->idpurl}' " .
'-- Remember to purge caches if you make changes in the database.';
debugging($message);
continue;
}

if (is_array($this->idpentityids[$idp->idpurl]) || is_object($this->idpentityids[$idp->idpurl])) {
$params = [
'wants' => $wantsurl,
'parentidp' => md5($idp->idpurl),
];

$idpurl = new moodle_url('/auth/saml2/selectidp.php', $params);
} else {
foreach ($this->metadataentities[$metadata->idpurl] as $idpentityid => $idp) {
$params = [
'wants' => $wantsurl,
'idp' => md5($this->idpentityids[$idp->idpurl]),
'idp' => $idpentityid,
];

// The wants url may already be routed via login.php so don't re-re-route it.
Expand All @@ -190,41 +195,45 @@ public function loginpage_idp_list($wantsurl) {
$idpurl = new moodle_url('/auth/saml2/login.php', $params);
}
$idpurl->param('passive', 'off');
}

// A default icon.
$idpicon = new pix_icon('i/user', 'Login');
// A default icon.
if (!empty($idp->logo)) {
$idpicon = $idp->logo;
} else {
$idpicon = new pix_icon('i/user', 'Login');
}

// Initially use the default name. This is suitable for a single IdP.
$idpname = $conf->idpdefaultname;
// Initially use the default name. This is suitable for a single IdP.
$idpname = $conf->idpdefaultname;

// When multiple IdPs are configured, use a different default based on the IdP.
if (count($this->idplist) > 1) {
$host = parse_url($idp->idpurl, PHP_URL_HOST);
$idpname = get_string('idpnamedefault_varaible', 'auth_saml2', $host);
}
// When multiple IdPs are configured, use a different default based on the IdP.
if ($this->multiidp) {
$host = parse_url($idp->entityid, PHP_URL_HOST);
$idpname = get_string('idpnamedefault_varaible', 'auth_saml2', $host);
}

// Use a forced override set in the idpmetadata field.
if (!empty($idp->idpname)) {
$idpname = $idp->idpname;
} else {
// There is no forced override, try to use the <mdui:DisplayName> if it exists.
if (!empty($this->idpmduinames[$idp->idpurl])) {
$idpname = $this->idpmduinames[$idp->idpurl];
// Use a forced override set in the idpmetadata field.
if (!empty($metadata->idpname)) {
$idpname = $metadata->idpname;
}
}

// Has the IdP label override been set in the admin configuration?
// This is best used with a single IdP. Multiple IdP overrides are different.
if (!empty($conf->idpname)) {
$idpname = $conf->idpname;
}
// Has the IdP label override been set in the admin configuration?
// This is best used with a single IdP. Multiple IdP overrides are different.
if (!empty($conf->idpname)) {
$idpname = $conf->idpname;
}

$idplist[] = [
'url' => $idpurl,
'icon' => $idpicon,
'name' => $idpname,
];
// Try to use the <mdui:DisplayName> if it exists.
if (!empty($idp->name)) {
$idpname = $idp->name;
}

$idplist[] = [
'url' => $idpurl,
'icon' => $idpicon,
'name' => $idpname,
];
}
}

return $idplist;
Expand Down Expand Up @@ -257,9 +266,9 @@ public function is_configured() {
return false;
}

$eids = $this->idpentityids;
foreach ($eids as $entityid) {
$file = $this->get_file_idp_metadata_file($entityid);
$eids = $this->metadataentities;
foreach ($eids as $metadataid => $idps) {
$file = $this->get_file_idp_metadata_file($metadataid);
if (!file_exists($file)) {
$this->log(__FUNCTION__ . ' file not found, ' . $file);
return false;
Expand Down Expand Up @@ -342,6 +351,7 @@ public function should_login_redirect() {
$this->log(__FUNCTION__ . ' enter');

$saml = optional_param('saml', null, PARAM_BOOL);
$multiidp = optional_param('multiidp', false, PARAM_BOOL);
// Also support noredirect param - used by other auth plugins.
$noredirect = optional_param('noredirect', 0, PARAM_BOOL);
if (!empty($noredirect)) {
Expand All @@ -361,6 +371,13 @@ public function should_login_redirect() {
return false;
}

// Redirect to the select IdP page if requested so.
if ($multiidp) {
$this->log(__FUNCTION__ . ' redirecting due to multiidp=on parameter');
$idpurl = new moodle_url('/auth/saml2/selectidp.php');
redirect($idpurl);
}

// Never redirect if has error.
if (!empty($_GET['SimpleSAML_Auth_State_exceptionId'])) {
$this->log(__FUNCTION__ . ' skipping due to SimpleSAML_Auth_State_exceptionId');
Expand Down Expand Up @@ -435,27 +452,38 @@ public function saml_login() {
require_once("$CFG->dirroot/login/lib.php");

// Set the default IdP to be the first in the list. Used when dual login is disabled.
$arr = array_reverse($saml2auth->idpentityids);
$idp = md5(array_pop($arr));
$arr = array_reverse($saml2auth->metadataentities);
$metadataentities = array_pop($arr);
$idpentity = array_pop($metadataentities);
$idp = md5($idpentity->entityid);

// Specify the default IdP to use.
$SESSION->saml2idp = $idp;

// We store the IdP in the session to generate the config/config.php array with the default local SP.
if (isset($_GET['idp'])) {
$idpalias = optional_param('idpalias', '', PARAM_TEXT);
if (!empty($idpalias)) {
$idpfound = false;

foreach ($saml2auth->metadataentities as $idpentities) {
foreach ($idpentities as $md5idpentityid => $idpentity) {
if ($idpalias == $idpentity->alias) {
$SESSION->saml2idp = $md5idpentityid;
$idpfound = true;
break 2;
}
}
}

if (!$idpfound) {
$this->error_page(get_string('noidpfound', 'auth_saml2', $idpalias));
}
} else if (isset($_GET['idp'])) {
$SESSION->saml2idp = $_GET['idp'];
} else if (is_null($SESSION->saml2idp)) {
// First IdP (default) is a multiple set IdP so we need to redirect the user to the selection page.
$arr = array_reverse($saml2auth->idpentityids);
$parentidp = md5(key($arr));
$wants = core_login_get_return_url();

$params = [
'wants' => $wants,
'parentidp' => $parentidp,
];

$idpurl = new moodle_url('/auth/saml2/selectidp.php', $params);
} else if (!is_null($saml2auth->defaultidp)) {
$SESSION->saml2idp = md5($saml2auth->defaultidp->entityid);
} else if ($saml2auth->multiidp) {
$idpurl = new moodle_url('/auth/saml2/selectidp.php');
redirect($idpurl);
}

Expand Down Expand Up @@ -524,6 +552,30 @@ public function saml_login() {
$this->error_page(get_string('wrongauth', 'auth_saml2', $uid));
}

// If admin has been set for this IdP we make the user an admin.
$adminidp = false;
foreach ($saml2auth->metadataentities as $idpentities) {
foreach ($idpentities as $md5idpentityid => $idpentity) {
if ($SESSION->saml2idp == $md5idpentityid) {
$adminidp = $idpentity->adminidp;
break 2;
}
}
}

if ($adminidp) {
$admins = array();
foreach (explode(',', $CFG->siteadmins) as $admin) {
$admin = (int)$admin;
if ($admin) {
$admins[$admin] = $admin;
}
}

$admins[$user->id] = $user->id;
set_config('siteadmins', implode(',', $admins));
}

// Make sure all user data is fetched.
$user = get_complete_user_data('username', $user->username);

Expand Down
40 changes: 28 additions & 12 deletions availableidps.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,34 @@
*/

require_once(__DIR__ . '/../../config.php');
require_once(__DIR__ . '/locallib.php');

global $DB;

require_login();
require_capability('moodle/site:config', context_system::instance());

$heading = get_string('manageidpsheading', 'auth_saml2');

$PAGE->set_url("$CFG->wwwroot/auth/saml2/avilableidps.php");
$PAGE->set_course($SITE);
$PAGE->set_title($SITE->shortname . ': ' . $heading);
$PAGE->set_heading($SITE->fullname);
$PAGE->set_pagelayout('standard');

$idpentityids = json_decode(get_config('auth_saml2', 'idpentityids'), true);
$idpmduinames = json_decode(get_config('auth_saml2', 'idpmduinames'), true);
$PAGE->navbar->add(get_string('administrationsite'));
$PAGE->navbar->add(get_string('plugins', 'admin'));
$PAGE->navbar->add(get_string('authentication', 'admin'));
$PAGE->navbar->add(get_string('pluginname', 'auth_saml2'),
new moodle_url('/admin/settings.php', array('section' => 'authsettingsaml2')));
$PAGE->navbar->add($heading);

$PAGE->requires->css('/auth/saml2/styles.css');

$metadataentities = auth_saml2_get_idps(false, true);

$data = [
'idpentityids' => $idpentityids,
'idpmduinames' => $idpmduinames
'metadataentities' => $metadataentities
];

$action = new moodle_url('/auth/saml2/availableidps.php');
Expand All @@ -46,18 +62,18 @@
}

if ($fromform = $mform->get_data()) {
// Go through each metadata group and override the idpentities.
// We don't overrride the whole lot because metadata entries with only 1 IdP entity won't be in the form.
foreach ($fromform->values as $metadata => $idpentityvalues) {
$idpentityids[$metadata] = $idpentityvalues;
// Go through each idp and update its flags.
foreach ($fromform->metadataentities as $idpentities) {
foreach ($idpentities as $idpentity) {
$DB->update_record('auth_saml2_idps', (object) $idpentity);
}
}

set_config('idpentityids', json_encode($idpentityids), 'auth_saml2');
} else {
$mform->set_data(array('values' => $idpentityids));
$mform->set_data(array('metadataentities' => $metadataentities));
}

echo $OUTPUT->header();
echo "<h1>Select available IdPs</h1>";
echo $OUTPUT->heading($heading);
echo get_string('multiidpinfo', 'auth_saml2');
$mform->display();
echo $OUTPUT->footer();
Loading

0 comments on commit c82ca47

Please sign in to comment.