forked from extern/egroupware
* SAML/Univention: support for Univention SAML IdP for SSO incl. docu
This commit is contained in:
parent
7bcfdbfa5e
commit
3c4b03ca3c
@ -42,6 +42,18 @@ use EGroupware\Api\Exception;
|
||||
* --> 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:<oid>" 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))
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
169
doc/UCS-SAML-SSO.md
Normal file
169
doc/UCS-SAML-SSO.md
Normal file
@ -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.<domain>``` 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]
|
||||
|
||||
<html>
|
||||
<table border="0" align="center" cellspacing="0" width="90%">
|
||||
<tbody>
|
||||
<tr class="th">
|
||||
<td colspan="2"><b>If using SAML 2.0 / Shibboleth / SimpleSAMLphp:</b></td>
|
||||
</tr>
|
||||
|
||||
<tr class="row_off">
|
||||
<td>Label to display as option on login page:<br>or leave empty and select SAML as authentication type above for single sign on</td>
|
||||
<td><input name="newsettings[saml_discovery]" placeholder="University Login" value="Test SSO" size="20"></td>
|
||||
</tr>
|
||||
|
||||
<tr class="row_on">
|
||||
<td>Identity Provider:<br>You can specify multiple IdP on separate lines.</td>
|
||||
<td><textarea name="newsettings[saml_idp]" placeholder="https://idp.uni-kl.de/idp/shibboleth" rows="3" cols="64">https://ucs.example.org/simplesamlphp/saml2/idp/metadata.php</textarea></td>
|
||||
</tr>
|
||||
|
||||
<tr class="row_off">
|
||||
<td>
|
||||
Metadata:
|
||||
refresh
|
||||
<select name="newsettings[saml_metadata_refresh]">
|
||||
<option value="daily">daily</option>
|
||||
<option value="weekly">weekly</option>
|
||||
<option value="no">not automatic</option>
|
||||
<option value="now" selected>just now</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input name="newsettings[saml_metadata]" placeholder="https://www.aai.dfn.de/fileadmin/metadata/dfn-aai-metadata.xml" value="https://ucs.example.org/simplesamlphp/saml2/idp/metadata.php" size="64"><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="row_on">
|
||||
<td>Certificate Metadata is signed with: (Will be downloaded once, unless changed.)</td>
|
||||
<td><input name="newsettings[saml_certificate]" placeholder="https://www.aai.dfn.de/fileadmin/metadata/dfn-aai.pem" value="https://ucs.example.org/simplesamlphp/saml2/idp/certificate" size="64"></td>
|
||||
</tr>
|
||||
<tr class="row_off">
|
||||
<td>Result data to use as username:</td>
|
||||
<td>
|
||||
<select name="newsettings[saml_username]">
|
||||
<option value="eduPersonPrincipalName">eduPersonPrincipalName</option>
|
||||
<option value="eduPersonUniqueId">eduPersonUniqueId</option>
|
||||
<option value="emailAddress">emailAddress</option>
|
||||
<option value="uid" selected="">uid</option>
|
||||
<option value="customOid">custom OID</option>
|
||||
</select>
|
||||
<input name="newsettings[saml_username_oid]" value="" placeholder="urn:oid:x.x.x.x" size="40">
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="row_on">
|
||||
<td>Result data to add or remove extra membership:</td>
|
||||
<td>
|
||||
<select name="newsettings[saml_affiliation]">
|
||||
<option value="eduPersonAffiliation" selected="">eduPersonAffiliation</option>
|
||||
<option value="custom">custom OID</option>
|
||||
</select>
|
||||
<input name="newsettings[saml_affiliation_oid]" value="" placeholder="urn:oid:x.x.x.x" size="40">
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="row_off">
|
||||
<td>Result values (comma-separated) and group-name to add or remove:</td>
|
||||
<td>
|
||||
<input name="newsettings[saml_affiliation_values]" value="" placeholder="staff, ..." size="30">
|
||||
<input name="newsettings[saml_affiliation_group]" value="" placeholder="Teachers" size="30">
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="row_on">
|
||||
<td>Allow SAML logins to join existing accounts:<br>(Requires SAML optional on login page and user to specify username and password)</td>
|
||||
<td>
|
||||
<select name="newsettings[saml_join]">
|
||||
<option value="">No</option>
|
||||
<option value="usernameemail">Replace username and email</option>
|
||||
<option value="username">Replace username and keep email</option>
|
||||
<option value="description">Use account description to store SAML username</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="row_off">
|
||||
<td>Match SAML usernames to existing ones (use strings or regular expression):</td>
|
||||
<td>
|
||||
<input name="newsettings[saml_replace]" placeholder="replace: '@uni-kl.de' or '/@(uni-kl\.de)$/'" value="" size="40">
|
||||
<input name="newsettings[saml_replace_with]" placeholder="with: '@rhrk.uni-kl.de' or '@rhrk.$1'" value="" size="35">
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="row_on" height="25">
|
||||
<td>Some information for the own Service Provider metadata:</td>
|
||||
<td><a href="/egroupware/saml/module.php/saml/sp/metadata.php/default-sp">Metadata URL</a></td>
|
||||
</tr>
|
||||
<tr class="row_off">
|
||||
<td>Name for Service Provider:</td>
|
||||
<td><input name="newsettings[saml_sp]" placeholder="EGroupware" value="EGroupware" size="40"></td>
|
||||
</tr>
|
||||
<tr class="row_on">
|
||||
<td>Technical contact:</td>
|
||||
<td>
|
||||
<input name="newsettings[saml_contact_name]" value="Ralf Becker" placeholder="Name" size="24">
|
||||
<input name="newsettings[saml_contact_email]" value="rb@egroupware.org" placeholder="Email" size="24">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</html>
|
||||
|
||||
> 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
|
@ -7,4 +7,14 @@
|
||||
|
||||
require_once('_include.php');
|
||||
|
||||
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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user