diff --git a/api/src/Auth/Saml.php b/api/src/Auth/Saml.php index 5a13d30eb8..7d3876cb20 100644 --- a/api/src/Auth/Saml.php +++ b/api/src/Auth/Saml.php @@ -38,10 +38,22 @@ use EGroupware\Api\Exception; * There are basically three possible scenarios currently supported: * a) a single IdP and SAML configured as authentication method * --> gives full SSO (login page is never displayed, it directly redirects to the IdP) - * b) one or multiple IdP, a discovery label and an other authentication type eg. SQL configured + * b) one or multiple IdP, a discovery label and another authentication type eg. SQL configured * --> uses the login page for local accounts plus a button or selectbox (depending on number of IdPs) to start SAML login * c) multiple IdP and SAML configured as authentication method * --> SimpleSAML discovery/selection page with a checkbox to remember the selection (SSO after first selection) + * + * EGroupware understands assertions / attributes send after authentication in the following ways: + * - as "urn:uuid:" uri as used eg. in the DFN federation + * - as LDAP attribute name send eg. by Univention IdP (either lower cased or in matching camelCase) + * + * SAML support in EGroupware automatically downgrades session cookies to SameSite=Lax, as our default SameSite=Strict + * does NOT work with SAML redirects from an IdP in a different domain (browser simply ignores the session cookies)! + * + * Please note: the following SimpleSAMLphp WARNING can be safely ignored (as EGroupware shares the session with it): + * There is already a PHP session with the same name as SimpleSAMLphp's session, or the + * 'session.phpsession.cookiename' configuration option is not set. Make sure to set SimpleSAMLphp's cookie name + * with a value not used by any other applications. */ class Saml implements BackendSSO { @@ -124,7 +136,10 @@ class Saml implements BackendSSO // get attributes for (automatic) account creation $attrs = $as->getAttributes(); - $username = $attrs[self::usernameOid()][0]; + if (!$attrs || empty($username = self::samlAttr(null, $attrs))) + { + throw new \Exception('Got NO '.(!$attrs ? 'attributes' : 'username attribute').' from SAML: '.json_encode($attrs)); + } // check if user already exists if (!$GLOBALS['egw']->accounts->name2id($username, 'account_lid', 'u')) @@ -142,9 +157,9 @@ class Saml implements BackendSSO return null; } $GLOBALS['auto_create_acct'] = [ - 'firstname' => $attrs[self::firstName][0], - 'lastname' => $attrs[self::lastName][0], - 'email' => $attrs[self::emailAddress][0], + 'firstname' => self::samlAttr(self::firstName, $attrs), + 'lastname' => self::samlAttr(self::lastName, $attrs), + 'email' => self::samlAttr(self::emailAddress, $attrs), ]; } } @@ -152,6 +167,12 @@ class Saml implements BackendSSO // check affiliation / group to add or remove self::checkAffiliation($username, $attrs, $GLOBALS['auto_create_acct']); + // Set SameSite attribute for cookies, as SAML redirect does NOT work with samesite=Strict, + // it requires as least Lax, otherwise the browser will ignore the session cookies set in Session::create()! + if ($GLOBALS['egw_info']['server']['cookie_samesite_attribute'] === 'Strict') + { + $GLOBALS['egw_info']['server']['cookie_samesite_attribute'] = 'Lax'; + } // return user session return $GLOBALS['egw']->session->create($username, null, null, false, false); } @@ -207,17 +228,17 @@ class Saml implements BackendSSO switch($GLOBALS['egw_info']['server']['saml_join']) { case 'usernameemail': - if (!empty($attrs[self::emailAddress])) + if (!empty(self::samlAttr(self::emailAddress, $attrs))) { unset($update['account_email']); // force email update } // fall through case 'username': - $update['account_lid'] = $attrs[self::usernameOid()][0]; + $update['account_lid'] = self::samlAttr(null, $attrs); break; case 'description': - $update['account_description'] = $attrs[self::usernameOid()][0]; + $update['account_description'] = self::samlAttr(null, $attrs); break; } // update other attributes @@ -227,9 +248,9 @@ class Saml implements BackendSSO 'account_lastname' => self::lastName, ] as $name => $oid) { - if (!empty($attrs[$oid]) && ($name !== 'account_email' || empty($update['account_email']))) + if (!empty($value = self::samlAttr($oid, $attrs)) && ($name !== 'account_email' || empty($update['account_email']))) { - $update[$name] = $attrs[$oid][0]; + $update[$name] = $value; } } // update account if necessary @@ -570,6 +591,60 @@ class Saml implements BackendSSO return self::emailAddress; } + /** + * Get SAML attribute by name, taking into account that some IdP, like eg. Univention, use not the oid but the LDAP name + * + * @param string $name attribute name or null for the configured username + * @param array $attrs as returned by SimpleSAML\Auth\Simple::getAttributes() + * @param ?array $config default use config from $GLOBALS['egw_info']['server'] + * @return string[]|string|null + */ + private static function samlAttr($name, array $attrs, array $config=null) + { + if (!isset($config)) $config = $GLOBALS['egw_info']['server']; + + switch($name ?: $config['saml_username'] ?? 'emailAddress') + { + case 'emailAddress': + case 'mailPrimaryAddress': + case self::emailAddress: + $keys = [self::emailAddress, 'emailAddress', 'mailPrimaryAddress']; + break; + case 'eduPersonPrincipalName': + case self::eduPersonPricipalName: + $keys = [self::eduPersonPricipalName, 'eduPersonPrincipalName']; + break; + case 'eduPersonUniqueId': + case self::eduPersonUniqueId: + $keys = [self::eduPersonUniqueId, 'eduPersonUniqueId']; + break; + case 'uid': + case self::uid: + $keys = [self::uid, 'uid']; + break; + case 'sn': + case 'surname': + case self::lastName: + $keys = [self::lastName, 'sn', 'surname']; + break; + case 'givenName': + case self::firstName: + $keys = [self::firstName, 'givenName']; + break; + case 'customOid': + $keys = ['urn:oid:'.$config['saml_username_oid'], $config['saml_username_oid']]; + break; + } + foreach($keys as $key) + { + if (isset($attrs[$key]) || isset($attrs[$key = strtolower($key)])) + { + return count($attrs[$key]) === 1 ? current($attrs[$key]) : $attrs[$key]; + } + } + return null; + } + /** * eduPersonAffiliation attribute */ @@ -589,7 +664,7 @@ class Saml implements BackendSSO // check if affiliation is configured and attribute returned by IdP $attr = $config['saml_affiliation'] === 'eduPersonAffiliation' ? self::eduPersonAffiliation : $config['saml_affiliation_oid']; - if (!empty($attr) && !empty($attrs[$attr]) && !empty($config['saml_affiliation_group']) && !empty($config['saml_affiliation_values']) && + if (!empty($attr) && !empty($affiliation = self::samlAttr($attr, $attrs)) && !empty($config['saml_affiliation_group']) && !empty($config['saml_affiliation_values']) && ($gid = $GLOBALS['egw']->accounts->name2id($config['saml_affiliation_group'], 'account_lid', 'g'))) { if (!isset($auto_create_acct) && ($accout_id = $GLOBALS['egw']->accounts->name2id($username, 'account_lid', 'u'))) @@ -597,7 +672,7 @@ class Saml implements BackendSSO $memberships = $GLOBALS['egw']->accounts->memberships($accout_id, true); } // check if attribute matches given values to add the extra membership - if (array_intersect($attrs[$attr], preg_split('/, */', $config['saml_affiliation_values']))) + if (array_intersect($affiliation, preg_split('/, */', $config['saml_affiliation_values']))) { if (isset($auto_create_acct)) { diff --git a/api/src/Framework.php b/api/src/Framework.php index ea631f0fab..f29a784c0c 100644 --- a/api/src/Framework.php +++ b/api/src/Framework.php @@ -254,7 +254,7 @@ abstract class Framework extends Framework\Extra // run egw destructor now explicit, in case a (notification) email is send via Egw::on_shutdown(), // as stream-wrappers used by Horde Smtp fail when PHP is already in destruction - $GLOBALS['egw']->__destruct(); + if (isset($GLOBALS['egw'])) $GLOBALS['egw']->__destruct(); exit; } diff --git a/doc/UCS-SAML-SSO.md b/doc/UCS-SAML-SSO.md new file mode 100644 index 0000000000..37fc02ea8d --- /dev/null +++ b/doc/UCS-SAML-SSO.md @@ -0,0 +1,169 @@ +## Configure EGroupware for SSO via SAML with Univention + +### SAML IdP need to be enabled, see [UCS Manual about login](https://docs.software-univention.de/manual/5.0/en/central-management-umc/login.html#central-management-umc-login) + +* ```ucs-sso.``` need to resolve to one or more primary or secondary domain controllers +* if you use LetsEncrypt, you should add the above domain to your certificate +* UCS config registry variable ```portal/auth-mode``` has to be set to ```saml``` +* portal server needs to be restarted: ```systemctl restart univention-portal-server.service``` + +### EGroupware needs to be configured for SAML via Setup (```https://egw.example.org/egroupware/setup/```) + * Login into setup with user ```admin``` and the password from ```/var/lib/egroupware/egroupware-docker-install.log``` + * Go to [Edit current configuration] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
If using SAML 2.0 / Shibboleth / SimpleSAMLphp:
Label to display as option on login page:
or leave empty and select SAML as authentication type above for single sign on
Identity Provider:
You can specify multiple IdP on separate lines.
+ Metadata: + refresh + + +
+
Certificate Metadata is signed with: (Will be downloaded once, unless changed.)
Result data to use as username: + + +
Result data to add or remove extra membership: + + +
Result values (comma-separated) and group-name to add or remove: + + +
Allow SAML logins to join existing accounts:
(Requires SAML optional on login page and user to specify username and password)
+ +
Match SAML usernames to existing ones (use strings or regular expression): + + +
Some information for the own Service Provider metadata:Metadata URL
Name for Service Provider:
Technical contact: + + +
+ + +> For Univention the Metadata-URL is also the ID of the IdP! + +### Configure EGroupware as service-provide in your UCS domain: **Domain > LDAP directory > SAML service provider** +* Add: Type: SAML service provider + +``` +X Service provider activation status +Service provider identifier: https://egw.example.org/egroupware/saml/module.php/saml/sp/metadata.php/default-sp +Respond to this service provider URL after login: https://egw.example.org/egroupware/saml/module.php/saml/sp/saml2-acs.php/default-sp +Single logout URL for this service provider: https://egw.example.org/egroupware/saml/module.php/saml/sp/saml2-logout.php/default-sp +Format of NameID attribute: +Name of the attribute that is used as NameID: uid +Name of the organization for this service provider: EGroupware +Description of this service provider: +X Enable signed Logouts +``` +* After saving the above, you have to edit the `Extended Settings` of your new Service Provide +``` +X Allow transfering LDAP attributes to the Service Provider +LDAP Attribute Name: uid +LDAP Attribute Name: mailPrimaryAddress +LDAP Attribute Name: givenName +LDAP Attribute Name: sn +``` + +* Some useful links + * [How does Single Sign-on work?](https://www.univention.com/blog-en/2021/08/how-does-single-sign-on-work-with-saml-and-openidconnect/) + * [Reconfigure UCS Single Sign On](https://help.univention.com/t/reconfigure-ucs-single-sign-on/16161) + * [Create an SSO Login for Applications to Groups](https://www.univention.com/blog-en/2020/07/sso-login-for-groups/) + * [Adding a new external service provider](https://docs.software-univention.de/manual/5.0/en/domain-ldap/saml.html#domain-saml-additional-serviceprovider) + +### Configure EMail access without password + +> EGroupware normally use the session password to authenticate with the mail-server / Dovecot. If you use SSO (single sign on), EGroupware does not know your password and therefore can not pass it to the mail server. + +* login via ssh as user root to your mailserver +* note the password from /etc/dovecot/master-users (secretpassword in the example below) +``` +dovecotadmin:{PLAIN}secretpassword:::::: +``` +* add the following line to your /etc/dovecot/global-acls +```shell +echo "* user=dovecotadmin lra" >> /etc/dovecot/global-acls +doveadm reload +``` +* login with a user that has EGroupware admin rights +* go to **Administration**, right-click on a user and select **mail-account** +* in IMAP tab fill in the credentials: +``` +Admin user: dovecotadmin +Password: secretpassword + X Use admin credentials to connect without a session-password, e.g. for SSO +``` +* log out and in again with SSO and check everything works \ No newline at end of file diff --git a/saml/module.php b/saml/module.php index ebdc6b19c4..1ba59fc911 100644 --- a/saml/module.php +++ b/saml/module.php @@ -7,4 +7,14 @@ require_once('_include.php'); -\SimpleSAML\Module::process()->send(); +try { + \SimpleSAML\Module::process()->send(); +} +catch(\SimpleSAML\Error\NoState $e) { + // fix/hack NOSTATE error caused by EGroupware and therefore SimpleSAMLphp session lost due logout + if (strpos($_SERVER['PHP_SELF'], '/saml/module.php/saml/sp/saml2-logout.php/default-sp') !== false) + { + \EGroupware\Api\Egw::redirect(str_replace('/saml/module.php/saml/sp/saml2-logout.php/default-sp', '/logout.php', $_SERVER['PHP_SELF'])); + } + throw $e; +} \ No newline at end of file