* SAML: support joining a SAML account to an existing one, if configured in setup

notification of user does not yet work, as redirect on login page looses Api\Framework::message() :(
This commit is contained in:
Ralf Becker 2020-06-11 16:03:30 +02:00
parent a993938134
commit b7ed148371
5 changed files with 181 additions and 28 deletions

View File

@ -654,7 +654,7 @@ class Accounts
}
// Include previous contact information to avoid blank history rows
$contact = array_merge((array)$GLOBALS['egw']->contacts->read($data['person_id']), array(
$contact = array_merge((array)$GLOBALS['egw']->contacts->read($data['person_id'], true), array(
'n_given' => $data['account_firstname'],
'n_family' => $data['account_lastname'],
'email' => $data['account_email'],

View File

@ -76,10 +76,11 @@ class Auth
*
* Type will be stored in session, to automatic use the same type eg. for conditional use of SAML.
*
* @param Backend $type =null default is type from session / auth or login, or if not set config
* @param string $type =null default is type from session / auth or login, or if not set config
* @param bool $save_in_session default true, false: do not store backend
* @return Auth\Backend|Auth\BackendSSO
*/
static function backend($type=null)
static function backend($type=null, $save_in_session=true)
{
if (is_null($type))
{
@ -106,7 +107,7 @@ class Auth
{
throw new Exception\AssertionFailed("Auth backend class $backend_class is NO EGroupware\\Api\Auth\\Backend!");
}
Cache::setSession(__CLASS__, 'backend', $type);
if ($save_in_session) Cache::setSession(__CLASS__, 'backend', $type);
return $backend;
}

View File

@ -127,21 +127,153 @@ class Saml implements BackendSSO
// check if user already exists
if (!$GLOBALS['egw']->accounts->name2id($username, 'account_lid', 'u'))
{
// fail if auto-creation of authenticated users is NOT configured
if (empty($GLOBALS['egw_info']['server']['auto_create_acct']))
if (($existing = $this->checkJoin($_GET['login'], $_GET['passwd'], $username)) ||
($existing = $this->checkReplaceUsername($username)))
{
return null;
$username = $this->updateJoinedAccount($existing, $attrs);
}
else
{
// fail if auto-creation of authenticated users is NOT configured
if (empty($GLOBALS['egw_info']['server']['auto_create_acct']))
{
return null;
}
$GLOBALS['auto_create_acct'] = [
'firstname' => $attrs[self::firstName][0],
'lastname' => $attrs[self::lastName][0],
'email' => $attrs[self::emailAddress][0],
];
}
$GLOBALS['auto_create_acct'] = [
'firstname' => $attrs[self::firstName][0],
'lastname' => $attrs[self::lastName][0],
'email' => $attrs[self::emailAddress][0],
];
}
// return user session
return $GLOBALS['egw']->session->create($username, null, null, false, false);
}
/**
* Check if joining a SAML account with an existing accounts is enabled and user specified correct credentials
*
* @param string $login login-name entered by user
* @param string $password password entered by user
* @param string $username SAML username
* @return string|null|false existing user-name to join or
* null if no joining configured or missing credentials or user does not exist or
* false if authentication with given credentials failed
*/
private function checkJoin($login, $password, $username)
{
// check SAML username is stored in account_description and we have a matching account
if ($GLOBALS['egw_info']['server']['saml_join'] === 'description' &&
($account_id = $GLOBALS['egw']->accounts->name2id($username, 'account_description', 'u')))
{
return Api\Accounts::id2name($account_id);
}
// check join configuration and if user specified credentials
if (empty($GLOBALS['egw_info']['server']['saml_join']) || empty($login) || empty($password))
{
return null;
}
$backend = Api\Auth::backend($GLOBALS['egw_info']['server']['auth_type'] ?: 'sql', false);
if (!$backend->authenticate($login, $password))
{
return false;
}
return $login;
}
/**
* Update joined account, if configured
*
* @param $account_lid existing account_lid
* @param array $attrs saml attributes incl. SAML username
* @return string username to use
*/
private function updateJoinedAccount($account_lid, array $attrs)
{
if (empty($GLOBALS['egw_info']['server']['saml_join']))
{
return $account_lid;
}
$account = $update = $GLOBALS['egw']->accounts->read($account_lid);
switch($GLOBALS['egw_info']['server']['saml_join'])
{
case 'usernameemail':
if (!empty($attrs[self::emailAddress]))
{
unset($update['account_email']); // force email update
}
// fall through
case 'username':
$update['account_lid'] = $attrs[self::usernameOid()][0];
break;
case 'description':
$update['account_description'] = $attrs[self::usernameOid()][0];
break;
}
// update other attributes
foreach([
'account_email' => self::emailAddress,
'account_firstname' => self::firstName,
'account_lastname' => self::lastName,
] as $name => $oid)
{
if (!empty($attrs[$oid]) && ($name !== 'account_email' || empty($update['account_email'])))
{
$update[$name] = $attrs[$oid][0];
}
}
// update account if necessary
if ($account != $update)
{
// notify user about successful update of existing account and evtl. updated account-name
if ($GLOBALS['egw']->accounts->save($update))
{
$msg = lang('Your account has been updated with new data from your identity provider.');
if ($account['account_lid'] !== $update['account_lid'])
{
$msg .= "\n".lang("Please remember to use '%1' as username for local login's from now on!", $update['account_lid']);
// rename home directory
Api\Vfs::$is_root = true;
Api\Vfs::rename('/home/'.$account['account_lid'], '/home/'.$update['account_lid']);
Api\Vfs::$is_root = false;
}
Api\Framework::message($msg, 'notice');
}
else
{
Api\Framework::message(lang('Updating your account with new data from your identity provider failed!'), 'error');
}
}
return $update['account_lid'];
}
/**
* Check if some replacement is configured to match SAML usernames to existing ones
*
* @param string $username SAML username
* @return string|null existing username or null if not found
*/
private function checkReplaceUsername($username)
{
if (empty($GLOBALS['egw_info']['server']['saml_replace']))
{
return null;
}
$replace = $GLOBALS['egw_info']['server']['saml_replace'];
$with = $GLOBALS['egw_info']['server']['saml_replace_with'] ?? '';
$replaced = $replace[0] === '/' ? preg_replace($replaced, $with, $username) : str_replace($replace, $with, $username);
if (empty($replaced) || !$GLOBALS['egw']->accounts->name2id($replaced, 'account_lid', 'u'))
{
return null;
}
return $replaced;
}
/**
* Logout SSO system
*/

View File

@ -51,6 +51,22 @@ if(isset($GLOBALS['sitemgr_info']) && $GLOBALS['egw_info']['user']['userid'] ==
}
}
$forward = isset($_GET['phpgw_forward']) ? urldecode($_GET['phpgw_forward']) : @$_POST['phpgw_forward'];
if (!$forward)
{
$extra_vars['cd'] = 'yes';
$forward = '/index.php';
}
else
{
list($forward,$extra_vars) = explode('?',$forward,2);
// only append cd=yes, if there is not already a cd value!
if (strpos($extra_vars, 'cd=') === false)
{
$extra_vars .= ($extra_vars ? '&' : '').'cd=yes';
}
}
// SSO login: CAS, SAML, ...
if (($GLOBALS['sessionid'] = Api\Auth::login()))
{
@ -241,22 +257,6 @@ else
// check if new translations are available
Api\Translation::check_invalidate_cache();
$forward = isset($_GET['phpgw_forward']) ? urldecode($_GET['phpgw_forward']) : @$_POST['phpgw_forward'];
if (!$forward)
{
$extra_vars['cd'] = 'yes';
$forward = '/index.php';
}
else
{
list($forward,$extra_vars) = explode('?',$forward,2);
// only append cd=yes, if there is not already a cd value!
if (strpos($extra_vars, 'cd=') === false)
{
$extra_vars .= ($extra_vars ? '&' : '').'cd=yes';
}
}
if(strpos($_SERVER['HTTP_REFERER'], $_SERVER['REQUEST_URI']) === false) {
// login requuest does not come from login.php
// redirect to referer on logout

View File

@ -521,6 +521,26 @@
</td>
</tr>
<tr class="row_on">
<td>{lang_Allow_SAML_logins_to_join_existing_accounts}:<br/>({lang_Requires_SAML_optional_on_login_page_and_user_to_specify_username_and_password})</td>
<td>
<select name="newsettings[saml_join]">
<option value="">{lang_No}</option>
<option value="usernameemail"{selected_saml_join_usernameemail}>{lang_Replace_username_and_email}</option>
<option value="username"{selected_saml_join_username}>{lang_Replace_username_and_keep_email}</option>
<option value="description"{selected_saml_join_description}>{lang_Use_account_description_to_store_SAML_username}</option>
</select>
</td>
</tr>
<tr class="row_off">
<td>{lang_Match_SAML_usernames_to_existing_ones_(use_strings_or_regular_expression)}:</td>
<td>
<input name="newsettings[saml_replace]" placeholder="{lang_replace}: '@uni-kl.de' {lang_or} '/@(uni-kl\.de)$/'" value="{value_saml_replace}" size="40"/>
<input name="newsettings[saml_replace_with]" placeholder="{lang_with}: '@rhrk.uni-kl.de' {lang_or} '@rhrk.$1'" value="{value_saml_replace_with}" size="35"/>
</td>
</tr>
<tr class="row_on" height="25">
<td>{lang_Some_information_for_the_own_Service_Provider_metadata}:</td>
<td><a href="{value_webserver_url}/saml/module.php/saml/sp/metadata.php/default-sp">{lang_Metadata_URL}</a></td>