* SAML/Shibboleth/SimpleSAMLphp authentication configurable through setup

This commit is contained in:
Ralf Becker 2020-05-28 23:23:54 +02:00
parent b5dceda99c
commit b1f79d1c40
7 changed files with 384 additions and 65 deletions

View File

@ -68,6 +68,9 @@ $setup_info['api']['hooks']['vfs_unlink'] = 'EGroupware\\Api\\Vfs\\Sharing::vfsU
$setup_info['api']['hooks']['vfs_rename'] = 'EGroupware\\Api\\Vfs\\Sharing::vfsUpdate';
$setup_info['api']['hooks']['vfs_rmdir'] = 'EGroupware\\Api\\Vfs\\Sharing::vfsUpdate';
// hook to update SimpleSAMLphp config
$setup_info['api']['hooks']['setup_config'] = \EGroupware\Api\Auth\Saml::class.'::setupConfig';
// installation checks
$setup_info['api']['check_install'] = array(
'' => array(

View File

@ -67,6 +67,12 @@ class Auth
*/
static function backend($type=null)
{
// do we have a hostname specific auth type set
if (is_null($type) && !empty($GLOBALS['egw_info']['server']['auth_type_host']) &&
Header\Http::host() === $GLOBALS['egw_info']['server']['auth_type_hostname'])
{
$type = $GLOBALS['egw_info']['server']['auth_type_host'];
}
if (is_null($type)) $type = $GLOBALS['egw_info']['server']['auth_type'];
$backend_class = __CLASS__.'\\'.ucfirst($type);

View File

@ -109,6 +109,14 @@ class Saml implements BackendSSO
return false;
}
/**
* Some urn:oid constants for common attributes
*/
const eduPersonPricipalName = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6';
const emailAddress = 'urn:oid:0.9.2342.19200300.100.1.1';
const firstName = 'urn:oid:2.5.4.42';
const lastName = 'urn:oid:2.5.4.4';
/**
* Attempt SSO login
*
@ -126,16 +134,24 @@ class Saml implements BackendSSO
// get attributes for (automatic) account creation
$attrs = $as->getAttributes();
$user = $attrs['urn:oid:0.9.2342.19200300.100.1.1'][0];
$GLOBALS['egw_info']['server']['auto_create_acct'] = true;
$GLOBALS['auto_create_acct'] = [
'firstname' => $attrs['urn:oid:2.5.4.42'][0],
'lastname' => $attrs['urn:oid:2.5.4.4'][0],
'email' => $attrs['urn:oid:0.9.2342.19200300.100.1.3'][0],
];
$username = $attrs[self::usernameOid()][0];
// 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']))
{
return null;
}
$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($user, null, null, false, false);
return $GLOBALS['egw']->session->create($username, null, null, false, false);
}
/**
@ -159,31 +175,200 @@ class Saml implements BackendSSO
{
return ['SimpleSAMLphp_SESSION'];
}
const ASYNC_JOB_ID = 'saml_metadata_refresh';
/**
* Hook called when setup configuration is being stored:
* - updating SimpleSAMLphp config files
* - creating/removing cron job to refresh metadata
*
* @param array $location key "newsettings" with reference to changed settings from setup > configuration
* @throws \Exception for errors
*/
public static function setupConfig(array $location)
{
$config =& $location['newsettings'];
/*error_log(__METHOD__."() ".json_encode(array_filter($config, function($value, $key) {
return substr($key, 0, 5) === 'saml_' || $key === 'auth_type';
}, ARRAY_FILTER_USE_BOTH), JSON_UNESCAPED_SLASHES));*/
if ($newsettings['auth_type'] !== 'saml') return; // nothing to do
if (file_exists($config['files_dir'].'/saml/config.php'))
{
self::updateConfig($config);
}
self::checkDefaultConfig($config);
// install or remove async job to refresh metadata
static $freq2times = [
'hourly' => ['min' => 4], // hourly at minute 4
'daily' => ['min' => 4, 'hour' => 4], // daily at 4:04am
'weekly' => ['min' => 4, 'hour' => 4, 'dow' => 5], // Saturdays as 4:04am
];
$async = new Api\Asyncservice();
if (isset($freq2times[$config['saml_metadata_refresh']]) &&
preg_match('|^https://|', $config['saml_metadata']))
{
$async->set_timer($freq2times[$config['saml_metadata_refresh']], self::ASYNC_JOB_ID, self::class.'::refreshMetadata');
}
else
{
$async->cancel_timer(self::ASYNC_JOB_ID);
}
if ($config['saml_metadata_refresh'] !== 'no')
{
self::refreshMetadata($config);
}
}
/**
* Refresh metadata
*
* @param array|null $config defaults to $GLOBALS['egw_info']['server']
* @throws \Exception
*/
public static function refreshMetadata(array $config=null)
{
if (!isset($config)) $config = $GLOBALS['egw_info']['server'];
$old_config = Api\Config::read('phpgwapi');
$saml_config = $config['files_dir'].'/saml';
SimpleSAML\Configuration::setConfigDir($saml_config);
$source = [
'src' => $config['saml_metadata'],
'whitelist' => [$config['saml_idp']], // only ready our idp, the whole thing can be huge
];
if (!empty($config['saml_certificate']))
{
$cert = $saml_config.'/cert/'.basename(parse_url($config['saml_certificate'], PHP_URL_PATH));
if ((!file_exists($cert) || $config['saml_certificate'] !== $old_config['saml_certificate']) &&
(!($content = file_get_contents($config['saml_certificate'])) ||
!file_put_contents($cert, $content)))
{
throw new \Exception("Could not load certificate from $config[saml_certificate]!");
}
$source['certificate'] = $cert;
}
$metaloader = new SimpleSAML\Module\metarefresh\MetaLoader();
$metaloader->loadSource($source);
$metaloader->writeMetadataFiles($saml_config.'/metadata');
}
/**
* Update config files
*
* @param array $config
*/
public static function updateConfig(array $config)
{
// some Api classes require the config in $GLOBALS['egw_info']['server']
$GLOBALS['egw_info']['server']['webserver_url'] = $config['webserver_url'];
$GLOBALS['egw_info']['server']['usecookies'] = true;
$config['baseurlpath'] = Api\Framework::getUrl(Api\Egw::link('/saml/'));
$config['username_oid'] = [self::usernameOid($config)];
// update config.php and default-sp in authsources.php
foreach([
'authsources.php' => [
'saml_idp' => "/('default-sp' => *\\[.*?'idp' => *).*?$/ms",
'saml_sp' => "/('default-sp' => *\\[.*?'name' => *\\[.*?'en' => *).*?$/ms",
'username_oid' => "/('default-sp' => *\\[.*?'attributes.required' => *)\\[.*?\\],$/ms",
],
'config.php' => [
'baseurlpath' => "/('baseurlpath' => *).*?$/ms",
'saml_contact_name' => "/('technicalcontact_name' => *).*?$/ms",
'saml_contact_email' => "/('technicalcontact_email' => *).*?$/ms",
]
] as $file => $replacements)
{
if (file_exists($path = $config['files_dir'] . '/saml/'.$file) &&
($content = file_get_contents($path)))
{
foreach($replacements as $conf => $reg_exp)
{
$content = preg_replace($reg_exp, '$1' . (is_array($config[$conf]) ?
"[".implode(',', array_map(self::class.'::quote', $config[$conf]))."]" :
self::quote($config[$conf])) . ',', $content);
}
if (!file_put_contents($path, $content))
{
throw new \Exception("Failed to update '$path'!");
}
}
}
}
/**
* @param string|null $str
* @param string $empty=null default value, if $str is empty
* @return string
*/
private static function quote($str, $empty=null)
{
return $str || isset($empty) ? "'".addslashes($str ?: $empty)."'" : 'null';
}
/**
* Get the urn:oid of the username
*
* @param array|null $config
* @return string
*/
private static function usernameOid(array $config=null)
{
if (!isset($config)) $config = $GLOBALS['egw_info']['server'];
switch($config['saml_username'])
{
case 'eduPersonPrincipalName':
return self::eduPersonPricipalName;
case 'emailAddress':
return self::emailAddress;
case 'customOid':
return $config['saml_username_oid'] ?: self::emailAddress;
}
return self::emailAddress;
}
/**
* Create simpleSAMLphp default configuration
*
* @param array $config=null default $GLOBALS['egw_info']['server']
* @throws Exception
*/
public static function checkDefaultConfig()
public static function checkDefaultConfig(array $config=null)
{
if (!isset($config)) $config = $GLOBALS['egw_info']['server'];
// some Api classes require the config in $GLOBALS['egw_info']['server']
$GLOBALS['egw_info']['server']['webserver_url'] = $config['webserver_url'];
$GLOBALS['egw_info']['server']['usecookies'] = true;
// use "saml" subdirectory of EGroupware files directory as simpleSAMLphp config-directory
$config_dir = $GLOBALS['egw_info']['server']['files_dir'].'/saml';
$config_dir = $config['files_dir'].'/saml';
if (!file_exists($config_dir) && !mkdir($config_dir))
{
throw new Exception("Can't create SAML config directory '$config_dir'!");
}
SimpleSAML\Configuration::setConfigDir($config_dir);
// create a default configuration
if ((!file_exists($config_dir.'/config.php') || filesize($config_dir.'/config.php') < 1000))
{
foreach(['cert', 'log', 'data', 'metadata'] as $dir)
// check if all necessary directories exist, if not create them
foreach(['cert', 'log', 'data', 'metadata', 'tmp'] as $dir)
{
if (!file_exists($config_dir.'/'.$dir) && !mkdir($config_dir.'/'.$dir, 700, true))
{
throw new Exception("Can't create $dir-directory '$config_dir/$dir'!");
}
}
// create a default configuration
if (!file_exists($config_dir.'/config.php') || filesize($config_dir.'/config.php') < 1000)
{
// create a key-pair
$cert_dir = $config_dir.'/cert';
$private_key_path = $cert_dir.'/saml.pem';
@ -229,44 +414,72 @@ class Saml implements BackendSSO
{
case 'config.php':
$cookie_domain = Api\Session::getCookieDomain($cookie_path, $cookie_secure);
if (!file_put_contents($config_dir.'/'.$file,
$c=strtr($t=file_get_contents($path), [
$replacements = [
"'baseurlpath' => 'simplesaml/'," => "'baseurlpath' => '".Api\Framework::getUrl(Api\Egw::link('/saml/'))."',",
"'timezone' => null," => "'timezone' => 'Europe/Berlin',", // ToDo: use default prefs
"'secretsalt' => 'defaultsecretsalt'" => "'secretsalt' => '".Api\Auth::randomstring(32)."',",
"'secretsalt' => 'defaultsecretsalt'," => "'secretsalt' => '".Api\Auth::randomstring(32)."',",
"'auth.adminpassword' => '123'," => "'auth.adminpassword' => '".Api\Auth::randomstring(12)."',",
"'admin.protectindexpage' => false," => "'admin.protectindexpage' => true,",
"'certdir' => 'cert/'," => "'certdir' => __DIR__.'/cert/',",
"'loggingdir' => 'log/'," => "'loggingdir' => __DIR__.'/log/',",
"'datadir' => 'data/'," => "'datadir' => __DIR__.'/data/',",
"'tempdir' => '/tmp/simplesaml'," => "'tempdir' => \$GLOBALS['egw_info']['server']['temp_dir'],",
"'tempdir' => '/tmp/simplesaml'," => "'tempdir' => __DIR__.'/tmp',",
"'metadatadir' => 'metadata'," => "'metadatadir' => __DIR__.'/metadata',",
"'logging.handler' => 'syslog'," => "'logging.handler' => 'errorlog',",
"'technicalcontact_name' => 'Administrator'" =>
"'technicalcontact_name' => ".self::quote($config['saml_contact_name'], 'Administrator'),
"'technicalcontact_email' => 'na@example.org'" =>
"'technicalcontact_email' => ".self::quote($config['saml_contact_email'], 'na@example.org'),
"'metadata.sign.privatekey' => null," => "'metadata.sign.privatekey' => 'saml.pem',",
//"'metadata.sign.privatekey_pass' => null," => "",
"'metadata.sign.certificate' => null," => "'metadata.sign.privatekey' => 'saml.crt',",
"'metadata.sign.certificate' => null," => "'metadata.sign.certificate' => 'saml.crt',",
//"'metadata.sign.algorithm' => null," => "",
// we have to use EGroupware session/cookie parameters
"'session.cookie.name' => 'SimpleSAMLSessionID'," => "'session.cookie.name' => 'sessionid',",
"'session.cookie.path' => '/'," => "'session.cookie.path' => '$cookie_path',",
"'session.cookie.domain' => null," => "'session.cookie.domain' => '$cookie_domain',",
"'session.cookie.domain' => null," => "'session.cookie.domain' => '.$cookie_domain',",
"'session.cookie.secure' => false," => "'session.cookie.secure' => ".($cookie_secure ? 'true' : 'false').',',
"'session.phpsession.cookiename' => 'SimpleSAML'," => "'session.phpsession.cookiename' => 'sessionid',",
])))
{
header('Content-Type: text/plain');
echo "template:\n$t\n\nconfig:\n$c\n\n";
throw new Exception("Can't write SAML config file '$config_dir/config.php'!");
}
];
break;
case 'authsources.php':
$replacements = [
"'idp' => null," => "'idp' => ".self::quote($config['saml_idp']).',',
"'discoURL' => null," => "'discoURL' => null,\n\n".
// add our private and public keys
"\t'privatekey' => 'saml.pem',\n\n".
"\t// to include certificate in metadata\n".
"\t'certificate' => 'saml.crt',\n\n".
"\t'name' => [\n".
"\t\t'en' => ".self::quote($config['saml_sp'] ?: 'EGroupware').",\n".
"\t],\n\n".
"\t'attributes' => [\n".
"\t\t'eduPersonPricipalName' => '".self::eduPersonPricipalName."',\n".
"\t\t'emailAddress' => '".self::emailAddress."',\n".
"\t\t'firstName' => '".self::firstName."',\n".
"\t\t'lastName' => '".self::lastName."',\n".
"\t],\n".
"\t'attributes.required' => [".self::quote(self::usernameOid($config))."],",
];
break;
default:
unset($replacements);
if (!copy($path, $config_dir.'/'.$file))
{
throw new Exception("Can't copy SAML config file '$config_dir/$file'!");
}
break;
}
if (isset($replacements) &&
!file_put_contents($config_dir.'/'.$file,
$c=strtr($t=file_get_contents($path), $replacements)))
{
header('Content-Type: text/plain');
echo "<pre>template:\n$t\n\nconfig:\n$c\n</pre>\n";
throw new Exception("Can't write SAML config file '$config_dir/config.php'!");
}
}
foreach(glob($simplesaml_dir.'/metadata-templates/*.php') as $path)
{

32
composer.lock generated
View File

@ -852,12 +852,12 @@
{
"name": "Chuck Hagenbuch",
"email": "chuck@horde.org",
"role": "Lead"
"role": "lead"
},
{
"name": "Jan Schneider",
"email": "jan@horde.org",
"role": "Lead"
"role": "lead"
},
{
"name": "Michael J Rubinsky",
@ -904,7 +904,7 @@
],
"description": "Compiled version of magicsuggest customized for EGroupware project.",
"homepage": "https://github.com/EGroupware/magicsuggest",
"time": "2018-06-21T13:36:37+00:00"
"time": "2018-06-21T10:14:03+00:00"
},
{
"name": "egroupware/news_admin",
@ -2046,7 +2046,7 @@
"license": [
"LGPL-2.1"
],
"description": "A library to wrap various compression techniques."
"description": "An API for various compression techniques."
},
{
"name": "pear-pear.horde.org/Horde_Crypt",
@ -2112,7 +2112,7 @@
"license": [
"LGPL-2.1"
],
"description": "A library that provides blowfish encryption/decryption for PHP string data."
"description": "Provides blowfish encryption/decryption for PHP string data."
},
{
"name": "pear-pear.horde.org/Horde_Date",
@ -2228,7 +2228,7 @@
"license": [
"BSD-2-Clause"
],
"description": "A library that wraps various backends providing IDNA (Internationalized Domain Names in Applications) support."
"description": "Normalized access to various backends providing IDNA (Internationalized Domain Names in Applications) support."
},
{
"name": "pear-pear.horde.org/Horde_Imap_Client",
@ -2266,7 +2266,7 @@
"license": [
"LGPL-2.1"
],
"description": "A library to access IMAP4rev1 (RFC 3501) mail servers. Also supports connections to POP3 (STD 53/RFC 1939)."
"description": "Interface to access IMAP4rev1 (RFC 3501) mail servers. Also supports connections to POP3 (STD 53/RFC 1939)."
},
{
"name": "pear-pear.horde.org/Horde_ListHeaders",
@ -2546,7 +2546,7 @@
"license": [
"LGPL-2.1"
],
"description": "A library that provides an abstract PHP network socket client."
"description": "Provides abstract class for use in creating PHP network socket clients."
},
{
"name": "pear-pear.horde.org/Horde_Stream",
@ -2687,7 +2687,7 @@
"license": [
"LGPL-2.1"
],
"description": "A library that provides a text-based diff engine and renderers for multiple diff output formats."
"description": "A text-based diff engine and renderers for multiple diff output formats."
},
{
"name": "pear-pear.horde.org/Horde_Text_Flowed",
@ -2715,7 +2715,7 @@
"license": [
"LGPL-2.1"
],
"description": "A library that provides common methods for manipulating text using the encoding described in RFC 3676 ('flowed' text)."
"description": "The Horde_Text_Flowed:: class provides common methods for manipulating text using the encoding described in RFC 3676 ('flowed' text)."
},
{
"name": "pear-pear.horde.org/Horde_Translation",
@ -2798,7 +2798,7 @@
"license": [
"LGPL-2.1"
],
"description": "A library that provides functionality useful for all kind of applications."
"description": "These classes provide functionality useful for all kind of applications."
},
{
"name": "pear/archive_tar",
@ -4029,16 +4029,16 @@
},
{
"name": "simplesamlphp/simplesamlphp",
"version": "v1.18.6",
"version": "v1.18.7",
"source": {
"type": "git",
"url": "https://github.com/simplesamlphp/simplesamlphp.git",
"reference": "9fd1c3c5e5f9e6fed8de3c36e14ccdcce31e3dc7"
"reference": "c62e32c807d50ada1fd6f4792e2054a0471100ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/simplesamlphp/simplesamlphp/zipball/9fd1c3c5e5f9e6fed8de3c36e14ccdcce31e3dc7",
"reference": "9fd1c3c5e5f9e6fed8de3c36e14ccdcce31e3dc7",
"url": "https://api.github.com/repos/simplesamlphp/simplesamlphp/zipball/c62e32c807d50ada1fd6f4792e2054a0471100ad",
"reference": "c62e32c807d50ada1fd6f4792e2054a0471100ad",
"shasum": ""
},
"require": {
@ -4150,7 +4150,7 @@
"sp",
"ws-federation"
],
"time": "2020-04-16T13:40:56+00:00"
"time": "2020-05-12T12:24:31+00:00"
},
{
"name": "simplesamlphp/simplesamlphp-module-adfs",

View File

@ -60,6 +60,18 @@ if(@$_POST['submit'] && @$newsettings)
/* Load hook file with functions to validate each config (one/none/all) */
$GLOBALS['egw_setup']->hook('config_validate','setup');
try
{
// allow apps to register hooks throwing Exceptions for errors
Api\Hooks::process([
'location' => 'setup_config',
'newsettings' => &$newsettings,
], [], true);
}
catch (\Exception $e) {
$GLOBALS['error'] .= '<b>'.$e->getMessage()."</b><br />\n";
}
$newsettings['tz_offset'] = date('Z')/3600;
$GLOBALS['egw_setup']->db->transaction_begin();

View File

@ -182,6 +182,10 @@ function auth_type_activesync($config)
{
return _options_from(setup_cmd_config::auth_types(),$config['auth_type_activesync']);
}
function auth_type_host($config)
{
return _options_from(setup_cmd_config::auth_types(),$config['auth_type_host']);
}
/**
* Make account-repository-types from setup_cmd_config available

View File

@ -123,7 +123,7 @@
<td colspan="2"><b>{lang_Authentication_/_Accounts}</b></td>
</tr>
<tr class="row_off">
<tr class="row_on">
<td>{lang_Select_which_type_of_authentication_you_are_using}:</td>
<td>
<select name="newsettings[auth_type]">
@ -132,7 +132,7 @@
</td>
</tr>
<tr class="row_on">
<tr class="row_off">
<td>{lang_Authentication_type_for_application}: <b>CalDAV/CardDAV Sync</b></td>
<td>
<select name="newsettings[auth_type_groupdav]">
@ -142,7 +142,7 @@
</td>
</tr>
<tr class="row_off">
<tr class="row_on">
<td>{lang_Authentication_type_for_application}: <b>eSync (ActiveSync)</b></td>
<td>
<select name="newsettings[auth_type_activesync]">
@ -152,6 +152,19 @@
</td>
</tr>
<tr class="row_off">
<td>
{lang_Authentication_type_for_HTTP_Host}:
<input name="newsettings[auth_type_hostname]" value="{value_auth_type_hostname}" size="40"/>
</td>
<td>
<select name="newsettings[auth_type_host]">
<option value="">{lang_Standard,_as_defined_above}</option>
{hook_auth_type_host}
</select>
</td>
</tr>
<tr class="row_on">
<td>{lang_HTTP_auth_types_(comma-separated)_to_use_without_login-page, eg. "NTLM"}:</td>
<td>
@ -461,6 +474,74 @@
<td colspan="2">&nbsp;</td>
</tr>
<tr class="th">
<td colspan="2"><b>{lang_If_using_SAML_2.0 / Shibboleth / SimpleSAMLphp}:</b></td>
</tr>
<tr class="row_on">
<td>{lang_Identity_Provider}:</td>
<td><input name="newsettings[saml_idp]" placeholder="https://idp.rhrk.uni-kl.de/idp/shibboleth" value="{value_saml_idp}" size="64" /></td>
</tr>
<tr class="row_off">
<td>
{lang_Metadata}:
{lang_refresh}
<select name="newsettings[saml_metadata_refresh]">
<option value="daily"{selected_saml_metadata_refresh_daily}>{lang_daily}</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="now"{selected_saml_metadata_refresh_now}>{lang_just_now}</option>
</select>
</td>
<td>
<input name="newsettings[saml_metadata]" placeholder="https://www.aai.dfn.de/fileadmin/metadata/dfn-aai-metadata.xml" value="{value_saml_metadata}" size="64" /><br/>
</td>
</tr>
<tr class="row_on">
<td>{lang_Certificate_Metadata_is_signed_with}: ({lang_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="{value_saml_certificate}" size="64" /></td>
</tr>
<tr class="row_off">
<td>{lang_Result_data_to_use_as_username}:</td>
<td>
<select name="newsettings[saml_username]">
<option value="eduPersonPrincipalName"{selected_saml_username_eduPersonPrincipalName}>eduPersonPrincipalName</option>
<option value="emailAddress"{selected_saml_username_emailAddress}>emailAddress</option>
<option value="custom"{selected_saml_username_customOid}>{lang_custom_OID}</option>
</select>
<input name="newsettings[saml_username_oid]" value="{value_saml_username_oid}" placeholder="urn:oid:x.x.x.x" size="40" />
</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>
</tr>
<tr class="row_off">
<td>{lang_Name_for_Service_Provider}:</td>
<td><input name="newsettings[saml_sp]" placeholder="EGroupware" value="{value_saml_sp}" size="40" /></td>
</tr>
<tr class="row_on">
<td>{lang_Technical_contact}:</td>
<td>
<input name="newsettings[saml_contact_name]" value="{value_saml_contact_name}" placeholder="{lang_Name}" size="24" />
<input name="newsettings[saml_contact_email]" value="{value_saml_contact_email}" placeholder="{lang_Email}" size="24" />
</td>
</tr>
<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>
</tr>
<tr class="row_off">
<td colspan="2">&nbsp;</td>
</tr>
<tr class="th">
<td colspan="2"><b>{lang_If_using_CAS_(Central_Authentication_Service):}</b></td>
</tr>