mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-21 23:43:17 +01:00
SAML/Shibboleth with multiple IdP or optional on regular login page
This commit is contained in:
parent
06d6887744
commit
4c131c1866
@ -57,6 +57,14 @@ egw_LAB.wait(function()
|
|||||||
{ "svg": egw_webserverUrl+"/api/templates/default/images/login_discourse.svg", "url": "https://help.egroupware.org" },
|
{ "svg": egw_webserverUrl+"/api/templates/default/images/login_discourse.svg", "url": "https://help.egroupware.org" },
|
||||||
{ "svg": egw_webserverUrl+"/api/templates/default/images/login_github.svg", "url": "https://github.com/EGroupware/egroupware" }
|
{ "svg": egw_webserverUrl+"/api/templates/default/images/login_github.svg", "url": "https://github.com/EGroupware/egroupware" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// automatic submit of SAML IdP selection
|
||||||
|
jQuery('select.onChangeSubmit').on('change', function() {
|
||||||
|
if (this.value) {
|
||||||
|
this.form.method = 'GET';
|
||||||
|
this.form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -70,6 +70,7 @@ $setup_info['api']['hooks']['vfs_rmdir'] = 'EGroupware\\Api\\Vfs\\Sharing::vfsUp
|
|||||||
|
|
||||||
// hook to update SimpleSAMLphp config
|
// hook to update SimpleSAMLphp config
|
||||||
$setup_info['api']['hooks']['setup_config'] = \EGroupware\Api\Auth\Saml::class.'::setupConfig';
|
$setup_info['api']['hooks']['setup_config'] = \EGroupware\Api\Auth\Saml::class.'::setupConfig';
|
||||||
|
$setup_info['api']['hooks']['login_discovery'] = \EGroupware\Api\Auth\Saml::class.'::discovery';
|
||||||
|
|
||||||
// installation checks
|
// installation checks
|
||||||
$setup_info['api']['check_install'] = array(
|
$setup_info['api']['check_install'] = array(
|
||||||
|
@ -53,20 +53,38 @@ class Auth
|
|||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
|
* @param Backend $type =null default is type from session / auth or login, or if not set config
|
||||||
* @throws Exception\AssertionFailed if backend is not an Auth\Backend
|
* @throws Exception\AssertionFailed if backend is not an Auth\Backend
|
||||||
*/
|
*/
|
||||||
function __construct()
|
function __construct($type=null)
|
||||||
{
|
{
|
||||||
$this->backend = self::backend();
|
$this->backend = self::backend($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current backend
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function backendType()
|
||||||
|
{
|
||||||
|
return Cache::getSession(__CLASS__, 'backend');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instanciate a backend
|
* Instanciate a backend
|
||||||
*
|
*
|
||||||
* @param Backend $type =null
|
* 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
|
||||||
|
* @return Auth\Backend|Auth\BackendSSO
|
||||||
*/
|
*/
|
||||||
static function backend($type=null)
|
static function backend($type=null)
|
||||||
{
|
{
|
||||||
|
if (is_null($type))
|
||||||
|
{
|
||||||
|
$type = Cache::getSession(__CLASS__, 'backend') ?: null;
|
||||||
|
}
|
||||||
// do we have a hostname specific auth type set
|
// do we have a hostname specific auth type set
|
||||||
if (is_null($type) && !empty($GLOBALS['egw_info']['server']['auth_type_host']) &&
|
if (is_null($type) && !empty($GLOBALS['egw_info']['server']['auth_type_host']) &&
|
||||||
Header\Http::host() === $GLOBALS['egw_info']['server']['auth_type_hostname'])
|
Header\Http::host() === $GLOBALS['egw_info']['server']['auth_type_hostname'])
|
||||||
@ -88,18 +106,49 @@ class Auth
|
|||||||
{
|
{
|
||||||
throw new Exception\AssertionFailed("Auth backend class $backend_class is NO EGroupware\\Api\Auth\\Backend!");
|
throw new Exception\AssertionFailed("Auth backend class $backend_class is NO EGroupware\\Api\Auth\\Backend!");
|
||||||
}
|
}
|
||||||
|
Cache::setSession(__CLASS__, 'backend', $type);
|
||||||
|
|
||||||
return $backend;
|
return $backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt a SSO login
|
* Attempt a SSO login
|
||||||
*
|
*
|
||||||
|
* A different then the default backend can be selected by setting request parameter auth to the backend or
|
||||||
|
* setting "auth=$backend" to an arbitrary value eg. with a submit button named like that.
|
||||||
|
* To secure this behavior the server config "${auth}_discovery" has to be set (to a non-empty value)!
|
||||||
|
*
|
||||||
* @return string sessionid on successful login or null
|
* @return string sessionid on successful login or null
|
||||||
* @throws Exception\AssertionFailed
|
* @throws Exception\AssertionFailed
|
||||||
*/
|
*/
|
||||||
static function login()
|
static function login()
|
||||||
{
|
{
|
||||||
$backend = self::backend();
|
if (!empty($_REQUEST['auth']))
|
||||||
|
{
|
||||||
|
$type = $_REQUEST['auth'];
|
||||||
|
}
|
||||||
|
elseif (($auth = array_filter($_REQUEST, function($key)
|
||||||
|
{
|
||||||
|
return substr($key, 0, 5) === 'auth=';
|
||||||
|
}, ARRAY_FILTER_USE_KEY)))
|
||||||
|
{
|
||||||
|
$type = substr(key($auth), 5);
|
||||||
|
}
|
||||||
|
// to not allow enabling all sort of auth plugins by simply calling login.php?auth=xyz we require the
|
||||||
|
// plugin to be enabled via "${auth}_discovery" server config
|
||||||
|
if (!empty($type) && empty($GLOBALS['egw_info']['server'][$type.'_discovery']))
|
||||||
|
{
|
||||||
|
$type = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we need a (not yet authenticated) session so SAML / auth source selected "survives" eg. the SAML redirects
|
||||||
|
if (!empty($type) && !Session::get_sessionid())
|
||||||
|
{
|
||||||
|
session_start();
|
||||||
|
Session::egw_setcookie(Session::EGW_SESSION_NAME, session_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
$backend = self::backend($type ?? null);
|
||||||
|
|
||||||
return $backend instanceof Auth\BackendSSO ? $backend->login() : null;
|
return $backend instanceof Auth\BackendSSO ? $backend->login() : null;
|
||||||
}
|
}
|
||||||
@ -110,11 +159,9 @@ class Auth
|
|||||||
* @return null
|
* @return null
|
||||||
* @throws Exception\AssertionFailed
|
* @throws Exception\AssertionFailed
|
||||||
*/
|
*/
|
||||||
static function logout()
|
function logout()
|
||||||
{
|
{
|
||||||
$backend = self::backend();
|
return $this->backend instanceof Auth\BackendSSO ? $this->backend->logout() : null;
|
||||||
|
|
||||||
return $backend instanceof Auth\BackendSSO ? $backend->logout() : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -125,11 +172,9 @@ class Auth
|
|||||||
*
|
*
|
||||||
* @return array of needed keys in session
|
* @return array of needed keys in session
|
||||||
*/
|
*/
|
||||||
static function needSession()
|
function needSession()
|
||||||
{
|
{
|
||||||
$backend = self::backend();
|
return method_exists($this->backend, 'needSession') ? $this->backend->needSession() : [];
|
||||||
|
|
||||||
return method_exists($backend, 'needSession') ? $backend->needSession() : [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* EGroupware API - Authentication via SAML or everything supported by SimpleSAMLphp
|
* EGroupware API - Authentication via SAML, Shibboleth or everything supported by SimpleSAMLphp
|
||||||
*
|
*
|
||||||
* @link https://www.egroupware.org
|
* @link https://www.egroupware.org
|
||||||
* @link https://simplesamlphp.org/docs/stable/
|
* @link https://simplesamlphp.org/docs/stable/
|
||||||
@ -16,59 +16,46 @@ use SimpleSAML;
|
|||||||
use EGroupware\Api\Exception;
|
use EGroupware\Api\Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authentication based on SAML or everything supported by SimpleSAMLphp
|
* Authentication based on SAML, Shibboleth or everything supported by SimpleSAMLphp
|
||||||
*
|
*
|
||||||
* SimpleSAMLphp is installed together with EGroupware and a default configuration is created in EGroupware
|
* SimpleSAMLphp is installed together with EGroupware and a default configuration is created in EGroupware
|
||||||
* files subdirectory "saml", once "Saml" is set as authentication method in setup and eg. the login page is loaded.
|
* files subdirectory "saml" eg. when you first store it's configuration in Setup > Configuration > SAML/Shibboleth
|
||||||
*
|
*
|
||||||
* It will NOT work, before you configure at least one IdP (Identity Provider) for the default-sp (Service Provider) in saml/authsourcres.php:
|
* Storing setup configuration modifies the following files:
|
||||||
|
* a) $files_dir/saml/config.php
|
||||||
|
* b) $files_dir/saml/authsources.php (only "default-sp" is used currently)
|
||||||
|
* c) $files_dir/saml/metadata/*
|
||||||
|
* d) $files_dir/saml/cert/*
|
||||||
|
* Modification is only on certain values, everything else can be edited to suit your needs.
|
||||||
*
|
*
|
||||||
* // An authentication source which can authenticate against both SAML 2.0
|
* Initially also a key-pair is generated as $files_dir/saml/cert/saml.{pem,crt}.
|
||||||
* // and Shibboleth 1.3 IdPs.
|
* If you want or have to use a different certificate, best replace these with your files (they are referenced multiple times!).
|
||||||
* 'default-sp' => [
|
* They must stay in the files directory and can NOT be symlinks to eg. /etc, as only files dir is mounted into the container!
|
||||||
* 'saml:SP',
|
|
||||||
*
|
*
|
||||||
* // The entity ID of this SP.
|
* Authentication / configuration can be tested independent of EGroupware by using https://example.org/egroupware/saml/
|
||||||
* // Can be NULL/unset, in which case an entity ID is generated based on the metadata URL.
|
* with the "admin" user and password stored in cleartext in $files_dir/saml/config.php under 'auth.adminpassword'.
|
||||||
* 'entityID' => null,
|
|
||||||
*
|
*
|
||||||
* // The entity ID of the IdP this SP should contact.
|
* There are basically three possible scenarios currently supported:
|
||||||
* // Can be NULL/unset, in which case the user will be shown a list of available IdPs.
|
* a) a single IdP and SAML configured as authentication method
|
||||||
* 'idp' => 'https://samltest.id/saml/idp',
|
* --> 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
|
||||||
* And the IdP's metadata in saml/metadata/saml20-idp-remote.php
|
* --> 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
|
||||||
* $metadata['https://samltest.id/saml/idp'] = [
|
* --> SimpleSAML discovery/selection page with a checkbox to remember the selection (SSO after first selection)
|
||||||
* 'SingleSignOnService' => 'https://samltest.id/idp/profile/SAML2/Redirect/SSO',
|
|
||||||
* 'SingleLogoutService' => 'https://samltest.id/idp/profile/Logout',
|
|
||||||
* 'certificate' => 'samltest.id.pem',
|
|
||||||
* ];
|
|
||||||
*
|
|
||||||
* https://samltest.id/ is just a SAML / Shibboleth test side allowing AFTER uploading your metadata to test with a couple of static test-accounts.
|
|
||||||
*
|
|
||||||
* The metadata can be downloaded by via https://example.org/egroupware/saml/ under Federation, it also allows to test the authentication.
|
|
||||||
* The required (random) Admin password can be found in /var/lib/egrouwpare/default/saml/config.php searching for auth.adminpassword.
|
|
||||||
*
|
|
||||||
* Alternativly you can also modify the following metadata example by replacing https://example.org/ with your domain:
|
|
||||||
*
|
|
||||||
* <?xml version="1.0"?>
|
|
||||||
* <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://example.org/egroupware/saml/module.php/saml/sp/metadata.php/default-sp">
|
|
||||||
* <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
|
|
||||||
* <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.org/egroupware/saml/module.php/saml/sp/saml2-logout.php/default-sp"/>
|
|
||||||
* <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.org/egroupware/saml/module.php/saml/sp/saml2-acs.php/default-sp" index="0"/>
|
|
||||||
* <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:browser-post" Location="https://example.org/egroupware/saml/module.php/saml/sp/saml1-acs.php/default-sp" index="1"/>
|
|
||||||
* <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://example.org/egroupware/saml/module.php/saml/sp/saml2-acs.php/default-sp" index="2"/>
|
|
||||||
* <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:artifact-01" Location="https://example.org/egroupware/saml/module.php/saml/sp/saml1-acs.php/default-sp/artifact" index="3"/>
|
|
||||||
* </md:SPSSODescriptor>
|
|
||||||
* <md:ContactPerson contactType="technical">
|
|
||||||
* <md:GivenName>Admin</md:GivenName>
|
|
||||||
* <md:SurName>Name</md:SurName>
|
|
||||||
* <md:EmailAddress>mailto:admin@example.org</md:EmailAddress>
|
|
||||||
* </md:ContactPerson>
|
|
||||||
* </md:EntityDescriptor>
|
|
||||||
*/
|
*/
|
||||||
class Saml implements BackendSSO
|
class Saml implements BackendSSO
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Which entry in authsources.php to use.
|
||||||
|
*
|
||||||
|
* Setup > configuration always modifies "default-sp"
|
||||||
|
*
|
||||||
|
* A different SP can be configured via header.inc.php by adding at the end:
|
||||||
|
*
|
||||||
|
* EGroupware\Api\Auth\Saml::$auth_source = "other-sp";
|
||||||
|
*/
|
||||||
|
static public $auth_source = 'default-sp';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
@ -79,7 +66,7 @@ class Saml implements BackendSSO
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* authentication against SAML
|
* Authentication against SAML
|
||||||
*
|
*
|
||||||
* @param string $username username of account to authenticate
|
* @param string $username username of account to authenticate
|
||||||
* @param string $passwd corresponding password
|
* @param string $passwd corresponding password
|
||||||
@ -89,7 +76,7 @@ class Saml implements BackendSSO
|
|||||||
function authenticate($username, $passwd, $passwd_type='text')
|
function authenticate($username, $passwd, $passwd_type='text')
|
||||||
{
|
{
|
||||||
// login (redirects to IdP)
|
// login (redirects to IdP)
|
||||||
$as = new SimpleSAML\Auth\Simple('default-sp');
|
$as = new SimpleSAML\Auth\Simple(self::$auth_source);
|
||||||
$as->requireAuth();
|
$as->requireAuth();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -125,12 +112,13 @@ class Saml implements BackendSSO
|
|||||||
function login()
|
function login()
|
||||||
{
|
{
|
||||||
// login (redirects to IdP)
|
// login (redirects to IdP)
|
||||||
$as = new SimpleSAML\Auth\Simple('default-sp');
|
$as = new SimpleSAML\Auth\Simple(self::$auth_source);
|
||||||
$as->requireAuth();
|
$as->requireAuth(preg_match('|^https://|', $_REQUEST['auth=saml']) ?
|
||||||
|
['saml:idp' => $_REQUEST['auth=saml']] : []);
|
||||||
|
|
||||||
// cleanup session for EGroupware
|
/* cleanup session for EGroupware: currently NOT used as we share the session with SimpleSAMLphp
|
||||||
$session = SimpleSAML\Session::getSessionFromRequest();
|
$session = SimpleSAML\Session::getSessionFromRequest();
|
||||||
$session->cleanup();
|
$session->cleanup();*/
|
||||||
|
|
||||||
// get attributes for (automatic) account creation
|
// get attributes for (automatic) account creation
|
||||||
$attrs = $as->getAttributes();
|
$attrs = $as->getAttributes();
|
||||||
@ -159,8 +147,8 @@ class Saml implements BackendSSO
|
|||||||
*/
|
*/
|
||||||
function logout()
|
function logout()
|
||||||
{
|
{
|
||||||
$as = new SimpleSAML\Auth\Simple('default-sp');
|
$as = new SimpleSAML\Auth\Simple(self::$auth_source);
|
||||||
$as->logout();
|
if ($as->isAuthenticated()) $as->logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -173,7 +161,48 @@ class Saml implements BackendSSO
|
|||||||
*/
|
*/
|
||||||
function needSession()
|
function needSession()
|
||||||
{
|
{
|
||||||
return ['SimpleSAMLphp_SESSION'];
|
return ['SimpleSAMLphp_SESSION', Api\Session::EGW_APPSESSION_VAR]; // Auth stores backend via Cache::setSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
const IDP_DISPLAY_NAME = 'OrganizationDisplayName';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a IdP selection / discovery
|
||||||
|
*
|
||||||
|
* Will be displayed if IdP(s) are added in setup and a discovery label is specified.
|
||||||
|
*
|
||||||
|
* @return string|null html to display in login page or null to disable the selection
|
||||||
|
*/
|
||||||
|
static public function discovery()
|
||||||
|
{
|
||||||
|
if (empty($GLOBALS['egw_info']['server']['saml_discovery']) ||
|
||||||
|
!($metadata = self::metadata()))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
//error_log(__METHOD__."() metadata=".json_encode($metadata));
|
||||||
|
$lang = Api\Translation::$userlang;
|
||||||
|
$select = ['' => $GLOBALS['egw_info']['server']['saml_discovery']];
|
||||||
|
foreach($metadata as $idp => $data)
|
||||||
|
{
|
||||||
|
$select[$idp] = $data[self::IDP_DISPLAY_NAME][$lang] ?: $data[self::IDP_DISPLAY_NAME]['en'];
|
||||||
|
}
|
||||||
|
return count($metadata) > 1 ?
|
||||||
|
Api\Html::select('auth=saml', '', $select, true, 'class="onChangeSubmit"') :
|
||||||
|
Api\Html::input('auth=saml', $GLOBALS['egw_info']['server']['saml_discovery'], 'submit', 'formmethod="get"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array IdP => metadata pairs
|
||||||
|
*/
|
||||||
|
static public function metadata($files_dir=null)
|
||||||
|
{
|
||||||
|
$metadata = [];
|
||||||
|
if (file_exists($file = ($files_dir ?: $GLOBALS['egw_info']['server']['files_dir']).'/saml/metadata/saml20-idp-remote.php'))
|
||||||
|
{
|
||||||
|
include $file;
|
||||||
|
}
|
||||||
|
return $metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ASYNC_JOB_ID = 'saml_metadata_refresh';
|
const ASYNC_JOB_ID = 'saml_metadata_refresh';
|
||||||
@ -190,11 +219,7 @@ class Saml implements BackendSSO
|
|||||||
{
|
{
|
||||||
$config =& $location['newsettings'];
|
$config =& $location['newsettings'];
|
||||||
|
|
||||||
/*error_log(__METHOD__."() ".json_encode(array_filter($config, function($value, $key) {
|
if (empty($config['saml_idp'])) return; // nothing to do, if no idp defined
|
||||||
return substr($key, 0, 5) === 'saml_' || $key === 'auth_type';
|
|
||||||
}, ARRAY_FILTER_USE_BOTH), JSON_UNESCAPED_SLASHES));*/
|
|
||||||
|
|
||||||
if (empty($config['saml_idp'])) return; // nothing to do, if not idp defined
|
|
||||||
|
|
||||||
if (file_exists($config['files_dir'].'/saml/config.php'))
|
if (file_exists($config['files_dir'].'/saml/config.php'))
|
||||||
{
|
{
|
||||||
@ -204,7 +229,6 @@ class Saml implements BackendSSO
|
|||||||
|
|
||||||
// install or remove async job to refresh metadata
|
// install or remove async job to refresh metadata
|
||||||
static $freq2times = [
|
static $freq2times = [
|
||||||
'hourly' => ['min' => 4], // hourly at minute 4
|
|
||||||
'daily' => ['min' => 4, 'hour' => 4], // daily at 4:04am
|
'daily' => ['min' => 4, 'hour' => 4], // daily at 4:04am
|
||||||
'weekly' => ['min' => 4, 'hour' => 4, 'dow' => 5], // Saturdays as 4:04am
|
'weekly' => ['min' => 4, 'hour' => 4, 'dow' => 5], // Saturdays as 4:04am
|
||||||
];
|
];
|
||||||
@ -219,12 +243,37 @@ class Saml implements BackendSSO
|
|||||||
$async->cancel_timer(self::ASYNC_JOB_ID);
|
$async->cancel_timer(self::ASYNC_JOB_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only refresh metadata if we have to, or request by user
|
||||||
if ($config['saml_metadata_refresh'] !== 'no')
|
if ($config['saml_metadata_refresh'] !== 'no')
|
||||||
{
|
{
|
||||||
self::refreshMetadata($config);
|
$metadata = self::metadata($config['files_dir']);
|
||||||
|
$idps = self::splitIdP($config['saml_idp']);
|
||||||
|
foreach($idps as $idp)
|
||||||
|
{
|
||||||
|
if (!isset($metadata[$idp]))
|
||||||
|
{
|
||||||
|
$metadata = [];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count($metadata) !== count($idps) || $config['saml_metadata_refresh'] === 'now')
|
||||||
|
{
|
||||||
|
self::refreshMetadata($config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split multiple IdP
|
||||||
|
*
|
||||||
|
* @param string $config
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private static function splitIdP($config)
|
||||||
|
{
|
||||||
|
return preg_split('/[\n\r ]+/', trim($config)) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh metadata
|
* Refresh metadata
|
||||||
*
|
*
|
||||||
@ -241,7 +290,8 @@ class Saml implements BackendSSO
|
|||||||
|
|
||||||
$source = [
|
$source = [
|
||||||
'src' => $config['saml_metadata'],
|
'src' => $config['saml_metadata'],
|
||||||
'whitelist' => [$config['saml_idp']], // only ready our idp, the whole thing can be huge
|
// only read/configure our idp(s), the whole thing can be huge
|
||||||
|
'whitelist' => self::splitIdP($config['saml_idp']),
|
||||||
];
|
];
|
||||||
if (!empty($config['saml_certificate']))
|
if (!empty($config['saml_certificate']))
|
||||||
{
|
{
|
||||||
@ -271,7 +321,15 @@ class Saml implements BackendSSO
|
|||||||
$GLOBALS['egw_info']['server']['usecookies'] = true;
|
$GLOBALS['egw_info']['server']['usecookies'] = true;
|
||||||
$config['baseurlpath'] = Api\Framework::getUrl(Api\Egw::link('/saml/'));
|
$config['baseurlpath'] = Api\Framework::getUrl(Api\Egw::link('/saml/'));
|
||||||
$config['username_oid'] = [self::usernameOid($config)];
|
$config['username_oid'] = [self::usernameOid($config)];
|
||||||
|
// if multiple IdP's are configured, do NOT specify one to let user select
|
||||||
|
if (count(self::splitIdP($config['saml_idp'])) > 1)
|
||||||
|
{
|
||||||
|
unset($config['saml_idp']);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$config['saml_idp'] = trim($config['saml_idp']);
|
||||||
|
}
|
||||||
// update config.php and default-sp in authsources.php
|
// update config.php and default-sp in authsources.php
|
||||||
foreach([
|
foreach([
|
||||||
'authsources.php' => [
|
'authsources.php' => [
|
||||||
@ -445,12 +503,18 @@ class Saml implements BackendSSO
|
|||||||
|
|
||||||
case 'authsources.php':
|
case 'authsources.php':
|
||||||
$replacements = [
|
$replacements = [
|
||||||
"'idp' => null," => "'idp' => ".self::quote($config['saml_idp']).',',
|
"'idp' => null," => "'idp' => ".self::quote(
|
||||||
|
count(self::splitIdP($config['saml_idp'])) <= 1 ? trim($config['saml_idp']) : null).',',
|
||||||
"'discoURL' => null," => "'discoURL' => null,\n\n".
|
"'discoURL' => null," => "'discoURL' => null,\n\n".
|
||||||
// add our private and public keys
|
// add our private and public keys
|
||||||
"\t'privatekey' => 'saml.pem',\n\n".
|
"\t'privatekey' => 'saml.pem',\n\n".
|
||||||
"\t// to include certificate in metadata\n".
|
"\t// to include certificate in metadata\n".
|
||||||
"\t'certificate' => 'saml.crt',\n\n".
|
"\t'certificate' => 'saml.crt',\n\n".
|
||||||
|
"\t// new certificates for rotation: add new, wait for IdP sync, swap old and new, wait, comment again\n".
|
||||||
|
"\t//'new_privatekey' => 'new-saml.pem',\n".
|
||||||
|
"\t//'new_certificate' => 'new-saml.crt',\n\n".
|
||||||
|
"\t// logout is NOT signed by default, but signature is required from the uni-kl.de IdP for logout\n".
|
||||||
|
"\t'sign.logout' => true,\n\n".
|
||||||
"\t'name' => [\n".
|
"\t'name' => [\n".
|
||||||
"\t\t'en' => ".self::quote($config['saml_sp'] ?: 'EGroupware').",\n".
|
"\t\t'en' => ".self::quote($config['saml_sp'] ?: 'EGroupware').",\n".
|
||||||
"\t],\n\n".
|
"\t],\n\n".
|
||||||
|
@ -402,7 +402,7 @@ class Cache
|
|||||||
*/
|
*/
|
||||||
static public function &getSession($app,$location,$callback=null,array $callback_params=array(),$expiration=0)
|
static public function &getSession($app,$location,$callback=null,array $callback_params=array(),$expiration=0)
|
||||||
{
|
{
|
||||||
if (isset($_SESSION[Session::EGW_SESSION_ENCRYPTED]))
|
if (!isset($_SESSION) || isset($_SESSION[Session::EGW_SESSION_ENCRYPTED]))
|
||||||
{
|
{
|
||||||
if (Session::ERROR_LOG_DEBUG) error_log(__METHOD__.' called after session was encrypted --> ignored!');
|
if (Session::ERROR_LOG_DEBUG) error_log(__METHOD__.' called after session was encrypted --> ignored!');
|
||||||
return null; // can no longer store something in the session, eg. because commit_session() was called
|
return null; // can no longer store something in the session, eg. because commit_session() was called
|
||||||
|
@ -585,7 +585,7 @@ class Db
|
|||||||
{
|
{
|
||||||
foreach(get_included_files() as $file)
|
foreach(get_included_files() as $file)
|
||||||
{
|
{
|
||||||
if (strpos($file,'adodb') !== false && !in_array($file,(array)$_SESSION['egw_required_files']))
|
if (strpos($file,'adodb') !== false && !in_array($file,(array)$_SESSION['egw_required_files']) && isset($_SESSION))
|
||||||
{
|
{
|
||||||
$_SESSION['egw_required_files'][] = $file;
|
$_SESSION['egw_required_files'][] = $file;
|
||||||
//error_log(__METHOD__."() egw_required_files[] = $file");
|
//error_log(__METHOD__."() egw_required_files[] = $file");
|
||||||
|
@ -81,6 +81,23 @@ class Login
|
|||||||
$tmpl->set_var('2fa_class', 'et2_required');
|
$tmpl->set_var('2fa_class', 'et2_required');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if we need some discovery (select login options eg. a SAML IdP), hide it if not
|
||||||
|
$discovery = '';
|
||||||
|
foreach(Api\Hooks::process('login_discovery', [], true) as $app => $data)
|
||||||
|
{
|
||||||
|
if (!empty($data)) $discovery .= $data;
|
||||||
|
}
|
||||||
|
if (!empty($discovery))
|
||||||
|
{
|
||||||
|
$tmpl->set_var('discovery', $discovery);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$tmpl->set_block('login_form','discovery_block');
|
||||||
|
$tmpl->set_var('discovery_block', '');
|
||||||
|
}
|
||||||
|
|
||||||
// hide change-password fields, if not requested
|
// hide change-password fields, if not requested
|
||||||
if (!$change_passwd)
|
if (!$change_passwd)
|
||||||
{
|
{
|
||||||
|
@ -1444,9 +1444,11 @@ class Session
|
|||||||
if (!$GLOBALS['egw_info']['user']['sessionid'] || $sessionid == $GLOBALS['egw_info']['user']['sessionid'])
|
if (!$GLOBALS['egw_info']['user']['sessionid'] || $sessionid == $GLOBALS['egw_info']['user']['sessionid'])
|
||||||
{
|
{
|
||||||
// eg. SAML logout will fail, if there is no more session --> remove everything else
|
// eg. SAML logout will fail, if there is no more session --> remove everything else
|
||||||
if (($needed = Auth::needSession()) && array_intersect($needed, array_keys($_SESSION)))
|
$auth = new Auth();
|
||||||
|
if (($needed = $auth->needSession()) && array_intersect($needed, array_keys($_SESSION)))
|
||||||
{
|
{
|
||||||
$_SESSION = array_intersect_key($_SESSION['SimpleSAMLphp_SESSION'], array_flip($needed));
|
$_SESSION = array_intersect_key($_SESSION, array_flip($needed));
|
||||||
|
Auth::backend($auth->backendType()); // backend is stored in session
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (self::ERROR_LOG_DEBUG) error_log(__METHOD__." ********* about to call session_destroy!");
|
if (self::ERROR_LOG_DEBUG) error_log(__METHOD__." ********* about to call session_destroy!");
|
||||||
|
@ -39,6 +39,8 @@ elseif(strpos($redirectTarget, '[?&]cd=') !== false)
|
|||||||
|
|
||||||
if ($verified)
|
if ($verified)
|
||||||
{
|
{
|
||||||
|
$auth = new Api\Auth();
|
||||||
|
|
||||||
// remove remember me cookie on explicit logout, unless it is a second factor
|
// remove remember me cookie on explicit logout, unless it is a second factor
|
||||||
if ($GLOBALS['egw']->session->removeRememberMeTokenOnLogout())
|
if ($GLOBALS['egw']->session->removeRememberMeTokenOnLogout())
|
||||||
{
|
{
|
||||||
@ -53,7 +55,7 @@ Api\Session::egw_setcookie('kp3');
|
|||||||
Api\Session::egw_setcookie('domain');
|
Api\Session::egw_setcookie('domain');
|
||||||
|
|
||||||
// SSO Logout (does not return for SSO systems)
|
// SSO Logout (does not return for SSO systems)
|
||||||
Api\Auth::logout();
|
if (isset($auth)) $auth->logout();
|
||||||
|
|
||||||
// $GLOBALS['egw']->redirect($redirectTarget);
|
// $GLOBALS['egw']->redirect($redirectTarget);
|
||||||
?>
|
?>
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -265,7 +265,7 @@ div#loginMainDiv.stockLoginBackground {
|
|||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
input[type="submit"] {
|
input[type="submit"], select.onChangeSubmit {
|
||||||
background-color: #0a5ca5;
|
background-color: #0a5ca5;
|
||||||
.color_0_gray;
|
.color_0_gray;
|
||||||
.fontsize_xxl;
|
.fontsize_xxl;
|
||||||
@ -275,6 +275,9 @@ div#loginMainDiv.stockLoginBackground {
|
|||||||
&:focus {}
|
&:focus {}
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
}
|
}
|
||||||
|
select.onChangeSubmit {
|
||||||
|
padding-left: 25px;
|
||||||
|
}
|
||||||
.registration {
|
.registration {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
a:not(:first-child) {
|
a:not(:first-child) {
|
||||||
|
@ -28,6 +28,13 @@
|
|||||||
<input type="hidden" name="account_type" value="u" />
|
<input type="hidden" name="account_type" value="u" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<!-- BEGIN discovery_block -->
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{discovery}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- END discovery_block -->
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="field_icons username"></span>
|
<span class="field_icons username"></span>
|
||||||
|
@ -75,6 +75,13 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- END domain_selection -->
|
<!-- END domain_selection -->
|
||||||
|
<!-- BEGIN discovery_block -->
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{discovery}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- END discovery_block -->
|
||||||
<!-- BEGIN change_password -->
|
<!-- BEGIN change_password -->
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
@ -9,21 +9,26 @@
|
|||||||
* @subpackage authentication
|
* @subpackage authentication
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use EGroupware\Api;
|
||||||
|
|
||||||
// we have to set session-cookie name used by EGroupware!
|
// we have to set session-cookie name used by EGroupware!
|
||||||
ini_set('session.name', 'sessionid');
|
ini_set('session.name', 'sessionid');
|
||||||
|
|
||||||
|
require_once __DIR__.'/../api/src/autoload.php';
|
||||||
|
|
||||||
$GLOBALS['egw_info'] = [
|
$GLOBALS['egw_info'] = [
|
||||||
'flags' => [
|
'flags' => [
|
||||||
//'currentapp' => 'login', // db connection, no auth
|
//'currentapp' => 'login', // db connection, no auth
|
||||||
'noapi' => true, // no db connection, but autoloader, files_dir MUST be set correct!
|
'noapi' => true, // no db connection, but autoloader, files_dir MUST be set correct!
|
||||||
],
|
],
|
||||||
'server' => [
|
'server' => [
|
||||||
'files_dir' => '/var/lib/egroupware/default/files',
|
// default files and temp directories for name based instances (eg. our hosting) or container installation
|
||||||
'temp_dir' => '/tmp',
|
'files_dir' => file_exists('/var/lib/egroupware/'.Api\Header\Http::host().'/files') ?
|
||||||
|
'/var/lib/egroupware/'.Api\Header\Http::host().'/files' : '/var/lib/egroupware/default/files',
|
||||||
|
'temp_dir' => file_exists('/var/lib/egroupware/'.Api\Header\Http::host().'/tmp') ?
|
||||||
|
'/var/lib/egroupware/'.Api\Header\Http::host().'/tmp' : '/tmp',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
require_once __DIR__.'/../header.inc.php';
|
require_once __DIR__.'/../header.inc.php';
|
||||||
|
|
||||||
use EGroupware\Api;
|
|
||||||
|
|
||||||
Api\Auth\Saml::checkDefaultConfig();
|
Api\Auth\Saml::checkDefaultConfig();
|
@ -478,9 +478,14 @@
|
|||||||
<td colspan="2"><b>{lang_If_using_SAML_2.0 / Shibboleth / SimpleSAMLphp}:</b></td>
|
<td colspan="2"><b>{lang_If_using_SAML_2.0 / Shibboleth / SimpleSAMLphp}:</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr class="row_off">
|
||||||
|
<td>{lang_Label_to_display_as_option_on_login_page}:<br/>{lang_or_leave_empty_and_select_SAML_as_authentication_type_above_for_single_sign_on}</td>
|
||||||
|
<td><input name="newsettings[saml_discovery]" placeholder="{lang_University_Login}" value="{value_saml_discovery}" size="20" /></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr class="row_on">
|
<tr class="row_on">
|
||||||
<td>{lang_Identity_Provider}:</td>
|
<td>{lang_Identity_Provider}:<br/>{lang_You_can_specify_multiple_IdP_on_separate_lines.}</td>
|
||||||
<td><input name="newsettings[saml_idp]" placeholder="https://idp.rhrk.uni-kl.de/idp/shibboleth" value="{value_saml_idp}" size="64" /></td>
|
<td><textarea name="newsettings[saml_idp]" placeholder="https://idp.rhrk.uni-kl.de/idp/shibboleth" rows="3" cols="64">{value_saml_idp}</textarea></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr class="row_off">
|
<tr class="row_off">
|
||||||
@ -490,7 +495,6 @@
|
|||||||
<select name="newsettings[saml_metadata_refresh]">
|
<select name="newsettings[saml_metadata_refresh]">
|
||||||
<option value="daily"{selected_saml_metadata_refresh_daily}>{lang_daily}</option>
|
<option value="daily"{selected_saml_metadata_refresh_daily}>{lang_daily}</option>
|
||||||
<option value="weekly"{selected_saml_metadata_refresh_weekly}>{lang_weekly}</option>
|
<option value="weekly"{selected_saml_metadata_refresh_weekly}>{lang_weekly}</option>
|
||||||
<option value="hourly"{selected_saml_metadata_refresh_hourly}>{lang_hourly}</option>
|
|
||||||
<option value="no"{selected_saml_metadata_refresh_no}>{lang_not_automatic}</option>
|
<option value="no"{selected_saml_metadata_refresh_no}>{lang_not_automatic}</option>
|
||||||
<option value="now"{selected_saml_metadata_refresh_now}>{lang_just_now}</option>
|
<option value="now"{selected_saml_metadata_refresh_now}>{lang_just_now}</option>
|
||||||
</select>
|
</select>
|
||||||
@ -536,7 +540,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr class="row_off">
|
<tr class="row_off">
|
||||||
<td colspan="2">{lang_The_used_SimpleSAMLphp_allows_a_lot_more_configuration_/_different_authentication_types_via_its_config_files in} {value_files_dir}/saml</td>
|
<td colspan="2">
|
||||||
|
{lang_The_used_SimpleSAMLphp_allows_a_lot_more_configuration_/_different_authentication_types_via_its_config_files in} {value_files_dir}/saml<br/>
|
||||||
|
{lang_More_information}: <a target="_blank" href="https://github.com/EGroupware/egroupware/blob/master/api/src/Auth/Saml.php#L19">
|
||||||
|
https://github.com/EGroupware/egroupware/blob/master/api/src/Auth/Saml.php</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr class="row_off">
|
<tr class="row_off">
|
||||||
|
Loading…
Reference in New Issue
Block a user