diff --git a/api/src/Accounts.php b/api/src/Accounts.php index 26ed7a0610..63df1cd3e9 100644 --- a/api/src/Accounts.php +++ b/api/src/Accounts.php @@ -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'], diff --git a/api/src/Auth.php b/api/src/Auth.php index 86a61f5e18..6523e9ad56 100644 --- a/api/src/Auth.php +++ b/api/src/Auth.php @@ -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; } diff --git a/api/src/Auth/Saml.php b/api/src/Auth/Saml.php index 51c8c2668e..896a964562 100644 --- a/api/src/Auth/Saml.php +++ b/api/src/Auth/Saml.php @@ -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 */ diff --git a/login.php b/login.php index d288ba0a2f..5e1143da0a 100755 --- a/login.php +++ b/login.php @@ -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 diff --git a/setup/templates/default/config.tpl b/setup/templates/default/config.tpl index d1fcd6ee92..efd07a269d 100644 --- a/setup/templates/default/config.tpl +++ b/setup/templates/default/config.tpl @@ -521,6 +521,26 @@ + + {lang_Allow_SAML_logins_to_join_existing_accounts}:
({lang_Requires_SAML_optional_on_login_page_and_user_to_specify_username_and_password}) + + + + + + + {lang_Match_SAML_usernames_to_existing_ones_(use_strings_or_regular_expression)}: + + + + + + {lang_Some_information_for_the_own_Service_Provider_metadata}: {lang_Metadata_URL}