runff 1.0 commit

This commit is contained in:
rock64
2019-04-29 16:09:00 +02:00
commit f1567f989b
1268 changed files with 98652 additions and 0 deletions

139
lib/SimpleSAML/Auth/Default.php Executable file
View File

@@ -0,0 +1,139 @@
<?php
/**
* Implements the default behaviour for authentication.
*
* This class contains an implementation for default behaviour when authenticating. It will
* save the session information it got from the authentication client in the users session.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*
* @deprecated This class will be removed in SSP 2.0.
*/
class SimpleSAML_Auth_Default
{
/**
* @deprecated This method will be removed in SSP 2.0. Use SimpleSAML_Auth_Source::initLogin() instead.
*/
public static function initLogin(
$authId,
$return,
$errorURL = null,
array $params = array()
) {
$as = self::getAuthSource($authId);
$as->initLogin($return, $errorURL, $params);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* SimpleSAML_Auth_State::getPersistentAuthData() instead.
*/
public static function extractPersistentAuthState(array &$state)
{
return SimpleSAML_Auth_State::getPersistentAuthData($state);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML_Auth_Source::loginCompleted() instead.
*/
public static function loginCompleted($state)
{
SimpleSAML_Auth_Source::loginCompleted($state);
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
public static function initLogoutReturn($returnURL, $authority)
{
assert(is_string($returnURL));
assert(is_string($authority));
$session = SimpleSAML_Session::getSessionFromRequest();
$state = $session->getAuthData($authority, 'LogoutState');
$session->doLogout($authority);
$state['SimpleSAML_Auth_Default.ReturnURL'] = $returnURL;
$state['LogoutCompletedHandler'] = array(get_class(), 'logoutCompleted');
$as = SimpleSAML_Auth_Source::getById($authority);
if ($as === null) {
// The authority wasn't an authentication source...
self::logoutCompleted($state);
}
$as->logout($state);
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
public static function initLogout($returnURL, $authority)
{
assert(is_string($returnURL));
assert(is_string($authority));
self::initLogoutReturn($returnURL, $authority);
\SimpleSAML\Utils\HTTP::redirectTrustedURL($returnURL);
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
public static function logoutCompleted($state)
{
assert(is_array($state));
assert(array_key_exists('SimpleSAML_Auth_Default.ReturnURL', $state));
\SimpleSAML\Utils\HTTP::redirectTrustedURL($state['SimpleSAML_Auth_Default.ReturnURL']);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML_Auth_Source::logoutCallback() instead.
*/
public static function logoutCallback($state)
{
SimpleSAML_Auth_Source::logoutCallback($state);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* sspmod_saml_Auth_Source_SP::handleUnsolicitedAuth() instead.
*/
public static function handleUnsolicitedAuth($authId, array $state, $redirectTo)
{
sspmod_saml_Auth_Source_SP::handleUnsolicitedAuth($authId, $state, $redirectTo);
}
/**
* Return an authentication source by ID.
*
* @param string $id The id of the authentication source.
* @return SimpleSAML_Auth_Source The authentication source.
* @throws Exception If the $id does not correspond with an authentication source.
*/
private static function getAuthSource($id)
{
$as = SimpleSAML_Auth_Source::getById($id);
if ($as === null) {
throw new Exception('Invalid authentication source: ' . $id);
}
return $as;
}
}

775
lib/SimpleSAML/Auth/LDAP.php Executable file
View File

@@ -0,0 +1,775 @@
<?php
/**
* Constants defining possible errors
*/
define('ERR_INTERNAL', 1);
define('ERR_NO_USER', 2);
define('ERR_WRONG_PW', 3);
define('ERR_AS_DATA_INCONSIST', 4);
define('ERR_AS_INTERNAL', 5);
define('ERR_AS_ATTRIBUTE', 6);
// not defined in earlier PHP versions
if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032);
}
/**
* The LDAP class holds helper functions to access an LDAP database.
*
* @author Andreas Aakre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Anders Lund, UNINETT AS. <anders.lund@uninett.no>
* @package SimpleSAMLphp
*/
class SimpleSAML_Auth_LDAP
{
/**
* LDAP link identifier.
*
* @var resource
*/
protected $ldap = null;
/**
* LDAP user: authz_id if SASL is in use, binding dn otherwise
*/
protected $authz_id = null;
/**
* Timeout value, in seconds.
*
* @var int
*/
protected $timeout = 0;
/**
* Private constructor restricts instantiation to getInstance().
*
* @param string $hostname
* @param bool $enable_tls
* @param bool $debug
* @param int $timeout
* @param int $port
* @param bool $referrals
*/
public function __construct($hostname, $enable_tls = true, $debug = false, $timeout = 0, $port = 389, $referrals = true)
{
// Debug
SimpleSAML\Logger::debug('Library - LDAP __construct(): Setup LDAP with '.
'host=\''.$hostname.
'\', tls='.var_export($enable_tls, true).
', debug='.var_export($debug, true).
', timeout='.var_export($timeout, true).
', referrals='.var_export($referrals, true));
/*
* Set debug level before calling connect. Note that this passes
* NULL to ldap_set_option, which is an undocumented feature.
*
* OpenLDAP 2.x.x or Netscape Directory SDK x.x needed for this option.
*/
if ($debug && !ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7)) {
SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set debug level (LDAP_OPT_DEBUG_LEVEL) to 7');
}
/*
* Prepare a connection for to this LDAP server. Note that this function
* doesn't actually connect to the server.
*/
$this->ldap = @ldap_connect($hostname, $port);
if ($this->ldap === false) {
throw $this->makeException('Library - LDAP __construct(): Unable to connect to \''.$hostname.'\'', ERR_INTERNAL);
}
// Enable LDAP protocol version 3
if (!@ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3)) {
throw $this->makeException('Library - LDAP __construct(): Failed to set LDAP Protocol version (LDAP_OPT_PROTOCOL_VERSION) to 3', ERR_INTERNAL);
}
// Set referral option
if (!@ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, $referrals)) {
throw $this->makeException('Library - LDAP __construct(): Failed to set LDAP Referrals (LDAP_OPT_REFERRALS) to '.$referrals, ERR_INTERNAL);
}
// Set timeouts, if supported
// (OpenLDAP 2.x.x or Netscape Directory SDK x.x needed)
$this->timeout = $timeout;
if ($timeout > 0) {
if (!@ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, $timeout)) {
SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_NETWORK_TIMEOUT) to '.$timeout);
}
if (!@ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, $timeout)) {
SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_TIMELIMIT) to '.$timeout);
}
}
// Enable TLS, if needed
if (stripos($hostname, "ldaps:") === false && $enable_tls) {
if (!@ldap_start_tls($this->ldap)) {
throw $this->makeException('Library - LDAP __construct(): Unable to force TLS', ERR_INTERNAL);
}
}
}
/**
* Convenience method to create an LDAPException as well as log the
* description.
*
* @param string $description
* The exception's description
* @return Exception
*/
private function makeException($description, $type = null)
{
$errNo = 0x00;
// Log LDAP code and description, if possible
if (empty($this->ldap)) {
SimpleSAML\Logger::error($description);
} else {
$errNo = @ldap_errno($this->ldap);
}
// Decide exception type and return
if ($type) {
if ($errNo !== 0) {
// Only log real LDAP errors; not success
SimpleSAML\Logger::error($description.'; cause: \''.ldap_error($this->ldap).'\' (0x'.dechex($errNo).')');
} else {
SimpleSAML\Logger::error($description);
}
switch ($type) {
case ERR_INTERNAL:// 1 - ExInternal
return new SimpleSAML_Error_Exception($description, $errNo);
case ERR_NO_USER:// 2 - ExUserNotFound
return new SimpleSAML_Error_UserNotFound($description, $errNo);
case ERR_WRONG_PW:// 3 - ExInvalidCredential
return new SimpleSAML_Error_InvalidCredential($description, $errNo);
case ERR_AS_DATA_INCONSIST:// 4 - ExAsDataInconsist
return new SimpleSAML_Error_AuthSource('ldap', $description);
case ERR_AS_INTERNAL:// 5 - ExAsInternal
return new SimpleSAML_Error_AuthSource('ldap', $description);
}
} else {
if ($errNo !== 0) {
$description .= '; cause: \''.ldap_error($this->ldap).'\' (0x'.dechex($errNo).')';
if (@ldap_get_option($this->ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extendedError) && !empty($extendedError)) {
$description .= '; additional: \''.$extendedError.'\'';
}
}
switch ($errNo) {
case 0x20://LDAP_NO_SUCH_OBJECT
SimpleSAML\Logger::warning($description);
return new SimpleSAML_Error_UserNotFound($description, $errNo);
case 0x31://LDAP_INVALID_CREDENTIALS
SimpleSAML\Logger::info($description);
return new SimpleSAML_Error_InvalidCredential($description, $errNo);
case -1://NO_SERVER_CONNECTION
SimpleSAML\Logger::error($description);
return new SimpleSAML_Error_AuthSource('ldap', $description);
default:
SimpleSAML\Logger::error($description);
return new SimpleSAML_Error_AuthSource('ldap', $description);
}
}
}
/**
* Search for DN from a single base.
*
* @param string $base
* Indication of root of subtree to search
* @param string|array $attribute
* The attribute name(s) to search for.
* @param string $value
* The attribute value to search for.
* Additional search filter
* @param string|null $searchFilter
* The scope of the search
* @param string $scope
* @return string
* The DN of the resulting found element.
* @throws SimpleSAML_Error_Exception if:
* - Attribute parameter is wrong type
* @throws SimpleSAML_Error_AuthSource if:
* - Not able to connect to LDAP server
* - False search result
* - Count return false
* - Searche found more than one result
* - Failed to get first entry from result
* - Failed to get DN for entry
* @throws SimpleSAML_Error_UserNotFound if:
* - Zero entries were found
*/
private function search($base, $attribute, $value, $searchFilter = null, $scope = "subtree")
{
// Create the search filter
$attribute = self::escape_filter_value($attribute, false);
$value = self::escape_filter_value($value, true);
$filter = '';
foreach ($attribute as $attr) {
$filter .= '('.$attr.'='.$value.')';
}
$filter = '(|'.$filter.')';
// Append LDAP filters if defined
if ($searchFilter != null) {
$filter = "(&".$filter."".$searchFilter.")";
}
// Search using generated filter
SimpleSAML\Logger::debug('Library - LDAP search(): Searching base ('.$scope.') \''.$base.'\' for \''.$filter.'\'');
if ($scope === 'base') {
$result = @ldap_read($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
} else if ($scope === 'onelevel') {
$result = @ldap_list($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
} else {
$result = @ldap_search($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
}
if ($result === false) {
throw $this->makeException('Library - LDAP search(): Failed search on base \''.$base.'\' for \''.$filter.'\'');
}
// Sanity checks on search results
$count = @ldap_count_entries($this->ldap, $result);
if ($count === false) {
throw $this->makeException('Library - LDAP search(): Failed to get number of entries returned');
} elseif ($count > 1) {
// More than one entry is found. External error
throw $this->makeException('Library - LDAP search(): Found '.$count.' entries searching base \''.$base.'\' for \''.$filter.'\'', ERR_AS_DATA_INCONSIST);
} elseif ($count === 0) {
// No entry is fond => wrong username is given (or not registered in the catalogue). User error
throw $this->makeException('Library - LDAP search(): Found no entries searching base \''.$base.'\' for \''.$filter.'\'', ERR_NO_USER);
}
// Resolve the DN from the search result
$entry = @ldap_first_entry($this->ldap, $result);
if ($entry === false) {
throw $this->makeException('Library - LDAP search(): Unable to retrieve result after searching base \''.$base.'\' for \''.$filter.'\'');
}
$dn = @ldap_get_dn($this->ldap, $entry);
if ($dn === false) {
throw $this->makeException('Library - LDAP search(): Unable to get DN after searching base \''.$base.'\' for \''.$filter.'\'');
}
return $dn;
}
/**
* Search for a DN.
*
* @param string|array $base
* The base, or bases, which to search from.
* @param string|array $attribute
* The attribute name(s) searched for.
* @param string $value
* The attribute value searched for.
* @param bool $allowZeroHits
* Determines if the method will throw an exception if no hits are found.
* Defaults to FALSE.
* @param string|null $searchFilter
* Additional searchFilter to be added to the (attribute=value) filter
* @param string $scope
* The scope of the search
* @return string
* The DN of the matching element, if found. If no element was found and
* $allowZeroHits is set to FALSE, an exception will be thrown; otherwise
* NULL will be returned.
* @throws SimpleSAML_Error_AuthSource if:
* - LDAP search encounter some problems when searching cataloge
* - Not able to connect to LDAP server
* @throws SimpleSAML_Error_UserNotFound if:
* - $allowZeroHits is FALSE and no result is found
*
*/
public function searchfordn($base, $attribute, $value, $allowZeroHits = false, $searchFilter = null, $scope = 'subtree')
{
// Traverse all search bases, returning DN if found
$bases = SimpleSAML\Utils\Arrays::arrayize($base);
foreach ($bases as $current) {
try {
// Single base search
$result = $this->search($current, $attribute, $value, $searchFilter, $scope);
// We don't hawe to look any futher if user is found
if (!empty($result)) {
return $result;
}
// If search failed, attempt the other base DNs
} catch (SimpleSAML_Error_UserNotFound $e) {
// Just continue searching
}
}
// Decide what to do for zero entries
SimpleSAML\Logger::debug('Library - LDAP searchfordn(): No entries found');
if ($allowZeroHits) {
// Zero hits allowed
return null;
} else {
// Zero hits not allowed
throw $this->makeException('Library - LDAP searchfordn(): LDAP search returned zero entries for filter \'('.
join(' | ', $attribute).' = '.$value.')\' on base(s) \'('.join(' & ', $bases).')\'', 2);
}
}
/**
* This method was created specifically for the ldap:AttributeAddUsersGroups->searchActiveDirectory()
* method, but could be used for other LDAP search needs. It will search LDAP and return all the entries.
*
* @throws Exception
* @param string|array $bases
* @param string|array $filters Array of 'attribute' => 'values' to be combined into the filter, or a raw filter string
* @param string|array $attributes Array of attributes requested from LDAP
* @param bool $and If multiple filters defined, then either bind them with & or |
* @param bool $escape Weather to escape the filter values or not
* @param string $scope The scope of the search
* @return array
*/
public function searchformultiple($bases, $filters, $attributes = array(), $and = true, $escape = true, $scope = 'subtree')
{
// Escape the filter values, if requested
if ($escape) {
$filters = $this->escape_filter_value($filters, false);
}
// Build search filter
$filter = '';
if (is_array($filters)) {
foreach ($filters as $attribute => $value) {
$filter .= "($attribute=$value)";
}
if (count($filters) > 1) {
$filter = ($and ? '(&' : '(|').$filter.')';
}
} elseif (is_string($filters)) {
$filter = $filters;
}
// Verify filter was created
if ($filter == '' || $filter == '(=)') {
throw $this->makeException('ldap:LdapConnection->search_manual : No search filters defined', ERR_INTERNAL);
}
// Verify at least one base was passed
$bases = (array) $bases;
if (empty($bases)) {
throw $this->makeException('ldap:LdapConnection->search_manual : No base DNs were passed', ERR_INTERNAL);
}
// Search each base until result is found
$result = false;
foreach ($bases as $base) {
if ($scope === 'base') {
$result = @ldap_read($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
} else if ($scope === 'onelevel') {
$result = @ldap_list($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
} else {
$result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
}
if ($result !== false && @ldap_count_entries($this->ldap, $result) > 0) {
break;
}
}
// Verify that a result was found in one of the bases
if ($result === false) {
throw $this->makeException(
'ldap:LdapConnection->search_manual : Failed to search LDAP using base(s) ['.
implode('; ', $bases).'] with filter ['.$filter.']. LDAP error ['.
ldap_error($this->ldap).']'
);
} elseif (@ldap_count_entries($this->ldap, $result) < 1) {
throw $this->makeException(
'ldap:LdapConnection->search_manual : No entries found in LDAP using base(s) ['.
implode('; ', $bases).'] with filter ['.$filter.']',
ERR_NO_USER
);
}
// Get all results
$results = ldap_get_entries($this->ldap, $result);
if ($results === false) {
throw $this->makeException(
'ldap:LdapConnection->search_manual : Unable to retrieve entries from search results'
);
}
// parse each entry and process its attributes
for ($i = 0; $i < $results['count']; $i++) {
$entry = $results[$i];
// iterate over the attributes of the entry
for ($j = 0; $j < $entry['count']; $j++) {
$name = $entry[$j];
$attribute = $entry[$name];
// decide whether to base64 encode or not
for ($k = 0; $k < $attribute['count']; $k++) {
// base64 encode binary attributes
if (strtolower($name) === 'jpegphoto' || strtolower($name) === 'objectguid') {
$results[$i][$name][$k] = base64_encode($attribute[$k]);
}
}
}
}
// Remove the count and return
unset($results['count']);
return $results;
}
/**
* Bind to LDAP with a specific DN and password. Simple wrapper around
* ldap_bind() with some additional logging.
*
* @param string $dn
* The DN used.
* @param string $password
* The password used.
* @param array $sasl_args
* Array of SASL options for SASL bind
* @return bool
* Returns TRUE if successful, FALSE if
* LDAP_INVALID_CREDENTIALS, LDAP_X_PROXY_AUTHZ_FAILURE,
* LDAP_INAPPROPRIATE_AUTH, LDAP_INSUFFICIENT_ACCESS
* @throws SimpleSAML_Error_Exception on other errors
*/
public function bind($dn, $password, array $sasl_args = null)
{
if ($sasl_args != null) {
if (!function_exists('ldap_sasl_bind')) {
$ex_msg = 'Library - missing SASL support';
throw $this->makeException($ex_msg);
}
// SASL Bind, with error handling
$authz_id = $sasl_args['authz_id'];
$error = @ldap_sasl_bind(
$this->ldap,
$dn,
$password,
$sasl_args['mech'],
$sasl_args['realm'],
$sasl_args['authc_id'],
$sasl_args['authz_id'],
$sasl_args['props']
);
} else {
// Simple Bind, with error handling
$authz_id = $dn;
$error = @ldap_bind($this->ldap, $dn, $password);
}
if ($error === true) {
// Good
$this->authz_id = $authz_id;
SimpleSAML\Logger::debug('Library - LDAP bind(): Bind successful with DN \''.$dn.'\'');
return true;
}
/* Handle errors
* LDAP_INVALID_CREDENTIALS
* LDAP_INSUFFICIENT_ACCESS */
switch (ldap_errno($this->ldap)) {
case 32: // LDAP_NO_SUCH_OBJECT
// no break
case 47: // LDAP_X_PROXY_AUTHZ_FAILURE
// no break
case 48: // LDAP_INAPPROPRIATE_AUTH
// no break
case 49: // LDAP_INVALID_CREDENTIALS
// no break
case 50: // LDAP_INSUFFICIENT_ACCESS
return false;
default:
break;
}
// Bad
throw $this->makeException('Library - LDAP bind(): Bind failed with DN \''.$dn.'\'');
}
/**
* Applies an LDAP option to the current connection.
*
* @throws Exception
* @param $option
* @param $value
* @return void
*/
public function setOption($option, $value)
{
// Attempt to set the LDAP option
if (!@ldap_set_option($this->ldap, $option, $value)) {
throw $this->makeException(
'ldap:LdapConnection->setOption : Failed to set LDAP option ['.
$option.'] with the value ['.$value.'] error: '.ldap_error($this->ldap),
ERR_INTERNAL
);
}
// Log debug message
SimpleSAML\Logger::debug(
'ldap:LdapConnection->setOption : Set the LDAP option ['.
$option.'] with the value ['.$value.']'
);
}
/**
* Search a given DN for attributes, and return the resulting associative
* array.
*
* @param string $dn
* The DN of an element.
* @param string|array $attributes
* The names of the attribute(s) to retrieve. Defaults to NULL; that is,
* all available attributes. Note that this is not very effective.
* @param int $maxsize
* The maximum size of any attribute's value(s). If exceeded, the attribute
* will not be returned.
* @return array
* The array of attributes and their values.
* @see http://no.php.net/manual/en/function.ldap-read.php
*/
public function getAttributes($dn, $attributes = null, $maxsize = null)
{
// Preparations, including a pretty debug message...
$description = 'all attributes';
if (is_array($attributes)) {
$description = '\''.join(',', $attributes).'\'';
} else {
// Get all attributes...
// TODO: Verify that this originally was the intended behaviour. Could $attributes be a string?
$attributes = array();
}
SimpleSAML\Logger::debug('Library - LDAP getAttributes(): Getting '.$description.' from DN \''.$dn.'\'');
// Attempt to get attributes
// TODO: Should aliases be dereferenced?
$result = @ldap_read($this->ldap, $dn, 'objectClass=*', $attributes, 0, 0, $this->timeout);
if ($result === false) {
throw $this->makeException('Library - LDAP getAttributes(): Failed to get attributes from DN \''.$dn.'\'');
}
$entry = @ldap_first_entry($this->ldap, $result);
if ($entry === false) {
throw $this->makeException('Library - LDAP getAttributes(): Could not get first entry from DN \''.$dn.'\'');
}
$attributes = @ldap_get_attributes($this->ldap, $entry); // Recycling $attributes... Possibly bad practice.
if ($attributes === false) {
throw $this->makeException('Library - LDAP getAttributes(): Could not get attributes of first entry from DN \''.$dn.'\'');
}
// Parsing each found attribute into our result set
$result = array(); // Recycling $result... Possibly bad practice.
for ($i = 0; $i < $attributes['count']; $i++) {
// Ignore attributes that exceed the maximum allowed size
$name = $attributes[$i];
$attribute = $attributes[$name];
// Deciding whether to base64 encode
$values = array();
for ($j = 0; $j < $attribute['count']; $j++) {
$value = $attribute[$j];
if (!empty($maxsize) && strlen($value) > $maxsize) {
// Ignoring and warning
SimpleSAML\Logger::warning('Library - LDAP getAttributes(): Attribute \''.
$name.'\' exceeded maximum allowed size by '.(strlen($value) - $maxsize));
continue;
}
// Base64 encode binary attributes
if (strtolower($name) === 'jpegphoto' || strtolower($name) === 'objectguid' || strtolower($name) === 'ms-ds-consistencyguid') {
$values[] = base64_encode($value);
} else {
$values[] = $value;
}
}
// Adding
$result[$name] = $values;
}
// We're done
SimpleSAML\Logger::debug('Library - LDAP getAttributes(): Found attributes \'('.join(',', array_keys($result)).')\'');
return $result;
}
/**
* Enter description here...
*
* @param array $config
* @param string $username
* @param string $password
* @return array|bool
*/
public function validate($config, $username, $password = null)
{
/* Escape any characters with a special meaning in LDAP. The following
* characters have a special meaning (according to RFC 2253):
* ',', '+', '"', '\', '<', '>', ';', '*'
* These characters are escaped by prefixing them with '\'.
*/
$username = addcslashes($username, ',+"\\<>;*');
if (isset($config['priv_user_dn'])) {
$this->bind($config['priv_user_dn'], $config['priv_user_pw']);
}
if (isset($config['dnpattern'])) {
$dn = str_replace('%username%', $username, $config['dnpattern']);
} else {
$dn = $this->searchfordn($config['searchbase'], $config['searchattributes'], $username);
}
if ($password !== null) { // checking users credentials ... assuming below that she may read her own attributes ...
// escape characters with a special meaning, also in the password
$password = addcslashes($password, ',+"\\<>;*');
if (!$this->bind($dn, $password)) {
SimpleSAML\Logger::info('Library - LDAP validate(): Failed to authenticate \''.$username.'\' using DN \''.$dn.'\'');
return false;
}
}
/*
* Retrieve attributes from LDAP
*/
$attributes = $this->getAttributes($dn, $config['attributes']);
return $attributes;
}
/**
* Borrowed function from PEAR:LDAP.
*
* Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
*
* Any control characters with an ACII code < 32 as well as the characters with special meaning in
* LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
* backslash followed by two hex digits representing the hexadecimal value of the character.
*
* @static
* @param string|array $values Array of values to escape
* @return array Array $values, but escaped
*/
public static function escape_filter_value($values = array(), $singleValue = true)
{
// Parameter validation
$values = \SimpleSAML\Utils\Arrays::arrayize($values);
foreach ($values as $key => $val) {
// Escaping of filter meta characters
$val = str_replace('\\', '\5c', $val);
$val = str_replace('*', '\2a', $val);
$val = str_replace('(', '\28', $val);
$val = str_replace(')', '\29', $val);
// ASCII < 32 escaping
$val = self::asc2hex32($val);
if (null === $val) {
$val = '\0'; // apply escaped "null" if string is empty
}
$values[$key] = $val;
}
if ($singleValue) {
return $values[0];
}
return $values;
}
/**
* Borrowed function from PEAR:LDAP.
*
* Converts all ASCII chars < 32 to "\HEX"
*
* @param string $string String to convert
*
* @static
* @return string
*/
public static function asc2hex32($string)
{
for ($i = 0; $i < strlen($string); $i++) {
$char = substr($string, $i, 1);
if (ord($char) < 32) {
$hex = dechex(ord($char));
if (strlen($hex) == 1) {
$hex = '0'.$hex;
}
$string = str_replace($char, '\\'.$hex, $string);
}
}
return $string;
}
/**
* Convert SASL authz_id into a DN
*/
private function authzid_to_dn($searchBase, $searchAttributes, $authz_id)
{
if (preg_match("/^dn:/", $authz_id)) {
return preg_replace("/^dn:/", "", $authz_id);
}
if (preg_match("/^u:/", $authz_id)) {
return $this->searchfordn(
$searchBase,
$searchAttributes,
preg_replace("/^u:/", "", $authz_id)
);
}
return $authz_id;
}
/**
* ldap_exop_whoami accessor, if available. Use requested authz_id
* otherwise.
*
* ldap_exop_whoami() has been provided as a third party patch that
* waited several years to get its way upstream:
* http://cvsweb.netbsd.org/bsdweb.cgi/pkgsrc/databases/php-ldap/files
*
* When it was integrated into PHP repository, the function prototype
* was changed, The new prototype was used in third party patch for
* PHP 7.0 and 7.1, hence the version test below.
*/
public function whoami($searchBase, $searchAttributes)
{
$authz_id = '';
if (function_exists('ldap_exop_whoami')) {
if (version_compare(phpversion(), '7', '<')) {
if (ldap_exop_whoami($this->ldap, $authz_id) !== true) {
throw $this->makeException('LDAP whoami exop failure');
}
} else {
if (($authz_id = ldap_exop_whoami($this->ldap)) === false) {
throw $this->makeException('LDAP whoami exop failure');
}
}
} else {
$authz_id = $this->authz_id;
}
$dn = $this->authzid_to_dn($searchBase, $searchAttributes, $authz_id);
if (!isset($dn) || ($dn == '')) {
throw $this->makeException('Cannot figure userID');
}
return $dn;
}
}

View File

@@ -0,0 +1,369 @@
<?php
/**
* Class for implementing authentication processing chains for IdPs.
*
* This class implements a system for additional steps which should be taken by an IdP before
* submitting a response to a SP. Examples of additional steps can be additional authentication
* checks, or attribute consent requirements.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Auth_ProcessingChain
{
/**
* The list of remaining filters which should be applied to the state.
*/
const FILTERS_INDEX = 'SimpleSAML_Auth_ProcessingChain.filters';
/**
* The stage we use for completed requests.
*/
const COMPLETED_STAGE = 'SimpleSAML_Auth_ProcessingChain.completed';
/**
* The request parameter we will use to pass the state identifier when we redirect after
* having completed processing of the state.
*/
const AUTHPARAM = 'AuthProcId';
/**
* All authentication processing filters, in the order they should be applied.
*/
private $filters;
/**
* Initialize an authentication processing chain for the given service provider
* and identity provider.
*
* @param array $idpMetadata The metadata for the IdP.
* @param array $spMetadata The metadata for the SP.
*/
public function __construct($idpMetadata, $spMetadata, $mode = 'idp')
{
assert(is_array($idpMetadata));
assert(is_array($spMetadata));
$this->filters = array();
$config = SimpleSAML_Configuration::getInstance();
$configauthproc = $config->getArray('authproc.' . $mode, null);
if (!empty($configauthproc)) {
$configfilters = self::parseFilterList($configauthproc);
self::addFilters($this->filters, $configfilters);
}
if (array_key_exists('authproc', $idpMetadata)) {
$idpFilters = self::parseFilterList($idpMetadata['authproc']);
self::addFilters($this->filters, $idpFilters);
}
if (array_key_exists('authproc', $spMetadata)) {
$spFilters = self::parseFilterList($spMetadata['authproc']);
self::addFilters($this->filters, $spFilters);
}
SimpleSAML\Logger::debug('Filter config for ' . $idpMetadata['entityid'] . '->' .
$spMetadata['entityid'] . ': ' . str_replace("\n", '', var_export($this->filters, true)));
}
/**
* Sort & merge filter configuration
*
* Inserts unsorted filters into sorted filter list. This sort operation is stable.
*
* @param array &$target Target filter list. This list must be sorted.
* @param array $src Source filters. May be unsorted.
*/
private static function addFilters(&$target, $src)
{
assert(is_array($target));
assert(is_array($src));
foreach ($src as $filter) {
$fp = $filter->priority;
// Find insertion position for filter
for ($i = count($target)-1; $i >= 0; $i--) {
if ($target[$i]->priority <= $fp) {
// The new filter should be inserted after this one
break;
}
}
/* $i now points to the filter which should preceede the current filter. */
array_splice($target, $i+1, 0, array($filter));
}
}
/**
* Parse an array of authentication processing filters.
*
* @param array $filterSrc Array with filter configuration.
* @return array Array of SimpleSAML_Auth_ProcessingFilter objects.
*/
private static function parseFilterList($filterSrc)
{
assert(is_array($filterSrc));
$parsedFilters = array();
foreach ($filterSrc as $priority => $filter) {
if (is_string($filter)) {
$filter = array('class' => $filter);
}
if (!is_array($filter)) {
throw new Exception('Invalid authentication processing filter configuration: ' .
'One of the filters wasn\'t a string or an array.');
}
$parsedFilters[] = self::parseFilter($filter, $priority);
}
return $parsedFilters;
}
/**
* Parse an authentication processing filter.
*
* @param array $config Array with the authentication processing filter configuration.
* @param int $priority The priority of the current filter, (not included in the filter
* definition.)
* @return SimpleSAML_Auth_ProcessingFilter The parsed filter.
*/
private static function parseFilter($config, $priority)
{
assert(is_array($config));
if (!array_key_exists('class', $config)) {
throw new Exception('Authentication processing filter without name given.');
}
$className = SimpleSAML\Module::resolveClass($config['class'], 'Auth_Process', 'SimpleSAML_Auth_ProcessingFilter');
$config['%priority'] = $priority;
unset($config['class']);
return new $className($config, null);
}
/**
* Process the given state.
*
* This function will only return if processing completes. If processing requires showing
* a page to the user, we will not be able to return from this function. There are two ways
* this can be handled:
* - Redirect to a URL: We will redirect to the URL set in $state['ReturnURL'].
* - Call a function: We will call the function set in $state['ReturnCall'].
*
* If an exception is thrown during processing, it should be handled by the caller of
* this function. If the user has redirected to a different page, the exception will be
* returned through the exception handler defined on the state array. See
* SimpleSAML_Auth_State for more information.
*
* @see SimpleSAML_Auth_State
* @see SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL
* @see SimpleSAML_Auth_State::EXCEPTION_HANDLER_FUNC
*
* @param array &$state The state we are processing.
*/
public function processState(&$state)
{
assert(is_array($state));
assert(array_key_exists('ReturnURL', $state) || array_key_exists('ReturnCall', $state));
assert(!array_key_exists('ReturnURL', $state) || !array_key_exists('ReturnCall', $state));
$state[self::FILTERS_INDEX] = $this->filters;
try {
// TODO: remove this in SSP 2.0
if (!array_key_exists('UserID', $state)) {
// No unique user ID present. Attempt to add one.
self::addUserID($state);
}
while (count($state[self::FILTERS_INDEX]) > 0) {
$filter = array_shift($state[self::FILTERS_INDEX]);
$filter->process($state);
}
} catch (SimpleSAML_Error_Exception $e) {
// No need to convert the exception
throw $e;
} catch (Exception $e) {
/*
* To be consistent with the exception we return after an redirect,
* we convert this exception before returning it.
*/
throw new SimpleSAML_Error_UnserializableException($e);
}
// Completed
}
/**
* Continues processing of the state.
*
* This function is used to resume processing by filters which for example needed to show
* a page to the user.
*
* This function will never return. Exceptions thrown during processing will be passed
* to whatever exception handler is defined in the state array.
*
* @param array $state The state we are processing.
*/
public static function resumeProcessing($state)
{
assert(is_array($state));
while (count($state[self::FILTERS_INDEX]) > 0) {
$filter = array_shift($state[self::FILTERS_INDEX]);
try {
$filter->process($state);
} catch (SimpleSAML_Error_Exception $e) {
SimpleSAML_Auth_State::throwException($state, $e);
} catch (Exception $e) {
$e = new SimpleSAML_Error_UnserializableException($e);
SimpleSAML_Auth_State::throwException($state, $e);
}
}
// Completed
assert(array_key_exists('ReturnURL', $state) || array_key_exists('ReturnCall', $state));
assert(!array_key_exists('ReturnURL', $state) || !array_key_exists('ReturnCall', $state));
if (array_key_exists('ReturnURL', $state)) {
/*
* Save state information, and redirect to the URL specified
* in $state['ReturnURL'].
*/
$id = SimpleSAML_Auth_State::saveState($state, self::COMPLETED_STAGE);
\SimpleSAML\Utils\HTTP::redirectTrustedURL($state['ReturnURL'], array(self::AUTHPARAM => $id));
} else {
/* Pass the state to the function defined in $state['ReturnCall']. */
// We are done with the state array in the session. Delete it.
SimpleSAML_Auth_State::deleteState($state);
$func = $state['ReturnCall'];
assert(is_callable($func));
call_user_func($func, $state);
assert(false);
}
}
/**
* Process the given state passivly.
*
* Modules with user interaction are expected to throw an \SimpleSAML\Module\saml\Error\NoPassive exception
* which are silently ignored. Exceptions of other types are passed further up the call stack.
*
* This function will only return if processing completes.
*
* @param array &$state The state we are processing.
*/
public function processStatePassive(&$state)
{
assert(is_array($state));
// Should not be set when calling this method
assert(!array_key_exists('ReturnURL', $state));
// Notify filters about passive request
$state['isPassive'] = true;
$state[self::FILTERS_INDEX] = $this->filters;
// TODO: remove this in SSP 2.0
if (!array_key_exists('UserID', $state)) {
// No unique user ID present. Attempt to add one.
self::addUserID($state);
}
while (count($state[self::FILTERS_INDEX]) > 0) {
$filter = array_shift($state[self::FILTERS_INDEX]);
try {
$filter->process($state);
// Ignore SimpleSAML_Error_NoPassive exceptions
} catch (SimpleSAML_Error_NoPassive $e) {
// @deprecated will be removed in 2.0
// Ignore \SimpleSAML\Error\NoPassive exceptions
} catch (\SimpleSAML\Module\saml\Error\NoPassive $e) {
// Ignore \SimpleSAML\Module\saml\Error\NoPassive exceptions
}
}
}
/**
* Retrieve a state which has finished processing.
*
* @param string $id The state identifier.
* @see SimpleSAML_Auth_State::parseStateID()
* @return Array The state referenced by the $id parameter.
*/
public static function fetchProcessedState($id)
{
assert(is_string($id));
return SimpleSAML_Auth_State::loadState($id, self::COMPLETED_STAGE);
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
private static function addUserID(&$state)
{
assert(is_array($state));
assert(array_key_exists('Attributes', $state));
if (isset($state['Destination']['userid.attribute'])) {
$attributeName = $state['Destination']['userid.attribute'];
SimpleSAML\Logger::warning("The 'userid.attribute' option has been deprecated.");
} elseif (isset($state['Source']['userid.attribute'])) {
$attributeName = $state['Source']['userid.attribute'];
SimpleSAML\Logger::warning("The 'userid.attribute' option has been deprecated.");
} else {
// Default attribute
$attributeName = 'eduPersonPrincipalName';
}
if (!array_key_exists($attributeName, $state['Attributes'])) {
return;
}
$uid = $state['Attributes'][$attributeName];
if (count($uid) === 0) {
SimpleSAML\Logger::warning('Empty user id attribute [' . $attributeName . '].');
return;
}
if (count($uid) > 1) {
SimpleSAML\Logger::warning('Multiple attribute values for user id attribute [' . $attributeName . '].');
return;
}
// TODO: the attribute value should be trimmed
$uid = $uid[0];
if (empty($uid)) {
SimpleSAML\Logger::warning('Empty value in attribute '.$attributeName.". on user. Cannot set UserID.");
return;
}
$state['UserID'] = $uid;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Base class for authentication processing filters.
*
* All authentication processing filters must support serialization.
*
* The current request is stored in an associative array. It has the following defined attributes:
* - 'Attributes' The attributes of the user.
* - 'Destination' Metadata of the destination (SP).
* - 'Source' Metadata of the source (IdP).
*
* It may also contain other attributes. If an authentication processing filter wishes to store other
* information in it, it should have a name on the form 'module:filter:attributename', to avoid name
* collisions.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
abstract class SimpleSAML_Auth_ProcessingFilter
{
/**
* Priority of this filter.
*
* Used when merging IdP and SP processing chains.
* The priority can be any integer. The default for most filters is 50. Filters may however
* specify their own default, if they typically should be amongst the first or the last filters.
*
* The prioroty can also be overridden by the user by specifying the '%priority' option.
*/
public $priority = 50;
/**
* Constructor for a processing filter.
*
* Any processing filter which implements its own constructor must call this
* constructor first.
*
* @param array &$config Configuration for this filter.
* @param mixed $reserved For future use.
*/
public function __construct(&$config, $reserved)
{
assert(is_array($config));
if (array_key_exists('%priority', $config)) {
$this->priority = $config['%priority'];
if (!is_int($this->priority)) {
throw new Exception('Invalid priority: ' . var_export($this->priority, true));
}
unset($config['%priority']);
}
}
/**
* Process a request.
*
* When a filter returns from this function, it is assumed to have completed its task.
*
* @param array &$request The request we are currently processing.
*/
abstract public function process(&$request);
}

401
lib/SimpleSAML/Auth/Simple.php Executable file
View File

@@ -0,0 +1,401 @@
<?php
namespace SimpleSAML\Auth;
use \SimpleSAML_Auth_Source as Source;
use \SimpleSAML_Auth_State as State;
use \SimpleSAML_Configuration as Configuration;
use \SimpleSAML_Error_AuthSource as AuthSourceError;
use \SimpleSAML\Module;
use \SimpleSAML_Session as Session;
use \SimpleSAML\Utils\HTTP;
/**
* Helper class for simple authentication applications.
*
* @package SimpleSAMLphp
*/
class Simple
{
/**
* The id of the authentication source we are accessing.
*
* @var string
*/
protected $authSource;
/**
* @var \SimpleSAML_Configuration|null
*/
protected $app_config;
/**
* Create an instance with the specified authsource.
*
* @param string $authSource The id of the authentication source.
*/
public function __construct($authSource)
{
assert(is_string($authSource));
$this->authSource = $authSource;
$this->app_config = Configuration::getInstance()->getConfigItem('application', null);
}
/**
* Retrieve the implementing authentication source.
*
* @return \SimpleSAML_Auth_Source The authentication source.
*
* @throws \SimpleSAML_Error_AuthSource If the requested auth source is unknown.
*/
public function getAuthSource()
{
$as = Source::getById($this->authSource);
if ($as === null) {
throw new AuthSourceError($this->authSource, 'Unknown authentication source.');
}
return $as;
}
/**
* Check if the user is authenticated.
*
* This function checks if the user is authenticated with the default authentication source selected by the
* 'default-authsource' option in 'config.php'.
*
* @return bool True if the user is authenticated, false if not.
*/
public function isAuthenticated()
{
$session = Session::getSessionFromRequest();
return $session->isValid($this->authSource);
}
/**
* Require the user to be authenticated.
*
* If the user is authenticated, this function returns immediately.
*
* If the user isn't authenticated, this function will authenticate the user with the authentication source, and
* then return the user to the current page.
*
* This function accepts an array $params, which controls some parts of the authentication. See the login()
* method for a description.
*
* @param array $params Various options to the authentication request. See the documentation.
*/
public function requireAuth(array $params = array())
{
$session = Session::getSessionFromRequest();
if ($session->isValid($this->authSource)) {
// Already authenticated
return;
}
$this->login($params);
}
/**
* Start an authentication process.
*
* This function accepts an array $params, which controls some parts of the authentication. The accepted parameters
* depends on the authentication source being used. Some parameters are generic:
* - 'ErrorURL': A URL that should receive errors from the authentication.
* - 'KeepPost': If the current request is a POST request, keep the POST data until after the authentication.
* - 'ReturnTo': The URL the user should be returned to after authentication.
* - 'ReturnCallback': The function we should call after the user has finished authentication.
*
* Please note: this function never returns.
*
* @param array $params Various options to the authentication request.
*/
public function login(array $params = array())
{
if (array_key_exists('KeepPost', $params)) {
$keepPost = (bool) $params['KeepPost'];
} else {
$keepPost = true;
}
if (array_key_exists('ReturnTo', $params)) {
$returnTo = (string) $params['ReturnTo'];
} else {
if (array_key_exists('ReturnCallback', $params)) {
$returnTo = (array) $params['ReturnCallback'];
} else {
$returnTo = HTTP::getSelfURL();
}
}
if (is_string($returnTo) && $keepPost && $_SERVER['REQUEST_METHOD'] === 'POST') {
$returnTo = HTTP::getPOSTRedirectURL($returnTo, $_POST);
}
if (array_key_exists('ErrorURL', $params)) {
$errorURL = (string) $params['ErrorURL'];
} else {
$errorURL = null;
}
if (!isset($params[State::RESTART]) && is_string($returnTo)) {
/*
* A URL to restart the authentication, in case the user bookmarks
* something, e.g. the discovery service page.
*/
$restartURL = $this->getLoginURL($returnTo);
$params[State::RESTART] = $restartURL;
}
$as = $this->getAuthSource();
$as->initLogin($returnTo, $errorURL, $params);
assert(false);
}
/**
* Log the user out.
*
* This function logs the user out. It will never return. By default, it will cause a redirect to the current page
* after logging the user out, but a different URL can be given with the $params parameter.
*
* Generic parameters are:
* - 'ReturnTo': The URL the user should be returned to after logout.
* - 'ReturnCallback': The function that should be called after logout.
* - 'ReturnStateParam': The parameter we should return the state in when redirecting.
* - 'ReturnStateStage': The stage the state array should be saved with.
*
* @param string|array|null $params Either the URL the user should be redirected to after logging out, or an array
* with parameters for the logout. If this parameter is null, we will return to the current page.
*/
public function logout($params = null)
{
assert(is_array($params) || is_string($params) || $params === null);
if ($params === null) {
$params = HTTP::getSelfURL();
}
if (is_string($params)) {
$params = array(
'ReturnTo' => $params,
);
}
assert(is_array($params));
assert(isset($params['ReturnTo']) || isset($params['ReturnCallback']));
if (isset($params['ReturnStateParam']) || isset($params['ReturnStateStage'])) {
assert(isset($params['ReturnStateParam'], $params['ReturnStateStage']));
}
$session = Session::getSessionFromRequest();
if ($session->isValid($this->authSource)) {
$state = $session->getAuthData($this->authSource, 'LogoutState');
if ($state !== null) {
$params = array_merge($state, $params);
}
$session->doLogout($this->authSource);
$params['LogoutCompletedHandler'] = array(get_class(), 'logoutCompleted');
$as = Source::getById($this->authSource);
if ($as !== null) {
$as->logout($params);
}
}
self::logoutCompleted($params);
}
/**
* Called when logout operation completes.
*
* This function never returns.
*
* @param array $state The state after the logout.
*/
public static function logoutCompleted($state)
{
assert(is_array($state));
assert(isset($state['ReturnTo']) || isset($state['ReturnCallback']));
if (isset($state['ReturnCallback'])) {
call_user_func($state['ReturnCallback'], $state);
assert(false);
} else {
$params = array();
if (isset($state['ReturnStateParam']) || isset($state['ReturnStateStage'])) {
assert(isset($state['ReturnStateParam'], $state['ReturnStateStage']));
$stateID = State::saveState($state, $state['ReturnStateStage']);
$params[$state['ReturnStateParam']] = $stateID;
}
\SimpleSAML\Utils\HTTP::redirectTrustedURL($state['ReturnTo'], $params);
}
}
/**
* Retrieve attributes of the current user.
*
* This function will retrieve the attributes of the current user if the user is authenticated. If the user isn't
* authenticated, it will return an empty array.
*
* @return array The users attributes.
*/
public function getAttributes()
{
if (!$this->isAuthenticated()) {
// Not authenticated
return array();
}
// Authenticated
$session = Session::getSessionFromRequest();
return $session->getAuthData($this->authSource, 'Attributes');
}
/**
* Retrieve authentication data.
*
* @param string $name The name of the parameter, e.g. 'Attributes', 'Expire' or 'saml:sp:IdP'.
*
* @return mixed|null The value of the parameter, or null if it isn't found or we are unauthenticated.
*/
public function getAuthData($name)
{
assert(is_string($name));
if (!$this->isAuthenticated()) {
return null;
}
$session = Session::getSessionFromRequest();
return $session->getAuthData($this->authSource, $name);
}
/**
* Retrieve all authentication data.
*
* @return array|null All persistent authentication data, or null if we aren't authenticated.
*/
public function getAuthDataArray()
{
if (!$this->isAuthenticated()) {
return null;
}
$session = Session::getSessionFromRequest();
return $session->getAuthState($this->authSource);
}
/**
* Retrieve a URL that can be used to log the user in.
*
* @param string|null $returnTo The page the user should be returned to afterwards. If this parameter is null, the
* user will be returned to the current page.
*
* @return string A URL which is suitable for use in link-elements.
*/
public function getLoginURL($returnTo = null)
{
assert($returnTo === null || is_string($returnTo));
if ($returnTo === null) {
$returnTo = HTTP::getSelfURL();
}
$login = Module::getModuleURL('core/as_login.php', array(
'AuthId' => $this->authSource,
'ReturnTo' => $returnTo,
));
return $login;
}
/**
* Retrieve a URL that can be used to log the user out.
*
* @param string|null $returnTo The page the user should be returned to afterwards. If this parameter is null, the
* user will be returned to the current page.
*
* @return string A URL which is suitable for use in link-elements.
*/
public function getLogoutURL($returnTo = null)
{
assert($returnTo === null || is_string($returnTo));
if ($returnTo === null) {
$returnTo = HTTP::getSelfURL();
}
$logout = Module::getModuleURL('core/as_logout.php', array(
'AuthId' => $this->authSource,
'ReturnTo' => $returnTo,
));
return $logout;
}
/**
* Process a URL and modify it according to the application/baseURL configuration option, if present.
*
* @param string|null $url The URL to process, or null if we want to use the current URL. Both partial and full
* URLs can be used as a parameter. The maximum precedence is given to the application/baseURL configuration option,
* then the URL specified (if it specifies scheme, host and port) and finally the environment observed in the
* server.
*
* @return string The URL modified according to the precedence rules.
*/
protected function getProcessedURL($url = null)
{
if ($url === null) {
$url = HTTP::getSelfURL();
}
$scheme = parse_url($url, PHP_URL_SCHEME);
$host = parse_url($url, PHP_URL_HOST) ?: HTTP::getSelfHost();
$port = parse_url($url, PHP_URL_PORT) ?: (
$scheme ? '' : trim(HTTP::getServerPort(), ':')
);
$scheme = $scheme ?: (HTTP::getServerHTTPS() ? 'https' : 'http');
$path = parse_url($url, PHP_URL_PATH) ?: '/';
$query = parse_url($url, PHP_URL_QUERY) ?: '';
$fragment = parse_url($url, PHP_URL_FRAGMENT) ?: '';
$port = !empty($port) ? ':'.$port : '';
if (($scheme === 'http' && $port === ':80') || ($scheme === 'https' && $port === ':443')) {
$port = '';
}
if (is_null($this->app_config)) {
// nothing more we can do here
return $scheme.'://'.$host.$port.$path.($query ? '?'.$query : '').($fragment ? '#'.$fragment : '');
}
$base = trim($this->app_config->getString(
'baseURL',
$scheme.'://'.$host.$port
), '/');
return $base.$path.($query ? '?'.$query : '').($fragment ? '#'.$fragment : '');
}
}

513
lib/SimpleSAML/Auth/Source.php Executable file
View File

@@ -0,0 +1,513 @@
<?php
use SimpleSAML\Auth\SourceFactory;
/**
* This class defines a base class for authentication source.
*
* An authentication source is any system which somehow authenticate the user.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
abstract class SimpleSAML_Auth_Source
{
/**
* The authentication source identifier. This identifier can be used to look up this object, for example when
* returning from a login form.
*
* @var string
*/
protected $authId;
/**
* Constructor for an authentication source.
*
* Any authentication source which implements its own constructor must call this
* constructor first.
*
* @param array $info Information about this authentication source.
* @param array &$config Configuration for this authentication source.
*/
public function __construct($info, &$config)
{
assert(is_array($info));
assert(is_array($config));
assert(array_key_exists('AuthId', $info));
$this->authId = $info['AuthId'];
}
/**
* Get sources of a specific type.
*
* @param string $type The type of the authentication source.
*
* @return SimpleSAML_Auth_Source[] Array of SimpleSAML_Auth_Source objects of the specified type.
* @throws Exception If the authentication source is invalid.
*/
public static function getSourcesOfType($type)
{
assert(is_string($type));
$config = SimpleSAML_Configuration::getConfig('authsources.php');
$ret = array();
$sources = $config->getOptions();
foreach ($sources as $id) {
$source = $config->getArray($id);
self::validateSource($source, $id);
if ($source[0] !== $type) {
continue;
}
$ret[] = self::parseAuthSource($id, $source);
}
return $ret;
}
/**
* Retrieve the ID of this authentication source.
*
* @return string The ID of this authentication source.
*/
public function getAuthId()
{
return $this->authId;
}
/**
* Process a request.
*
* If an authentication source returns from this function, it is assumed to have
* authenticated the user, and should have set elements in $state with the attributes
* of the user.
*
* If the authentication process requires additional steps which make it impossible to
* complete before returning from this function, the authentication source should
* save the state, and at a later stage, load the state, update it with the authentication
* information about the user, and call completeAuth with the state array.
*
* @param array &$state Information about the current authentication.
*/
abstract public function authenticate(&$state);
/**
* Reauthenticate an user.
*
* This function is called by the IdP to give the authentication source a chance to
* interact with the user even in the case when the user is already authenticated.
*
* @param array &$state Information about the current authentication.
*/
public function reauthenticate(array &$state)
{
assert(isset($state['ReturnCallback']));
// the default implementation just copies over the previous authentication data
$session = SimpleSAML_Session::getSessionFromRequest();
$data = $session->getAuthState($this->authId);
foreach ($data as $k => $v) {
$state[$k] = $v;
}
}
/**
* Complete authentication.
*
* This function should be called if authentication has completed. It will never return,
* except in the case of exceptions. Exceptions thrown from this page should not be caught,
* but should instead be passed to the top-level exception handler.
*
* @param array &$state Information about the current authentication.
*/
public static function completeAuth(&$state)
{
assert(is_array($state));
assert(array_key_exists('LoginCompletedHandler', $state));
SimpleSAML_Auth_State::deleteState($state);
$func = $state['LoginCompletedHandler'];
assert(is_callable($func));
call_user_func($func, $state);
assert(false);
}
/**
* Start authentication.
*
* This method never returns.
*
* @param string|array $return The URL or function we should direct the user to after authentication. If using a
* URL obtained from user input, please make sure to check it by calling \SimpleSAML\Utils\HTTP::checkURLAllowed().
* @param string|null $errorURL The URL we should direct the user to after failed authentication. Can be null, in
* which case a standard error page will be shown. If using a URL obtained from user input, please make sure to
* check it by calling \SimpleSAML\Utils\HTTP::checkURLAllowed().
* @param array $params Extra information about the login. Different authentication requestors may provide different
* information. Optional, will default to an empty array.
*/
public function initLogin($return, $errorURL = null, array $params = array())
{
assert(is_string($return) || is_array($return));
assert(is_string($errorURL) || $errorURL === null);
$state = array_merge($params, array(
'SimpleSAML_Auth_Default.id' => $this->authId, // TODO: remove in 2.0
'SimpleSAML_Auth_Source.id' => $this->authId,
'SimpleSAML_Auth_Default.Return' => $return, // TODO: remove in 2.0
'SimpleSAML_Auth_Source.Return' => $return,
'SimpleSAML_Auth_Default.ErrorURL' => $errorURL, // TODO: remove in 2.0
'SimpleSAML_Auth_Source.ErrorURL' => $errorURL,
'LoginCompletedHandler' => array(get_class(), 'loginCompleted'),
'LogoutCallback' => array(get_class(), 'logoutCallback'),
'LogoutCallbackState' => array(
'SimpleSAML_Auth_Default.logoutSource' => $this->authId, // TODO: remove in 2.0
'SimpleSAML_Auth_Source.logoutSource' => $this->authId,
),
));
if (is_string($return)) {
$state['SimpleSAML_Auth_Default.ReturnURL'] = $return; // TODO: remove in 2.0
$state['SimpleSAML_Auth_Source.ReturnURL'] = $return;
}
if ($errorURL !== null) {
$state[SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL] = $errorURL;
}
try {
$this->authenticate($state);
} catch (SimpleSAML_Error_Exception $e) {
SimpleSAML_Auth_State::throwException($state, $e);
} catch (Exception $e) {
$e = new SimpleSAML_Error_UnserializableException($e);
SimpleSAML_Auth_State::throwException($state, $e);
}
self::loginCompleted($state);
}
/**
* Called when a login operation has finished.
*
* This method never returns.
*
* @param array $state The state after the login has completed.
*/
public static function loginCompleted($state)
{
assert(is_array($state));
assert(array_key_exists('SimpleSAML_Auth_Source.Return', $state));
assert(array_key_exists('SimpleSAML_Auth_Source.id', $state));
assert(array_key_exists('Attributes', $state));
assert(!array_key_exists('LogoutState', $state) || is_array($state['LogoutState']));
$return = $state['SimpleSAML_Auth_Source.Return'];
// save session state
$session = SimpleSAML_Session::getSessionFromRequest();
$authId = $state['SimpleSAML_Auth_Source.id'];
$session->doLogin($authId, SimpleSAML_Auth_State::getPersistentAuthData($state));
if (is_string($return)) { // redirect...
\SimpleSAML\Utils\HTTP::redirectTrustedURL($return);
} else {
call_user_func($return, $state);
}
assert(false);
}
/**
* Log out from this authentication source.
*
* This function should be overridden if the authentication source requires special
* steps to complete a logout operation.
*
* If the logout process requires a redirect, the state should be saved. Once the
* logout operation is completed, the state should be restored, and completeLogout
* should be called with the state. If this operation can be completed without
* showing the user a page, or redirecting, this function should return.
*
* @param array &$state Information about the current logout operation.
*/
public function logout(&$state)
{
assert(is_array($state));
// default logout handler which doesn't do anything
}
/**
* Complete logout.
*
* This function should be called after logout has completed. It will never return,
* except in the case of exceptions. Exceptions thrown from this page should not be caught,
* but should instead be passed to the top-level exception handler.
*
* @param array &$state Information about the current authentication.
*/
public static function completeLogout(&$state)
{
assert(is_array($state));
assert(array_key_exists('LogoutCompletedHandler', $state));
SimpleSAML_Auth_State::deleteState($state);
$func = $state['LogoutCompletedHandler'];
assert(is_callable($func));
call_user_func($func, $state);
assert(false);
}
/**
* Create authentication source object from configuration array.
*
* This function takes an array with the configuration for an authentication source object,
* and returns the object.
*
* @param string $authId The authentication source identifier.
* @param array $config The configuration.
*
* @return SimpleSAML_Auth_Source The parsed authentication source.
* @throws Exception If the authentication source is invalid.
*/
private static function parseAuthSource($authId, $config)
{
assert(is_string($authId));
assert(is_array($config));
self::validateSource($config, $authId);
$id = $config[0];
$info = array('AuthId' => $authId);
$authSource = null;
unset($config[0]);
try {
// Check whether or not there's a factory responsible for instantiating our Auth Source instance
$factoryClass = SimpleSAML\Module::resolveClass($id, 'Auth_Source_Factory', 'SimpleSAML\Auth\SourceFactory');
/** @var SourceFactory $factory */
$factory = new $factoryClass;
$authSource = $factory->create($info, $config);
} catch (Exception $e) {
// If not, instantiate the Auth Source here
$className = SimpleSAML\Module::resolveClass($id, 'Auth_Source', 'SimpleSAML_Auth_Source');
$authSource = new $className($info, $config);
}
return $authSource;
}
/**
* Retrieve authentication source.
*
* This function takes an id of an authentication source, and returns the
* AuthSource object. If no authentication source with the given id can be found,
* NULL will be returned.
*
* If the $type parameter is specified, this function will return an
* authentication source of the given type. If no authentication source or if an
* authentication source of a different type is found, an exception will be thrown.
*
* @param string $authId The authentication source identifier.
* @param string|NULL $type The type of authentication source. If NULL, any type will be accepted.
*
* @return SimpleSAML_Auth_Source|NULL The AuthSource object, or NULL if no authentication
* source with the given identifier is found.
* @throws SimpleSAML_Error_Exception If no such authentication source is found or it is invalid.
*/
public static function getById($authId, $type = null)
{
assert(is_string($authId));
assert($type === null || is_string($type));
// for now - load and parse config file
$config = SimpleSAML_Configuration::getConfig('authsources.php');
$authConfig = $config->getArray($authId, null);
if ($authConfig === null) {
if ($type !== null) {
throw new SimpleSAML_Error_Exception(
'No authentication source with id '.
var_export($authId, true).' found.'
);
}
return null;
}
$ret = self::parseAuthSource($authId, $authConfig);
if ($type === null || $ret instanceof $type) {
return $ret;
}
// the authentication source doesn't have the correct type
throw new SimpleSAML_Error_Exception(
'Invalid type of authentication source '.
var_export($authId, true).'. Was '.var_export(get_class($ret), true).
', should be '.var_export($type, true).'.'
);
}
/**
* Called when the authentication source receives an external logout request.
*
* @param array $state State array for the logout operation.
*/
public static function logoutCallback($state)
{
assert(is_array($state));
assert(array_key_exists('SimpleSAML_Auth_Source.logoutSource', $state));
$source = $state['SimpleSAML_Auth_Source.logoutSource'];
$session = SimpleSAML_Session::getSessionFromRequest();
if (!$session->isValid($source)) {
SimpleSAML\Logger::warning(
'Received logout from an invalid authentication source '.
var_export($source, true)
);
return;
}
$session->doLogout($source);
}
/**
* Add a logout callback association.
*
* This function adds a logout callback association, which allows us to initiate
* a logout later based on the $assoc-value.
*
* Note that logout-associations exists per authentication source. A logout association
* from one authentication source cannot be called from a different authentication source.
*
* @param string $assoc The identifier for this logout association.
* @param array $state The state array passed to the authenticate-function.
*/
protected function addLogoutCallback($assoc, $state)
{
assert(is_string($assoc));
assert(is_array($state));
if (!array_key_exists('LogoutCallback', $state)) {
// the authentication requester doesn't have a logout callback
return;
}
$callback = $state['LogoutCallback'];
if (array_key_exists('LogoutCallbackState', $state)) {
$callbackState = $state['LogoutCallbackState'];
} else {
$callbackState = array();
}
$id = strlen($this->authId).':'.$this->authId.$assoc;
$data = array(
'callback' => $callback,
'state' => $callbackState,
);
$session = SimpleSAML_Session::getSessionFromRequest();
$session->setData(
'SimpleSAML_Auth_Source.LogoutCallbacks',
$id,
$data,
SimpleSAML_Session::DATA_TIMEOUT_SESSION_END
);
}
/**
* Call a logout callback based on association.
*
* This function calls a logout callback based on an association saved with
* addLogoutCallback(...).
*
* This function always returns.
*
* @param string $assoc The logout association which should be called.
*/
protected function callLogoutCallback($assoc)
{
assert(is_string($assoc));
$id = strlen($this->authId).':'.$this->authId.$assoc;
$session = SimpleSAML_Session::getSessionFromRequest();
$data = $session->getData('SimpleSAML_Auth_Source.LogoutCallbacks', $id);
if ($data === null) {
// FIXME: fix for IdP-first flow (issue 397) -> reevaluate logout callback infrastructure
$session->doLogout($this->authId);
return;
}
assert(is_array($data));
assert(array_key_exists('callback', $data));
assert(array_key_exists('state', $data));
$callback = $data['callback'];
$callbackState = $data['state'];
$session->deleteData('SimpleSAML_Auth_Source.LogoutCallbacks', $id);
call_user_func($callback, $callbackState);
}
/**
* Retrieve list of authentication sources.
*
* @return array The id of all authentication sources.
*/
public static function getSources()
{
$config = SimpleSAML_Configuration::getOptionalConfig('authsources.php');
return $config->getOptions();
}
/**
* Make sure that the first element of an auth source is its identifier.
*
* @param array $source An array with the auth source configuration.
* @param string $id The auth source identifier.
*
* @throws Exception If the first element of $source is not an identifier for the auth source.
*/
protected static function validateSource($source, $id)
{
if (!array_key_exists(0, $source) || !is_string($source[0])) {
throw new Exception(
'Invalid authentication source \''.$id.
'\': First element must be a string which identifies the authentication source.'
);
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace SimpleSAML\Auth;
use SimpleSAML_Auth_Source;
interface SourceFactory
{
/**
* @param array $info
* @param array $config
* @return SimpleSAML_Auth_Source
*/
public function create(array $info, array $config);
}

420
lib/SimpleSAML/Auth/State.php Executable file
View File

@@ -0,0 +1,420 @@
<?php
/**
* This is a helper class for saving and loading state information.
*
* The state must be an associative array. This class will add additional keys to this
* array. These keys will always start with 'SimpleSAML_Auth_State.'.
*
* It is also possible to add a restart URL to the state. If state information is lost, for
* example because it timed out, or the user loaded a bookmarked page, the loadState function
* will redirect to this URL. To use this, set $state[SimpleSAML_Auth_State::RESTART] to this
* URL.
*
* Both the saveState and the loadState function takes in a $stage parameter. This parameter is
* a security feature, and is used to prevent the user from taking a state saved one place and
* using it as input a different place.
*
* The $stage parameter must be a unique string. To maintain uniqueness, it must be on the form
* "<classname>.<identifier>" or "<module>:<identifier>".
*
* There is also support for passing exceptions through the state.
* By defining an exception handler when creating the state array, users of the state
* array can call throwException with the state and the exception. This exception will
* be passed to the handler defined by the EXCEPTION_HANDLER_URL or EXCEPTION_HANDLER_FUNC
* elements of the state array.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Auth_State
{
/**
* The index in the state array which contains the identifier.
*/
const ID = 'SimpleSAML_Auth_State.id';
/**
* The index in the cloned state array which contains the identifier of the
* original state.
*/
const CLONE_ORIGINAL_ID = 'SimpleSAML_Auth_State.cloneOriginalId';
/**
* The index in the state array which contains the current stage.
*/
const STAGE = 'SimpleSAML_Auth_State.stage';
/**
* The index in the state array which contains the restart URL.
*/
const RESTART = 'SimpleSAML_Auth_State.restartURL';
/**
* The index in the state array which contains the exception handler URL.
*/
const EXCEPTION_HANDLER_URL = 'SimpleSAML_Auth_State.exceptionURL';
/**
* The index in the state array which contains the exception handler function.
*/
const EXCEPTION_HANDLER_FUNC = 'SimpleSAML_Auth_State.exceptionFunc';
/**
* The index in the state array which contains the exception data.
*/
const EXCEPTION_DATA = 'SimpleSAML_Auth_State.exceptionData';
/**
* The stage of a state with an exception.
*/
const EXCEPTION_STAGE = 'SimpleSAML_Auth_State.exceptionStage';
/**
* The URL parameter which contains the exception state id.
*/
const EXCEPTION_PARAM = 'SimpleSAML_Auth_State_exceptionId';
/**
* State timeout.
*/
private static $stateTimeout = null;
/**
* Get the persistent authentication state from the state array.
*
* @param array $state The state array to analyze.
*
* @return array The persistent authentication state.
*/
public static function getPersistentAuthData(array $state)
{
// save persistent authentication data
$persistent = array();
if (array_key_exists('PersistentAuthData', $state)) {
foreach ($state['PersistentAuthData'] as $key) {
if (isset($state[$key])) {
$persistent[$key] = $state[$key];
}
}
}
// add those that should always be included
$mandatory = array(
'Attributes',
'Expire',
'LogoutState',
'AuthInstant',
'RememberMe',
'saml:sp:NameID'
);
foreach ($mandatory as $key) {
if (isset($state[$key])) {
$persistent[$key] = $state[$key];
}
}
return $persistent;
}
/**
* Retrieve the ID of a state array.
*
* Note that this function will not save the state.
*
* @param array &$state The state array.
* @param bool $rawId Return a raw ID, without a restart URL. Defaults to FALSE.
*
* @return string Identifier which can be used to retrieve the state later.
*/
public static function getStateId(&$state, $rawId = false)
{
assert(is_array($state));
assert(is_bool($rawId));
if (!array_key_exists(self::ID, $state)) {
$state[self::ID] = SimpleSAML\Utils\Random::generateID();
}
$id = $state[self::ID];
if ($rawId || !array_key_exists(self::RESTART, $state)) {
// Either raw ID or no restart URL. In any case, return the raw ID.
return $id;
}
// We have a restart URL. Return the ID with that URL.
return $id.':'.$state[self::RESTART];
}
/**
* Retrieve state timeout.
*
* @return integer State timeout.
*/
private static function getStateTimeout()
{
if (self::$stateTimeout === null) {
$globalConfig = SimpleSAML_Configuration::getInstance();
self::$stateTimeout = $globalConfig->getInteger('session.state.timeout', 60 * 60);
}
return self::$stateTimeout;
}
/**
* Save the state.
*
* This function saves the state, and returns an id which can be used to
* retrieve it later. It will also update the $state array with the identifier.
*
* @param array &$state The login request state.
* @param string $stage The current stage in the login process.
* @param bool $rawId Return a raw ID, without a restart URL.
*
* @return string Identifier which can be used to retrieve the state later.
*/
public static function saveState(&$state, $stage, $rawId = false)
{
assert(is_array($state));
assert(is_string($stage));
assert(is_bool($rawId));
$return = self::getStateId($state, $rawId);
$id = $state[self::ID];
// Save stage
$state[self::STAGE] = $stage;
// Save state
$serializedState = serialize($state);
$session = SimpleSAML_Session::getSessionFromRequest();
$session->setData('SimpleSAML_Auth_State', $id, $serializedState, self::getStateTimeout());
SimpleSAML\Logger::debug('Saved state: '.var_export($return, true));
return $return;
}
/**
* Clone the state.
*
* This function clones and returns the new cloned state.
*
* @param array $state The original request state.
*
* @return array Cloned state data.
*/
public static function cloneState(array $state)
{
$clonedState = $state;
if (array_key_exists(self::ID, $state)) {
$clonedState[self::CLONE_ORIGINAL_ID] = $state[self::ID];
unset($clonedState[self::ID]);
SimpleSAML\Logger::debug('Cloned state: '.var_export($state[self::ID], true));
} else {
SimpleSAML\Logger::debug('Cloned state with undefined id.');
}
return $clonedState;
}
/**
* Retrieve saved state.
*
* This function retrieves saved state information. If the state information has been lost,
* it will attempt to restart the request by calling the restart URL which is embedded in the
* state information. If there is no restart information available, an exception will be thrown.
*
* @param string $id State identifier (with embedded restart information).
* @param string $stage The stage the state should have been saved in.
* @param bool $allowMissing Whether to allow the state to be missing.
*
* @throws SimpleSAML_Error_NoState If we couldn't find the state and there's no URL defined to redirect to.
* @throws Exception If the stage of the state is invalid and there's no URL defined to redirect to.
*
* @return array|NULL State information, or null if the state is missing and $allowMissing is true.
*/
public static function loadState($id, $stage, $allowMissing = false)
{
assert(is_string($id));
assert(is_string($stage));
assert(is_bool($allowMissing));
SimpleSAML\Logger::debug('Loading state: '.var_export($id, true));
$sid = self::parseStateID($id);
$session = SimpleSAML_Session::getSessionFromRequest();
$state = $session->getData('SimpleSAML_Auth_State', $sid['id']);
if ($state === null) {
// Could not find saved data
if ($allowMissing) {
return null;
}
if ($sid['url'] === null) {
throw new SimpleSAML_Error_NoState();
}
\SimpleSAML\Utils\HTTP::redirectUntrustedURL($sid['url']);
}
$state = unserialize($state);
assert(is_array($state));
assert(array_key_exists(self::ID, $state));
assert(array_key_exists(self::STAGE, $state));
// Verify stage
if ($state[self::STAGE] !== $stage) {
/* This could be a user trying to bypass security, but most likely it is just
* someone using the back-button in the browser. We try to restart the
* request if that is possible. If not, show an error.
*/
$msg = 'Wrong stage in state. Was \''.$state[self::STAGE].
'\', should be \''.$stage.'\'.';
SimpleSAML\Logger::warning($msg);
if ($sid['url'] === null) {
throw new Exception($msg);
}
\SimpleSAML\Utils\HTTP::redirectUntrustedURL($sid['url']);
}
return $state;
}
/**
* Delete state.
*
* This function deletes the given state to prevent the user from reusing it later.
*
* @param array &$state The state which should be deleted.
*/
public static function deleteState(&$state)
{
assert(is_array($state));
if (!array_key_exists(self::ID, $state)) {
// This state hasn't been saved
return;
}
SimpleSAML\Logger::debug('Deleting state: '.var_export($state[self::ID], true));
$session = SimpleSAML_Session::getSessionFromRequest();
$session->deleteData('SimpleSAML_Auth_State', $state[self::ID]);
}
/**
* Throw exception to the state exception handler.
*
* @param array $state The state array.
* @param SimpleSAML_Error_Exception $exception The exception.
*
* @throws SimpleSAML_Error_Exception If there is no exception handler defined, it will just throw the $exception.
*/
public static function throwException($state, SimpleSAML_Error_Exception $exception)
{
assert(is_array($state));
if (array_key_exists(self::EXCEPTION_HANDLER_URL, $state)) {
// Save the exception
$state[self::EXCEPTION_DATA] = $exception;
$id = self::saveState($state, self::EXCEPTION_STAGE);
// Redirect to the exception handler
\SimpleSAML\Utils\HTTP::redirectTrustedURL(
$state[self::EXCEPTION_HANDLER_URL],
array(self::EXCEPTION_PARAM => $id)
);
} elseif (array_key_exists(self::EXCEPTION_HANDLER_FUNC, $state)) {
// Call the exception handler
$func = $state[self::EXCEPTION_HANDLER_FUNC];
assert(is_callable($func));
call_user_func($func, $exception, $state);
assert(false);
} else {
/*
* No exception handler is defined for the current state.
*/
throw $exception;
}
}
/**
* Retrieve an exception state.
*
* @param string|NULL $id The exception id. Can be NULL, in which case it will be retrieved from the request.
*
* @return array|NULL The state array with the exception, or NULL if no exception was thrown.
*/
public static function loadExceptionState($id = null)
{
assert(is_string($id) || $id === null);
if ($id === null) {
if (!array_key_exists(self::EXCEPTION_PARAM, $_REQUEST)) {
// No exception
return null;
}
$id = $_REQUEST[self::EXCEPTION_PARAM];
}
$state = self::loadState($id, self::EXCEPTION_STAGE);
assert(array_key_exists(self::EXCEPTION_DATA, $state));
return $state;
}
/**
* Get the ID and (optionally) a URL embedded in a StateID, in the form 'id:url'.
*
* @param string $stateId The state ID to use.
*
* @return array A hashed array with the ID and the URL (if any), in the 'id' and 'url' keys, respectively. If
* there's no URL in the input parameter, NULL will be returned as the value for the 'url' key.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function parseStateID($stateId)
{
$tmp = explode(':', $stateId, 2);
$id = $tmp[0];
$url = null;
if (count($tmp) === 2) {
$url = $tmp[1];
}
return array('id' => $id, 'url' => $url);
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace SimpleSAML\Auth;
/**
* A class that generates and verifies time-limited tokens.
*/
class TimeLimitedToken
{
/**
* @var string
*/
protected $secretSalt;
/**
* @var int
*/
protected $lifetime;
/**
* @var int
*/
protected $skew;
/**
* @var string
*/
protected $algo;
/**
* Create a new time-limited token.
*
* Please note that the default algorithm will change in SSP 1.15.0 to SHA-256 instead of SHA-1.
*
* @param int $lifetime Token lifetime in seconds. Defaults to 900 (15 min).
* @param string $secretSalt A random and unique salt per installation. Defaults to the salt in the configuration.
* @param int $skew The allowed time skew (in seconds) to correct clock deviations. Defaults to 1 second.
* @param string $algo The hash algorithm to use to generate the tokens. Defaults to SHA-1.
*
* @throws \InvalidArgumentException if the given parameters are invalid.
*/
public function __construct($lifetime = 900, $secretSalt = null, $skew = 1, $algo = 'sha1')
{
if ($secretSalt === null) {
$secretSalt = \SimpleSAML\Utils\Config::getSecretSalt();
}
if (!in_array($algo, hash_algos(), true)) {
throw new \InvalidArgumentException('Invalid hash algorithm "'.$algo.'"');
}
$this->secretSalt = $secretSalt;
$this->lifetime = $lifetime;
$this->skew = $skew;
$this->algo = $algo;
}
/**
* Add some given data to the current token. This data will be needed later too for token validation.
*
* This mechanism can be used to provide context for a token, such as a user identifier of the only subject
* authorised to use it. Note also that multiple data can be added to the token. This means that upon validation,
* not only the same data must be added, but also in the same order.
*
* @param string $data The data to incorporate into the current token.
*/
public function addVerificationData($data)
{
$this->secretSalt .= '|'.$data;
}
/**
* Calculates a token value for a given offset.
*
* @param int $offset The offset to use.
* @param int|null $time The time stamp to which the offset is relative to. Defaults to the current time.
*
* @return string The token for the given time and offset.
*/
private function calculateTokenValue($offset, $time = null)
{
if ($time === null) {
$time = time();
}
// a secret salt that should be randomly generated for each installation
return hash(
$this->algo,
$offset.':'.floor(($time - $offset) / ($this->lifetime + $this->skew)).':'.$this->secretSalt
);
}
/**
* Generates a token that contains an offset and a token value, using the current offset.
*
* @return string A time-limited token with the offset respect to the beginning of its time slot prepended.
*/
public function generate()
{
$time = time();
$current_offset = ($time - $this->skew) % ($this->lifetime + $this->skew);
return dechex($current_offset).'-'.$this->calculateTokenValue($current_offset, $time);
}
/**
* @see generate
* @deprecated This method will be removed in SSP 2.0. Use generate() instead.
*/
public function generate_token()
{
return $this->generate();
}
/**
* Validates a token by calculating the token value for the provided offset and comparing it.
*
* @param string $token The token to validate.
*
* @return boolean True if the given token is currently valid, false otherwise.
*/
public function validate($token)
{
$splittoken = explode('-', $token);
if (count($splittoken) !== 2) {
return false;
}
$offset = intval(hexdec($splittoken[0]));
$value = $splittoken[1];
return ($this->calculateTokenValue($offset) === $value);
}
/**
* @see validate
* @deprecated This method will be removed in SSP 2.0. Use validate() instead.
*/
public function validate_token($token)
{
return $this->validate($token);
}
}

166
lib/SimpleSAML/AuthMemCookie.php Executable file
View File

@@ -0,0 +1,166 @@
<?php
/**
* This is a helper class for the Auth MemCookie module.
* It handles the configuration, and implements the logout handler.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*
* @deprecated This class has been deprecated and will be removed in SSP 2.0. Use the memcookie module instead.
*/
class SimpleSAML_AuthMemCookie
{
/**
* @var SimpleSAML_AuthMemCookie This is the singleton instance of this class.
*/
private static $instance = null;
/**
* @var SimpleSAML_Configuration The configuration for Auth MemCookie.
*/
private $amcConfig;
/**
* This function is used to retrieve the singleton instance of this class.
*
* @return SimpleSAML_AuthMemCookie The singleton instance of this class.
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new SimpleSAML_AuthMemCookie();
}
return self::$instance;
}
/**
* This function implements the constructor for this class. It loads the Auth MemCookie configuration.
*/
private function __construct()
{
// load AuthMemCookie configuration
$this->amcConfig = SimpleSAML_Configuration::getConfig('authmemcookie.php');
}
/**
* Retrieve the authentication source that should be used to authenticate the user.
*
* @return string The login type which should be used for Auth MemCookie.
*/
public function getAuthSource()
{
return $this->amcConfig->getString('authsource');
}
/**
* This function retrieves the name of the cookie from the configuration.
*
* @return string The name of the cookie.
* @throws Exception If the value of the 'cookiename' configuration option is invalid.
*/
public function getCookieName()
{
$cookieName = $this->amcConfig->getString('cookiename', 'AuthMemCookie');
if (!is_string($cookieName) || strlen($cookieName) === 0) {
throw new Exception(
"Configuration option 'cookiename' contains an invalid value. This option should be a string."
);
}
return $cookieName;
}
/**
* This function retrieves the name of the attribute which contains the username from the configuration.
*
* @return string The name of the attribute which contains the username.
*/
public function getUsernameAttr()
{
$usernameAttr = $this->amcConfig->getString('username', null);
return $usernameAttr;
}
/**
* This function retrieves the name of the attribute which contains the groups from the configuration.
*
* @return string The name of the attribute which contains the groups.
*/
public function getGroupsAttr()
{
$groupsAttr = $this->amcConfig->getString('groups', null);
return $groupsAttr;
}
/**
* This function creates and initializes a Memcache object from our configuration.
*
* @return Memcache A Memcache object initialized from our configuration.
* @throws Exception If the servers configuration is invalid.
*/
public function getMemcache()
{
$memcacheHost = $this->amcConfig->getString('memcache.host', '127.0.0.1');
$memcachePort = $this->amcConfig->getInteger('memcache.port', 11211);
$class = class_exists('Memcache') ? 'Memcache' : (class_exists('Memcached') ? 'Memcached' : false);
if (!$class) {
throw new Exception('Missing Memcached implementation. You must install either the Memcache or Memcached extension.');
}
// Create the Memcache(d) object.
$memcache = new $class();
foreach (explode(',', $memcacheHost) as $memcacheHost) {
$memcache->addServer($memcacheHost, $memcachePort);
}
return $memcache;
}
/**
* This function logs the user out by deleting the session information from memcache.
*/
private function doLogout()
{
$cookieName = $this->getCookieName();
// check if we have a valid cookie
if (!array_key_exists($cookieName, $_COOKIE)) {
return;
}
$sessionID = $_COOKIE[$cookieName];
// delete the session from memcache
$memcache = $this->getMemcache();
$memcache->delete($sessionID);
// delete the session cookie
\SimpleSAML\Utils\HTTP::setCookie($cookieName, null);
}
/**
* This function implements the logout handler. It deletes the information from Memcache.
*/
public static function logoutHandler()
{
self::getInstance()->doLogout();
}
}

View File

@@ -0,0 +1,190 @@
<?php
/**
* Implementation of the Shibboleth 1.3 Artifact binding.
*
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Bindings\Shib13;
use SAML2\DOMDocumentFactory;
use SimpleSAML\Utils\Config;
use SimpleSAML\Utils\HTTP;
use SimpleSAML\Utils\Random;
use SimpleSAML\Utils\System;
use SimpleSAML\Utils\Time;
use SimpleSAML\Utils\XML;
class Artifact
{
/**
* Parse the query string, and extract the SAMLart parameters.
*
* This function is required because each query contains multiple
* artifact with the same parameter name.
*
* @return array The artifacts.
*/
private static function getArtifacts()
{
assert(array_key_exists('QUERY_STRING', $_SERVER));
// We need to process the query string manually, to capture all SAMLart parameters
$artifacts = array();
$elements = explode('&', $_SERVER['QUERY_STRING']);
foreach ($elements as $element) {
list($name, $value) = explode('=', $element, 2);
$name = urldecode($name);
$value = urldecode($value);
if ($name === 'SAMLart') {
$artifacts[] = $value;
}
}
return $artifacts;
}
/**
* Build the request we will send to the IdP.
*
* @param array $artifacts The artifacts we will request.
* @return string The request, as an XML string.
*/
private static function buildRequest(array $artifacts)
{
$msg = '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">' .
'<SOAP-ENV:Body>' .
'<samlp:Request xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol"' .
' RequestID="' . Random::generateID() . '"' .
' MajorVersion="1" MinorVersion="1"' .
' IssueInstant="' . Time::generateTimestamp() . '"' .
'>';
foreach ($artifacts as $a) {
$msg .= '<samlp:AssertionArtifact>' . htmlspecialchars($a) . '</samlp:AssertionArtifact>';
}
$msg .= '</samlp:Request>' .
'</SOAP-ENV:Body>' .
'</SOAP-ENV:Envelope>';
return $msg;
}
/**
* Extract the response element from the SOAP response.
*
* @param string $soapResponse The SOAP response.
* @return string The <saml1p:Response> element, as a string.
* @throws \SimpleSAML_Error_Exception
*/
private static function extractResponse($soapResponse)
{
assert(is_string($soapResponse));
try {
$doc = DOMDocumentFactory::fromString($soapResponse);
} catch (\Exception $e) {
throw new \SimpleSAML_Error_Exception('Error parsing SAML 1 artifact response.');
}
$soapEnvelope = $doc->firstChild;
if (!XML::isDOMNodeOfType($soapEnvelope, 'Envelope', 'http://schemas.xmlsoap.org/soap/envelope/')) {
throw new \SimpleSAML_Error_Exception('Expected artifact response to contain a <soap:Envelope> element.');
}
$soapBody = XML::getDOMChildren($soapEnvelope, 'Body', 'http://schemas.xmlsoap.org/soap/envelope/');
if (count($soapBody) === 0) {
throw new \SimpleSAML_Error_Exception('Couldn\'t find <soap:Body> in <soap:Envelope>.');
}
$soapBody = $soapBody[0];
$responseElement = XML::getDOMChildren($soapBody, 'Response', 'urn:oasis:names:tc:SAML:1.0:protocol');
if (count($responseElement) === 0) {
throw new \SimpleSAML_Error_Exception('Couldn\'t find <saml1p:Response> in <soap:Body>.');
}
$responseElement = $responseElement[0];
/*
* Save the <saml1p:Response> element. Note that we need to import it
* into a new document, in order to preserve namespace declarations.
*/
$newDoc = DOMDocumentFactory::create();
$newDoc->appendChild($newDoc->importNode($responseElement, true));
$responseXML = $newDoc->saveXML();
return $responseXML;
}
/**
* This function receives a SAML 1.1 artifact.
*
* @param \SimpleSAML_Configuration $spMetadata The metadata of the SP.
* @param \SimpleSAML_Configuration $idpMetadata The metadata of the IdP.
* @return string The <saml1p:Response> element, as an XML string.
* @throws \SimpleSAML_Error_Exception
*/
public static function receive(\SimpleSAML_Configuration $spMetadata, \SimpleSAML_Configuration $idpMetadata)
{
$artifacts = self::getArtifacts();
$request = self::buildRequest($artifacts);
XML::debugSAMLMessage($request, 'out');
$url = $idpMetadata->getDefaultEndpoint('ArtifactResolutionService', array('urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding'));
$url = $url['Location'];
$peerPublicKeys = $idpMetadata->getPublicKeys('signing', true);
$certData = '';
foreach ($peerPublicKeys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
$certData .= "-----BEGIN CERTIFICATE-----\n" .
chunk_split($key['X509Certificate'], 64) .
"-----END CERTIFICATE-----\n";
}
$file = System::getTempDir() . DIRECTORY_SEPARATOR . sha1($certData) . '.crt';
if (!file_exists($file)) {
System::writeFile($file, $certData);
}
$spKeyCertFile = Config::getCertPath($spMetadata->getString('privatekey'));
$opts = array(
'ssl' => array(
'verify_peer' => true,
'cafile' => $file,
'local_cert' => $spKeyCertFile,
'capture_peer_cert' => true,
'capture_peer_chain' => true,
),
'http' => array(
'method' => 'POST',
'content' => $request,
'header' => 'SOAPAction: http://www.oasis-open.org/committees/security' . "\r\n" .
'Content-Type: text/xml',
),
);
// Fetch the artifact
$response = HTTP::fetch($url, $opts);
/** @var string $response */
XML::debugSAMLMessage($response, 'in');
// Find the response in the SOAP message
$response = self::extractResponse($response);
return $response;
}
}

View File

@@ -0,0 +1,154 @@
<?php
/**
* Implementation of the Shibboleth 1.3 HTTP-POST binding.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Bindings\Shib13;
use SAML2\DOMDocumentFactory;
use SimpleSAML\Utils\Crypto;
use SimpleSAML\Utils\HTTP;
use SimpleSAML\Utils\XML;
use SimpleSAML\XML\Shib13\AuthnResponse;
use SimpleSAML\XML\Signer;
class HTTPPost
{
/**
* @var \SimpleSAML_Configuration
*/
private $configuration = null;
/**
* @var \SimpleSAML_Metadata_MetaDataStorageHandler
*/
private $metadata = null;
/**
* Constructor for the \SimpleSAML\Bindings\Shib13\HTTPPost class.
*
* @param \SimpleSAML_Configuration $configuration The configuration to use.
* @param \SimpleSAML_Metadata_MetaDataStorageHandler $metadatastore A store where to find metadata.
*/
public function __construct(
\SimpleSAML_Configuration $configuration,
\SimpleSAML_Metadata_MetaDataStorageHandler $metadatastore
) {
$this->configuration = $configuration;
$this->metadata = $metadatastore;
}
/**
* Send an authenticationResponse using HTTP-POST.
*
* @param string $response The response which should be sent.
* @param \SimpleSAML_Configuration $idpmd The metadata of the IdP which is sending the response.
* @param \SimpleSAML_Configuration $spmd The metadata of the SP which is receiving the response.
* @param string|null $relayState The relaystate for the SP.
* @param string $shire The shire which should receive the response.
*/
public function sendResponse(
$response,
\SimpleSAML_Configuration $idpmd,
\SimpleSAML_Configuration $spmd,
$relayState,
$shire
) {
XML::checkSAMLMessage($response, 'saml11');
$privatekey = Crypto::loadPrivateKey($idpmd, true);
$publickey = Crypto::loadPublicKey($idpmd, true);
$responsedom = DOMDocumentFactory::fromString(str_replace("\r", "", $response));
$responseroot = $responsedom->getElementsByTagName('Response')->item(0);
$firstassertionroot = $responsedom->getElementsByTagName('Assertion')->item(0);
/* Determine what we should sign - either the Response element or the Assertion. The default is to sign the
* Assertion, but that can be overridden by the 'signresponse' option in the SP metadata or
* 'saml20.signresponse' in the global configuration.
*
* TODO: neither 'signresponse' nor 'shib13.signresponse' are valid options any longer. Remove!
*/
if ($spmd->hasValue('signresponse')) {
$signResponse = $spmd->getBoolean('signresponse');
} else {
$signResponse = $this->configuration->getBoolean('shib13.signresponse', true);
}
// check if we have an assertion to sign. Force to sign the response if not
if ($firstassertionroot === null) {
$signResponse = true;
}
$signer = new Signer(array(
'privatekey_array' => $privatekey,
'publickey_array' => $publickey,
'id' => ($signResponse ? 'ResponseID' : 'AssertionID'),
));
if ($idpmd->hasValue('certificatechain')) {
$signer->addCertificate($idpmd->getString('certificatechain'));
}
if ($signResponse) {
// sign the response - this must be done after encrypting the assertion
// we insert the signature before the saml2p:Status element
$statusElements = XML::getDOMChildren($responseroot, 'Status', '@saml1p');
assert(count($statusElements) === 1);
$signer->sign($responseroot, $responseroot, $statusElements[0]);
} else {
// Sign the assertion
$signer->sign($firstassertionroot, $firstassertionroot);
}
$response = $responsedom->saveXML();
XML::debugSAMLMessage($response, 'out');
HTTP::submitPOSTData($shire, array(
'TARGET' => $relayState,
'SAMLResponse' => base64_encode($response),
));
}
/**
* Decode a received response.
*
* @param array $post POST data received.
* @return \SimpleSAML\XML\Shib13\AuthnResponse The response decoded into an object.
* @throws \Exception If there is no SAMLResponse parameter.
*/
public function decodeResponse($post)
{
assert(is_array($post));
if (!array_key_exists('SAMLResponse', $post)) {
throw new \Exception('Missing required SAMLResponse parameter.');
}
$rawResponse = $post['SAMLResponse'];
$samlResponseXML = base64_decode($rawResponse);
XML::debugSAMLMessage($samlResponseXML, 'in');
XML::checkSAMLMessage($samlResponseXML, 'saml11');
$samlResponse = new AuthnResponse();
$samlResponse->setXML($samlResponseXML);
if (array_key_exists('TARGET', $post)) {
$samlResponse->setRelayState($post['TARGET']);
}
return $samlResponse;
}
}

1392
lib/SimpleSAML/Configuration.php Executable file

File diff suppressed because it is too large Load Diff

293
lib/SimpleSAML/Database.php Executable file
View File

@@ -0,0 +1,293 @@
<?php
namespace SimpleSAML;
/**
* This file implements functions to read and write to a group of database servers.
*
* This database class supports a single database, or a master/slave configuration with as many defined slaves as a
* user would like.
*
* The goal of this class is to provide a single mechanism to connect to a database that can be reused by any component
* within SimpleSAMLphp including modules. When using this class, the global configuration should be passed here, but in
* the case of a module that has a good reason to use a different database, such as sqlauth, an alternative config file
* can be provided.
*
* @author Tyler Antonio, University of Alberta. <tantonio@ualberta.ca>
* @package SimpleSAMLphp
*/
class Database
{
/**
* This variable holds the instance of the session - Singleton approach.
*/
private static $instance = array();
/**
* PDO Object for the Master database server
*/
private $dbMaster;
/**
* Array of PDO Objects for configured database slaves
*/
private $dbSlaves = array();
/**
* Prefix to apply to the tables
*/
private $tablePrefix;
/**
* Array with information on the last error occurred.
*/
private $lastError;
/**
* Retrieves the current database instance. Will create a new one if there isn't an existing connection.
*
* @param \SimpleSAML_Configuration $altConfig Optional: Instance of a SimpleSAML_Configuration class
*
* @return \SimpleSAML\Database The shared database connection.
*/
public static function getInstance($altConfig = null)
{
$config = ($altConfig) ? $altConfig : \SimpleSAML_Configuration::getInstance();
$instanceId = self::generateInstanceId($config);
// check if we already have initialized the session
if (isset(self::$instance[$instanceId])) {
return self::$instance[$instanceId];
}
// create a new session
self::$instance[$instanceId] = new Database($config);
return self::$instance[$instanceId];
}
/**
* Private constructor that restricts instantiation to getInstance().
*
* @param \SimpleSAML_Configuration $config Instance of the SimpleSAML_Configuration class
*/
private function __construct($config)
{
$driverOptions = $config->getArray('database.driver_options', array());
if ($config->getBoolean('database.persistent', true)) {
$driverOptions = array(\PDO::ATTR_PERSISTENT => true);
}
// connect to the master
$this->dbMaster = $this->connect(
$config->getString('database.dsn'),
$config->getString('database.username', null),
$config->getString('database.password', null),
$driverOptions
);
// connect to any configured slaves
$slaves = $config->getArray('database.slaves', array());
foreach ($slaves as $slave) {
array_push(
$this->dbSlaves,
$this->connect(
$slave['dsn'],
$slave['username'],
$slave['password'],
$driverOptions
)
);
}
$this->tablePrefix = $config->getString('database.prefix', '');
}
/**
* Generate an Instance ID based on the database configuration.
*
* @param \SimpleSAML_Configuration $config Configuration class
*
* @return string $instanceId
*/
private static function generateInstanceId($config)
{
$assembledConfig = array(
'master' => array(
'database.dsn' => $config->getString('database.dsn'),
'database.username' => $config->getString('database.username', null),
'database.password' => $config->getString('database.password', null),
'database.prefix' => $config->getString('database.prefix', ''),
'database.persistent' => $config->getBoolean('database.persistent', false),
),
'slaves' => $config->getArray('database.slaves', array()),
);
return sha1(serialize($assembledConfig));
}
/**
* This function connects to a database.
*
* @param string $dsn Database connection string
* @param string $username SQL user
* @param string $password SQL password
* @param array $options PDO options
*
* @throws \Exception If an error happens while trying to connect to the database.
* @return \PDO object
*/
private function connect($dsn, $username, $password, $options)
{
try {
$db = new \PDO($dsn, $username, $password, $options);
$db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
return $db;
} catch (\PDOException $e) {
throw new \Exception("Database error: ".$e->getMessage());
}
}
/**
* This function randomly selects a slave database server to query. In the event no slaves are configured, it will
* return the master.
*
* @return \PDO object
*/
private function getSlave()
{
if (count($this->dbSlaves) > 0) {
$slaveId = rand(0, count($this->dbSlaves) - 1);
return $this->dbSlaves[$slaveId];
} else {
return $this->dbMaster;
}
}
/**
* This function simply applies the table prefix to a supplied table name.
*
* @param string $table Table to apply prefix to, if configured
*
* @return string Table with configured prefix
*/
public function applyPrefix($table)
{
return $this->tablePrefix.$table;
}
/**
* This function queries the database
*
* @param \PDO $db PDO object to use
* @param string $stmt Prepared SQL statement
* @param array $params Parameters
*
* @throws \Exception If an error happens while trying to execute the query.
* @return \PDOStatement object
*/
private function query($db, $stmt, $params)
{
assert(is_object($db));
assert(is_string($stmt));
assert(is_array($params));
try {
$query = $db->prepare($stmt);
foreach ($params as $param => $value) {
if (is_array($value)) {
$query->bindValue(":$param", $value[0], ($value[1]) ? $value[1] : \PDO::PARAM_STR);
} else {
$query->bindValue(":$param", $value, \PDO::PARAM_STR);
}
}
$query->execute();
return $query;
} catch (\PDOException $e) {
$this->lastError = $db->errorInfo();
throw new \Exception("Database error: ".$e->getMessage());
}
}
/**
* This function queries the database without using a prepared statement.
*
* @param \PDO $db PDO object to use
* @param string $stmt An SQL statement to execute, previously escaped.
*
* @throws \Exception If an error happens while trying to execute the query.
* @return int The number of rows affected.
*/
private function exec($db, $stmt)
{
assert(is_object($db));
assert(is_string($stmt));
try {
return $db->exec($stmt);
} catch (\PDOException $e) {
$this->lastError = $db->errorInfo();
throw new \Exception("Database error: ".$e->getMessage());
}
}
/**
* This executes queries directly on the master.
*
* @param string $stmt Prepared SQL statement
* @param array $params Parameters
*
* @return int The number of rows affected by the query.
*/
public function write($stmt, $params = array())
{
$db = $this->dbMaster;
if (is_array($params)) {
$obj = $this->query($db, $stmt, $params);
return $obj->rowCount();
} else {
return $this->exec($db, $stmt);
}
}
/**
* This executes queries on a database server that is determined by this::getSlave().
*
* @param string $stmt Prepared SQL statement
* @param array $params Parameters
*
* @return \PDOStatement object
*/
public function read($stmt, $params = array())
{
$db = $this->getSlave();
return $this->query($db, $stmt, $params);
}
/**
* Return an array with information about the last operation performed in the database.
*
* @return array The array with error information.
*/
public function getLastError()
{
return $this->lastError;
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* Class for creating exceptions from assertion failures.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_Assertion extends SimpleSAML_Error_Exception
{
/**
* The assertion which failed, or null if only an expression was passed to the
* assert-function.
*/
private $assertion;
/**
* Constructor for the assertion exception.
*
* Should only be called from the onAssertion handler.
*
* @param string|null $assertion The assertion which failed, or null if the assert-function was
* given an expression.
*/
public function __construct($assertion = null)
{
assert($assertion === null || is_string($assertion));
$msg = 'Assertion failed: ' . var_export($assertion, true);
parent::__construct($msg);
$this->assertion = $assertion;
}
/**
* Retrieve the assertion which failed.
*
* @return string|null The assertion which failed, or null if the assert-function was called with an expression.
*/
public function getAssertion()
{
return $this->assertion;
}
/**
* Install this assertion handler.
*
* This function will register this assertion handler. If will not enable assertions if they are
* disabled.
*/
public static function installHandler()
{
assert_options(ASSERT_WARNING, 0);
assert_options(ASSERT_QUIET_EVAL, 0);
assert_options(ASSERT_CALLBACK, array('SimpleSAML_Error_Assertion', 'onAssertion'));
}
/**
* Handle assertion.
*
* This function handles an assertion.
*
* @param string $file The file assert was called from.
* @param int $line The line assert was called from.
* @param mixed $message The expression which was passed to the assert-function.
*/
public static function onAssertion($file, $line, $message)
{
if (!empty($message)) {
$exception = new self($message);
} else {
$exception = new self();
}
$exception->logError();
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* Baseclass for auth source exceptions.
*
* @package SimpleSAMLphp_base
*
*/
class SimpleSAML_Error_AuthSource extends SimpleSAML_Error_Error
{
/**
* Authsource module name.
*/
private $authsource;
/**
* Reason why this request was invalid.
*/
private $reason;
/**
* Create a new AuthSource error.
*
* @param string $authsource Authsource module name from where this error was thrown.
* @param string $reason Description of the error.
*/
public function __construct($authsource, $reason, $cause = null)
{
assert(is_string($authsource));
assert(is_string($reason));
$this->authsource = $authsource;
$this->reason = $reason;
parent::__construct(
array(
'AUTHSOURCEERROR',
'%AUTHSOURCE%' => htmlspecialchars(var_export($this->authsource, true)),
'%REASON%' => htmlspecialchars(var_export($this->reason, true))
),
$cause
);
$this->message = "Error with authentication source '$authsource': $reason";
}
/**
* Retrieve the authsource module name from where this error was thrown.
*
* @return string Authsource module name.
*/
public function getAuthSource()
{
return $this->authsource;
}
/**
* Retrieve the reason why the request was invalid.
*
* @return string The reason why the request was invalid.
*/
public function getReason()
{
return $this->reason;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Exception which will show a 400 Bad Request error page.
*
* This exception can be thrown from within an module page handler. The user will then be
* shown a 400 Bad Request error page.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_BadRequest extends SimpleSAML_Error_Error
{
/**
* Reason why this request was invalid.
*/
private $reason;
/**
* Create a new BadRequest error.
*
* @param string $reason Description of why the request was unacceptable.
*/
public function __construct($reason)
{
assert(is_string($reason));
$this->reason = $reason;
parent::__construct(array('BADREQUEST', '%REASON%' => $this->reason));
$this->httpCode = 400;
}
/**
* Retrieve the reason why the request was invalid.
*
* @return string The reason why the request was invalid.
*/
public function getReason()
{
return $this->reason;
}
}

View File

@@ -0,0 +1,12 @@
<?php
/**
* Exception indicating illegal innput from user.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class SimpleSAML_Error_BadUserInput extends SimpleSAML_Error_User
{
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Exception to indicate that we cannot set a cookie.
*
* @author Jaime Pérez Crespo <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Error;
class CannotSetCookie extends \SimpleSAML_Error_Exception
{
/**
* The exception was thrown for unknown reasons.
*
* @var int
*/
const UNKNOWN = 0;
/**
* The exception was due to the HTTP headers being already sent, and therefore we cannot send additional headers to
* set the cookie.
*
* @var int
*/
const HEADERS_SENT = 1;
/**
* The exception was due to trying to set a secure cookie over an insecure channel.
*
* @var int
*/
const SECURE_COOKIE = 2;
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* This exception represents a configuration error.
*
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Error;
class ConfigurationError extends \SimpleSAML_Error_Error
{
/**
* The reason for this exception.
*
* @var null|string
*/
protected $reason;
/**
* The configuration file that caused this exception.
*
* @var null|string
*/
protected $config_file;
/**
* ConfigurationError constructor.
*
* @param string|null $reason The reason for this exception.
* @param string|null $file The configuration file that originated this error.
* @param array|null $config The configuration array that led to this problem.
*/
public function __construct($reason = null, $file = null, array $config = null)
{
$file_str = '';
$reason_str = '.';
$params = array('CONFIG');
if ($file !== null) {
$params['%FILE%'] = $file;
$basepath = dirname(dirname(dirname(dirname(__FILE__)))).'/';
$file_str = '('.str_replace($basepath, '', $file).') ';
}
if ($reason !== null) {
$params['%REASON%'] = $reason;
$reason_str = ': '.$reason;
}
$this->reason = $reason;
$this->config_file = $file;
parent::__construct($params);
$this->message = 'The configuration '.$file_str.'is invalid'.$reason_str;
}
/**
* Get the reason for this exception.
*
* @return null|string The reason for this exception.
*/
public function getReason()
{
return $this->reason;
}
/**
* Get the configuration file that caused this exception.
*
* @return null|string The configuration file that caused this exception.
*/
public function getConfFile()
{
return $this->config_file;
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* This exception represents a configuration error that we cannot recover from.
*
* Throwing a critical configuration error indicates that the configuration available is not usable, and as such
* SimpleSAMLphp should not try to use it. However, in certain situations we might find a specific configuration
* error that makes part of the configuration unusable, while the rest we can still use. In those cases, we can
* just pass a configuration array to the constructor, making sure the offending configuration options are removed,
* reset to defaults or guessed to some usable value.
*
* If, for example, we have an error in the 'baseurlpath' configuration option, we can still load the configuration
* and substitute the value of that option with one guessed from the environment, using
* \SimpleSAML\Utils\HTTP::guessPath(). Doing so, the error is still critical, but at least we can recover up to a
* certain point and inform about the error in an ordered manner, without blank pages, logs out of place or even
* segfaults.
*
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Error;
class CriticalConfigurationError extends ConfigurationError
{
/**
* This is the bare minimum configuration that we can use.
*
* @var array
*/
private static $minimum_config = array(
'logging.handler' => 'errorlog',
'logging.level' => \SimpleSAML\Logger::DEBUG,
'errorreporting' => false,
'debug' => true,
);
/**
* CriticalConfigurationError constructor.
*
* @param string|null $reason The reason for this critical error.
* @param string|null $file The configuration file that originated this error.
* @param array|null The configuration array that led to this problem.
*/
public function __construct($reason = null, $file = null, $config = null)
{
if ($config === null) {
$config = self::$minimum_config;
$config['baseurlpath'] = \SimpleSAML\Utils\HTTP::guessBasePath();
}
\SimpleSAML_Configuration::loadFromArray(
$config,
'',
'simplesaml'
);
parent::__construct($reason, $file);
}
/**
* @param \Exception $exception
*
* @return CriticalConfigurationError
*/
public static function fromException(\Exception $exception)
{
$reason = null;
$file = null;
if ($exception instanceof ConfigurationError) {
$reason = $exception->getReason();
$file = $exception->getConfFile();
} else {
$reason = $exception->getMessage();
}
return new CriticalConfigurationError($reason, $file);
}
}

296
lib/SimpleSAML/Error/Error.php Executable file
View File

@@ -0,0 +1,296 @@
<?php
/**
* Class that wraps SimpleSAMLphp errors in exceptions.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_Error extends SimpleSAML_Error_Exception
{
/**
* The error code.
*
* @var string
*/
private $errorCode;
/**
* The http code.
*
* @var integer
*/
protected $httpCode = 500;
/**
* The error title tag in dictionary.
*
* @var string
*/
private $dictTitle;
/**
* The error description tag in dictionary.
*
* @var string
*/
private $dictDescr;
/**
* The name of module that threw the error.
*
* @var string|null
*/
private $module = null;
/**
* The parameters for the error.
*
* @var array
*/
private $parameters;
/**
* Name of custom include template for the error.
*
* @var string|null
*/
protected $includeTemplate = null;
/**
* Constructor for this error.
*
* The error can either be given as a string, or as an array. If it is an array, the first element in the array
* (with index 0), is the error code, while the other elements are replacements for the error text.
*
* @param mixed $errorCode One of the error codes defined in the errors dictionary.
* @param Exception $cause The exception which caused this fatal error (if any). Optional.
* @param int|null $httpCode The HTTP response code to use. Optional.
*/
public function __construct($errorCode, Exception $cause = null, $httpCode = null)
{
assert(is_string($errorCode) || is_array($errorCode));
if (is_array($errorCode)) {
$this->parameters = $errorCode;
unset($this->parameters[0]);
$this->errorCode = $errorCode[0];
} else {
$this->parameters = array();
$this->errorCode = $errorCode;
}
if (isset($httpCode)) {
$this->httpCode = $httpCode;
}
$moduleCode = explode(':', $this->errorCode, 2);
if (count($moduleCode) === 2) {
$this->module = $moduleCode[0];
$this->dictTitle = '{'.$this->module.':errors:title_'.$moduleCode[1].'}';
$this->dictDescr = '{'.$this->module.':errors:descr_'.$moduleCode[1].'}';
} else {
$this->dictTitle = SimpleSAML\Error\ErrorCodes::getErrorCodeTitle($this->errorCode);
$this->dictDescr = SimpleSAML\Error\ErrorCodes::getErrorCodeDescription($this->errorCode);
}
if (!empty($this->parameters)) {
$msg = $this->errorCode.'(';
foreach ($this->parameters as $k => $v) {
if ($k === 0) {
continue;
}
$msg .= var_export($k, true).' => '.var_export($v, true).', ';
}
$msg = substr($msg, 0, -2).')';
} else {
$msg = $this->errorCode;
}
parent::__construct($msg, -1, $cause);
}
/**
* Retrieve the error code given when throwing this error.
*
* @return string The error code.
*/
public function getErrorCode()
{
return $this->errorCode;
}
/**
* Retrieve the error parameters given when throwing this error.
*
* @return array The parameters.
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Retrieve the error title tag in dictionary.
*
* @return string The error title tag.
*/
public function getDictTitle()
{
return $this->dictTitle;
}
/**
* Retrieve the error description tag in dictionary.
*
* @return string The error description tag.
*/
public function getDictDescr()
{
return $this->dictDescr;
}
/**
* Set the HTTP return code for this error.
*
* This should be overridden by subclasses who want a different return code than 500 Internal Server Error.
*/
protected function setHTTPCode()
{
// Some mostly used HTTP codes
$httpCodesMap = array(
400 => 'HTTP/1.0 400 Bad Request',
403 => 'HTTP/1.0 403 Forbidden',
404 => 'HTTP/1.0 404 Not Found',
405 => 'HTTP/1.0 405 Method Not Allowed',
500 => 'HTTP/1.0 500 Internal Server Error',
501 => 'HTTP/1.0 501 Method Not Implemented',
503 => 'HTTP/1.0 503 Service Temporarily Unavailable',
);
$httpCode = $this->httpCode;
if (function_exists('http_response_code')) {
http_response_code($httpCode);
return;
}
if (!array_key_exists($this->httpCode, $httpCodesMap)) {
$httpCode = 500;
SimpleSAML\Logger::warning('HTTP response code not defined: '.var_export($this->httpCode, true));
}
header($httpCodesMap[$httpCode]);
}
/**
* Save an error report.
*
* @return array The array with the error report data.
*/
protected function saveError()
{
$data = $this->format(true);
$emsg = array_shift($data);
$etrace = implode("\n", $data);
$reportId = bin2hex(openssl_random_pseudo_bytes(4));
SimpleSAML\Logger::error('Error report with id '.$reportId.' generated.');
$config = SimpleSAML_Configuration::getInstance();
$session = SimpleSAML_Session::getSessionFromRequest();
if (isset($_SERVER['HTTP_REFERER'])) {
$referer = $_SERVER['HTTP_REFERER'];
// remove anything after the first '?' or ';', just in case it contains any sensitive data
$referer = explode('?', $referer, 2);
$referer = $referer[0];
$referer = explode(';', $referer, 2);
$referer = $referer[0];
} else {
$referer = 'unknown';
}
$errorData = array(
'exceptionMsg' => $emsg,
'exceptionTrace' => $etrace,
'reportId' => $reportId,
'trackId' => $session->getTrackID(),
'url' => \SimpleSAML\Utils\HTTP::getSelfURLNoQuery(),
'version' => $config->getVersion(),
'referer' => $referer,
);
$session->setData('core:errorreport', $reportId, $errorData);
return $errorData;
}
/**
* Display this error.
*
* This method displays a standard SimpleSAMLphp error page and exits.
*/
public function show()
{
$this->setHTTPCode();
// log the error message
$this->logError();
$errorData = $this->saveError();
$config = SimpleSAML_Configuration::getInstance();
$data = array();
$data['showerrors'] = $config->getBoolean('showerrors', true);
$data['error'] = $errorData;
$data['errorCode'] = $this->errorCode;
$data['parameters'] = $this->parameters;
$data['module'] = $this->module;
$data['dictTitle'] = $this->dictTitle;
$data['dictDescr'] = $this->dictDescr;
$data['includeTemplate'] = $this->includeTemplate;
$data['clipboard.js'] = true;
// check if there is a valid technical contact email address
if ($config->getBoolean('errorreporting', true) &&
$config->getString('technicalcontact_email', 'na@example.org') !== 'na@example.org'
) {
// enable error reporting
$baseurl = \SimpleSAML\Utils\HTTP::getBaseURL();
$data['errorReportAddress'] = $baseurl.'errorreport.php';
}
$data['email'] = '';
$session = SimpleSAML_Session::getSessionFromRequest();
$authorities = $session->getAuthorities();
foreach ($authorities as $authority) {
$attributes = $session->getAuthData($authority, 'Attributes');
if ($attributes !== null && array_key_exists('mail', $attributes) && count($attributes['mail']) > 0) {
$data['email'] = $attributes['mail'][0];
break; // enough, don't need to get all available mails, if more than one
}
}
$show_function = $config->getArray('errors.show_function', null);
if (isset($show_function)) {
assert(is_callable($show_function));
call_user_func($show_function, $config, $data);
assert(false);
} else {
$t = new SimpleSAML_XHTML_Template($config, 'error.php', 'errors');
$t->data = array_merge($t->data, $data);
$t->data['dictTitleTranslated'] = $t->getTranslator()->t($t->data['dictTitle']);
$t->data['dictDescrTranslated'] = $t->getTranslator()->t($t->data['dictDescr'], $t->data['parameters']);
$t->show();
}
exit;
}
}

View File

@@ -0,0 +1,188 @@
<?php
/**
* Class that maps SimpleSAMLphp error codes to translateable strings.
*
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Error;
class ErrorCodes
{
/**
* Fetch all default translation strings for error code titles.
*
* @return array A map from error code to error code title
*/
final public static function defaultGetAllErrorCodeTitles()
{
return array(
'ACSPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:title_ACSPARAMS}'),
'ARSPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:title_ARSPARAMS}'),
'AUTHSOURCEERROR' => \SimpleSAML\Locale\Translate::noop('{errors:title_AUTHSOURCEERROR}'),
'BADREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:title_BADREQUEST}'),
'CASERROR' => \SimpleSAML\Locale\Translate::noop('{errors:title_CASERROR}'),
'CONFIG' => \SimpleSAML\Locale\Translate::noop('{errors:title_CONFIG}'),
'CREATEREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:title_CREATEREQUEST}'),
'DISCOPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:title_DISCOPARAMS}'),
'GENERATEAUTHNRESPONSE' => \SimpleSAML\Locale\Translate::noop('{errors:title_GENERATEAUTHNRESPONSE}'),
'INVALIDCERT' => \SimpleSAML\Locale\Translate::noop('{errors:title_INVALIDCERT}'),
'LDAPERROR' => \SimpleSAML\Locale\Translate::noop('{errors:title_LDAPERROR}'),
'LOGOUTINFOLOST' => \SimpleSAML\Locale\Translate::noop('{errors:title_LOGOUTINFOLOST}'),
'LOGOUTREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:title_LOGOUTREQUEST}'),
'MEMCACHEDOWN' => \SimpleSAML\Locale\Translate::noop('{errors:title_MEMCACHEDOWN}'),
'METADATA' => \SimpleSAML\Locale\Translate::noop('{errors:title_METADATA}'),
'METADATANOTFOUND' => \SimpleSAML\Locale\Translate::noop('{errors:title_METADATANOTFOUND}'),
'NOACCESS' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOACCESS}'),
'NOCERT' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOCERT}'),
'NORELAYSTATE' => \SimpleSAML\Locale\Translate::noop('{errors:title_NORELAYSTATE}'),
'NOSTATE' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOSTATE}'),
'NOTFOUND' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOTFOUND}'),
'NOTFOUNDREASON' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOTFOUNDREASON}'),
'NOTSET' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOTSET}'),
'NOTVALIDCERT' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOTVALIDCERT}'),
'PROCESSASSERTION' => \SimpleSAML\Locale\Translate::noop('{errors:title_PROCESSASSERTION}'),
'PROCESSAUTHNREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:title_PROCESSAUTHNREQUEST}'),
'RESPONSESTATUSNOSUCCESS' => \SimpleSAML\Locale\Translate::noop('{errors:title_RESPONSESTATUSNOSUCCESS}'),
'SLOSERVICEPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:title_SLOSERVICEPARAMS}'),
'SSOPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:title_SSOPARAMS}'),
'UNHANDLEDEXCEPTION' => \SimpleSAML\Locale\Translate::noop('{errors:title_UNHANDLEDEXCEPTION}'),
'UNKNOWNCERT' => \SimpleSAML\Locale\Translate::noop('{errors:title_UNKNOWNCERT}'),
'USERABORTED' => \SimpleSAML\Locale\Translate::noop('{errors:title_USERABORTED}'),
'WRONGUSERPASS' => \SimpleSAML\Locale\Translate::noop('{errors:title_WRONGUSERPASS}'),
);
}
/**
* Fetch all translation strings for error code titles.
*
* Extend this to add error codes.
*
* @return array A map from error code to error code title
*/
public static function getAllErrorCodeTitles()
{
return self::defaultGetAllErrorCodeTitles();
}
/**
* Fetch all default translation strings for error code descriptions.
*
* @return string A map from error code to error code description
*/
final public static function defaultGetAllErrorCodeDescriptions()
{
return array(
'ACSPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_ACSPARAMS}'),
'ARSPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_ARSPARAMS}'),
'AUTHSOURCEERROR' => \SimpleSAML\Locale\Translate::noop('{errors:descr_AUTHSOURCEERROR}'),
'BADREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:descr_BADREQUEST}'),
'CASERROR' => \SimpleSAML\Locale\Translate::noop('{errors:descr_CASERROR}'),
'CONFIG' => \SimpleSAML\Locale\Translate::noop('{errors:descr_CONFIG}'),
'CREATEREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:descr_CREATEREQUEST}'),
'DISCOPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_DISCOPARAMS}'),
'GENERATEAUTHNRESPONSE' => \SimpleSAML\Locale\Translate::noop('{errors:descr_GENERATEAUTHNRESPONSE}'),
'INVALIDCERT' => \SimpleSAML\Locale\Translate::noop('{errors:descr_INVALIDCERT}'),
'LDAPERROR' => \SimpleSAML\Locale\Translate::noop('{errors:descr_LDAPERROR}'),
'LOGOUTINFOLOST' => \SimpleSAML\Locale\Translate::noop('{errors:descr_LOGOUTINFOLOST}'),
'LOGOUTREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:descr_LOGOUTREQUEST}'),
'MEMCACHEDOWN' => \SimpleSAML\Locale\Translate::noop('{errors:descr_MEMCACHEDOWN}'),
'METADATA' => \SimpleSAML\Locale\Translate::noop('{errors:descr_METADATA}'),
'METADATANOTFOUND' => \SimpleSAML\Locale\Translate::noop('{errors:descr_METADATANOTFOUND}'),
'NOACCESS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOACCESS}'),
'NOCERT' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOCERT}'),
'NORELAYSTATE' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NORELAYSTATE}'),
'NOSTATE' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOSTATE}'),
'NOTFOUND' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOTFOUND}'),
'NOTFOUNDREASON' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOTFOUNDREASON}'),
'NOTSET' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOTSET}'),
'NOTVALIDCERT' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOTVALIDCERT}'),
'PROCESSASSERTION' => \SimpleSAML\Locale\Translate::noop('{errors:descr_PROCESSASSERTION}'),
'PROCESSAUTHNREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:descr_PROCESSAUTHNREQUEST}'),
'RESPONSESTATUSNOSUCCESS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_RESPONSESTATUSNOSUCCESS}'),
'SLOSERVICEPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_SLOSERVICEPARAMS}'),
'SSOPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_SSOPARAMS}'),
'UNHANDLEDEXCEPTION' => \SimpleSAML\Locale\Translate::noop('{errors:descr_UNHANDLEDEXCEPTION}'),
'UNKNOWNCERT' => \SimpleSAML\Locale\Translate::noop('{errors:descr_UNKNOWNCERT}'),
'USERABORTED' => \SimpleSAML\Locale\Translate::noop('{errors:descr_USERABORTED}'),
'WRONGUSERPASS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_WRONGUSERPASS}'),
);
}
/**
* Fetch all translation strings for error code descriptions.
*
* Extend this to add error codes.
*
* @return string A map from error code to error code description
*/
public static function getAllErrorCodeDescriptions()
{
return self::defaultGetAllErrorCodeDescriptions();
}
/**
* Get a map of both errorcode titles and descriptions
*
* Convenience-method for template-callers
*
* @return array An array containing both errorcode maps.
*/
public static function getAllErrorCodeMessages()
{
return array(
'title' => self::getAllErrorCodeTitles(),
'descr' => self::getAllErrorCodeDescriptions(),
);
}
/**
* Fetch a translation string for a title for a given error code.
*
* @param string $errorCode The error code to look up
*
* @return string A string to translate
*/
public static function getErrorCodeTitle($errorCode)
{
$errorCodeTitles = self::getAllErrorCodeTitles();
return $errorCodeTitles[$errorCode];
}
/**
* Fetch a translation string for a description for a given error code.
*
* @param string $errorCode The error code to look up
*
* @return string A string to translate
*/
public static function getErrorCodeDescription($errorCode)
{
$errorCodeDescriptions = self::getAllErrorCodeDescriptions();
return $errorCodeDescriptions[$errorCode];
}
/**
* Get both title and description for a specific error code
*
* Convenience-method for template-callers
*
* @param string $errorCode The error code to look up
*
* @return array An array containing both errorcode strings.
*/
public static function getErrorCodeMessage($errorCode)
{
return array(
'title' => self::getErrorCodeTitle($errorCode),
'descr' => self::getErrorCodeDescription($errorCode),
);
}
}

View File

@@ -0,0 +1,316 @@
<?php
/**
* Base class for SimpleSAMLphp Exceptions
*
* This class tries to make sure that every exception is serializable.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_Exception extends Exception
{
/**
* The backtrace for this exception.
*
* We need to save the backtrace, since we cannot rely on
* serializing the Exception::trace-variable.
*
* @var array
*/
private $backtrace;
/**
* The cause of this exception.
*
* @var SimpleSAML_Error_Exception
*/
private $cause;
/**
* Constructor for this error.
*
* Note that the cause will be converted to a SimpleSAML_Error_UnserializableException unless it is a subclass of
* SimpleSAML_Error_Exception.
*
* @param string $message Exception message
* @param int $code Error code
* @param Exception|null $cause The cause of this exception.
*/
public function __construct($message, $code = 0, Exception $cause = null)
{
assert(is_string($message));
assert(is_int($code));
parent::__construct($message, $code);
$this->initBacktrace($this);
if ($cause !== null) {
$this->cause = SimpleSAML_Error_Exception::fromException($cause);
}
}
/**
* Convert any exception into a SimpleSAML_Error_Exception.
*
* @param Exception $e The exception.
*
* @return SimpleSAML_Error_Exception The new exception.
*/
public static function fromException(Exception $e)
{
if ($e instanceof SimpleSAML_Error_Exception) {
return $e;
}
return new SimpleSAML_Error_UnserializableException($e);
}
/**
* Load the backtrace from the given exception.
*
* @param Exception $exception The exception we should fetch the backtrace from.
*/
protected function initBacktrace(Exception $exception)
{
$this->backtrace = array();
// position in the top function on the stack
$pos = $exception->getFile().':'.$exception->getLine();
foreach ($exception->getTrace() as $t) {
$function = $t['function'];
if (array_key_exists('class', $t)) {
$function = $t['class'].'::'.$function;
}
$this->backtrace[] = $pos.' ('.$function.')';
if (array_key_exists('file', $t)) {
$pos = $t['file'].':'.$t['line'];
} else {
$pos = '[builtin]';
}
}
$this->backtrace[] = $pos.' (N/A)';
}
/**
* Retrieve the backtrace.
*
* @return array An array where each function call is a single item.
*/
public function getBacktrace()
{
return $this->backtrace;
}
/**
* Retrieve the cause of this exception.
*
* @return SimpleSAML_Error_Exception|null The cause of this exception.
*/
public function getCause()
{
return $this->cause;
}
/**
* Retrieve the class of this exception.
*
* @return string The name of the class.
*/
public function getClass()
{
return get_class($this);
}
/**
* Format this exception for logging.
*
* Create an array of lines for logging.
*
* @param boolean $anonymize Whether the resulting messages should be anonymized or not.
*
* @return array Log lines that should be written out.
*/
public function format($anonymize = false)
{
$ret = array(
$this->getClass().': '.$this->getMessage(),
);
return array_merge($ret, $this->formatBacktrace($anonymize));
}
/**
* Format the backtrace for logging.
*
* Create an array of lines for logging from the backtrace.
*
* @param boolean $anonymize Whether the resulting messages should be anonymized or not.
*
* @return array All lines of the backtrace, properly formatted.
*/
public function formatBacktrace($anonymize = false)
{
$ret = array();
$basedir = SimpleSAML_Configuration::getInstance()->getBaseDir();
$e = $this;
do {
if ($e !== $this) {
$ret[] = 'Caused by: '.$e->getClass().': '.$e->getMessage();
}
$ret[] = 'Backtrace:';
$depth = count($e->backtrace);
foreach ($e->backtrace as $i => $trace) {
if ($anonymize) {
$trace = str_replace($basedir, '', $trace);
}
$ret[] = ($depth - $i - 1).' '.$trace;
}
$e = $e->cause;
} while ($e !== null);
return $ret;
}
/**
* Print the backtrace to the log if the 'debug' option is enabled in the configuration.
*/
protected function logBacktrace($level = \SimpleSAML\Logger::DEBUG)
{
// see if debugging is enabled for backtraces
$debug = SimpleSAML_Configuration::getInstance()->getArrayize('debug', array('backtraces' => false));
if (!(in_array('backtraces', $debug, true) // implicitly enabled
|| (array_key_exists('backtraces', $debug) && $debug['backtraces'] === true) // explicitly set
// TODO: deprecate the old style and remove it in 2.0
|| (array_key_exists(0, $debug) && $debug[0] === true) // old style 'debug' configuration option
)) {
return;
}
$backtrace = $this->formatBacktrace();
$callback = array('\SimpleSAML\Logger');
$functions = array(
\SimpleSAML\Logger::ERR => 'error',
\SimpleSAML\Logger::WARNING => 'warning',
\SimpleSAML\Logger::INFO => 'info',
\SimpleSAML\Logger::DEBUG => 'debug',
);
$callback[] = $functions[$level];
foreach ($backtrace as $line) {
call_user_func($callback, $line);
}
}
/**
* Print the exception to the log, by default with log level error.
*
* Override to allow errors extending this class to specify the log level themselves.
*
* @param int $default_level The log level to use if this method was not overridden.
*/
public function log($default_level)
{
$fn = array(
SimpleSAML\Logger::ERR => 'logError',
SimpleSAML\Logger::WARNING => 'logWarning',
SimpleSAML\Logger::INFO => 'logInfo',
SimpleSAML\Logger::DEBUG => 'logDebug',
);
call_user_func(array($this, $fn[$default_level]), $default_level);
}
/**
* Print the exception to the log with log level error.
*
* This function will write this exception to the log, including a full backtrace.
*/
public function logError()
{
SimpleSAML\Logger::error($this->getClass().': '.$this->getMessage());
$this->logBacktrace(\SimpleSAML\Logger::ERR);
}
/**
* Print the exception to the log with log level warning.
*
* This function will write this exception to the log, including a full backtrace.
*/
public function logWarning()
{
SimpleSAML\Logger::warning($this->getClass().': '.$this->getMessage());
$this->logBacktrace(\SimpleSAML\Logger::WARNING);
}
/**
* Print the exception to the log with log level info.
*
* This function will write this exception to the log, including a full backtrace.
*/
public function logInfo()
{
SimpleSAML\Logger::info($this->getClass().': '.$this->getMessage());
$this->logBacktrace(\SimpleSAML\Logger::INFO);
}
/**
* Print the exception to the log with log level debug.
*
* This function will write this exception to the log, including a full backtrace.
*/
public function logDebug()
{
SimpleSAML\Logger::debug($this->getClass().': '.$this->getMessage());
$this->logBacktrace(\SimpleSAML\Logger::DEBUG);
}
/**
* Function for serialization.
*
* This function builds a list of all variables which should be serialized. It will serialize all variables except
* the Exception::trace variable.
*
* @return array Array with the variables that should be serialized.
*/
public function __sleep()
{
$ret = array_keys((array) $this);
foreach ($ret as $i => $e) {
if ($e === "\0Exception\0trace") {
unset($ret[$i]);
}
}
return $ret;
}
}

View File

@@ -0,0 +1,12 @@
<?php
/**
* Exception indicating wrong password given by user.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class SimpleSAML_Error_InvalidCredential extends SimpleSAML_Error_User
{
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Error for missing metadata.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_MetadataNotFound extends SimpleSAML_Error_Error
{
/**
* Create the error
*
* @param string $entityId The entityID we were unable to locate.
*/
public function __construct($entityId)
{
assert(is_string($entityId));
$this->includeTemplate = 'core:no_metadata.tpl.php';
parent::__construct(array(
'METADATANOTFOUND',
'%ENTITYID%' => htmlspecialchars(var_export($entityId, true))
));
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* Class SimpleSAML_Error_NoPassive
*
* @deprecated This class has been deprecated and will be removed in SimpleSAMLphp 2.0. Please use
* SimpleSAML\Module\saml\Error\NoPassive instead.
*
* @see \SimpleSAML\Module\saml\Error\NoPassive
*/
class SimpleSAML_Error_NoPassive extends SimpleSAML_Error_Exception
{
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* Exception which will show a page telling the user
* that we don't know what to do.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_NoState extends SimpleSAML_Error_Error
{
/**
* Create the error
*/
public function __construct()
{
$this->includeTemplate = 'core:no_state.tpl.php';
parent::__construct('NOSTATE');
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Exception which will show a 404 Not Found error page.
*
* This exception can be thrown from within a module page handler. The user will then be shown a 404 Not Found error
* page.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_NotFound extends SimpleSAML_Error_Error
{
/**
* Reason why the given page could not be found.
*/
private $reason;
/**
* Create a new NotFound error
*
* @param string $reason Optional description of why the given page could not be found.
*/
public function __construct($reason = null)
{
assert($reason === null || is_string($reason));
$url = \SimpleSAML\Utils\HTTP::getSelfURL();
if ($reason === null) {
parent::__construct(array('NOTFOUND', '%URL%' => $url));
$this->message = "The requested page '$url' could not be found.";
} else {
parent::__construct(array('NOTFOUNDREASON', '%URL%' => $url, '%REASON%' => $reason));
$this->message = "The requested page '$url' could not be found. ".$reason;
}
$this->reason = $reason;
$this->httpCode = 404;
}
/**
* Retrieve the reason why the given page could not be found.
*
* @return string|null The reason why the page could not be found.
*/
public function getReason()
{
return $this->reason;
}
/**
* NotFound exceptions don't need to display a backtrace, as they are very simple and the trace is usually trivial,
* so just log the message without any backtrace at all.
*
* @param bool $anonymize Whether to anonymize the trace or not.
*
* @return array
*/
public function format($anonymize = false)
{
return array(
$this->getClass().': '.$this->getMessage(),
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* Class SimpleSAML_Error_ProxyCountExceeded
*
* @deprecated This class has been deprecated and will be removed in SimpleSAMLphp 2.0. Please use
* SimpleSAML\Module\saml\Error\ProxyCountExceeded instead.
*
* @see \SimpleSAML\Module\saml\Error\ProxyCountExceeded
*/
class SimpleSAML_Error_ProxyCountExceeded extends SimpleSAML_Error_Exception
{
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Class for saving normal exceptions for serialization.
*
* This class is used by the SimpleSAML_Auth_State class when it needs
* to serialize an exception which doesn't subclass the
* SimpleSAML_Error_Exception class.
*
* It creates a new exception which contains the backtrace and message
* of the original exception.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_UnserializableException extends SimpleSAML_Error_Exception
{
/**
* The classname of the original exception.
*
* @var string
*/
private $class;
/**
* Create a serializable exception representing an unserializable exception.
*
* @param Exception $original The original exception.
*/
public function __construct(Exception $original)
{
$this->class = get_class($original);
$msg = $original->getMessage();
$code = $original->getCode();
if (!is_int($code)) {
// PDOException uses a string as the code. Filter it out here.
$code = -1;
}
parent::__construct($msg, $code);
$this->initBacktrace($original);
}
/**
* Retrieve the class of this exception.
*
* @return string The classname.
*/
public function getClass()
{
return $this->class;
}
}

14
lib/SimpleSAML/Error/User.php Executable file
View File

@@ -0,0 +1,14 @@
<?php
/**
* Baseclass for user error exceptions
*
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class SimpleSAML_Error_User extends SimpleSAML_Error_Exception
{
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Exception indicating user aborting the authentication process.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_UserAborted extends SimpleSAML_Error_Error
{
/**
* Create the error
*
* @param Exception|null $cause The exception that caused this error.
*/
public function __construct(Exception $cause = null)
{
parent::__construct('USERABORTED', $cause);
}
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* Exception indicating user not found by authsource.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class SimpleSAML_Error_UserNotFound extends SimpleSAML_Error_User
{
}

558
lib/SimpleSAML/IdP.php Executable file
View File

@@ -0,0 +1,558 @@
<?php
/**
* IdP class.
*
* This class implements the various functions used by IdP.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_IdP
{
/**
* A cache for resolving IdP id's.
*
* @var array
*/
private static $idpCache = array();
/**
* The identifier for this IdP.
*
* @var string
*/
private $id;
/**
* The "association group" for this IdP.
*
* We use this to support cross-protocol logout until
* we implement a cross-protocol IdP.
*
* @var string
*/
private $associationGroup;
/**
* The configuration for this IdP.
*
* @var SimpleSAML_Configuration
*/
private $config;
/**
* Our authsource.
*
* @var \SimpleSAML\Auth\Simple
*/
private $authSource;
/**
* Initialize an IdP.
*
* @param string $id The identifier of this IdP.
*
* @throws SimpleSAML_Error_Exception If the IdP is disabled or no such auth source was found.
*/
private function __construct($id)
{
assert(is_string($id));
$this->id = $id;
$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
$globalConfig = SimpleSAML_Configuration::getInstance();
if (substr($id, 0, 6) === 'saml2:') {
if (!$globalConfig->getBoolean('enable.saml20-idp', false)) {
throw new SimpleSAML_Error_Exception('enable.saml20-idp disabled in config.php.');
}
$this->config = $metadata->getMetaDataConfig(substr($id, 6), 'saml20-idp-hosted');
} elseif (substr($id, 0, 6) === 'saml1:') {
if (!$globalConfig->getBoolean('enable.shib13-idp', false)) {
throw new SimpleSAML_Error_Exception('enable.shib13-idp disabled in config.php.');
}
$this->config = $metadata->getMetaDataConfig(substr($id, 6), 'shib13-idp-hosted');
} elseif (substr($id, 0, 5) === 'adfs:') {
if (!$globalConfig->getBoolean('enable.adfs-idp', false)) {
throw new SimpleSAML_Error_Exception('enable.adfs-idp disabled in config.php.');
}
$this->config = $metadata->getMetaDataConfig(substr($id, 5), 'adfs-idp-hosted');
try {
// this makes the ADFS IdP use the same SP associations as the SAML 2.0 IdP
$saml2EntityId = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted');
$this->associationGroup = 'saml2:'.$saml2EntityId;
} catch (Exception $e) {
// probably no SAML 2 IdP configured for this host. Ignore the error
}
} else {
assert(false);
}
if ($this->associationGroup === null) {
$this->associationGroup = $this->id;
}
$auth = $this->config->getString('auth');
if (SimpleSAML_Auth_Source::getById($auth) !== null) {
$this->authSource = new \SimpleSAML\Auth\Simple($auth);
} else {
throw new SimpleSAML_Error_Exception('No such "'.$auth.'" auth source found.');
}
}
/**
* Retrieve the ID of this IdP.
*
* @return string The ID of this IdP.
*/
public function getId()
{
return $this->id;
}
/**
* Retrieve an IdP by ID.
*
* @param string $id The identifier of the IdP.
*
* @return SimpleSAML_IdP The IdP.
*/
public static function getById($id)
{
assert(is_string($id));
if (isset(self::$idpCache[$id])) {
return self::$idpCache[$id];
}
$idp = new self($id);
self::$idpCache[$id] = $idp;
return $idp;
}
/**
* Retrieve the IdP "owning" the state.
*
* @param array &$state The state array.
*
* @return SimpleSAML_IdP The IdP.
*/
public static function getByState(array &$state)
{
assert(isset($state['core:IdP']));
return self::getById($state['core:IdP']);
}
/**
* Retrieve the configuration for this IdP.
*
* @return SimpleSAML_Configuration The configuration object.
*/
public function getConfig()
{
return $this->config;
}
/**
* Get SP name.
*
* @param string $assocId The association identifier.
*
* @return array|null The name of the SP, as an associative array of language => text, or null if this isn't an SP.
*/
public function getSPName($assocId)
{
assert(is_string($assocId));
$prefix = substr($assocId, 0, 4);
$spEntityId = substr($assocId, strlen($prefix) + 1);
$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
if ($prefix === 'saml') {
try {
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
} catch (Exception $e) {
try {
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'shib13-sp-remote');
} catch (Exception $e) {
return null;
}
}
} else {
if ($prefix === 'adfs') {
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'adfs-sp-remote');
} else {
return null;
}
}
if ($spMetadata->hasValue('name')) {
return $spMetadata->getLocalizedString('name');
} elseif ($spMetadata->hasValue('OrganizationDisplayName')) {
return $spMetadata->getLocalizedString('OrganizationDisplayName');
} else {
return array('en' => $spEntityId);
}
}
/**
* Add an SP association.
*
* @param array $association The SP association.
*/
public function addAssociation(array $association)
{
assert(isset($association['id']));
assert(isset($association['Handler']));
$association['core:IdP'] = $this->id;
$session = SimpleSAML_Session::getSessionFromRequest();
$session->addAssociation($this->associationGroup, $association);
}
/**
* Retrieve list of SP associations.
*
* @return array List of SP associations.
*/
public function getAssociations()
{
$session = SimpleSAML_Session::getSessionFromRequest();
return $session->getAssociations($this->associationGroup);
}
/**
* Remove an SP association.
*
* @param string $assocId The association id.
*/
public function terminateAssociation($assocId)
{
assert(is_string($assocId));
$session = SimpleSAML_Session::getSessionFromRequest();
$session->terminateAssociation($this->associationGroup, $assocId);
}
/**
* Is the current user authenticated?
*
* @return boolean True if the user is authenticated, false otherwise.
*/
public function isAuthenticated()
{
return $this->authSource->isAuthenticated();
}
/**
* Called after authproc has run.
*
* @param array $state The authentication request state array.
*/
public static function postAuthProc(array $state)
{
assert(is_callable($state['Responder']));
if (isset($state['core:SP'])) {
$session = SimpleSAML_Session::getSessionFromRequest();
$session->setData(
'core:idp-ssotime',
$state['core:IdP'].';'.$state['core:SP'],
time(),
SimpleSAML_Session::DATA_TIMEOUT_SESSION_END
);
}
call_user_func($state['Responder'], $state);
assert(false);
}
/**
* The user is authenticated.
*
* @param array $state The authentication request state array.
*
* @throws SimpleSAML_Error_Exception If we are not authenticated.
*/
public static function postAuth(array $state)
{
$idp = SimpleSAML_IdP::getByState($state);
if (!$idp->isAuthenticated()) {
throw new SimpleSAML_Error_Exception('Not authenticated.');
}
$state['Attributes'] = $idp->authSource->getAttributes();
if (isset($state['SPMetadata'])) {
$spMetadata = $state['SPMetadata'];
} else {
$spMetadata = array();
}
if (isset($state['core:SP'])) {
$session = SimpleSAML_Session::getSessionFromRequest();
$previousSSOTime = $session->getData('core:idp-ssotime', $state['core:IdP'].';'.$state['core:SP']);
if ($previousSSOTime !== null) {
$state['PreviousSSOTimestamp'] = $previousSSOTime;
}
}
$idpMetadata = $idp->getConfig()->toArray();
$pc = new SimpleSAML_Auth_ProcessingChain($idpMetadata, $spMetadata, 'idp');
$state['ReturnCall'] = array('SimpleSAML_IdP', 'postAuthProc');
$state['Destination'] = $spMetadata;
$state['Source'] = $idpMetadata;
$pc->processState($state);
self::postAuthProc($state);
}
/**
* Authenticate the user.
*
* This function authenticates the user.
*
* @param array &$state The authentication request state.
*
* @throws \SimpleSAML\Module\saml\Error\NoPassive If we were asked to do passive authentication.
*/
private function authenticate(array &$state)
{
if (isset($state['isPassive']) && (bool) $state['isPassive']) {
throw new \SimpleSAML\Module\saml\Error\NoPassive('Passive authentication not supported.');
}
$this->authSource->login($state);
}
/**
* Re-authenticate the user.
*
* This function re-authenticates an user with an existing session. This gives the authentication source a chance
* to do additional work when re-authenticating for SSO.
*
* Note: This function is not used when ForceAuthn=true.
*
* @param array &$state The authentication request state.
*
* @throws SimpleSAML_Error_Exception If there is no auth source defined for this IdP.
*/
private function reauthenticate(array &$state)
{
$sourceImpl = $this->authSource->getAuthSource();
if ($sourceImpl === null) {
throw new SimpleSAML_Error_Exception('No such auth source defined.');
}
$sourceImpl->reauthenticate($state);
}
/**
* Process authentication requests.
*
* @param array &$state The authentication request state.
*/
public function handleAuthenticationRequest(array &$state)
{
assert(isset($state['Responder']));
$state['core:IdP'] = $this->id;
if (isset($state['SPMetadata']['entityid'])) {
$spEntityId = $state['SPMetadata']['entityid'];
} elseif (isset($state['SPMetadata']['entityID'])) {
$spEntityId = $state['SPMetadata']['entityID'];
} else {
$spEntityId = null;
}
$state['core:SP'] = $spEntityId;
// first, check whether we need to authenticate the user
if (isset($state['ForceAuthn']) && (bool) $state['ForceAuthn']) {
// force authentication is in effect
$needAuth = true;
} else {
$needAuth = !$this->isAuthenticated();
}
$state['IdPMetadata'] = $this->getConfig()->toArray();
$state['ReturnCallback'] = array('SimpleSAML_IdP', 'postAuth');
try {
if ($needAuth) {
$this->authenticate($state);
assert(false);
} else {
$this->reauthenticate($state);
}
$this->postAuth($state);
} catch (SimpleSAML_Error_Exception $e) {
SimpleSAML_Auth_State::throwException($state, $e);
} catch (Exception $e) {
$e = new SimpleSAML_Error_UnserializableException($e);
SimpleSAML_Auth_State::throwException($state, $e);
}
}
/**
* Find the logout handler of this IdP.
*
* @return \SimpleSAML\IdP\LogoutHandlerInterface The logout handler class.
*
* @throws SimpleSAML_Error_Exception If we cannot find a logout handler.
*/
public function getLogoutHandler()
{
// find the logout handler
$logouttype = $this->getConfig()->getString('logouttype', 'traditional');
switch ($logouttype) {
case 'traditional':
$handler = 'SimpleSAML\IdP\TraditionalLogoutHandler';
break;
case 'iframe':
$handler = 'SimpleSAML\IdP\IFrameLogoutHandler';
break;
default:
throw new SimpleSAML_Error_Exception('Unknown logout handler: '.var_export($logouttype, true));
}
return new $handler($this);
}
/**
* Finish the logout operation.
*
* This function will never return.
*
* @param array &$state The logout request state.
*/
public function finishLogout(array &$state)
{
assert(isset($state['Responder']));
$idp = SimpleSAML_IdP::getByState($state);
call_user_func($state['Responder'], $idp, $state);
assert(false);
}
/**
* Process a logout request.
*
* This function will never return.
*
* @param array &$state The logout request state.
* @param string|null $assocId The association we received the logout request from, or null if there was no
* association.
*/
public function handleLogoutRequest(array &$state, $assocId)
{
assert(isset($state['Responder']));
assert(is_string($assocId) || $assocId === null);
$state['core:IdP'] = $this->id;
$state['core:TerminatedAssocId'] = $assocId;
if ($assocId !== null) {
$this->terminateAssociation($assocId);
$session = SimpleSAML_Session::getSessionFromRequest();
$session->deleteData('core:idp-ssotime', $this->id.':'.$state['saml:SPEntityId']);
}
// terminate the local session
$id = SimpleSAML_Auth_State::saveState($state, 'core:Logout:afterbridge');
$returnTo = SimpleSAML\Module::getModuleURL('core/idp/resumelogout.php', array('id' => $id));
$this->authSource->logout($returnTo);
$handler = $this->getLogoutHandler();
$handler->startLogout($state, $assocId);
assert(false);
}
/**
* Process a logout response.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param SimpleSAML_Error_Exception|null $error The error that occurred during session termination (if any).
*/
public function handleLogoutResponse($assocId, $relayState, SimpleSAML_Error_Exception $error = null)
{
assert(is_string($assocId));
assert(is_string($relayState) || $relayState === null);
$session = SimpleSAML_Session::getSessionFromRequest();
$session->deleteData('core:idp-ssotime', $this->id.';'.substr($assocId, strpos($assocId, ':') + 1));
$handler = $this->getLogoutHandler();
$handler->onResponse($assocId, $relayState, $error);
assert(false);
}
/**
* Log out, then redirect to a URL.
*
* This function never returns.
*
* @param string $url The URL the user should be returned to after logout.
*/
public function doLogoutRedirect($url)
{
assert(is_string($url));
$state = array(
'Responder' => array('SimpleSAML_IdP', 'finishLogoutRedirect'),
'core:Logout:URL' => $url,
);
$this->handleLogoutRequest($state, null);
assert(false);
}
/**
* Redirect to a URL after logout.
*
* This function never returns.
*
* @param SimpleSAML_IdP $idp Deprecated. Will be removed.
* @param array &$state The logout state from doLogoutRedirect().
*/
public static function finishLogoutRedirect(SimpleSAML_IdP $idp, array $state)
{
assert(isset($state['core:Logout:URL']));
\SimpleSAML\Utils\HTTP::redirectTrustedURL($state['core:Logout:URL']);
assert(false);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace SimpleSAML\IdP;
use SimpleSAML\Module;
use SimpleSAML\Utils\HTTP;
/**
* Class that handles iframe logout.
*
* @package SimpleSAMLphp
*/
class IFrameLogoutHandler implements LogoutHandlerInterface
{
/**
* The IdP we are logging out from.
*
* @var \SimpleSAML_IdP
*/
private $idp;
/**
* LogoutIFrame constructor.
*
* @param \SimpleSAML_IdP $idp The IdP to log out from.
*/
public function __construct(\SimpleSAML_IdP $idp)
{
$this->idp = $idp;
}
/**
* Start the logout operation.
*
* @param array &$state The logout state.
* @param string|null $assocId The SP we are logging out from.
*/
public function startLogout(array &$state, $assocId)
{
assert(is_string($assocId) || $assocId === null);
$associations = $this->idp->getAssociations();
if (count($associations) === 0) {
$this->idp->finishLogout($state);
}
foreach ($associations as $id => &$association) {
$idp = \SimpleSAML_IdP::getByState($association);
$association['core:Logout-IFrame:Name'] = $idp->getSPName($id);
$association['core:Logout-IFrame:State'] = 'onhold';
}
$state['core:Logout-IFrame:Associations'] = $associations;
if (!is_null($assocId)) {
$spName = $this->idp->getSPName($assocId);
if ($spName === null) {
$spName = array('en' => $assocId);
}
$state['core:Logout-IFrame:From'] = $spName;
} else {
$state['core:Logout-IFrame:From'] = null;
}
$params = array(
'id' => \SimpleSAML_Auth_State::saveState($state, 'core:Logout-IFrame'),
);
if (isset($state['core:Logout-IFrame:InitType'])) {
$params['type'] = $state['core:Logout-IFrame:InitType'];
}
$url = Module::getModuleURL('core/idp/logout-iframe.php', $params);
HTTP::redirectTrustedURL($url);
}
/**
* Continue the logout operation.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param \SimpleSAML_Error_Exception|null $error The error that occurred during session termination (if any).
*/
public function onResponse($assocId, $relayState, \SimpleSAML_Error_Exception $error = null)
{
assert(is_string($assocId));
$spId = sha1($assocId);
$this->idp->terminateAssociation($assocId);
$header = <<<HEADER
<!DOCTYPE html>
<html>
<head>
<title>Logout response from %s</title>
<script>
HEADER;
printf($header, htmlspecialchars(var_export($assocId, true)));
if ($error) {
$errorMsg = $error->getMessage();
echo('window.parent.logoutFailed("'.$spId.'", "'.addslashes($errorMsg).'");');
} else {
echo('window.parent.logoutCompleted("'.$spId.'");');
}
echo <<<FOOTER
</script>
</head>
<body>
</body>
</html>
FOOTER;
exit(0);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace SimpleSAML\IdP;
/**
* Interface that all logout handlers must implement.
*
* @package SimpleSAMLphp
*/
interface LogoutHandlerInterface
{
/**
* Initialize this logout handler.
*
* @param \SimpleSAML_IdP $idp The IdP we are logging out from.
*/
public function __construct(\SimpleSAML_IdP $idp);
/**
* Start a logout operation.
*
* This function must never return.
*
* @param array &$state The logout state.
* @param string|null $assocId The association that started the logout.
*/
public function startLogout(array &$state, $assocId);
/**
* Handles responses to our logout requests.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param \SimpleSAML_Error_Exception|null $error The error that occurred during session termination (if any).
*/
public function onResponse($assocId, $relayState, \SimpleSAML_Error_Exception $error = null);
}

View File

@@ -0,0 +1,119 @@
<?php
namespace SimpleSAML\IdP;
use SimpleSAML\Logger;
use SimpleSAML\Utils\HTTP;
/**
* Class that handles traditional logout.
*
* @package SimpleSAMLphp
*/
class TraditionalLogoutHandler implements LogoutHandlerInterface
{
/**
* The IdP we are logging out from.
*
* @var \SimpleSAML_IdP
*/
private $idp;
/**
* TraditionalLogout constructor.
*
* @param \SimpleSAML_IdP $idp The IdP to log out from.
*/
public function __construct(\SimpleSAML_IdP $idp)
{
$this->idp = $idp;
}
/**
* Picks the next SP and issues a logout request.
*
* This function never returns.
*
* @param array &$state The logout state.
*/
private function logoutNextSP(array &$state)
{
$association = array_pop($state['core:LogoutTraditional:Remaining']);
if ($association === null) {
$this->idp->finishLogout($state);
}
$relayState = \SimpleSAML_Auth_State::saveState($state, 'core:LogoutTraditional', true);
$id = $association['id'];
Logger::info('Logging out of '.var_export($id, true).'.');
try {
$idp = \SimpleSAML_IdP::getByState($association);
$url = call_user_func(array($association['Handler'], 'getLogoutURL'), $idp, $association, $relayState);
HTTP::redirectTrustedURL($url);
} catch (\Exception $e) {
Logger::warning('Unable to initialize logout to '.var_export($id, true).'.');
$this->idp->terminateAssociation($id);
$state['core:Failed'] = true;
// Try the next SP
$this->logoutNextSP($state);
assert(false);
}
}
/**
* Start the logout operation.
*
* This function never returns.
*
* @param array &$state The logout state.
* @param string $assocId The association that started the logout.
*/
public function startLogout(array &$state, $assocId)
{
$state['core:LogoutTraditional:Remaining'] = $this->idp->getAssociations();
$this->logoutNextSP($state);
}
/**
* Continue the logout operation.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param \SimpleSAML_Error_Exception|null $error The error that occurred during session termination (if any).
*
* @throws \SimpleSAML_Error_Exception If the RelayState was lost during logout.
*/
public function onResponse($assocId, $relayState, \SimpleSAML_Error_Exception $error = null)
{
assert(is_string($assocId));
assert(is_string($relayState) || $relayState === null);
if ($relayState === null) {
throw new \SimpleSAML_Error_Exception('RelayState lost during logout.');
}
$state = \SimpleSAML_Auth_State::loadState($relayState, 'core:LogoutTraditional');
if ($error === null) {
Logger::info('Logged out of '.var_export($assocId, true).'.');
$this->idp->terminateAssociation($assocId);
} else {
Logger::warning('Error received from '.var_export($assocId, true).' during logout:');
$error->logWarning();
$state['core:Failed'] = true;
}
$this->logoutNextSP($state);
}
}

View File

@@ -0,0 +1,424 @@
<?php
/**
* Choosing the language to localize to for our minimalistic XHTML PHP based template system.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Locale;
use SimpleSAML\Utils\HTTP;
class Language
{
/**
* This is the default language map. It is used to map languages codes from the user agent to other language codes.
*/
private static $defaultLanguageMap = array('nb' => 'no');
/**
* The configuration to use.
*
* @var \SimpleSAML_Configuration
*/
private $configuration;
/**
* An array holding a list of languages available.
*
* @var array
*/
private $availableLanguages;
/**
* The language currently in use.
*
* @var null|string
*/
private $language = null;
/**
* The language to use by default.
*
* @var string
*/
private $defaultLanguage;
/**
* An array holding a list of languages that are written from right to left.
*
* @var array
*/
private $rtlLanguages;
/**
* HTTP GET language parameter name.
*
* @var string
*/
private $languageParameterName;
/**
* A custom function to use in order to determine the language in use.
*
* @var callable|null
*/
private $customFunction;
/**
* A list of languages supported with their names localized.
* Indexed by something that mostly resembles ISO 639-1 code,
* with some charming SimpleSAML-specific variants...
* that must remain before 2.0 due to backwards compatibility
*
* @var array
*/
private $language_names = array(
'no' => 'Bokmål', // Norwegian Bokmål
'nn' => 'Nynorsk', // Norwegian Nynorsk
'se' => 'Sámegiella', // Northern Sami
'sma' => 'Åarjelh-saemien giele', // Southern Sami
'da' => 'Dansk', // Danish
'en' => 'English',
'de' => 'Deutsch', // German
'sv' => 'Svenska', // Swedish
'fi' => 'Suomeksi', // Finnish
'es' => 'Español', // Spanish
'ca' => 'Català', // Catalan
'fr' => 'Français', // French
'it' => 'Italiano', // Italian
'nl' => 'Nederlands', // Dutch
'lb' => 'Lëtzebuergesch', // Luxembourgish
'cs' => 'Čeština', // Czech
'sl' => 'Slovenščina', // Slovensk
'lt' => 'Lietuvių kalba', // Lithuanian
'hr' => 'Hrvatski', // Croatian
'hu' => 'Magyar', // Hungarian
'pl' => 'Język polski', // Polish
'pt' => 'Português', // Portuguese
'pt-br' => 'Português brasileiro', // Portuguese
'ru' => 'русский язык', // Russian
'et' => 'eesti keel', // Estonian
'tr' => 'Türkçe', // Turkish
'el' => 'ελληνικά', // Greek
'ja' => '日本語', // Japanese
'zh' => '简体中文', // Chinese (simplified)
'zh-tw' => '繁體中文', // Chinese (traditional)
'ar' => 'العربية', // Arabic
'fa' => 'پارسی', // Persian
'ur' => 'اردو', // Urdu
'he' => 'עִבְרִית', // Hebrew
'id' => 'Bahasa Indonesia', // Indonesian
'sr' => 'Srpski', // Serbian
'lv' => 'Latviešu', // Latvian
'ro' => 'Românește', // Romanian
'eu' => 'Euskara', // Basque
'af' => 'Afrikaans', // Afrikaans
);
/**
* A mapping of SSP languages to locales
*
* @var array
*/
private $languagePosixMapping = array(
'no' => 'nb_NO',
'nn' => 'nn_NO',
);
/**
* Constructor
*
* @param \SimpleSAML_Configuration $configuration Configuration object
*/
public function __construct(\SimpleSAML_Configuration $configuration)
{
$this->configuration = $configuration;
$this->availableLanguages = $this->getInstalledLanguages();
$this->defaultLanguage = $this->configuration->getString('language.default', 'en');
$this->languageParameterName = $this->configuration->getString('language.parameter.name', 'language');
$this->customFunction = $this->configuration->getArray('language.get_language_function', null);
$this->rtlLanguages = $this->configuration->getArray('language.rtl', array());
if (isset($_GET[$this->languageParameterName])) {
$this->setLanguage(
$_GET[$this->languageParameterName],
$this->configuration->getBoolean('language.parameter.setcookie', true)
);
}
}
/**
* Filter configured (available) languages against installed languages.
*
* @return array The set of languages both in 'language.available' and $this->language_names.
*/
private function getInstalledLanguages()
{
$configuredAvailableLanguages = $this->configuration->getArray('language.available', array('en'));
$availableLanguages = array();
foreach ($configuredAvailableLanguages as $code) {
if (array_key_exists($code, $this->language_names) && isset($this->language_names[$code])) {
$availableLanguages[] = $code;
} else {
\SimpleSAML\Logger::error("Language \"$code\" not installed. Check config.");
}
}
return $availableLanguages;
}
/**
* Rename to non-idiosyncratic language code.
*
* @param string $language Language code for the language to rename, if necessary.
*
* @return string The language code.
*/
public function getPosixLanguage($language)
{
if (isset($this->languagePosixMapping[$language])) {
return $this->languagePosixMapping[$language];
}
return $language;
}
/**
* This method will set a cookie for the user's browser to remember what language was selected.
*
* @param string $language Language code for the language to set.
* @param boolean $setLanguageCookie Whether to set the language cookie or not. Defaults to true.
*/
public function setLanguage($language, $setLanguageCookie = true)
{
$language = strtolower($language);
if (in_array($language, $this->availableLanguages, true)) {
$this->language = $language;
if ($setLanguageCookie === true) {
self::setLanguageCookie($language);
}
}
}
/**
* This method will return the language selected by the user, or the default language. It looks first for a cached
* language code, then checks for a language cookie, then it tries to calculate the preferred language from HTTP
* headers.
*
* @return string The language selected by the user according to the processing rules specified, or the default
* language in any other case.
*/
public function getLanguage()
{
// language is set in object
if (isset($this->language)) {
return $this->language;
}
// run custom getLanguage function if defined
if (isset($this->customFunction) && is_callable($this->customFunction)) {
$customLanguage = call_user_func($this->customFunction, $this);
if ($customLanguage !== null && $customLanguage !== false) {
return $customLanguage;
}
}
// language is provided in a stored cookie
$languageCookie = Language::getLanguageCookie();
if ($languageCookie !== null) {
$this->language = $languageCookie;
return $languageCookie;
}
// check if we can find a good language from the Accept-Language HTTP header
$httpLanguage = $this->getHTTPLanguage();
if ($httpLanguage !== null) {
return $httpLanguage;
}
// language is not set, and we get the default language from the configuration
return $this->getDefaultLanguage();
}
/**
* Get the localized name of a language, by ISO 639-2 code.
*
* @param string $code The ISO 639-2 code of the language.
*
* @return string The localized name of the language.
*/
public function getLanguageLocalizedName($code)
{
if (array_key_exists($code, $this->language_names) && isset($this->language_names[$code])) {
return $this->language_names[$code];
}
\SimpleSAML\Logger::error("Name for language \"$code\" not found. Check config.");
return null;
}
/**
* Get the language parameter name.
*
* @return string The language parameter name.
*/
public function getLanguageParameterName()
{
return $this->languageParameterName;
}
/**
* This method returns the preferred language for the user based on the Accept-Language HTTP header.
*
* @return string The preferred language based on the Accept-Language HTTP header, or null if none of the languages
* in the header is available.
*/
private function getHTTPLanguage()
{
$languageScore = HTTP::getAcceptLanguage();
// for now we only use the default language map. We may use a configurable language map in the future
$languageMap = self::$defaultLanguageMap;
// find the available language with the best score
$bestLanguage = null;
$bestScore = -1.0;
foreach ($languageScore as $language => $score) {
// apply the language map to the language code
if (array_key_exists($language, $languageMap)) {
$language = $languageMap[$language];
}
if (!in_array($language, $this->availableLanguages, true)) {
// skip this language - we don't have it
continue;
}
/* Some user agents use very limited precision of the quality value, but order the elements in descending
* order. Therefore we rely on the order of the output from getAcceptLanguage() matching the order of the
* languages in the header when two languages have the same quality.
*/
if ($score > $bestScore) {
$bestLanguage = $language;
$bestScore = $score;
}
}
return $bestLanguage;
}
/**
* Return the default language according to configuration.
*
* @return string The default language that has been configured. Defaults to english if not configured.
*/
public function getDefaultLanguage()
{
return $this->defaultLanguage;
}
/**
* Return an alias for a language code, if any.
*
* @return string The alias, or null if the alias was not found.
*/
public function getLanguageCodeAlias($langcode)
{
if (isset(self::$defaultLanguageMap[$langcode])) {
return self::$defaultLanguageMap[$langcode];
}
// No alias found, which is fine
return null;
}
/**
* Return an indexed list of all languages available.
*
* @return array An array holding all the languages available as the keys of the array. The value for each key is
* true in case that the language specified by that key is currently active, or false otherwise.
*/
public function getLanguageList()
{
$current = $this->getLanguage();
$list = array_fill_keys($this->availableLanguages, false);
$list[$current] = true;
return $list;
}
/**
* Check whether a language is written from the right to the left or not.
*
* @return boolean True if the language is right-to-left, false otherwise.
*/
public function isLanguageRTL()
{
return in_array($this->getLanguage(), $this->rtlLanguages, true);
}
/**
* Retrieve the user-selected language from a cookie.
*
* @return string|null The selected language or null if unset.
*/
public static function getLanguageCookie()
{
$config = \SimpleSAML_Configuration::getInstance();
$availableLanguages = $config->getArray('language.available', array('en'));
$name = $config->getString('language.cookie.name', 'language');
if (isset($_COOKIE[$name])) {
$language = strtolower((string) $_COOKIE[$name]);
if (in_array($language, $availableLanguages, true)) {
return $language;
}
}
return null;
}
/**
* This method will attempt to set the user-selected language in a cookie. It will do nothing if the language
* specified is not in the list of available languages, or the headers have already been sent to the browser.
*
* @param string $language The language set by the user.
*/
public static function setLanguageCookie($language)
{
assert(is_string($language));
$language = strtolower($language);
$config = \SimpleSAML_Configuration::getInstance();
$availableLanguages = $config->getArray('language.available', array('en'));
if (!in_array($language, $availableLanguages, true) || headers_sent()) {
return;
}
$name = $config->getString('language.cookie.name', 'language');
$params = array(
'lifetime' => ($config->getInteger('language.cookie.lifetime', 60 * 60 * 24 * 900)),
'domain' => ($config->getString('language.cookie.domain', null)),
'path' => ($config->getString('language.cookie.path', '/')),
'secure' => ($config->getBoolean('language.cookie.secure', false)),
'httponly' => ($config->getBoolean('language.cookie.httponly', false)),
);
HTTP::setCookie($name, $language, $params, false);
}
}

View File

@@ -0,0 +1,265 @@
<?php
/**
* Glue to connect one or more translation/locale systems to the rest
*
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Locale;
use Gettext\Translations;
use Gettext\Translator;
class Localization
{
/**
* The configuration to use.
*
* @var \SimpleSAML_Configuration
*/
private $configuration;
/**
* The default gettext domain.
*/
const DEFAULT_DOMAIN = 'messages';
/**
* Old internationalization backend included in SimpleSAMLphp.
*/
const SSP_I18N_BACKEND = 'SimpleSAMLphp';
/**
* An internationalization backend implemented purely in PHP.
*/
const GETTEXT_I18N_BACKEND = 'gettext/gettext';
/**
* The default locale directory
*/
private $localeDir;
/**
* Where specific domains are stored
*/
private $localeDomainMap = array();
/**
* Pointer to currently active translator
*/
private $translator;
/**
* Pointer to current Language
*/
private $language;
/**
* Language code representing the current Language
*/
private $langcode;
/**
* The language backend to use
*/
public $i18nBackend;
/**
* Constructor
*
* @param \SimpleSAML_Configuration $configuration Configuration object
*/
public function __construct(\SimpleSAML_Configuration $configuration)
{
$this->configuration = $configuration;
$this->localeDir = $this->configuration->resolvePath('locales');
$this->language = new Language($configuration);
$this->langcode = $this->language->getPosixLanguage($this->language->getLanguage());
$this->i18nBackend = $this->configuration->getString('language.i18n.backend', self::SSP_I18N_BACKEND);
$this->setupL10N();
}
/**
* Dump the default locale directory
*/
public function getLocaleDir()
{
return $this->localeDir;
}
/**
* Get the default locale dir for a specific module aka. domain
*
* @param string $domain Name of module/domain
*/
public function getDomainLocaleDir($domain)
{
$localeDir = $this->configuration->resolvePath('modules') . '/' . $domain . '/locales';
return $localeDir;
}
/*
* Add a new translation domain from a module
* (We're assuming that each domain only exists in one place)
*
* @param string $module Module name
* @param string $localeDir Absolute path if the module is housed elsewhere
*/
public function addModuleDomain($module, $localeDir = null)
{
if (!$localeDir) {
$localeDir = $this->getDomainLocaleDir($module);
}
$this->addDomain($localeDir, $module);
}
/*
* Add a new translation domain
* (We're assuming that each domain only exists in one place)
*
* @param string $localeDir Location of translations
* @param string $domain Domain at location
*/
public function addDomain($localeDir, $domain)
{
$this->localeDomainMap[$domain] = $localeDir;
\SimpleSAML\Logger::debug("Localization: load domain '$domain' at '$localeDir'");
$this->loadGettextGettextFromPO($domain);
}
/*
* Get and check path of localization file
*
* @param string $domain Name of localization domain
* @throws Exception If the path does not exist even for the default, fallback language
*/
public function getLangPath($domain = self::DEFAULT_DOMAIN)
{
$langcode = explode('_', $this->langcode);
$langcode = $langcode[0];
$localeDir = $this->localeDomainMap[$domain];
$langPath = $localeDir.'/'.$langcode.'/LC_MESSAGES/';
\SimpleSAML\Logger::debug("Trying langpath for '$langcode' as '$langPath'");
if (is_dir($langPath) && is_readable($langPath)) {
return $langPath;
}
// Some langcodes have aliases..
$alias = $this->language->getLanguageCodeAlias($langcode);
if (isset($alias)) {
$langPath = $localeDir.'/'.$alias.'/LC_MESSAGES/';
\SimpleSAML\Logger::debug("Trying langpath for alternative '$alias' as '$langPath'");
if (is_dir($langPath) && is_readable($langPath)) {
return $langPath;
}
}
// Language not found, fall back to default
$defLangcode = $this->language->getDefaultLanguage();
$langPath = $localeDir.'/'.$defLangcode.'/LC_MESSAGES/';
if (is_dir($langPath) && is_readable($langPath)) {
// Report that the localization for the preferred language is missing
$error = "Localization not found for langcode '$langcode' at '$langPath', falling back to langcode '".
$defLangcode."'";
\SimpleSAML\Logger::error($_SERVER['PHP_SELF'].' - '.$error);
return $langPath;
}
// Locale for default language missing even, error out
$error = "Localization directory missing/broken for langcode '$langcode' and domain '$domain'";
\SimpleSAML\Logger::critical($_SERVER['PHP_SELF'].' - '.$error);
throw new \Exception($error);
}
/**
* Setup the translator
*/
private function setupTranslator()
{
$this->translator = new Translator();
$this->translator->register();
}
/**
* Load translation domain from Gettext/Gettext using .po
*
* Note: Since Twig I18N does not support domains, all loaded files are
* merged. Use contexts if identical strings need to be disambiguated.
*
* @param string $domain Name of domain
* @param boolean $catchException Whether to catch an exception on error or return early
*
* @throws \Exception If something is wrong with the locale file for the domain and activated language
*/
private function loadGettextGettextFromPO($domain = self::DEFAULT_DOMAIN, $catchException = true)
{
try {
$langPath = $this->getLangPath($domain);
} catch (\Exception $e) {
$error = "Something went wrong when trying to get path to language file, cannot load domain '$domain'.";
\SimpleSAML\Logger::error($_SERVER['PHP_SELF'].' - '.$error);
if ($catchException) {
// bail out!
return;
} else {
throw $e;
}
}
$poFile = $domain.'.po';
$poPath = $langPath.$poFile;
if (file_exists($poPath) && is_readable($poPath)) {
$translations = Translations::fromPoFile($poPath);
$this->translator->loadTranslations($translations);
} else {
$error = "Localization file '$poFile' not found in '$langPath', falling back to default";
\SimpleSAML\Logger::error($_SERVER['PHP_SELF'].' - '.$error);
}
}
/**
* Test to check if backend is set to default
*
* (if false: backend unset/there's an error)
*/
public function isI18NBackendDefault()
{
if ($this->i18nBackend === $this::SSP_I18N_BACKEND) {
return true;
}
return false;
}
/**
* Set up L18N if configured or fallback to old system
*/
private function setupL10N()
{
if ($this->i18nBackend === self::SSP_I18N_BACKEND) {
\SimpleSAML\Logger::debug("Localization: using old system");
return;
}
$this->setupTranslator();
// setup default domain
$this->addDomain($this->localeDir, self::DEFAULT_DOMAIN);
}
/**
* Show which domains are registered
*/
public function getRegisteredDomains()
{
return $this->localeDomainMap;
}
}

View File

@@ -0,0 +1,546 @@
<?php
/**
* The translation-relevant bits from our original minimalistic XHTML PHP based template system.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Locale;
class Translate
{
/**
* The configuration to be used for this translator.
*
* @var \SimpleSAML_Configuration
*/
private $configuration;
private $langtext = array();
/**
* Associative array of dictionaries.
*/
private $dictionaries = array();
/**
* The default dictionary.
*/
private $defaultDictionary = null;
/**
* The language object we'll use internally.
*
* @var \SimpleSAML\Locale\Language
*/
private $language;
/**
* Constructor
*
* @param \SimpleSAML_Configuration $configuration Configuration object
* @param string|null $defaultDictionary The default dictionary where tags will come from.
*/
public function __construct(\SimpleSAML_Configuration $configuration, $defaultDictionary = null)
{
$this->configuration = $configuration;
$this->language = new Language($configuration);
if ($defaultDictionary !== null && substr($defaultDictionary, -4) === '.php') {
// TODO: drop this entire if clause for 2.0
// for backwards compatibility - print warning
$backtrace = debug_backtrace();
$where = $backtrace[0]['file'].':'.$backtrace[0]['line'];
\SimpleSAML\Logger::warning(
'Deprecated use of new SimpleSAML\Locale\Translate(...) at '.$where.
'. The last parameter is now a dictionary name, which should not end in ".php".'
);
$this->defaultDictionary = substr($defaultDictionary, 0, -4);
} else {
$this->defaultDictionary = $defaultDictionary;
}
}
/**
* Return the internal language object used by this translator.
*
* @return \SimpleSAML\Locale\Language
*/
public function getLanguage()
{
return $this->language;
}
/**
* This method retrieves a dictionary with the name given.
*
* @param string $name The name of the dictionary, as the filename in the dictionary directory, without the
* '.php' ending.
*
* @return array An associative array with the dictionary.
*/
private function getDictionary($name)
{
assert(is_string($name));
if (!array_key_exists($name, $this->dictionaries)) {
$sepPos = strpos($name, ':');
if ($sepPos !== false) {
$module = substr($name, 0, $sepPos);
$fileName = substr($name, $sepPos + 1);
$dictDir = \SimpleSAML\Module::getModuleDir($module).'/dictionaries/';
} else {
$dictDir = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
$fileName = $name;
}
$this->dictionaries[$name] = $this->readDictionaryFile($dictDir.$fileName);
}
return $this->dictionaries[$name];
}
/**
* This method retrieves a tag as an array with language => string mappings.
*
* @param string $tag The tag name. The tag name can also be on the form '{<dictionary>:<tag>}', to retrieve a tag
* from the specific dictionary.
*
* @return array An associative array with language => string mappings, or null if the tag wasn't found.
*/
public function getTag($tag)
{
assert(is_string($tag));
// first check translations loaded by the includeInlineTranslation and includeLanguageFile methods
if (array_key_exists($tag, $this->langtext)) {
return $this->langtext[$tag];
}
// check whether we should use the default dictionary or a dictionary specified in the tag
if (substr($tag, 0, 1) === '{' && preg_match('/^{((?:\w+:)?\w+?):(.*)}$/D', $tag, $matches)) {
$dictionary = $matches[1];
$tag = $matches[2];
} else {
$dictionary = $this->defaultDictionary;
if ($dictionary === null) {
// we don't have any dictionary to load the tag from
return null;
}
}
$dictionary = $this->getDictionary($dictionary);
if (!array_key_exists($tag, $dictionary)) {
return null;
}
return $dictionary[$tag];
}
/**
* Retrieve the preferred translation of a given text.
*
* @param array $translations The translations, as an associative array with language => text mappings.
*
* @return string The preferred translation.
*
* @throws \Exception If there's no suitable translation.
*/
public function getPreferredTranslation($translations)
{
assert(is_array($translations));
// look up translation of tag in the selected language
$selected_language = $this->language->getLanguage();
if (array_key_exists($selected_language, $translations)) {
return $translations[$selected_language];
}
// look up translation of tag in the default language
$default_language = $this->language->getDefaultLanguage();
if (array_key_exists($default_language, $translations)) {
return $translations[$default_language];
}
// check for english translation
if (array_key_exists('en', $translations)) {
return $translations['en'];
}
// pick the first translation available
if (count($translations) > 0) {
$languages = array_keys($translations);
return $translations[$languages[0]];
}
// we don't have anything to return
throw new \Exception('Nothing to return from translation.');
}
/**
* Translate the name of an attribute.
*
* @param string $name The attribute name.
*
* @return string The translated attribute name, or the original attribute name if no translation was found.
*/
public function getAttributeTranslation($name)
{
// normalize attribute name
$normName = strtolower($name);
$normName = str_replace(":", "_", $normName);
// check for an extra dictionary
$extraDict = $this->configuration->getString('attributes.extradictionary', null);
if ($extraDict !== null) {
$dict = $this->getDictionary($extraDict);
if (array_key_exists($normName, $dict)) {
return $this->getPreferredTranslation($dict[$normName]);
}
}
// search the default attribute dictionary
$dict = $this->getDictionary('attributes');
if (array_key_exists('attribute_'.$normName, $dict)) {
return $this->getPreferredTranslation($dict['attribute_'.$normName]);
}
// no translations found
return $name;
}
/**
* Mark a string for translation without translating it.
*
* @param string $tag A tag name to mark for translation.
*
* @return string The tag, unchanged.
*/
public static function noop($tag)
{
return $tag;
}
/**
* Translate a tag into the current language, with a fallback to english.
*
* This function is used to look up a translation tag in dictionaries, and return the translation into the current
* language. If no translation into the current language can be found, english will be tried, and if that fails,
* placeholder text will be returned.
*
* An array can be passed as the tag. In that case, the array will be assumed to be on the form (language => text),
* and will be used as the source of translations.
*
* This function can also do replacements into the translated tag. It will search the translated tag for the keys
* provided in $replacements, and replace any found occurrences with the value of the key.
*
* @param string|array $tag A tag name for the translation which should be looked up, or an array with
* (language => text) mappings. The array version will go away in 2.0
* @param array $replacements An associative array of keys that should be replaced with values in the
* translated string.
* @param boolean $fallbackdefault Default translation to use as a fallback if no valid translation was found.
* @deprecated Not used in twig, gettext
*
* @return string The translated tag, or a placeholder value if the tag wasn't found.
*/
public function t(
$tag,
$replacements = array(),
// TODO: remove this for 2.0. Assume true
$fallbackdefault = true,
// TODO: remove this for 2.0
$oldreplacements = array(),
// TODO: remove this for 2.0
$striptags = false
) {
$backtrace = debug_backtrace();
$where = $backtrace[0]['file'].':'.$backtrace[0]['line'];
if (!$fallbackdefault) {
\SimpleSAML\Logger::warning(
'Deprecated use of new SimpleSAML\Locale\Translate::t(...) at '.$where.
'. This parameter will go away, the fallback will become' .
' identical to the $tag in 2.0.'
);
}
if (!is_array($replacements)) {
// TODO: remove this entire if for 2.0
// old style call to t(...). Print warning to log
\SimpleSAML\Logger::warning(
'Deprecated use of SimpleSAML\Locale\Translate::t(...) at '.$where.
'. Please update the code to use the new style of parameters.'
);
// for backwards compatibility
if (!$replacements && $this->getTag($tag) === null) {
\SimpleSAML\Logger::warning(
'Code which uses $fallbackdefault === FALSE should be updated to use the getTag() method instead.'
);
return null;
}
$replacements = $oldreplacements;
}
if (is_array($tag)) {
$tagData = $tag;
\SimpleSAML\Logger::warning(
'Deprecated use of new SimpleSAML\Locale\Translate::t(...) at '.$where.
'. The $tag-parameter can only be a string in 2.0.'
);
} else {
$tagData = $this->getTag($tag);
if ($tagData === null) {
// tag not found
\SimpleSAML\Logger::info('Template: Looking up ['.$tag.']: not translated at all.');
return $this->getStringNotTranslated($tag, $fallbackdefault);
}
}
$translated = $this->getPreferredTranslation($tagData);
foreach ($replacements as $k => $v) {
// try to translate if no replacement is given
if ($v == null) {
$v = $this->t($k);
}
$translated = str_replace($k, $v, $translated);
}
return $translated;
}
/**
* Return the string that should be used when no translation was found.
*
* @param string $tag A name tag of the string that should be returned.
* @param boolean $fallbacktag If set to true and string was not found in any languages, return the tag itself. If
* false return null.
*
* @return string The string that should be used, or the tag name if $fallbacktag is set to false.
*/
private function getStringNotTranslated($tag, $fallbacktag)
{
if ($fallbacktag) {
return 'not translated ('.$tag.')';
} else {
return $tag;
}
}
/**
* Include a translation inline instead of putting translations in dictionaries. This function is recommended to be
* used ONLU from variable data, or when the translation is already provided by an external source, as a database
* or in metadata.
*
* @param string $tag The tag that has a translation
* @param array|string $translation The translation array
*
* @throws \Exception If $translation is neither a string nor an array.
*/
public function includeInlineTranslation($tag, $translation)
{
if (is_string($translation)) {
$translation = array('en' => $translation);
} elseif (!is_array($translation)) {
throw new \Exception("Inline translation should be string or array. Is ".gettype($translation)." now!");
}
\SimpleSAML\Logger::debug('Template: Adding inline language translation for tag ['.$tag.']');
$this->langtext[$tag] = $translation;
}
/**
* Include a language file from the dictionaries directory.
*
* @param string $file File name of dictionary to include
* @param \SimpleSAML_Configuration|null $otherConfig Optionally provide a different configuration object than the
* one provided in the constructor to be used to find the directory of the dictionary. This allows to combine
* dictionaries inside the SimpleSAMLphp main code distribution together with external dictionaries. Defaults to
* null.
*/
public function includeLanguageFile($file, $otherConfig = null)
{
if (!empty($otherConfig)) {
$filebase = $otherConfig->getPathValue('dictionarydir', 'dictionaries/');
} else {
$filebase = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
}
$lang = $this->readDictionaryFile($filebase.$file);
\SimpleSAML\Logger::debug('Template: Merging language array. Loading ['.$file.']');
$this->langtext = array_merge($this->langtext, $lang);
}
/**
* Read a dictionary file in JSON format.
*
* @param string $filename The absolute path to the dictionary file, minus the .definition.json ending.
*
* @return array An array holding all the translations in the file.
*/
private function readDictionaryJSON($filename)
{
$definitionFile = $filename.'.definition.json';
assert(file_exists($definitionFile));
$fileContent = file_get_contents($definitionFile);
$lang = json_decode($fileContent, true);
if (empty($lang)) {
\SimpleSAML\Logger::error('Invalid dictionary definition file ['.$definitionFile.']');
return array();
}
$translationFile = $filename.'.translation.json';
if (file_exists($translationFile)) {
$fileContent = file_get_contents($translationFile);
$moreTrans = json_decode($fileContent, true);
if (!empty($moreTrans)) {
$lang = array_merge_recursive($lang, $moreTrans);
}
}
return $lang;
}
/**
* Read a dictionary file in PHP format.
*
* @param string $filename The absolute path to the dictionary file.
*
* @return array An array holding all the translations in the file.
*/
private function readDictionaryPHP($filename)
{
$phpFile = $filename.'.php';
assert(file_exists($phpFile));
$lang = null;
include($phpFile);
if (isset($lang)) {
return $lang;
}
return array();
}
/**
* Read a dictionary file.
*
* @param string $filename The absolute path to the dictionary file.
*
* @return array An array holding all the translations in the file.
*/
private function readDictionaryFile($filename)
{
assert(is_string($filename));
\SimpleSAML\Logger::debug('Template: Reading ['.$filename.']');
$jsonFile = $filename.'.definition.json';
if (file_exists($jsonFile)) {
return $this->readDictionaryJSON($filename);
}
$phpFile = $filename.'.php';
if (file_exists($phpFile)) {
return $this->readDictionaryPHP($filename);
}
\SimpleSAML\Logger::error(
$_SERVER['PHP_SELF'].' - Template: Could not find dictionary file at ['.$filename.']'
);
return array();
}
public static function translateSingularGettext($original)
{
$text = \Gettext\BaseTranslator::$current->gettext($original);
if (func_num_args() === 1) {
return $text;
}
$args = array_slice(func_get_args(), 1);
return strtr($text, is_array($args[0]) ? $args[0] : $args);
}
public static function translatePluralGettext($original, $plural, $value)
{
$text = \Gettext\BaseTranslator::$current->ngettext($original, $plural, $value);
if (func_num_args() === 3) {
return $text;
}
$args = array_slice(func_get_args(), 3);
return strtr($text, is_array($args[0]) ? $args[0] : $args);
}
/**
* Pick a translation from a given array of translations for the current language.
*
* @param array $context An array of options. The current language must be specified as an ISO 639 code accessible
* with the key "currentLanguage" in the array.
* @param array $translations An array of translations. Each translation has an ISO 639 code as its key, identifying
* the language it corresponds to.
*
* @return null|string The translation appropriate for the current language, or null if none found. If the
* $context or $translations arrays are null, or $context['currentLanguage'] is not defined, null is also returned.
*/
public static function translateFromArray($context, $translations)
{
if (!is_array($translations) || $translations === null) {
return null;
}
if (!is_array($context) || !isset($context['currentLanguage'])) {
return null;
}
if (isset($translations[$context['currentLanguage']])) {
return $translations[$context['currentLanguage']];
}
// we don't have a translation for the current language, load alternative priorities
$sspcfg = \SimpleSAML_Configuration::getInstance();
$langcfg = $sspcfg->getConfigItem('language', null);
$priorities = array();
if ($langcfg instanceof \SimpleSAML_Configuration) {
$priorities = $langcfg->getArray('priorities', array());
}
foreach ($priorities[$context['currentLanguage']] as $lang) {
if (isset($translations[$lang])) {
return $translations[$lang];
}
}
// nothing we can use, return null so that we can set a default
return null;
}
}

457
lib/SimpleSAML/Logger.php Executable file
View File

@@ -0,0 +1,457 @@
<?php
namespace SimpleSAML;
/**
* The main logger class for SimpleSAMLphp.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Jaime Pérez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
class Logger
{
/**
* @var \SimpleSAML\Logger\LoggingHandlerInterface|false|null
*/
private static $loggingHandler = null;
/**
* @var integer|null
*/
private static $logLevel = null;
/**
* @var boolean
*/
private static $captureLog = false;
/**
* @var array
*/
private static $capturedLog = array();
/**
* Array with messages logged before the logging handler was initialized.
*
* @var array
*/
private static $earlyLog = array();
/**
* List of log levels.
*
* This list is used to restore the log levels after some log levels have been disabled.
*
* @var array
*/
private static $logLevelStack = array();
/**
* The current mask of log levels disabled.
*
* Note: this mask is not directly related to the PHP error reporting level.
*
* @var int
*/
private static $logMask = 0;
/**
* This constant defines the string we set the track ID to while we are fetching the track ID from the session
* class. This is used to prevent infinite recursion.
*/
const NO_TRACKID = '_NOTRACKIDYET_';
/**
* This variable holds the track ID we have retrieved from the session class. It can also be NULL, in which case
* we haven't fetched the track ID yet, or self::NO_TRACKID, which means that we are fetching the track ID now.
*/
private static $trackid = self::NO_TRACKID;
/**
* This variable holds the format used to log any message. Its use varies depending on the log handler used (for
* instance, you cannot control here how dates are displayed when using syslog or errorlog handlers), but in
* general the options are:
*
* - %date{<format>}: the date and time, with its format specified inside the brackets. See the PHP documentation
* of the strftime() function for more information on the format. If the brackets are omitted, the standard
* format is applied. This can be useful if you just want to control the placement of the date, but don't care
* about the format.
*
* - %process: the name of the SimpleSAMLphp process. Remember you can configure this in the 'logging.processname'
* option. The SyslogLoggingHandler will just remove this.
*
* - %level: the log level (name or number depending on the handler used). Please note different logging handlers
* will print the log level differently.
*
* - %stat: if the log entry is intended for statistical purposes, it will print the string 'STAT ' (bear in mind
* the trailing space).
*
* - %trackid: the track ID, an identifier that allows you to track a single session.
*
* - %srcip: the IP address of the client. If you are behind a proxy, make sure to modify the
* $_SERVER['REMOTE_ADDR'] variable on your code accordingly to the X-Forwarded-For header.
*
* - %msg: the message to be logged.
*
* @var string The format of the log line.
*/
private static $format = '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg';
/**
* This variable tells if we have a shutdown function registered or not.
*
* @var bool
*/
private static $shutdownRegistered = false;
/**
* This variable tells if we are shutting down.
*
* @var bool
*/
private static $shuttingDown = false;
const EMERG = 0;
const ALERT = 1;
const CRIT = 2;
const ERR = 3;
const WARNING = 4;
const NOTICE = 5;
const INFO = 6;
const DEBUG = 7;
/**
* Log an emergency message.
*
* @var string $string The message to log.
*/
public static function emergency($string)
{
self::log(self::EMERG, $string);
}
/**
* Log a critical message.
*
* @var string $string The message to log.
*/
public static function critical($string)
{
self::log(self::CRIT, $string);
}
/**
* Log an alert.
*
* @var string $string The message to log.
*/
public static function alert($string)
{
self::log(self::ALERT, $string);
}
/**
* Log an error.
*
* @var string $string The message to log.
*/
public static function error($string)
{
self::log(self::ERR, $string);
}
/**
* Log a warning.
*
* @var string $string The message to log.
*/
public static function warning($string)
{
self::log(self::WARNING, $string);
}
/**
* We reserve the notice level for statistics, so do not use this level for other kind of log messages.
*
* @var string $string The message to log.
*/
public static function notice($string)
{
self::log(self::NOTICE, $string);
}
/**
* Info messages are a bit less verbose than debug messages. This is useful to trace a session.
*
* @var string $string The message to log.
*/
public static function info($string)
{
self::log(self::INFO, $string);
}
/**
* Debug messages are very verbose, and will contain more information than what is necessary for a production
* system.
*
* @var string $string The message to log.
*/
public static function debug($string)
{
self::log(self::DEBUG, $string);
}
/**
* Statistics.
*
* @var string $string The message to log.
*/
public static function stats($string)
{
self::log(self::NOTICE, $string, true);
}
/**
* Set the logger to capture logs.
*
* @var boolean $val Whether to capture logs or not. Defaults to TRUE.
*/
public static function setCaptureLog($val = true)
{
self::$captureLog = $val;
}
/**
* Get the captured log.
*/
public static function getCapturedLog()
{
return self::$capturedLog;
}
/**
* Set the track identifier to use in all logs.
*
* @param $trackId string The track identifier to use during this session.
*/
public static function setTrackId($trackId)
{
self::$trackid = $trackId;
}
/**
* Flush any pending log messages to the logging handler.
*
* This method is intended to be registered as a shutdown handler, so that any pending messages that weren't sent
* to the logging handler at that point, can still make it. It is therefore not intended to be called manually.
*
*/
public static function flush()
{
try {
$s = \SimpleSAML_Session::getSessionFromRequest();
} catch (\Exception $e) {
// loading session failed. We don't care why, at this point we have a transient session, so we use that
self::error('Cannot load or create session: '.$e->getMessage());
$s = \SimpleSAML_Session::getSessionFromRequest();
}
self::$trackid = $s->getTrackID();
self::$shuttingDown = true;
foreach (self::$earlyLog as $msg) {
self::log($msg['level'], $msg['string'], $msg['statsLog']);
}
}
/**
* Evaluate whether errors of a certain error level are masked or not.
*
* @param int $errno The level of the error to check.
*
* @return bool True if the error is masked, false otherwise.
*/
public static function isErrorMasked($errno)
{
return ($errno & self::$logMask) || !($errno & error_reporting());
}
/**
* Disable error reporting for the given log levels.
*
* Every call to this function must be followed by a call to popErrorMask().
*
* @param int $mask The log levels that should be masked.
*/
public static function maskErrors($mask)
{
assert(is_int($mask));
$currentEnabled = error_reporting();
self::$logLevelStack[] = array($currentEnabled, self::$logMask);
$currentEnabled &= ~$mask;
error_reporting($currentEnabled);
self::$logMask |= $mask;
}
/**
* Pop an error mask.
*
* This function restores the previous error mask.
*/
public static function popErrorMask()
{
$lastMask = array_pop(self::$logLevelStack);
error_reporting($lastMask[0]);
self::$logMask = $lastMask[1];
}
/**
* Defer a message for later logging.
*
* @param int $level The log level corresponding to this message.
* @param string $message The message itself to log.
* @param boolean $stats Whether this is a stats message or a regular one.
*/
private static function defer($level, $message, $stats)
{
// save the message for later
self::$earlyLog[] = array('level' => $level, 'string' => $message, 'statsLog' => $stats);
// register a shutdown handler if needed
if (!self::$shutdownRegistered) {
register_shutdown_function(array('SimpleSAML\Logger', 'flush'));
self::$shutdownRegistered = true;
}
}
private static function createLoggingHandler($handler = null)
{
// set to false to indicate that it is being initialized
self::$loggingHandler = false;
// a set of known logging handlers
$known_handlers = array(
'syslog' => 'SimpleSAML\Logger\SyslogLoggingHandler',
'file' => 'SimpleSAML\Logger\FileLoggingHandler',
'errorlog' => 'SimpleSAML\Logger\ErrorLogLoggingHandler',
);
// get the configuration
$config = \SimpleSAML_Configuration::getInstance();
assert($config instanceof \SimpleSAML_Configuration);
// setting minimum log_level
self::$logLevel = $config->getInteger('logging.level', self::INFO);
// get the metadata handler option from the configuration
if (is_null($handler)) {
$handler = $config->getString('logging.handler', 'syslog');
}
if (!array_key_exists($handler, $known_handlers) && class_exists($handler)) {
if (!in_array('SimpleSAML\Logger\LoggingHandlerInterface', class_implements($handler), true)) {
throw new \Exception("The logging handler '$handler' is invalid.");
}
} else {
$handler = strtolower($handler);
if (!array_key_exists($handler, $known_handlers)) {
throw new \Exception(
"Invalid value for the 'logging.handler' configuration option. Unknown handler '".$handler."''."
);
}
$handler = $known_handlers[$handler];
}
self::$loggingHandler = new $handler($config);
self::$format = $config->getString('logging.format', self::$format);
self::$loggingHandler->setLogFormat(self::$format);
}
private static function log($level, $string, $statsLog = false)
{
if (self::$loggingHandler === false) {
// some error occurred while initializing logging
self::defer($level, $string, $statsLog);
return;
} elseif (php_sapi_name() === 'cli' || defined('STDIN')) {
// we are being executed from the CLI, nowhere to log
if (is_null(self::$loggingHandler)) {
self::createLoggingHandler('SimpleSAML\Logger\StandardErrorLoggingHandler');
}
$_SERVER['REMOTE_ADDR'] = "CLI";
if (self::$trackid === self::NO_TRACKID) {
self::$trackid = 'CL'.bin2hex(openssl_random_pseudo_bytes(4));
}
} elseif (self::$loggingHandler === null) {
// Initialize logging
self::createLoggingHandler();
if (!empty(self::$earlyLog)) {
// output messages which were logged before we properly initialized logging
foreach (self::$earlyLog as $msg) {
self::log($msg['level'], $msg['string'], $msg['statsLog']);
}
}
}
if (self::$captureLog) {
$ts = microtime(true);
$msecs = (int) (($ts - (int) $ts) * 1000);
$ts = gmdate('H:i:s', $ts).sprintf('.%03d', $msecs).'Z';
self::$capturedLog[] = $ts.' '.$string;
}
if (self::$logLevel >= $level || $statsLog) {
if (is_array($string)) {
$string = implode(",", $string);
}
$formats = array('%trackid', '%msg', '%srcip', '%stat');
$replacements = array(self::$trackid, $string, $_SERVER['REMOTE_ADDR']);
$stat = '';
if ($statsLog) {
$stat = 'STAT ';
}
array_push($replacements, $stat);
if (self::$trackid === self::NO_TRACKID && !self::$shuttingDown) {
// we have a log without track ID and we are not still shutting down, so defer logging
self::defer($level, $string, $statsLog);
return;
} elseif (self::$trackid === self::NO_TRACKID) {
// shutting down without a track ID, prettify it
array_shift($replacements);
array_unshift($replacements, 'N/A');
}
// we either have a track ID or we are shutting down, so just log the message
$string = str_replace($formats, $replacements, self::$format);
self::$loggingHandler->log($level, $string);
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace SimpleSAML\Logger;
use SimpleSAML\Logger;
/**
* A class for logging to the default php error log.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class ErrorLogLoggingHandler implements LoggingHandlerInterface
{
/**
* This array contains the mappings from syslog log level to names.
*/
private static $levelNames = array(
Logger::EMERG => 'EMERG',
Logger::ALERT => 'ALERT',
Logger::CRIT => 'CRIT',
Logger::ERR => 'ERR',
Logger::WARNING => 'WARNING',
Logger::NOTICE => 'NOTICE',
Logger::INFO => 'INFO',
Logger::DEBUG => 'DEBUG',
);
/**
* The name of this process.
*
* @var string
*/
private $processname;
/**
* ErrorLogLoggingHandler constructor.
*
* @param \SimpleSAML_Configuration $config The configuration object for this handler.
*/
public function __construct(\SimpleSAML_Configuration $config)
{
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
}
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
*/
public function setLogFormat($format)
{
// we don't need the format here
}
/**
* Log a message to syslog.
*
* @param int $level The log level.
* @param string $string The formatted message to log.
*/
public function log($level, $string)
{
if (array_key_exists($level, self::$levelNames)) {
$levelName = self::$levelNames[$level];
} else {
$levelName = sprintf('UNKNOWN%d', $level);
}
$formats = array('%process', '%level');
$replacements = array($this->processname, $levelName);
$string = str_replace($formats, $replacements, $string);
$string = preg_replace('/%\w+(\{[^\}]+\})?/', '', $string);
$string = trim($string);
error_log($string);
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace SimpleSAML\Logger;
use SimpleSAML\Logger;
/**
* A logging handler that dumps logs to files.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class FileLoggingHandler implements LoggingHandlerInterface
{
/**
* A string with the path to the file where we should log our messages.
*
* @var null|string
*/
protected $logFile = null;
/**
* This array contains the mappings from syslog log levels to names. Copied more or less directly from
* SimpleSAML\Logger\ErrorLogLoggingHandler.
*/
private static $levelNames = array(
Logger::EMERG => 'EMERGENCY',
Logger::ALERT => 'ALERT',
Logger::CRIT => 'CRITICAL',
Logger::ERR => 'ERROR',
Logger::WARNING => 'WARNING',
Logger::NOTICE => 'NOTICE',
Logger::INFO => 'INFO',
Logger::DEBUG => 'DEBUG',
);
protected $processname = null;
protected $format;
/**
* Build a new logging handler based on files.
*/
public function __construct(\SimpleSAML_Configuration $config)
{
// get the metadata handler option from the configuration
$this->logFile = $config->getPathValue('loggingdir', 'log/').
$config->getString('logging.logfile', 'simplesamlphp.log');
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
if (@file_exists($this->logFile)) {
if (!@is_writeable($this->logFile)) {
throw new \Exception("Could not write to logfile: ".$this->logFile);
}
} else {
if (!@touch($this->logFile)) {
throw new \Exception(
"Could not create logfile: ".$this->logFile.
" The logging directory is not writable for the web server user."
);
}
}
\SimpleSAML\Utils\Time::initTimezone();
}
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
*/
public function setLogFormat($format)
{
$this->format = $format;
}
/**
* Log a message to the log file.
*
* @param int $level The log level.
* @param string $string The formatted message to log.
*/
public function log($level, $string)
{
if (!is_null($this->logFile)) {
// set human-readable log level. Copied from SimpleSAML\Logger\ErrorLogLoggingHandler.
$levelName = sprintf('UNKNOWN%d', $level);
if (array_key_exists($level, self::$levelNames)) {
$levelName = self::$levelNames[$level];
}
$formats = array('%process', '%level');
$replacements = array($this->processname, $levelName);
$matches = array();
if (preg_match('/%date(?:\{([^\}]+)\})?/', $this->format, $matches)) {
$format = "%b %d %H:%M:%S";
if (isset($matches[1])) {
$format = $matches[1];
}
array_push($formats, $matches[0]);
array_push($replacements, strftime($format));
}
$string = str_replace($formats, $replacements, $string);
file_put_contents($this->logFile, $string.PHP_EOL, FILE_APPEND);
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace SimpleSAML\Logger;
/**
* The interface that must be implemented by any log handler.
*
* @author Jaime Perez Crespo, UNINETT AS.
* @package SimpleSAMLphp
*/
interface LoggingHandlerInterface
{
/**
* Constructor for log handlers. It must accept receiving a \SimpleSAML_Configuration object.
*
* @param \SimpleSAML_Configuration $config The configuration to use in this log handler.
*/
public function __construct(\SimpleSAML_Configuration $config);
/**
* Log a message to its destination.
*
* @param int $level The log level.
* @param string $string The message to log.
*/
public function log($level, $string);
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
*/
public function setLogFormat($format);
}

View File

@@ -0,0 +1,24 @@
<?php
namespace SimpleSAML\Logger;
/**
* A logging handler that outputs all messages to standard error.
*
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
class StandardErrorLoggingHandler extends FileLoggingHandler
{
/**
* StandardError constructor.
*
* It runs the parent constructor and sets the log file to be the standard error descriptor.
*/
public function __construct(\SimpleSAML_Configuration $config)
{
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
$this->logFile = 'php://stderr';
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace SimpleSAML\Logger;
use SimpleSAML\Utils\System;
/**
* A logger that sends messages to syslog.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class SyslogLoggingHandler implements LoggingHandlerInterface
{
private $isWindows = false;
private $format;
/**
* Build a new logging handler based on syslog.
*/
public function __construct(\SimpleSAML_Configuration $config)
{
$facility = $config->getInteger('logging.facility', defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER);
$processname = $config->getString('logging.processname', 'SimpleSAMLphp');
// Setting facility to LOG_USER (only valid in Windows), enable log level rewrite on windows systems
if (System::getOS() === System::WINDOWS) {
$this->isWindows = true;
$facility = LOG_USER;
}
openlog($processname, LOG_PID, $facility);
}
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
*/
public function setLogFormat($format)
{
$this->format = $format;
}
/**
* Log a message to syslog.
*
* @param int $level The log level.
* @param string $string The formatted message to log.
*/
public function log($level, $string)
{
// changing log level to supported levels if OS is Windows
if ($this->isWindows) {
if ($level <= 4) {
$level = LOG_ERR;
} else {
$level = LOG_INFO;
}
}
$formats = array('%process', '%level');
$replacements = array('', $level);
$string = str_replace($formats, $replacements, $string);
$string = preg_replace('/%\w+(\{[^\}]+\})?/', '', $string);
$string = trim($string);
syslog($level, $string);
}
}

492
lib/SimpleSAML/Memcache.php Executable file
View File

@@ -0,0 +1,492 @@
<?php
/**
* This file implements functions to read and write to a group of memcache
* servers.
*
* The goals of this storage class is to provide failover, redudancy and load
* balancing. This is accomplished by storing the data object to several
* groups of memcache servers. Each data object is replicated to every group
* of memcache servers, but it is only stored to one server in each group.
*
* For this code to work correctly, all web servers accessing the data must
* have the same clock (as measured by the time()-function). Different clock
* values will lead to incorrect behaviour.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Memcache
{
/**
* Cache of the memcache servers we are using.
*
* @var Memcache[]|null
*/
private static $serverGroups = null;
/**
* The flavor of memcache PHP extension we are using.
*
* @var string
*/
private static $extension = '';
/**
* Find data stored with a given key.
*
* @param string $key The key of the data.
*
* @return mixed The data stored with the given key, or null if no data matching the key was found.
*/
public static function get($key)
{
SimpleSAML\Logger::debug("loading key $key from memcache");
$latestInfo = null;
$latestTime = 0.0;
$latestData = null;
$mustUpdate = false;
$allDown = true;
// search all the servers for the given id
foreach (self::getMemcacheServers() as $server) {
$serializedInfo = $server->get($key);
if ($serializedInfo === false) {
// either the server is down, or we don't have the value stored on that server
$mustUpdate = true;
$up = $server->getstats();
if ($up !== false) {
$allDown = false;
}
continue;
}
$allDown = false;
// unserialize the object
$info = unserialize($serializedInfo);
/*
* Make sure that this is an array with two keys:
* - 'timestamp': The time the data was saved.
* - 'data': The data.
*/
if (!is_array($info)) {
SimpleSAML\Logger::warning(
'Retrieved invalid data from a memcache server. Data was not an array.'
);
continue;
}
if (!array_key_exists('timestamp', $info)) {
SimpleSAML\Logger::warning(
'Retrieved invalid data from a memcache server. Missing timestamp.'
);
continue;
}
if (!array_key_exists('data', $info)) {
SimpleSAML\Logger::warning(
'Retrieved invalid data from a memcache server. Missing data.'
);
continue;
}
if ($latestInfo === null) {
// first info found
$latestInfo = $serializedInfo;
$latestTime = $info['timestamp'];
$latestData = $info['data'];
continue;
}
if ($info['timestamp'] === $latestTime && $serializedInfo === $latestInfo) {
// this data matches the data from the other server(s)
continue;
}
// different data from different servers. We need to update at least one of them to maintain sync
$mustUpdate = true;
// update if data in $info is newer than $latestData
if ($latestTime < $info['timestamp']) {
$latestInfo = $serializedInfo;
$latestTime = $info['timestamp'];
$latestData = $info['data'];
}
}
if ($latestData === null) {
if ($allDown) {
// all servers are down, panic!
$e = new SimpleSAML_Error_Error('MEMCACHEDOWN', null, 503);
throw new SimpleSAML_Error_Exception('All memcache servers are down', 503, $e);
}
// we didn't find any data matching the key
SimpleSAML\Logger::debug("key $key not found in memcache");
return null;
}
if ($mustUpdate) {
// we found data matching the key, but some of the servers need updating
SimpleSAML\Logger::debug("Memcache servers out of sync for $key, forcing sync");
self::set($key, $latestData);
}
return $latestData;
}
/**
* Save a key-value pair to the memcache servers.
*
* @param string $key The key of the data.
* @param mixed $value The value of the data.
* @param integer|null $expire The expiration timestamp of the data.
*/
public static function set($key, $value, $expire = null)
{
SimpleSAML\Logger::debug("saving key $key to memcache");
$savedInfo = array(
'timestamp' => microtime(true),
'data' => $value
);
if ($expire === null) {
$expire = self::getExpireTime();
}
$savedInfoSerialized = serialize($savedInfo);
// store this object to all groups of memcache servers
foreach (self::getMemcacheServers() as $server) {
if (self::$extension === 'memcached') {
$server->set($key, $savedInfoSerialized, $expire);
} else {
$server->set($key, $savedInfoSerialized, 0, $expire);
}
}
}
/**
* Delete a key-value pair from the memcache servers.
*
* @param string $key The key we should delete.
*/
public static function delete($key)
{
assert(is_string($key));
SimpleSAML\Logger::debug("deleting key $key from memcache");
// store this object to all groups of memcache servers
foreach (self::getMemcacheServers() as $server) {
$server->delete($key);
}
}
/**
* This function adds a server from the 'memcache_store.servers'
* configuration option to a Memcache object.
*
* The server parameter is an array with the following keys:
* - hostname
* Hostname or ip address to the memcache server.
* - port (optional)
* port number the memcache server is running on. This
* defaults to memcache.default_port if no value is given.
* The default value of memcache.default_port is 11211.
* - weight (optional)
* The weight of this server in the load balancing
* cluster.
* - timeout (optional)
* The timeout for contacting this server, in seconds.
* The default value is 3 seconds.
*
* @param Memcache $memcache The Memcache object we should add this server to.
* @param array $server An associative array with the configuration options for the server to add.
*
* @throws Exception If any configuration option for the server is invalid.
*/
private static function addMemcacheServer($memcache, $server)
{
// the hostname option is required
if (!array_key_exists('hostname', $server)) {
throw new Exception(
"hostname setting missing from server in the 'memcache_store.servers' configuration option."
);
}
$hostname = $server['hostname'];
// the hostname must be a valid string
if (!is_string($hostname)) {
throw new Exception(
"Invalid hostname for server in the 'memcache_store.servers' configuration option. The hostname is".
' supposed to be a string.'
);
}
// check if we are told to use a socket
$socket = false;
if (strpos($hostname, 'unix:///') === 0) {
$socket = true;
}
// check if the user has specified a port number
if ($socket) {
// force port to be 0 for sockets
$port = 0;
} elseif (array_key_exists('port', $server)) {
// get the port number from the array, and validate it
$port = (int) $server['port'];
if (($port <= 0) || ($port > 65535)) {
throw new Exception(
"Invalid port for server in the 'memcache_store.servers' configuration option. The port number".
' is supposed to be an integer between 0 and 65535.'
);
}
} else {
// use the default port number from the ini-file
$port = (int) ini_get('memcache.default_port');
if ($port <= 0 || $port > 65535) {
// invalid port number from the ini-file. fall back to the default
$port = 11211;
}
}
// check if the user has specified a weight for this server
if (array_key_exists('weight', $server)) {
// get the weight and validate it
$weight = (int) $server['weight'];
if ($weight <= 0) {
throw new Exception(
"Invalid weight for server in the 'memcache_store.servers' configuration option. The weight is".
' supposed to be a positive integer.'
);
}
} else {
// use a default weight of 1
$weight = 1;
}
// check if the user has specified a timeout for this server
if (array_key_exists('timeout', $server)) {
// get the timeout and validate it
$timeout = (int) $server['timeout'];
if ($timeout <= 0) {
throw new Exception(
"Invalid timeout for server in the 'memcache_store.servers' configuration option. The timeout is".
' supposed to be a positive integer.'
);
}
} else {
// use a default timeout of 3 seconds
$timeout = 3;
}
// add this server to the Memcache object
if (self::$extension === 'memcached') {
$memcache->addServer($hostname, $port);
} else {
$memcache->addServer($hostname, $port, true, $weight, $timeout, $timeout, true);
}
}
/**
* This function takes in a list of servers belonging to a group and
* creates a Memcache object from the servers in the group.
*
* @param array $group Array of servers which should be created as a group.
*
* @return Memcache A Memcache object of the servers in the group
*
* @throws Exception If the servers configuration is invalid.
*/
private static function loadMemcacheServerGroup(array $group)
{
$class = class_exists('Memcache') ? 'Memcache' : (class_exists('Memcached') ? 'Memcached' : false);
if (!$class) {
throw new Exception('Missing Memcached implementation. You must install either the Memcache or Memcached extension.');
}
self::$extension = strtolower($class);
// create the Memcache object
$memcache = new $class();
// iterate over all the servers in the group and add them to the Memcache object
foreach ($group as $index => $server) {
// make sure that we don't have an index. An index would be a sign of invalid configuration
if (!is_int($index)) {
throw new Exception(
"Invalid index on element in the 'memcache_store.servers' configuration option. Perhaps you".
' have forgotten to add an array(...) around one of the server groups? The invalid index was: '.
$index
);
}
// make sure that the server object is an array. Each server is an array with name-value pairs
if (!is_array($server)) {
throw new Exception(
'Invalid value for the server with index '.$index.
'. Remeber that the \'memcache_store.servers\' configuration option'.
' contains an array of arrays of arrays.'
);
}
self::addMemcacheServer($memcache, $server);
}
return $memcache;
}
/**
* This function gets a list of all configured memcache servers. This list is initialized based
* on the content of 'memcache_store.servers' in the configuration.
*
* @return Memcache[] Array with Memcache objects.
*
* @throws Exception If the servers configuration is invalid.
*/
private static function getMemcacheServers()
{
// check if we have loaded the servers already
if (self::$serverGroups != null) {
return self::$serverGroups;
}
// initialize the servers-array
self::$serverGroups = array();
// load the configuration
$config = SimpleSAML_Configuration::getInstance();
$groups = $config->getArray('memcache_store.servers');
// iterate over all the groups in the 'memcache_store.servers' configuration option
foreach ($groups as $index => $group) {
// make sure that the group doesn't have an index. An index would be a sign of invalid configuration
if (!is_int($index)) {
throw new Exception(
"Invalid index on element in the 'memcache_store.servers'".
' configuration option. Perhaps you have forgotten to add an array(...)'.
' around one of the server groups? The invalid index was: '.$index
);
}
/*
* Make sure that the group is an array. Each group is an array of servers. Each server is
* an array of name => value pairs for that server.
*/
if (!is_array($group)) {
throw new Exception(
"Invalid value for the server with index ".$index.
". Remeber that the 'memcache_store.servers' configuration option".
' contains an array of arrays of arrays.'
);
}
// parse and add this group to the server group list
self::$serverGroups[] = self::loadMemcacheServerGroup($group);
}
return self::$serverGroups;
}
/**
* This is a helper-function which returns the expire value of data
* we should store to the memcache servers.
*
* The value is set depending on the configuration. If no value is
* set in the configuration, then we will use a default value of 0.
* 0 means that the item will never expire.
*
* @return integer The value which should be passed in the set(...) calls to the memcache objects.
*
* @throws Exception If the option 'memcache_store.expires' has a negative value.
*/
private static function getExpireTime()
{
// get the configuration instance
$config = SimpleSAML_Configuration::getInstance();
assert($config instanceof SimpleSAML_Configuration);
// get the expire-value from the configuration
$expire = $config->getInteger('memcache_store.expires', 0);
// it must be a positive integer
if ($expire < 0) {
throw new Exception(
"The value of 'memcache_store.expires' in the configuration can't be a negative integer."
);
}
/* If the configuration option is 0, then we should return 0. This allows the user to specify that the data
* shouldn't expire.
*/
if ($expire == 0) {
return 0;
}
/* The expire option is given as the number of seconds into the future an item should expire. We convert this
* to an actual timestamp.
*/
$expireTime = time() + $expire;
return $expireTime;
}
/**
* This function retrieves statistics about all memcache server groups.
*
* @return array Array with the names of each stat and an array with the value for each server group.
*
* @throws Exception If memcache server status couldn't be retrieved.
*/
public static function getStats()
{
$ret = array();
foreach (self::getMemcacheServers() as $sg) {
$stats = method_exists($sg, 'getExtendedStats') ? $sg->getExtendedStats() : $sg->getStats();
foreach ($stats as $server => $data) {
if ($data === false) {
throw new Exception('Failed to get memcache server status.');
}
}
$stats = SimpleSAML\Utils\Arrays::transpose($stats);
$ret = array_merge_recursive($ret, $stats);
}
return $ret;
}
/**
* Retrieve statistics directly in the form returned by getExtendedStats, for
* all server groups.
*
* @return array An array with the extended stats output for each server group.
*/
public static function getRawStats()
{
$ret = array();
foreach (self::getMemcacheServers() as $sg) {
$stats = method_exists($sg, 'getExtendedStats') ? $sg->getExtendedStats() : $sg->getStats();
$ret[] = $stats;
}
return $ret;
}
}

View File

@@ -0,0 +1,361 @@
<?php
/**
* This file defines a class for metadata handling.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_MetaDataStorageHandler
{
/**
* This static variable contains a reference to the current
* instance of the metadata handler. This variable will be null if
* we haven't instantiated a metadata handler yet.
*
* @var SimpleSAML_Metadata_MetaDataStorageHandler
*/
private static $metadataHandler = null;
/**
* This is a list of all the metadata sources we have in our metadata
* chain. When we need metadata, we will look through this chain from start to end.
*
* @var SimpleSAML_Metadata_MetaDataStorageSource[]
*/
private $sources;
/**
* This function retrieves the current instance of the metadata handler.
* The metadata handler will be instantiated if this is the first call
* to this function.
*
* @return SimpleSAML_Metadata_MetaDataStorageHandler The current metadata handler instance.
*/
public static function getMetadataHandler()
{
if (self::$metadataHandler === null) {
self::$metadataHandler = new SimpleSAML_Metadata_MetaDataStorageHandler();
}
return self::$metadataHandler;
}
/**
* This constructor initializes this metadata storage handler. It will load and
* parse the configuration, and initialize the metadata source list.
*/
protected function __construct()
{
$config = SimpleSAML_Configuration::getInstance();
$sourcesConfig = $config->getArray('metadata.sources', null);
// for backwards compatibility, and to provide a default configuration
if ($sourcesConfig === null) {
$type = $config->getString('metadata.handler', 'flatfile');
$sourcesConfig = array(array('type' => $type));
}
try {
$this->sources = SimpleSAML_Metadata_MetaDataStorageSource::parseSources($sourcesConfig);
} catch (Exception $e) {
throw new Exception(
"Invalid configuration of the 'metadata.sources' configuration option: ".$e->getMessage()
);
}
}
/**
* This function is used to generate some metadata elements automatically.
*
* @param string $property The metadata property which should be auto-generated.
* @param string $set The set we the property comes from.
*
* @return string The auto-generated metadata property.
* @throws Exception If the metadata cannot be generated automatically.
*/
public function getGenerated($property, $set)
{
// first we check if the user has overridden this property in the metadata
try {
$metadataSet = $this->getMetaDataCurrent($set);
if (array_key_exists($property, $metadataSet)) {
return $metadataSet[$property];
}
} catch (Exception $e) {
// probably metadata wasn't found. In any case we continue by generating the metadata
}
// get the configuration
$config = SimpleSAML_Configuration::getInstance();
assert($config instanceof SimpleSAML_Configuration);
$baseurl = \SimpleSAML\Utils\HTTP::getSelfURLHost().$config->getBasePath();
if ($set == 'saml20-sp-hosted') {
if ($property === 'SingleLogoutServiceBinding') {
return \SAML2\Constants::BINDING_HTTP_REDIRECT;
}
} elseif ($set == 'saml20-idp-hosted') {
switch ($property) {
case 'SingleSignOnService':
return $baseurl.'saml2/idp/SSOService.php';
case 'SingleSignOnServiceBinding':
return \SAML2\Constants::BINDING_HTTP_REDIRECT;
case 'SingleLogoutService':
return $baseurl.'saml2/idp/SingleLogoutService.php';
case 'SingleLogoutServiceBinding':
return \SAML2\Constants::BINDING_HTTP_REDIRECT;
}
} elseif ($set == 'shib13-idp-hosted') {
if ($property === 'SingleSignOnService') {
return $baseurl.'shib13/idp/SSOService.php';
}
}
throw new Exception('Could not generate metadata property '.$property.' for set '.$set.'.');
}
/**
* This function lists all known metadata in the given set. It is returned as an associative array
* where the key is the entity id.
*
* @param string $set The set we want to list metadata from.
*
* @return array An associative array with the metadata from from the given set.
*/
public function getList($set = 'saml20-idp-remote')
{
assert(is_string($set));
$result = array();
foreach ($this->sources as $source) {
$srcList = $source->getMetadataSet($set);
foreach ($srcList as $key => $le) {
if (array_key_exists('expire', $le)) {
if ($le['expire'] < time()) {
unset($srcList[$key]);
SimpleSAML\Logger::warning(
"Dropping metadata entity ".var_export($key, true).", expired ".
SimpleSAML\Utils\Time::generateTimestamp($le['expire'])."."
);
}
}
}
/* $result is the last argument to array_merge because we want the content already
* in $result to have precedence.
*/
$result = array_merge($srcList, $result);
}
return $result;
}
/**
* This function retrieves metadata for the current entity based on the hostname/path the request
* was directed to. It will throw an exception if it is unable to locate the metadata.
*
* @param string $set The set we want metadata from.
*
* @return array An associative array with the metadata.
*/
public function getMetaDataCurrent($set)
{
return $this->getMetaData(null, $set);
}
/**
* This function locates the current entity id based on the hostname/path combination the user accessed.
* It will throw an exception if it is unable to locate the entity id.
*
* @param string $set The set we look for the entity id in.
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
*
* @return string The entity id which is associated with the current hostname/path combination.
* @throws Exception If no default metadata can be found in the set for the current host.
*/
public function getMetaDataCurrentEntityID($set, $type = 'entityid')
{
assert(is_string($set));
// first we look for the hostname/path combination
$currenthostwithpath = \SimpleSAML\Utils\HTTP::getSelfHostWithPath(); // sp.example.org/university
foreach ($this->sources as $source) {
$index = $source->getEntityIdFromHostPath($currenthostwithpath, $set, $type);
if ($index !== null) {
return $index;
}
}
// then we look for the hostname
$currenthost = \SimpleSAML\Utils\HTTP::getSelfHost(); // sp.example.org
foreach ($this->sources as $source) {
$index = $source->getEntityIdFromHostPath($currenthost, $set, $type);
if ($index !== null) {
return $index;
}
}
// then we look for the DEFAULT entry
foreach ($this->sources as $source) {
$entityId = $source->getEntityIdFromHostPath('__DEFAULT__', $set, $type);
if ($entityId !== null) {
return $entityId;
}
}
// we were unable to find the hostname/path in any metadata source
throw new Exception(
'Could not find any default metadata entities in set ['.$set.'] for host ['.$currenthost.' : '.
$currenthostwithpath.']'
);
}
/**
* This method will call getPreferredEntityIdFromCIDRhint() on all of the
* sources.
*
* @param string $set Which set of metadata we are looking it up in.
* @param string $ip IP address
*
* @return string The entity id of a entity which have a CIDR hint where the provided
* IP address match.
*/
public function getPreferredEntityIdFromCIDRhint($set, $ip)
{
foreach ($this->sources as $source) {
$entityId = $source->getPreferredEntityIdFromCIDRhint($set, $ip);
if ($entityId !== null) {
return $entityId;
}
}
return null;
}
/**
* This function looks up the metadata for the given entity id in the given set. It will throw an
* exception if it is unable to locate the metadata.
*
* @param string $index The entity id we are looking up. This parameter may be NULL, in which case we look up
* the current entity id based on the current hostname/path.
* @param string $set The set of metadata we are looking up the entity id in.
*
* @return array The metadata array describing the specified entity.
* @throws Exception If metadata for the specified entity is expired.
* @throws SimpleSAML_Error_MetadataNotFound If no metadata for the entity specified can be found.
*/
public function getMetaData($index, $set)
{
assert(is_string($set));
if ($index === null) {
$index = $this->getMetaDataCurrentEntityID($set, 'metaindex');
}
assert(is_string($index));
foreach ($this->sources as $source) {
$metadata = $source->getMetaData($index, $set);
if ($metadata !== null) {
if (array_key_exists('expire', $metadata)) {
if ($metadata['expire'] < time()) {
throw new Exception(
'Metadata for the entity ['.$index.'] expired '.
(time() - $metadata['expire']).' seconds ago.'
);
}
}
$metadata['metadata-index'] = $index;
$metadata['metadata-set'] = $set;
assert(array_key_exists('entityid', $metadata));
return $metadata;
}
}
throw new SimpleSAML_Error_MetadataNotFound($index);
}
/**
* Retrieve the metadata as a configuration object.
*
* This function will throw an exception if it is unable to locate the metadata.
*
* @param string $entityId The entity ID we are looking up.
* @param string $set The metadata set we are searching.
*
* @return SimpleSAML_Configuration The configuration object representing the metadata.
* @throws SimpleSAML_Error_MetadataNotFound If no metadata for the entity specified can be found.
*/
public function getMetaDataConfig($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
$metadata = $this->getMetaData($entityId, $set);
return SimpleSAML_Configuration::loadFromArray($metadata, $set.'/'.var_export($entityId, true));
}
/**
* Search for an entity's metadata, given the SHA1 digest of its entity ID.
*
* @param string $sha1 The SHA1 digest of the entity ID.
* @param string $set The metadata set we are searching.
*
* @return null|SimpleSAML_Configuration The metadata corresponding to the entity, or null if the entity cannot be
* found.
*/
public function getMetaDataConfigForSha1($sha1, $set)
{
assert(is_string($sha1));
assert(is_string($set));
$result = array();
foreach ($this->sources as $source) {
$srcList = $source->getMetadataSet($set);
/* $result is the last argument to array_merge because we want the content already
* in $result to have precedence.
*/
$result = array_merge($srcList, $result);
}
foreach ($result as $remote_provider) {
if (sha1($remote_provider['entityid']) == $sha1) {
$remote_provider['metadata-set'] = $set;
return SimpleSAML_Configuration::loadFromArray(
$remote_provider,
$set.'/'.var_export($remote_provider['entityid'], true)
);
}
}
return null;
}
}

View File

@@ -0,0 +1,144 @@
<?php
/**
* This file defines a flat file metadata source.
* Instantiation of session handler objects should be done through
* the class method getMetadataHandler().
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_MetaDataStorageHandlerFlatFile extends SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* This is the directory we will load metadata files from. The path will always end
* with a '/'.
*
* @var string
*/
private $directory;
/**
* This is an associative array which stores the different metadata sets we have loaded.
*
* @var array
*/
private $cachedMetadata = array();
/**
* This constructor initializes the flatfile metadata storage handler with the
* specified configuration. The configuration is an associative array with the following
* possible elements:
* - 'directory': The directory we should load metadata from. The default directory is
* set in the 'metadatadir' configuration option in 'config.php'.
*
* @param array $config An associative array with the configuration for this handler.
*/
protected function __construct($config)
{
assert(is_array($config));
// get the configuration
$globalConfig = SimpleSAML_Configuration::getInstance();
// find the path to the directory we should search for metadata in
if (array_key_exists('directory', $config)) {
$this->directory = $config['directory'];
} else {
$this->directory = $globalConfig->getString('metadatadir', 'metadata/');
}
/* Resolve this directory relative to the SimpleSAMLphp directory (unless it is
* an absolute path).
*/
$this->directory = $globalConfig->resolvePath($this->directory).'/';
}
/**
* This function loads the given set of metadata from a file our metadata directory.
* This function returns null if it is unable to locate the given set in the metadata directory.
*
* @param string $set The set of metadata we are loading.
*
* @return array An associative array with the metadata, or null if we are unable to load metadata from the given
* file.
* @throws Exception If the metadata set cannot be loaded.
*/
private function load($set)
{
$metadatasetfile = $this->directory.$set.'.php';
if (!file_exists($metadatasetfile)) {
return null;
}
$metadata = array();
include($metadatasetfile);
if (!is_array($metadata)) {
throw new Exception('Could not load metadata set ['.$set.'] from file: '.$metadatasetfile);
}
return $metadata;
}
/**
* This function retrieves the given set of metadata. It will return an empty array if it is
* unable to locate it.
*
* @param string $set The set of metadata we are retrieving.
*
* @return array An associative array with the metadata. Each element in the array is an entity, and the
* key is the entity id.
*/
public function getMetadataSet($set)
{
if (array_key_exists($set, $this->cachedMetadata)) {
return $this->cachedMetadata[$set];
}
$metadataSet = $this->load($set);
if ($metadataSet === null) {
$metadataSet = array();
}
// add the entity id of an entry to each entry in the metadata
foreach ($metadataSet as $entityId => &$entry) {
if (preg_match('/__DYNAMIC(:[0-9]+)?__/', $entityId)) {
$entry['entityid'] = $this->generateDynamicHostedEntityID($set);
} else {
$entry['entityid'] = $entityId;
}
}
$this->cachedMetadata[$set] = $metadataSet;
return $metadataSet;
}
private function generateDynamicHostedEntityID($set)
{
// get the configuration
$baseurl = \SimpleSAML\Utils\HTTP::getBaseURL();
if ($set === 'saml20-idp-hosted') {
return $baseurl.'saml2/idp/metadata.php';
} elseif ($set === 'shib13-idp-hosted') {
return $baseurl.'shib13/idp/metadata.php';
} elseif ($set === 'wsfed-sp-hosted') {
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost();
} elseif ($set === 'adfs-idp-hosted') {
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost().':idp';
} else {
throw new Exception('Can not generate dynamic EntityID for metadata of this type: ['.$set.']');
}
}
}

View File

@@ -0,0 +1,306 @@
<?php
/**
* Class for handling metadata files stored in a database.
*
* This class has been based off a previous version written by
* mooknarf@gmail.com and patched to work with the latest version
* of SimpleSAMLphp
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_MetaDataStorageHandlerPdo extends SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* The PDO object
*/
private $db;
/**
* Prefix to apply to the metadata table
*/
private $tablePrefix;
/**
* This is an associative array which stores the different metadata sets we have loaded.
*/
private $cachedMetadata = array();
/**
* All the metadata sets supported by this MetaDataStorageHandler
*/
public $supportedSets = array(
'adfs-idp-hosted',
'adfs-sp-remote',
'saml20-idp-hosted',
'saml20-idp-remote',
'saml20-sp-remote',
'shib13-idp-hosted',
'shib13-idp-remote',
'shib13-sp-hosted',
'shib13-sp-remote',
'wsfed-idp-remote',
'wsfed-sp-hosted'
);
/**
* This constructor initializes the PDO metadata storage handler with the specified
* configuration. The configuration is an associative array with the following
* possible elements (set in config.php):
* - 'usePersistentConnection': TRUE/FALSE if database connection should be persistent.
* - 'dsn': The database connection string.
* - 'username': Database user name
* - 'password': Password for the database user.
*
* @param array $config An associative array with the configuration for this handler.
*/
public function __construct($config)
{
assert(is_array($config));
$this->db = SimpleSAML\Database::getInstance();
}
/**
* This function loads the given set of metadata from a file to a configured database.
* This function returns NULL if it is unable to locate the given set in the metadata directory.
*
* @param string $set The set of metadata we are loading.
*
* @return array $metadata Associative array with the metadata, or NULL if we are unable to load metadata from the
* given file.
*
* @throws Exception If a database error occurs.
* @throws SimpleSAML_Error_Exception If the metadata can be retrieved from the database, but cannot be decoded.
*/
private function load($set)
{
assert(is_string($set));
$tableName = $this->getTableName($set);
if (!in_array($set, $this->supportedSets, true)) {
return null;
}
$stmt = $this->db->read("SELECT entity_id, entity_data FROM $tableName");
if ($stmt->execute()) {
$metadata = array();
while ($d = $stmt->fetch()) {
$data = json_decode($d['entity_data'], true);
if ($data === null) {
throw new SimpleSAML_Error_Exception("Cannot decode metadata for entity '${d['entity_id']}'");
}
if (!array_key_exists('entityid', $data)) {
$data['entityid'] = $d['entity_id'];
}
$metadata[$d['entity_id']] = $data;
}
return $metadata;
} else {
throw new Exception('PDO metadata handler: Database error: '.var_export($this->db->getLastError(), true));
}
}
/**
* Retrieve a list of all available metadata for a given set.
*
* @param string $set The set we are looking for metadata in.
*
* @return array $metadata An associative array with all the metadata for the given set.
*/
public function getMetadataSet($set)
{
assert(is_string($set));
if (array_key_exists($set, $this->cachedMetadata)) {
return $this->cachedMetadata[$set];
}
$metadataSet = $this->load($set);
if ($metadataSet === null) {
$metadataSet = array();
}
foreach ($metadataSet as $entityId => &$entry) {
if (preg_match('/__DYNAMIC(:[0-9]+)?__/', $entityId)) {
$entry['entityid'] = $this->generateDynamicHostedEntityID($set);
} else {
$entry['entityid'] = $entityId;
}
}
$this->cachedMetadata[$set] = $metadataSet;
return $metadataSet;
}
/**
* Retrieve a metadata entry.
*
* @param string $entityId The entityId we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
*/
public function getMetaData($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
$tableName = $this->getTableName($set);
if (!in_array($set, $this->supportedSets, true)) {
return null;
}
$stmt = $this->db->read("SELECT entity_id, entity_data FROM $tableName WHERE entity_id=:entityId", array('entityId' => $entityId));
if ($stmt->execute()) {
$rowCount = 0;
$data = null;
while ($d = $stmt->fetch()) {
if (++$rowCount > 1) {
SimpleSAML\Logger::warning("Duplicate match for $entityId in set $set");
break;
}
$data = json_decode($d['entity_data'], true);
if ($data === null) {
throw new SimpleSAML_Error_Exception("Cannot decode metadata for entity '${d['entity_id']}'");
}
if (!array_key_exists('entityid', $data)) {
$data['entityid'] = $d['entity_id'];
}
}
return $data;
} else {
throw new Exception('PDO metadata handler: Database error: '.var_export($this->db->getLastError(), true));
}
}
private function generateDynamicHostedEntityID($set)
{
assert(is_string($set));
// get the configuration
$baseurl = \SimpleSAML\Utils\HTTP::getBaseURL();
if ($set === 'saml20-idp-hosted') {
return $baseurl.'saml2/idp/metadata.php';
} elseif ($set === 'saml20-sp-hosted') {
return $baseurl.'saml2/sp/metadata.php';
} elseif ($set === 'shib13-idp-hosted') {
return $baseurl.'shib13/idp/metadata.php';
} elseif ($set === 'shib13-sp-hosted') {
return $baseurl.'shib13/sp/metadata.php';
} elseif ($set === 'wsfed-sp-hosted') {
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost();
} elseif ($set === 'adfs-idp-hosted') {
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost().':idp';
} else {
throw new Exception('Can not generate dynamic EntityID for metadata of this type: ['.$set.']');
}
}
/**
* Add metadata to the configured database
*
* @param string $index Entity ID
* @param string $set The set to add the metadata to
* @param array $entityData Metadata
*
* @return bool True/False if entry was successfully added
*/
public function addEntry($index, $set, $entityData)
{
assert(is_string($index));
assert(is_string($set));
assert(is_array($entityData));
if (!in_array($set, $this->supportedSets, true)) {
return false;
}
$tableName = $this->getTableName($set);
$metadata = $this->db->read(
"SELECT entity_id, entity_data FROM $tableName WHERE entity_id = :entity_id",
array(
'entity_id' => $index,
)
);
$retrivedEntityIDs = $metadata->fetch();
$params = array(
'entity_id' => $index,
'entity_data' => json_encode($entityData),
);
if ($retrivedEntityIDs !== false && count($retrivedEntityIDs) > 0) {
$rows = $this->db->write(
"UPDATE $tableName SET entity_data = :entity_data WHERE entity_id = :entity_id",
$params
);
} else {
$rows = $this->db->write(
"INSERT INTO $tableName (entity_id, entity_data) VALUES (:entity_id, :entity_data)",
$params
);
}
return $rows === 1;
}
/**
* Replace the -'s to an _ in table names for Metadata sets
* since SQL does not allow a - in a table name.
*
* @param string $table Table
*
* @return string Replaced table name
*/
private function getTableName($table)
{
assert(is_string($table));
return $this->db->applyPrefix(str_replace("-", "_", $this->tablePrefix.$table));
}
/**
* Initialize the configured database
*
* @return int|false The number of SQL statements successfully executed, false if some error occurred.
*/
public function initDatabase()
{
$stmt = 0;
$fine = true;
foreach ($this->supportedSets as $set) {
$tableName = $this->getTableName($set);
$rows = $this->db->write(
"CREATE TABLE IF NOT EXISTS $tableName (entity_id VARCHAR(255) PRIMARY KEY NOT NULL, entity_data ".
"TEXT NOT NULL)"
);
if ($rows === 0) {
$fine = false;
} else {
$stmt += $rows;
}
}
if (!$fine) {
return false;
}
return $stmt;
}
}

View File

@@ -0,0 +1,286 @@
<?php
/**
* Class for handling metadata files in serialized format.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_MetaDataStorageHandlerSerialize extends SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* The file extension we use for our metadata files.
*
* @var string
*/
const EXTENSION = '.serialized';
/**
* The base directory where metadata is stored.
*
* @var string
*/
private $directory;
/**
* Constructor for this metadata handler.
*
* Parses configuration.
*
* @param array $config The configuration for this metadata handler.
*/
public function __construct($config)
{
assert(is_array($config));
$globalConfig = SimpleSAML_Configuration::getInstance();
$cfgHelp = SimpleSAML_Configuration::loadFromArray($config, 'serialize metadata source');
$this->directory = $cfgHelp->getString('directory');
/* Resolve this directory relative to the SimpleSAMLphp directory (unless it is
* an absolute path).
*/
$this->directory = $globalConfig->resolvePath($this->directory);
}
/**
* Helper function for retrieving the path of a metadata file.
*
* @param string $entityId The entity ID.
* @param string $set The metadata set.
*
* @return string The path to the metadata file.
*/
private function getMetadataPath($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
return $this->directory.'/'.rawurlencode($set).'/'.rawurlencode($entityId).self::EXTENSION;
}
/**
* Retrieve a list of all available metadata sets.
*
* @return array An array with the available sets.
*/
public function getMetadataSets()
{
$ret = array();
$dh = @opendir($this->directory);
if ($dh === false) {
SimpleSAML\Logger::warning(
'Serialize metadata handler: Unable to open directory: '.var_export($this->directory, true)
);
return $ret;
}
while (($entry = readdir($dh)) !== false) {
if ($entry[0] === '.') {
// skip '..', '.' and hidden files
continue;
}
$path = $this->directory.'/'.$entry;
if (!is_dir($path)) {
SimpleSAML\Logger::warning(
'Serialize metadata handler: Metadata directory contained a file where only directories should '.
'exist: '.var_export($path, true)
);
continue;
}
$ret[] = rawurldecode($entry);
}
closedir($dh);
return $ret;
}
/**
* Retrieve a list of all available metadata for a given set.
*
* @param string $set The set we are looking for metadata in.
*
* @return array An associative array with all the metadata for the given set.
*/
public function getMetadataSet($set)
{
assert(is_string($set));
$ret = array();
$dir = $this->directory.'/'.rawurlencode($set);
if (!is_dir($dir)) {
// probably some code asked for a metadata set which wasn't available
return $ret;
}
$dh = @opendir($dir);
if ($dh === false) {
SimpleSAML\Logger::warning('Serialize metadata handler: Unable to open directory: '.var_export($dir, true));
return $ret;
}
$extLen = strlen(self::EXTENSION);
while (($file = readdir($dh)) !== false) {
if (strlen($file) <= $extLen) {
continue;
}
if (substr($file, -$extLen) !== self::EXTENSION) {
continue;
}
$entityId = substr($file, 0, -$extLen);
$entityId = rawurldecode($entityId);
$md = $this->getMetaData($entityId, $set);
if ($md !== null) {
$ret[$entityId] = $md;
}
}
closedir($dh);
return $ret;
}
/**
* Retrieve a metadata entry.
*
* @param string $entityId The entityId we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
*/
public function getMetaData($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
$filePath = $this->getMetadataPath($entityId, $set);
if (!file_exists($filePath)) {
return null;
}
$data = @file_get_contents($filePath);
if ($data === false) {
$error = error_get_last();
SimpleSAML\Logger::warning(
'Error reading file '.$filePath.': '.$error['message']
);
return null;
}
$data = @unserialize($data);
if ($data === false) {
SimpleSAML\Logger::warning('Error unserializing file: '.$filePath);
return null;
}
if (!array_key_exists('entityid', $data)) {
$data['entityid'] = $entityId;
}
return $data;
}
/**
* Save a metadata entry.
*
* @param string $entityId The entityId of the metadata entry.
* @param string $set The metadata set this metadata entry belongs to.
* @param array $metadata The metadata.
*
* @return boolean True if successfully saved, false otherwise.
*/
public function saveMetadata($entityId, $set, $metadata)
{
assert(is_string($entityId));
assert(is_string($set));
assert(is_array($metadata));
$filePath = $this->getMetadataPath($entityId, $set);
$newPath = $filePath.'.new';
$dir = dirname($filePath);
if (!is_dir($dir)) {
SimpleSAML\Logger::info('Creating directory: '.$dir);
$res = @mkdir($dir, 0777, true);
if ($res === false) {
$error = error_get_last();
SimpleSAML\Logger::error('Failed to create directory '.$dir.': '.$error['message']);
return false;
}
}
$data = serialize($metadata);
SimpleSAML\Logger::debug('Writing: '.$newPath);
$res = file_put_contents($newPath, $data);
if ($res === false) {
$error = error_get_last();
SimpleSAML\Logger::error('Error saving file '.$newPath.': '.$error['message']);
return false;
}
$res = rename($newPath, $filePath);
if ($res === false) {
$error = error_get_last();
SimpleSAML\Logger::error('Error renaming '.$newPath.' to '.$filePath.': '.$error['message']);
return false;
}
return true;
}
/**
* Delete a metadata entry.
*
* @param string $entityId The entityId of the metadata entry.
* @param string $set The metadata set this metadata entry belongs to.
*/
public function deleteMetadata($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
$filePath = $this->getMetadataPath($entityId, $set);
if (!file_exists($filePath)) {
SimpleSAML\Logger::warning(
'Attempted to erase nonexistent metadata entry '.
var_export($entityId, true).' in set '.var_export($set, true).'.'
);
return;
}
$res = unlink($filePath);
if ($res === false) {
$error = error_get_last();
SimpleSAML\Logger::error(
'Failed to delete file '.$filePath.
': '.$error['message']
);
}
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* This class implements a metadata source which loads metadata from XML files.
* The XML files should be in the SAML 2.0 metadata format.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_MetaDataStorageHandlerXML extends SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* This variable contains an associative array with the parsed metadata.
*
* @var array
*/
private $metadata;
/**
* This function initializes the XML metadata source. The configuration must contain one of
* the following options:
* - 'file': Path to a file with the metadata. This path is relative to the SimpleSAMLphp
* base directory.
* - 'url': URL we should download the metadata from. This is only meant for testing.
*
* @param array $config The configuration for this instance of the XML metadata source.
*
* @throws Exception If neither the 'file' or 'url' options are defined in the configuration.
*/
protected function __construct($config)
{
$src = $srcXml = null;
if (array_key_exists('file', $config)) {
// get the configuration
$globalConfig = SimpleSAML_Configuration::getInstance();
$src = $globalConfig->resolvePath($config['file']);
} elseif (array_key_exists('url', $config)) {
$src = $config['url'];
} elseif (array_key_exists('xml', $config)) {
$srcXml = $config['xml'];
} else {
throw new Exception("Missing one of 'file', 'url' and 'xml' in XML metadata source configuration.");
}
$SP1x = array();
$IdP1x = array();
$SP20 = array();
$IdP20 = array();
$AAD = array();
if(isset($src)) {
$entities = SimpleSAML_Metadata_SAMLParser::parseDescriptorsFile($src);
} elseif(isset($srcXml)) {
$entities = SimpleSAML_Metadata_SAMLParser::parseDescriptorsString($srcXml);
} else {
throw new Exception("Neither source file path/URI nor string data provided");
}
foreach ($entities as $entityId => $entity) {
$md = $entity->getMetadata1xSP();
if ($md !== null) {
$SP1x[$entityId] = $md;
}
$md = $entity->getMetadata1xIdP();
if ($md !== null) {
$IdP1x[$entityId] = $md;
}
$md = $entity->getMetadata20SP();
if ($md !== null) {
$SP20[$entityId] = $md;
}
$md = $entity->getMetadata20IdP();
if ($md !== null) {
$IdP20[$entityId] = $md;
}
$md = $entity->getAttributeAuthorities();
if (count($md) > 0) {
$AAD[$entityId] = $md[0];
}
}
$this->metadata = array(
'shib13-sp-remote' => $SP1x,
'shib13-idp-remote' => $IdP1x,
'saml20-sp-remote' => $SP20,
'saml20-idp-remote' => $IdP20,
'attributeauthority-remote' => $AAD,
);
}
/**
* This function returns an associative array with metadata for all entities in the given set. The
* key of the array is the entity id.
*
* @param string $set The set we want to list metadata for.
*
* @return array An associative array with all entities in the given set.
*/
public function getMetadataSet($set)
{
if (array_key_exists($set, $this->metadata)) {
return $this->metadata[$set];
}
// we don't have this metadata set
return array();
}
}

View File

@@ -0,0 +1,275 @@
<?php
/**
* This abstract class defines an interface for metadata storage sources.
*
* It also contains the overview of the different metadata storage sources.
* A metadata storage source can be loaded by passing the configuration of it
* to the getSource static function.
*
* @author Olav Morken, UNINETT AS.
* @author Andreas Aakre Solberg, UNINETT AS.
* @package SimpleSAMLphp
*/
abstract class SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* Parse array with metadata sources.
*
* This function accepts an array with metadata sources, and returns an array with
* each metadata source as an object.
*
* @param array $sourcesConfig Array with metadata source configuration.
*
* @return array Parsed metadata configuration.
*
* @throws Exception If something is wrong in the configuration.
*/
public static function parseSources($sourcesConfig)
{
assert(is_array($sourcesConfig));
$sources = array();
foreach ($sourcesConfig as $sourceConfig) {
if (!is_array($sourceConfig)) {
throw new Exception("Found an element in metadata source configuration which wasn't an array.");
}
$sources[] = self::getSource($sourceConfig);
}
return $sources;
}
/**
* This function creates a metadata source based on the given configuration.
* The type of source is based on the 'type' parameter in the configuration.
* The default type is 'flatfile'.
*
* @param array $sourceConfig Associative array with the configuration for this metadata source.
*
* @return mixed An instance of a metadata source with the given configuration.
*
* @throws Exception If the metadata source type is invalid.
*/
public static function getSource($sourceConfig)
{
assert(is_array($sourceConfig));
if (array_key_exists('type', $sourceConfig)) {
$type = $sourceConfig['type'];
} else {
$type = 'flatfile';
}
switch ($type) {
case 'flatfile':
return new SimpleSAML_Metadata_MetaDataStorageHandlerFlatFile($sourceConfig);
case 'xml':
return new SimpleSAML_Metadata_MetaDataStorageHandlerXML($sourceConfig);
case 'serialize':
return new SimpleSAML_Metadata_MetaDataStorageHandlerSerialize($sourceConfig);
case 'mdx':
case 'mdq':
return new \SimpleSAML\Metadata\Sources\MDQ($sourceConfig);
case 'pdo':
return new SimpleSAML_Metadata_MetaDataStorageHandlerPdo($sourceConfig);
default:
// metadata store from module
try {
$className = SimpleSAML\Module::resolveClass(
$type,
'MetadataStore',
'SimpleSAML_Metadata_MetaDataStorageSource'
);
} catch (Exception $e) {
throw new SimpleSAML\Error\CriticalConfigurationError(
"Invalid 'type' for metadata source. Cannot find store '$type'.",
null
);
}
return new $className($sourceConfig);
}
}
/**
* This function attempts to generate an associative array with metadata for all entities in the
* given set. The key of the array is the entity id.
*
* A subclass should override this function if it is able to easily generate this list.
*
* @param string $set The set we want to list metadata for.
*
* @return array An associative array with all entities in the given set, or an empty array if we are
* unable to generate this list.
*/
public function getMetadataSet($set)
{
return array();
}
/**
* This function resolves an host/path combination to an entity id.
*
* This class implements this function using the getMetadataSet-function. A subclass should
* override this function if it doesn't implement the getMetadataSet function, or if the
* implementation of getMetadataSet is slow.
*
* @param string $hostPath The host/path combination we are looking up.
* @param string $set Which set of metadata we are looking it up in.
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
*
* @return string|null An entity id which matches the given host/path combination, or NULL if
* we are unable to locate one which matches.
*/
public function getEntityIdFromHostPath($hostPath, $set, $type = 'entityid')
{
$metadataSet = $this->getMetadataSet($set);
if ($metadataSet === null) {
// this metadata source does not have this metadata set
return null;
}
foreach ($metadataSet as $index => $entry) {
if (!array_key_exists('host', $entry)) {
continue;
}
if ($hostPath === $entry['host']) {
if ($type === 'entityid') {
return $entry['entityid'];
} else {
return $index;
}
}
}
// no entries matched, we should return null
return null;
}
/**
* This function will go through all the metadata, and check the DiscoHints->IPHint
* parameter, which defines a network space (ip range) for each remote entry.
* This function returns the entityID for any of the entities that have an
* IP range which the IP falls within.
*
* @param string $set Which set of metadata we are looking it up in.
* @param string $ip IP address
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
*
* @return string The entity id of a entity which have a CIDR hint where the provided
* IP address match.
*/
public function getPreferredEntityIdFromCIDRhint($set, $ip, $type = 'entityid')
{
$metadataSet = $this->getMetadataSet($set);
foreach ($metadataSet as $index => $entry) {
$cidrHints = array();
// support hint.cidr for idp discovery
if (array_key_exists('hint.cidr', $entry) && is_array($entry['hint.cidr'])) {
$cidrHints = $entry['hint.cidr'];
}
// support discohints in idp metadata for idp discovery
if (array_key_exists('DiscoHints', $entry)
&& array_key_exists('IPHint', $entry['DiscoHints'])
&& is_array($entry['DiscoHints']['IPHint'])) {
// merge with hints derived from discohints, but prioritize hint.cidr in case it is used
$cidrHints = array_merge($entry['DiscoHints']['IPHint'], $cidrHints);
}
if (empty($cidrHints)) {
continue;
}
foreach ($cidrHints as $hint_entry) {
if (SimpleSAML\Utils\Net::ipCIDRcheck($hint_entry, $ip)) {
if ($type === 'entityid') {
return $entry['entityid'];
} else {
return $index;
}
}
}
}
// no entries matched, we should return null
return null;
}
/*
*
*/
private function lookupIndexFromEntityId($entityId, $set)
{
assert(is_string($entityId));
assert(isset($set));
$metadataSet = $this->getMetadataSet($set);
// check for hostname
$currenthost = \SimpleSAML\Utils\HTTP::getSelfHost(); // sp.example.org
foreach ($metadataSet as $index => $entry) {
if ($index === $entityId) {
return $index;
}
if ($entry['entityid'] === $entityId) {
if ($entry['host'] === '__DEFAULT__' || $entry['host'] === $currenthost) {
return $index;
}
}
}
return null;
}
/**
* This function retrieves metadata for the given entity id in the given set of metadata.
* It will return NULL if it is unable to locate the metadata.
*
* This class implements this function using the getMetadataSet-function. A subclass should
* override this function if it doesn't implement the getMetadataSet function, or if the
* implementation of getMetadataSet is slow.
*
* @param string $index The entityId or metaindex we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
*/
public function getMetaData($index, $set)
{
assert(is_string($index));
assert(isset($set));
$metadataSet = $this->getMetadataSet($set);
if (array_key_exists($index, $metadataSet)) {
return $metadataSet[$index];
}
$indexlookup = $this->lookupIndexFromEntityId($index, $set);
if (isset($indexlookup) && array_key_exists($indexlookup, $metadataSet)) {
return $metadataSet[$indexlookup];
}
return null;
}
}

View File

@@ -0,0 +1,777 @@
<?php
/**
* Class for generating SAML 2.0 metadata from SimpleSAMLphp metadata arrays.
*
* This class builds SAML 2.0 metadata for an entity by examining the metadata for the entity.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_SAMLBuilder
{
/**
* The EntityDescriptor we are building.
*
* @var \SAML2\XML\md\EntityDescriptor
*/
private $entityDescriptor;
/**
* The maximum time in seconds the metadata should be cached.
*
* @var int|null
*/
private $maxCache = null;
/**
* The maximum time in seconds since the current time that this metadata should be considered valid.
*
* @var int|null
*/
private $maxDuration = null;
/**
* Initialize the SAML builder.
*
* @param string $entityId The entity id of the entity.
* @param double|null $maxCache The maximum time in seconds the metadata should be cached. Defaults to null
* @param double|null $maxDuration The maximum time in seconds this metadata should be considered valid. Defaults
* to null.
*/
public function __construct($entityId, $maxCache = null, $maxDuration = null)
{
assert(is_string($entityId));
$this->maxCache = $maxCache;
$this->maxDuration = $maxDuration;
$this->entityDescriptor = new \SAML2\XML\md\EntityDescriptor();
$this->entityDescriptor->entityID = $entityId;
}
private function setExpiration($metadata)
{
if (array_key_exists('expire', $metadata)) {
if ($metadata['expire'] - time() < $this->maxDuration) {
$this->maxDuration = $metadata['expire'] - time();
}
}
if ($this->maxCache !== null) {
$this->entityDescriptor->cacheDuration = 'PT'.$this->maxCache.'S';
}
if ($this->maxDuration !== null) {
$this->entityDescriptor->validUntil = time() + $this->maxDuration;
}
}
/**
* Retrieve the EntityDescriptor element which is generated for this entity.
*
* @return DOMElement The EntityDescriptor element of this entity.
*/
public function getEntityDescriptor()
{
$xml = $this->entityDescriptor->toXML();
$xml->ownerDocument->appendChild($xml);
return $xml;
}
/**
* Retrieve the EntityDescriptor as text.
*
* This function serializes this EntityDescriptor, and returns it as text.
*
* @param bool $formatted Whether the returned EntityDescriptor should be formatted first.
*
* @return string The serialized EntityDescriptor.
*/
public function getEntityDescriptorText($formatted = true)
{
assert(is_bool($formatted));
$xml = $this->getEntityDescriptor();
if ($formatted) {
SimpleSAML\Utils\XML::formatDOMElement($xml);
}
return $xml->ownerDocument->saveXML();
}
/**
* Add a SecurityTokenServiceType for ADFS metadata.
*
* @param array $metadata The metadata with the information about the SecurityTokenServiceType.
*/
public function addSecurityTokenServiceType($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$defaultEndpoint = $metadata->getDefaultEndpoint('SingleSignOnService');
$e = new sspmod_adfs_SAML2_XML_fed_SecurityTokenServiceType();
$e->Location = $defaultEndpoint['Location'];
$this->addCertificate($e, $metadata);
$this->entityDescriptor->RoleDescriptor[] = $e;
}
/**
* Add extensions to the metadata.
*
* @param SimpleSAML_Configuration $metadata The metadata to get extensions from.
* @param \SAML2\XML\md\RoleDescriptor $e Reference to the element where the Extensions element should be included.
*/
private function addExtensions(SimpleSAML_Configuration $metadata, \SAML2\XML\md\RoleDescriptor $e)
{
if ($metadata->hasValue('tags')) {
$a = new \SAML2\XML\saml\Attribute();
$a->Name = 'tags';
foreach ($metadata->getArray('tags') as $tag) {
$a->AttributeValue[] = new \SAML2\XML\saml\AttributeValue($tag);
}
$e->Extensions[] = $a;
}
if ($metadata->hasValue('hint.cidr')) {
$a = new \SAML2\XML\saml\Attribute();
$a->Name = 'hint.cidr';
foreach ($metadata->getArray('hint.cidr') as $hint) {
$a->AttributeValue[] = new \SAML2\XML\saml\AttributeValue($hint);
}
$e->Extensions[] = $a;
}
if ($metadata->hasValue('scope')) {
foreach ($metadata->getArray('scope') as $scopetext) {
$s = new \SAML2\XML\shibmd\Scope();
$s->scope = $scopetext;
// Check whether $ ^ ( ) * | \ are in a scope -> assume regex.
if (1 === preg_match('/[\$\^\)\(\*\|\\\\]/', $scopetext)) {
$s->regexp = true;
} else {
$s->regexp = false;
}
$e->Extensions[] = $s;
}
}
if ($metadata->hasValue('EntityAttributes')) {
$ea = new \SAML2\XML\mdattr\EntityAttributes();
foreach ($metadata->getArray('EntityAttributes') as $attributeName => $attributeValues) {
$a = new \SAML2\XML\saml\Attribute();
$a->Name = $attributeName;
$a->NameFormat = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri';
// Attribute names that is not URI is prefixed as this: '{nameformat}name'
if (preg_match('/^\{(.*?)\}(.*)$/', $attributeName, $matches)) {
$a->Name = $matches[2];
$nameFormat = $matches[1];
if ($nameFormat !== \SAML2\Constants::NAMEFORMAT_UNSPECIFIED) {
$a->NameFormat = $nameFormat;
}
}
foreach ($attributeValues as $attributeValue) {
$a->AttributeValue[] = new \SAML2\XML\saml\AttributeValue($attributeValue);
}
$ea->children[] = $a;
}
$this->entityDescriptor->Extensions[] = $ea;
}
if ($metadata->hasValue('RegistrationInfo')) {
$ri = new \SAML2\XML\mdrpi\RegistrationInfo();
foreach ($metadata->getArray('RegistrationInfo') as $riName => $riValues) {
switch ($riName) {
case 'authority':
$ri->registrationAuthority = $riValues;
break;
case 'instant':
$ri->registrationInstant = \SAML2\Utils::xsDateTimeToTimestamp($riValues);
break;
case 'policies':
$ri->RegistrationPolicy = $riValues;
break;
}
}
$this->entityDescriptor->Extensions[] = $ri;
}
if ($metadata->hasValue('UIInfo')) {
$ui = new \SAML2\XML\mdui\UIInfo();
foreach ($metadata->getArray('UIInfo') as $uiName => $uiValues) {
switch ($uiName) {
case 'DisplayName':
$ui->DisplayName = $uiValues;
break;
case 'Description':
$ui->Description = $uiValues;
break;
case 'InformationURL':
$ui->InformationURL = $uiValues;
break;
case 'PrivacyStatementURL':
$ui->PrivacyStatementURL = $uiValues;
break;
case 'Keywords':
foreach ($uiValues as $lang => $keywords) {
$uiItem = new \SAML2\XML\mdui\Keywords();
$uiItem->lang = $lang;
$uiItem->Keywords = $keywords;
$ui->Keywords[] = $uiItem;
}
break;
case 'Logo':
foreach ($uiValues as $logo) {
$uiItem = new \SAML2\XML\mdui\Logo();
$uiItem->url = $logo['url'];
$uiItem->width = $logo['width'];
$uiItem->height = $logo['height'];
if (isset($logo['lang'])) {
$uiItem->lang = $logo['lang'];
}
$ui->Logo[] = $uiItem;
}
break;
}
}
$e->Extensions[] = $ui;
}
if ($metadata->hasValue('DiscoHints')) {
$dh = new \SAML2\XML\mdui\DiscoHints();
foreach ($metadata->getArray('DiscoHints') as $dhName => $dhValues) {
switch ($dhName) {
case 'IPHint':
$dh->IPHint = $dhValues;
break;
case 'DomainHint':
$dh->DomainHint = $dhValues;
break;
case 'GeolocationHint':
$dh->GeolocationHint = $dhValues;
break;
}
}
$e->Extensions[] = $dh;
}
}
/**
* Add an Organization element based on data passed as parameters
*
* @param array $orgName An array with the localized OrganizationName.
* @param array $orgDisplayName An array with the localized OrganizationDisplayName.
* @param array $orgURL An array with the localized OrganizationURL.
*/
public function addOrganization(array $orgName, array $orgDisplayName, array $orgURL)
{
$org = new \SAML2\XML\md\Organization();
$org->OrganizationName = $orgName;
$org->OrganizationDisplayName = $orgDisplayName;
$org->OrganizationURL = $orgURL;
$this->entityDescriptor->Organization = $org;
}
/**
* Add an Organization element based on metadata array.
*
* @param array $metadata The metadata we should extract the organization information from.
*/
public function addOrganizationInfo(array $metadata)
{
if (empty($metadata['OrganizationName']) ||
empty($metadata['OrganizationDisplayName']) ||
empty($metadata['OrganizationURL'])
) {
// empty or incomplete organization information
return;
}
$orgName = SimpleSAML\Utils\Arrays::arrayize($metadata['OrganizationName'], 'en');
$orgDisplayName = SimpleSAML\Utils\Arrays::arrayize($metadata['OrganizationDisplayName'], 'en');
$orgURL = SimpleSAML\Utils\Arrays::arrayize($metadata['OrganizationURL'], 'en');
$this->addOrganization($orgName, $orgDisplayName, $orgURL);
}
/**
* Add a list of endpoints to metadata.
*
* @param array $endpoints The endpoints.
* @param bool $indexed Whether the endpoints should be indexed.
*
* @return array An array of endpoint objects, either \SAML2\XML\md\EndpointType or \SAML2\XML\md\IndexedEndpointType.
*/
private static function createEndpoints(array $endpoints, $indexed)
{
assert(is_bool($indexed));
$ret = array();
foreach ($endpoints as &$ep) {
if ($indexed) {
$t = new \SAML2\XML\md\IndexedEndpointType();
} else {
$t = new \SAML2\XML\md\EndpointType();
}
$t->Binding = $ep['Binding'];
$t->Location = $ep['Location'];
if (isset($ep['ResponseLocation'])) {
$t->ResponseLocation = $ep['ResponseLocation'];
}
if (isset($ep['hoksso:ProtocolBinding'])) {
$t->setAttributeNS(
\SAML2\Constants::NS_HOK,
'hoksso:ProtocolBinding',
\SAML2\Constants::BINDING_HTTP_REDIRECT
);
}
if ($indexed) {
if (!isset($ep['index'])) {
// Find the maximum index
$maxIndex = -1;
foreach ($endpoints as $ep) {
if (!isset($ep['index'])) {
continue;
}
if ($ep['index'] > $maxIndex) {
$maxIndex = $ep['index'];
}
}
$ep['index'] = $maxIndex + 1;
}
$t->index = $ep['index'];
}
$ret[] = $t;
}
return $ret;
}
/**
* Add an AttributeConsumingService element to the metadata.
*
* @param \SAML2\XML\md\SPSSODescriptor $spDesc The SPSSODescriptor element.
* @param SimpleSAML_Configuration $metadata The metadata.
*/
private function addAttributeConsumingService(
\SAML2\XML\md\SPSSODescriptor $spDesc,
SimpleSAML_Configuration $metadata
) {
$attributes = $metadata->getArray('attributes', array());
$name = $metadata->getLocalizedString('name', null);
if ($name === null || count($attributes) == 0) {
// we cannot add an AttributeConsumingService without name and attributes
return;
}
$attributesrequired = $metadata->getArray('attributes.required', array());
/*
* Add an AttributeConsumingService element with information as name and description and list
* of requested attributes
*/
$attributeconsumer = new \SAML2\XML\md\AttributeConsumingService();
$attributeconsumer->index = $metadata->getInteger('attributes.index', 0);
if ($metadata->hasValue('attributes.isDefault')) {
$attributeconsumer->isDefault = $metadata->getBoolean('attributes.isDefault', false);
}
$attributeconsumer->ServiceName = $name;
$attributeconsumer->ServiceDescription = $metadata->getLocalizedString('description', array());
$nameFormat = $metadata->getString('attributes.NameFormat', \SAML2\Constants::NAMEFORMAT_UNSPECIFIED);
foreach ($attributes as $friendlyName => $attribute) {
$t = new \SAML2\XML\md\RequestedAttribute();
$t->Name = $attribute;
if (!is_int($friendlyName)) {
$t->FriendlyName = $friendlyName;
}
if ($nameFormat !== \SAML2\Constants::NAMEFORMAT_UNSPECIFIED) {
$t->NameFormat = $nameFormat;
}
if (in_array($attribute, $attributesrequired, true)) {
$t->isRequired = true;
}
$attributeconsumer->RequestedAttribute[] = $t;
}
$spDesc->AttributeConsumingService[] = $attributeconsumer;
}
/**
* Add a specific type of metadata to an entity.
*
* @param string $set The metadata set this metadata comes from.
* @param array $metadata The metadata.
*/
public function addMetadata($set, $metadata)
{
assert(is_string($set));
assert(is_array($metadata));
$this->setExpiration($metadata);
switch ($set) {
case 'saml20-sp-remote':
$this->addMetadataSP20($metadata);
break;
case 'saml20-idp-remote':
$this->addMetadataIdP20($metadata);
break;
case 'shib13-sp-remote':
$this->addMetadataSP11($metadata);
break;
case 'shib13-idp-remote':
$this->addMetadataIdP11($metadata);
break;
case 'attributeauthority-remote':
$this->addAttributeAuthority($metadata);
break;
default:
SimpleSAML\Logger::warning('Unable to generate metadata for unknown type \''.$set.'\'.');
}
}
/**
* Add SAML 2.0 SP metadata.
*
* @param array $metadata The metadata.
* @param array $protocols The protocols supported. Defaults to \SAML2\Constants::NS_SAMLP.
*/
public function addMetadataSP20($metadata, $protocols = array(\SAML2\Constants::NS_SAMLP))
{
assert(is_array($metadata));
assert(is_array($protocols));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new \SAML2\XML\md\SPSSODescriptor();
$e->protocolSupportEnumeration = $protocols;
if ($metadata->hasValue('saml20.sign.assertion')) {
$e->WantAssertionsSigned = $metadata->getBoolean('saml20.sign.assertion');
}
if ($metadata->hasValue('redirect.validate')) {
$e->AuthnRequestsSigned = $metadata->getBoolean('redirect.validate');
} elseif ($metadata->hasValue('validate.authnrequest')) {
$e->AuthnRequestsSigned = $metadata->getBoolean('validate.authnrequest');
}
$this->addExtensions($metadata, $e);
$this->addCertificate($e, $metadata);
$e->SingleLogoutService = self::createEndpoints($metadata->getEndpoints('SingleLogoutService'), false);
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
$endpoints = $metadata->getEndpoints('AssertionConsumerService');
foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', array()) as $acs) {
$endpoints[] = array(
'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact',
'Location' => $acs,
);
}
$e->AssertionConsumerService = self::createEndpoints($endpoints, true);
$this->addAttributeConsumingService($e, $metadata);
$this->entityDescriptor->RoleDescriptor[] = $e;
foreach ($metadata->getArray('contacts', array()) as $contact) {
if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
$this->addContact($contact['contactType'], \SimpleSAML\Utils\Config\Metadata::getContact($contact));
}
}
}
/**
* Add metadata of a SAML 2.0 identity provider.
*
* @param array $metadata The metadata.
*/
public function addMetadataIdP20($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new \SAML2\XML\md\IDPSSODescriptor();
$e->protocolSupportEnumeration[] = 'urn:oasis:names:tc:SAML:2.0:protocol';
if ($metadata->hasValue('sign.authnrequest')) {
$e->WantAuthnRequestsSigned = $metadata->getBoolean('sign.authnrequest');
} elseif ($metadata->hasValue('redirect.sign')) {
$e->WantAuthnRequestsSigned = $metadata->getBoolean('redirect.sign');
}
$this->addExtensions($metadata, $e);
$this->addCertificate($e, $metadata);
if ($metadata->hasValue('ArtifactResolutionService')) {
$e->ArtifactResolutionService = self::createEndpoints(
$metadata->getEndpoints('ArtifactResolutionService'),
true
);
}
$e->SingleLogoutService = self::createEndpoints($metadata->getEndpoints('SingleLogoutService'), false);
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
$e->SingleSignOnService = self::createEndpoints($metadata->getEndpoints('SingleSignOnService'), false);
$this->entityDescriptor->RoleDescriptor[] = $e;
foreach ($metadata->getArray('contacts', array()) as $contact) {
if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
$this->addContact($contact['contactType'], \SimpleSAML\Utils\Config\Metadata::getContact($contact));
}
}
}
/**
* Add metadata of a SAML 1.1 service provider.
*
* @param array $metadata The metadata.
*/
public function addMetadataSP11($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new \SAML2\XML\md\SPSSODescriptor();
$e->protocolSupportEnumeration[] = 'urn:oasis:names:tc:SAML:1.1:protocol';
$this->addCertificate($e, $metadata);
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
$endpoints = $metadata->getEndpoints('AssertionConsumerService');
foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', array()) as $acs) {
$endpoints[] = array(
'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01',
'Location' => $acs,
);
}
$e->AssertionConsumerService = self::createEndpoints($endpoints, true);
$this->addAttributeConsumingService($e, $metadata);
$this->entityDescriptor->RoleDescriptor[] = $e;
}
/**
* Add metadata of a SAML 1.1 identity provider.
*
* @param array $metadata The metadata.
*/
public function addMetadataIdP11($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new \SAML2\XML\md\IDPSSODescriptor();
$e->protocolSupportEnumeration[] = 'urn:oasis:names:tc:SAML:1.1:protocol';
$e->protocolSupportEnumeration[] = 'urn:mace:shibboleth:1.0';
$this->addCertificate($e, $metadata);
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
$e->SingleSignOnService = self::createEndpoints($metadata->getEndpoints('SingleSignOnService'), false);
$this->entityDescriptor->RoleDescriptor[] = $e;
}
/**
* Add metadata of a SAML attribute authority.
*
* @param array $metadata The AttributeAuthorityDescriptor, in the format returned by
* SimpleSAML_Metadata_SAMLParser.
*/
public function addAttributeAuthority(array $metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new \SAML2\XML\md\AttributeAuthorityDescriptor();
$e->protocolSupportEnumeration = $metadata->getArray('protocols', array(\SAML2\Constants::NS_SAMLP));
$this->addExtensions($metadata, $e);
$this->addCertificate($e, $metadata);
$e->AttributeService = self::createEndpoints($metadata->getEndpoints('AttributeService'), false);
$e->AssertionIDRequestService = self::createEndpoints(
$metadata->getEndpoints('AssertionIDRequestService'),
false
);
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
$this->entityDescriptor->RoleDescriptor[] = $e;
}
/**
* Add contact information.
*
* Accepts a contact type, and a contact array that must be previously sanitized.
*
* WARNING: This function will change its signature and no longer parse a 'name' element.
*
* @param string $type The type of contact. Deprecated.
* @param array $details The details about the contact.
*
* @todo Change the signature to remove $type.
* @todo Remove the capability to pass a name and parse it inside the method.
*/
public function addContact($type, $details)
{
assert(is_string($type));
assert(is_array($details));
assert(in_array($type, array('technical', 'support', 'administrative', 'billing', 'other'), true));
// TODO: remove this check as soon as getContact() is called always before calling this function
$details = \SimpleSAML\Utils\Config\Metadata::getContact($details);
$e = new \SAML2\XML\md\ContactPerson();
$e->contactType = $type;
if (!empty($details['attributes'])) {
$e->ContactPersonAttributes = $details['attributes'];
}
if (isset($details['company'])) {
$e->Company = $details['company'];
}
if (isset($details['givenName'])) {
$e->GivenName = $details['givenName'];
}
if (isset($details['surName'])) {
$e->SurName = $details['surName'];
}
if (isset($details['emailAddress'])) {
$eas = $details['emailAddress'];
if (!is_array($eas)) {
$eas = array($eas);
}
foreach ($eas as $ea) {
$e->EmailAddress[] = $ea;
}
}
if (isset($details['telephoneNumber'])) {
$tlfNrs = $details['telephoneNumber'];
if (!is_array($tlfNrs)) {
$tlfNrs = array($tlfNrs);
}
foreach ($tlfNrs as $tlfNr) {
$e->TelephoneNumber[] = $tlfNr;
}
}
$this->entityDescriptor->ContactPerson[] = $e;
}
/**
* Add a KeyDescriptor with an X509 certificate.
*
* @param \SAML2\XML\md\RoleDescriptor $rd The RoleDescriptor the certificate should be added to.
* @param string $use The value of the 'use' attribute.
* @param string $x509data The certificate data.
*/
private function addX509KeyDescriptor(\SAML2\XML\md\RoleDescriptor $rd, $use, $x509data)
{
assert(in_array($use, array('encryption', 'signing'), true));
assert(is_string($x509data));
$keyDescriptor = \SAML2\Utils::createKeyDescriptor($x509data);
$keyDescriptor->use = $use;
$rd->KeyDescriptor[] = $keyDescriptor;
}
/**
* Add a certificate.
*
* Helper function for adding a certificate to the metadata.
*
* @param \SAML2\XML\md\RoleDescriptor $rd The RoleDescriptor the certificate should be added to.
* @param SimpleSAML_Configuration $metadata The metadata of the entity.
*/
private function addCertificate(\SAML2\XML\md\RoleDescriptor $rd, SimpleSAML_Configuration $metadata)
{
$keys = $metadata->getPublicKeys();
foreach ($keys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
if (!isset($key['signing']) || $key['signing'] === true) {
$this->addX509KeyDescriptor($rd, 'signing', $key['X509Certificate']);
}
if (!isset($key['encryption']) || $key['encryption'] === true) {
$this->addX509KeyDescriptor($rd, 'encryption', $key['X509Certificate']);
}
}
if ($metadata->hasValue('https.certData')) {
$this->addX509KeyDescriptor($rd, 'signing', $metadata->getString('https.certData'));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,283 @@
<?php
/**
* This class implements a helper function for signing of metadata.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_Signer
{
/**
* This functions finds what key & certificate files should be used to sign the metadata
* for the given entity.
*
* @param SimpleSAML_Configuration $config Our SimpleSAML_Configuration instance.
* @param array $entityMetadata The metadata of the entity.
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or
* 'Shib 1.3 SP'.
*
* @return array An associative array with the keys 'privatekey', 'certificate', and optionally 'privatekey_pass'.
* @throws Exception If the key and certificate used to sign is unknown.
*/
private static function findKeyCert($config, $entityMetadata, $type)
{
// first we look for metadata.privatekey and metadata.certificate in the metadata
if (array_key_exists('metadata.sign.privatekey', $entityMetadata)
|| array_key_exists('metadata.sign.certificate', $entityMetadata)
) {
if (!array_key_exists('metadata.sign.privatekey', $entityMetadata)
|| !array_key_exists('metadata.sign.certificate', $entityMetadata)
) {
throw new Exception(
'Missing either the "metadata.sign.privatekey" or the'.
' "metadata.sign.certificate" configuration option in the metadata for'.
' the '.$type.' "'.$entityMetadata['entityid'].'". If one of'.
' these options is specified, then the other must also be specified.'
);
}
$ret = array(
'privatekey' => $entityMetadata['metadata.sign.privatekey'],
'certificate' => $entityMetadata['metadata.sign.certificate']
);
if (array_key_exists('metadata.sign.privatekey_pass', $entityMetadata)) {
$ret['privatekey_pass'] = $entityMetadata['metadata.sign.privatekey_pass'];
}
return $ret;
}
// then we look for default values in the global configuration
$privatekey = $config->getString('metadata.sign.privatekey', null);
$certificate = $config->getString('metadata.sign.certificate', null);
if ($privatekey !== null || $certificate !== null) {
if ($privatekey === null || $certificate === null) {
throw new Exception(
'Missing either the "metadata.sign.privatekey" or the'.
' "metadata.sign.certificate" configuration option in the global'.
' configuration. If one of these options is specified, then the other'.
' must also be specified.'
);
}
$ret = array('privatekey' => $privatekey, 'certificate' => $certificate);
$privatekey_pass = $config->getString('metadata.sign.privatekey_pass', null);
if ($privatekey_pass !== null) {
$ret['privatekey_pass'] = $privatekey_pass;
}
return $ret;
}
// as a last resort we attempt to use the privatekey and certificate option from the metadata
if (array_key_exists('privatekey', $entityMetadata)
|| array_key_exists('certificate', $entityMetadata)
) {
if (!array_key_exists('privatekey', $entityMetadata)
|| !array_key_exists('certificate', $entityMetadata)
) {
throw new Exception(
'Both the "privatekey" and the "certificate" option must'.
' be set in the metadata for the '.$type.' "'.
$entityMetadata['entityid'].'" before it is possible to sign metadata'.
' from this entity.'
);
}
$ret = array(
'privatekey' => $entityMetadata['privatekey'],
'certificate' => $entityMetadata['certificate']
);
if (array_key_exists('privatekey_pass', $entityMetadata)) {
$ret['privatekey_pass'] = $entityMetadata['privatekey_pass'];
}
return $ret;
}
throw new Exception(
'Could not find what key & certificate should be used to sign the metadata'.
' for the '.$type.' "'.$entityMetadata['entityid'].'".'
);
}
/**
* Determine whether metadata signing is enabled for the given metadata.
*
* @param SimpleSAML_Configuration $config Our SimpleSAML_Configuration instance.
* @param array $entityMetadata The metadata of the entity.
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or
* 'Shib 1.3 SP'.
*
* @return boolean True if metadata signing is enabled, false otherwise.
* @throws Exception If the value of the 'metadata.sign.enable' option is not a boolean.
*/
private static function isMetadataSigningEnabled($config, $entityMetadata, $type)
{
// first check the metadata for the entity
if (array_key_exists('metadata.sign.enable', $entityMetadata)) {
if (!is_bool($entityMetadata['metadata.sign.enable'])) {
throw new Exception(
'Invalid value for the "metadata.sign.enable" configuration option for'.
' the '.$type.' "'.$entityMetadata['entityid'].'". This option'.
' should be a boolean.'
);
}
return $entityMetadata['metadata.sign.enable'];
}
$enabled = $config->getBoolean('metadata.sign.enable', false);
return $enabled;
}
/**
* Determine the signature and digest algorithms to use when signing metadata.
*
* This method will look for the 'metadata.sign.algorithm' key in the $entityMetadata array, or look for such
* a configuration option in the $config object.
*
* @param SimpleSAML_Configuration $config The global configuration.
* @param array $entityMetadata An array containing the metadata related to this entity.
* @param string $type A string describing the type of entity. E.g. 'SAML 2 IdP' or 'Shib 1.3 SP'.
*
* @return array An array with two keys, 'algorithm' and 'digest', corresponding to the signature and digest
* algorithms to use, respectively.
*
* @throws \SimpleSAML\Error\CriticalConfigurationError
*/
private static function getMetadataSigningAlgorithm($config, $entityMetadata, $type)
{
// configure the algorithm to use
if (array_key_exists('metadata.sign.algorithm', $entityMetadata)) {
if (!is_string($entityMetadata['metadata.sign.algorithm'])) {
throw new \SimpleSAML\Error\CriticalConfigurationError(
"Invalid value for the 'metadata.sign.algorithm' configuration option for the ".$type.
"'".$entityMetadata['entityid']."'. This option has restricted values"
);
}
$alg = $entityMetadata['metadata.sign.algorithm'];
} else {
$alg = $config->getString('metadata.sign.algorithm', XMLSecurityKey::RSA_SHA256);
}
$supported_algs = array(
XMLSecurityKey::RSA_SHA1,
XMLSecurityKey::RSA_SHA256,
XMLSecurityKey::RSA_SHA384,
XMLSecurityKey::RSA_SHA512,
);
if (!in_array($alg, $supported_algs, true)) {
throw new \SimpleSAML\Error\CriticalConfigurationError("Unknown signature algorithm '$alg'");
}
switch ($alg) {
case XMLSecurityKey::RSA_SHA256:
$digest = XMLSecurityDSig::SHA256;
break;
case XMLSecurityKey::RSA_SHA384:
$digest = XMLSecurityDSig::SHA384;
break;
case XMLSecurityKey::RSA_SHA512:
$digest = XMLSecurityDSig::SHA512;
break;
default:
$digest = XMLSecurityDSig::SHA1;
}
return array(
'algorithm' => $alg,
'digest' => $digest,
);
}
/**
* Signs the given metadata if metadata signing is enabled.
*
* @param string $metadataString A string with the metadata.
* @param array $entityMetadata The metadata of the entity.
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or 'Shib 1.3 SP'.
*
* @return string The $metadataString with the signature embedded.
* @throws Exception If the certificate or private key cannot be loaded, or the metadata doesn't parse properly.
*/
public static function sign($metadataString, $entityMetadata, $type)
{
$config = SimpleSAML_Configuration::getInstance();
// check if metadata signing is enabled
if (!self::isMetadataSigningEnabled($config, $entityMetadata, $type)) {
return $metadataString;
}
// find the key & certificate which should be used to sign the metadata
$keyCertFiles = self::findKeyCert($config, $entityMetadata, $type);
$keyFile = \SimpleSAML\Utils\Config::getCertPath($keyCertFiles['privatekey']);
if (!file_exists($keyFile)) {
throw new Exception('Could not find private key file ['.$keyFile.'], which is needed to sign the metadata');
}
$keyData = file_get_contents($keyFile);
$certFile = \SimpleSAML\Utils\Config::getCertPath($keyCertFiles['certificate']);
if (!file_exists($certFile)) {
throw new Exception(
'Could not find certificate file ['.$certFile.'], which is needed to sign the metadata'
);
}
$certData = file_get_contents($certFile);
// convert the metadata to a DOM tree
try {
$xml = \SAML2\DOMDocumentFactory::fromString($metadataString);
} catch (Exception $e) {
throw new Exception('Error parsing self-generated metadata.');
}
$signature_cf = self::getMetadataSigningAlgorithm($config, $entityMetadata, $type);
// load the private key
$objKey = new XMLSecurityKey($signature_cf['algorithm'], array('type' => 'private'));
if (array_key_exists('privatekey_pass', $keyCertFiles)) {
$objKey->passphrase = $keyCertFiles['privatekey_pass'];
}
$objKey->loadKey($keyData, false);
// get the EntityDescriptor node we should sign
$rootNode = $xml->firstChild;
// sign the metadata with our private key
$objXMLSecDSig = new XMLSecurityDSig();
$objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$objXMLSecDSig->addReferenceList(
array($rootNode),
$signature_cf['digest'],
array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N),
array('id_name' => 'ID')
);
$objXMLSecDSig->sign($objKey);
// add the certificate to the signature
$objXMLSecDSig->add509Cert($certData, true);
// add the signature to the metadata
$objXMLSecDSig->insertSignature($rootNode, $rootNode->firstChild);
// return the DOM tree as a string
return $xml->saveXML();
}
}

View File

@@ -0,0 +1,337 @@
<?php
namespace SimpleSAML\Metadata\Sources;
use SimpleSAML\Logger;
use SimpleSAML\Utils\HTTP;
/**
* This class implements SAML Metadata Query Protocol
*
* @author Andreas Åkre Solberg, UNINETT AS.
* @author Olav Morken, UNINETT AS.
* @author Tamas Frank, NIIFI
* @package SimpleSAMLphp
*/
class MDQ extends \SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* The URL of MDQ server (url:port)
*
* @var string
*/
private $server;
/**
* The fingerprint of the certificate used to sign the metadata. You don't need this option if you don't want to
* validate the signature on the metadata.
*
* @var string|null
*/
private $validateFingerprint;
/**
* The cache directory, or null if no cache directory is configured.
*
* @var string|null
*/
private $cacheDir;
/**
* The maximum cache length, in seconds.
*
* @var integer
*/
private $cacheLength;
/**
* This function initializes the dynamic XML metadata source.
*
* Options:
* - 'server': URL of the MDQ server (url:port). Mandatory.
* - 'validateFingerprint': The fingerprint of the certificate used to sign the metadata.
* You don't need this option if you don't want to validate the signature on the metadata.
* Optional.
* - 'cachedir': Directory where metadata can be cached. Optional.
* - 'cachelength': Maximum time metadata cah be cached, in seconds. Default to 24
* hours (86400 seconds).
*
* @param array $config The configuration for this instance of the XML metadata source.
*
* @throws \Exception If no server option can be found in the configuration.
*/
protected function __construct($config)
{
assert(is_array($config));
if (!array_key_exists('server', $config)) {
throw new \Exception(__CLASS__.": the 'server' configuration option is not set.");
} else {
$this->server = $config['server'];
}
if (array_key_exists('validateFingerprint', $config)) {
$this->validateFingerprint = $config['validateFingerprint'];
} else {
$this->validateFingerprint = null;
}
if (array_key_exists('cachedir', $config)) {
$globalConfig = \SimpleSAML_Configuration::getInstance();
$this->cacheDir = $globalConfig->resolvePath($config['cachedir']);
} else {
$this->cacheDir = null;
}
if (array_key_exists('cachelength', $config)) {
$this->cacheLength = $config['cachelength'];
} else {
$this->cacheLength = 86400;
}
}
/**
* This function is not implemented.
*
* @param string $set The set we want to list metadata for.
*
* @return array An empty array.
*/
public function getMetadataSet($set)
{
// we don't have this metadata set
return array();
}
/**
* Find the cache file name for an entity,
*
* @param string $set The metadata set this entity belongs to.
* @param string $entityId The entity id of this entity.
*
* @return string The full path to the cache file.
*/
private function getCacheFilename($set, $entityId)
{
assert(is_string($set));
assert(is_string($entityId));
$cachekey = sha1($entityId);
return $this->cacheDir.'/'.$set.'-'.$cachekey.'.cached.xml';
}
/**
* Load a entity from the cache.
*
* @param string $set The metadata set this entity belongs to.
* @param string $entityId The entity id of this entity.
*
* @return array|NULL The associative array with the metadata for this entity, or NULL
* if the entity could not be found.
* @throws \Exception If an error occurs while loading metadata from cache.
*/
private function getFromCache($set, $entityId)
{
assert(is_string($set));
assert(is_string($entityId));
if (empty($this->cacheDir)) {
return null;
}
$cachefilename = $this->getCacheFilename($set, $entityId);
if (!file_exists($cachefilename)) {
return null;
}
if (!is_readable($cachefilename)) {
throw new \Exception(__CLASS__.': could not read cache file for entity ['.$cachefilename.']');
}
Logger::debug(__CLASS__.': reading cache ['.$entityId.'] => ['.$cachefilename.']');
/* Ensure that this metadata isn't older that the cachelength option allows. This
* must be verified based on the file, since this option may be changed after the
* file is written.
*/
$stat = stat($cachefilename);
if ($stat['mtime'] + $this->cacheLength <= time()) {
Logger::debug(__CLASS__.': cache file older that the cachelength option allows.');
return null;
}
$rawData = file_get_contents($cachefilename);
if (empty($rawData)) {
$error = error_get_last();
throw new \Exception(
__CLASS__.': error reading metadata from cache file "'.$cachefilename.'": '.$error['message']
);
}
$data = unserialize($rawData);
if ($data === false) {
throw new \Exception(__CLASS__.': error unserializing cached data from file "'.$cachefilename.'".');
}
if (!is_array($data)) {
throw new \Exception(__CLASS__.': Cached metadata from "'.$cachefilename.'" wasn\'t an array.');
}
return $data;
}
/**
* Save a entity to the cache.
*
* @param string $set The metadata set this entity belongs to.
* @param string $entityId The entity id of this entity.
* @param array $data The associative array with the metadata for this entity.
*
* @throws \Exception If metadata cannot be written to cache.
*/
private function writeToCache($set, $entityId, $data)
{
assert(is_string($set));
assert(is_string($entityId));
assert(is_array($data));
if (empty($this->cacheDir)) {
return;
}
$cachefilename = $this->getCacheFilename($set, $entityId);
if (!is_writable(dirname($cachefilename))) {
throw new \Exception(__CLASS__.': could not write cache file for entity ['.$cachefilename.']');
}
Logger::debug(__CLASS__.': Writing cache ['.$entityId.'] => ['.$cachefilename.']');
file_put_contents($cachefilename, serialize($data));
}
/**
* Retrieve metadata for the correct set from a SAML2Parser.
*
* @param \SimpleSAML_Metadata_SAMLParser $entity A SAML2Parser representing an entity.
* @param string $set The metadata set we are looking for.
*
* @return array|NULL The associative array with the metadata, or NULL if no metadata for
* the given set was found.
*/
private static function getParsedSet(\SimpleSAML_Metadata_SAMLParser $entity, $set)
{
assert(is_string($set));
switch ($set) {
case 'saml20-idp-remote':
return $entity->getMetadata20IdP();
case 'saml20-sp-remote':
return $entity->getMetadata20SP();
case 'shib13-idp-remote':
return $entity->getMetadata1xIdP();
case 'shib13-sp-remote':
return $entity->getMetadata1xSP();
case 'attributeauthority-remote':
$ret = $entity->getAttributeAuthorities();
return $ret[0];
default:
Logger::warning(__CLASS__.': unknown metadata set: \''.$set.'\'.');
}
return null;
}
/**
* Overriding this function from the superclass SimpleSAML_Metadata_MetaDataStorageSource.
*
* This function retrieves metadata for the given entity id in the given set of metadata.
* It will return NULL if it is unable to locate the metadata.
*
* This class implements this function using the getMetadataSet-function. A subclass should
* override this function if it doesn't implement the getMetadataSet function, or if the
* implementation of getMetadataSet is slow.
*
* @param string $index The entityId or metaindex we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
* @throws \Exception If an error occurs while validating the signature or the metadata is in an
* incorrect set.
*/
public function getMetaData($index, $set)
{
assert(is_string($index));
assert(is_string($set));
Logger::info(__CLASS__.': loading metadata entity ['.$index.'] from ['.$set.']');
// read from cache if possible
try {
$data = $this->getFromCache($set, $index);
} catch (\Exception $e) {
Logger::error($e->getMessage());
// proceed with fetching metadata even if the cache is broken
$data = null;
}
if ($data !== null && array_key_exists('expires', $data) && $data['expires'] < time()) {
// metadata has expired
$data = null;
}
if (isset($data)) {
// metadata found in cache and not expired
Logger::debug(__CLASS__.': using cached metadata for: '.$index.'.');
return $data;
}
// look at Metadata Query Protocol: https://github.com/iay/md-query/blob/master/draft-young-md-query.txt
$mdq_url = $this->server.'/entities/'.urlencode($index);
Logger::debug(__CLASS__.': downloading metadata for "'.$index.'" from ['.$mdq_url.']');
try {
$xmldata = HTTP::fetch($mdq_url);
} catch (\Exception $e) {
// Avoid propagating the exception, make sure we can handle the error later
$xmldata = false;
}
if (empty($xmldata)) {
$error = error_get_last();
Logger::info('Unable to fetch metadata for "'.$index.'" from '.$mdq_url.': '.
(is_array($error) ? $error['message'] : 'no error available'));
return null;
}
/** @var string $xmldata */
$entity = \SimpleSAML_Metadata_SAMLParser::parseString($xmldata);
Logger::debug(__CLASS__.': completed parsing of ['.$mdq_url.']');
if ($this->validateFingerprint !== null) {
if (!$entity->validateFingerprint($this->validateFingerprint)) {
throw new \Exception(__CLASS__.': error, could not verify signature for entity: '.$index.'".');
}
}
$data = self::getParsedSet($entity, $set);
if ($data === null) {
throw new \Exception(__CLASS__.': no metadata for set "'.$set.'" available from "'.$index.'".');
}
try {
$this->writeToCache($set, $index, $data);
} catch (\Exception $e) {
// Proceed without writing to cache
Logger::error('Error writing MDQ result to cache: '.$e->getMessage());
}
return $data;
}
}

311
lib/SimpleSAML/Module.php Executable file
View File

@@ -0,0 +1,311 @@
<?php
namespace SimpleSAML;
/**
* Helper class for accessing information about modules.
*
* @author Olav Morken <olav.morken@uninett.no>, UNINETT AS.
* @author Boy Baukema, SURFnet.
* @author Jaime Perez <jaime.perez@uninett.no>, UNINETT AS.
* @package SimpleSAMLphp
*/
class Module
{
/**
* A list containing the modules currently installed.
*
* @var array
*/
public static $modules = array();
/**
* A cache containing specific information for modules, like whether they are enabled or not, or their hooks.
*
* @var array
*/
public static $module_info = array();
/**
* Retrieve the base directory for a module.
*
* The returned path name will be an absolute path.
*
* @param string $module Name of the module
*
* @return string The base directory of a module.
*/
public static function getModuleDir($module)
{
$baseDir = dirname(dirname(dirname(__FILE__))).'/modules';
$moduleDir = $baseDir.'/'.$module;
return $moduleDir;
}
/**
* Determine whether a module is enabled.
*
* Will return false if the given module doesn't exist.
*
* @param string $module Name of the module
*
* @return bool True if the given module is enabled, false otherwise.
*
* @throws \Exception If module.enable is set and is not boolean.
*/
public static function isModuleEnabled($module)
{
$config = \SimpleSAML_Configuration::getOptionalConfig();
return self::isModuleEnabledWithConf($module, $config->getArray('module.enable', array()));
}
private static function isModuleEnabledWithConf($module, $mod_config)
{
if (isset(self::$module_info[$module]['enabled'])) {
return self::$module_info[$module]['enabled'];
}
if (!empty(self::$modules) && !in_array($module, self::$modules, true)) {
return false;
}
$moduleDir = self::getModuleDir($module);
if (!is_dir($moduleDir)) {
self::$module_info[$module]['enabled'] = false;
return false;
}
if (isset($mod_config[$module])) {
if (is_bool($mod_config[$module])) {
self::$module_info[$module]['enabled'] = $mod_config[$module];
return $mod_config[$module];
}
throw new \Exception("Invalid module.enable value for the '$module' module.");
}
if (assert_options(ASSERT_ACTIVE) &&
!file_exists($moduleDir.'/default-enable') &&
!file_exists($moduleDir.'/default-disable')
) {
\SimpleSAML\Logger::error("Missing default-enable or default-disable file for the module $module");
}
if (file_exists($moduleDir.'/enable')) {
self::$module_info[$module]['enabled'] = true;
return true;
}
if (!file_exists($moduleDir.'/disable') && file_exists($moduleDir.'/default-enable')) {
self::$module_info[$module]['enabled'] = true;
return true;
}
self::$module_info[$module]['enabled'] = false;
return false;
}
/**
* Get available modules.
*
* @return array One string for each module.
*
* @throws \Exception If we cannot open the module's directory.
*/
public static function getModules()
{
if (!empty(self::$modules)) {
return self::$modules;
}
$path = self::getModuleDir('.');
$dh = scandir($path);
if ($dh === false) {
throw new \Exception('Unable to open module directory "'.$path.'".');
}
foreach ($dh as $f) {
if ($f[0] === '.') {
continue;
}
if (!is_dir($path.'/'.$f)) {
continue;
}
self::$modules[] = $f;
}
return self::$modules;
}
/**
* Resolve module class.
*
* This function takes a string on the form "<module>:<class>" and converts it to a class
* name. It can also check that the given class is a subclass of a specific class. The
* resolved classname will be "sspmod_<module>_<$type>_<class>.
*
* It is also possible to specify a full classname instead of <module>:<class>.
*
* An exception will be thrown if the class can't be resolved.
*
* @param string $id The string we should resolve.
* @param string $type The type of the class.
* @param string|null $subclass The class should be a subclass of this class. Optional.
*
* @return string The classname.
*
* @throws \Exception If the class cannot be resolved.
*/
public static function resolveClass($id, $type, $subclass = null)
{
assert(is_string($id));
assert(is_string($type));
assert(is_string($subclass) || $subclass === null);
$tmp = explode(':', $id, 2);
if (count($tmp) === 1) { // no module involved
$className = $tmp[0];
if (!class_exists($className)) {
throw new \Exception("Could not resolve '$id': no class named '$className'.");
}
} else { // should be a module
// make sure empty types are handled correctly
$type = (empty($type)) ? '_' : '_'.$type.'_';
// check for the old-style class names
$className = 'sspmod_'.$tmp[0].$type.$tmp[1];
if (!class_exists($className)) {
// check for the new-style class names, using namespaces
$type = str_replace('_', '\\', $type);
$newClassName = 'SimpleSAML\Module\\'.$tmp[0].$type.$tmp[1];
if (!class_exists($newClassName)) {
throw new \Exception("Could not resolve '$id': no class named '$className' or '$newClassName'.");
}
$className = $newClassName;
}
}
if ($subclass !== null && !is_subclass_of($className, $subclass)) {
throw new \Exception(
'Could not resolve \''.$id.'\': The class \''.$className.'\' isn\'t a subclass of \''.$subclass.'\'.'
);
}
return $className;
}
/**
* Get absolute URL to a specified module resource.
*
* This function creates an absolute URL to a resource stored under ".../modules/<module>/www/".
*
* @param string $resource Resource path, on the form "<module name>/<resource>"
* @param array $parameters Extra parameters which should be added to the URL. Optional.
*
* @return string The absolute URL to the given resource.
*/
public static function getModuleURL($resource, array $parameters = array())
{
assert(is_string($resource));
assert($resource[0] !== '/');
$url = Utils\HTTP::getBaseURL().'module.php/'.$resource;
if (!empty($parameters)) {
$url = Utils\HTTP::addURLParameters($url, $parameters);
}
return $url;
}
/**
* Get the available hooks for a given module.
*
* @param string $module The module where we should look for hooks.
*
* @return array An array with the hooks available for this module. Each element is an array with two keys: 'file'
* points to the file that contains the hook, and 'func' contains the name of the function implementing that hook.
* When there are no hooks defined, an empty array is returned.
*/
public static function getModuleHooks($module)
{
if (isset(self::$modules[$module]['hooks'])) {
return self::$modules[$module]['hooks'];
}
$hook_dir = self::getModuleDir($module).'/hooks';
if (!is_dir($hook_dir)) {
return array();
}
$hooks = array();
$files = scandir($hook_dir);
foreach ($files as $file) {
if ($file[0] === '.') {
continue;
}
if (!preg_match('/hook_(\w+)\.php/', $file, $matches)) {
continue;
}
$hook_name = $matches[1];
$hook_func = $module.'_hook_'.$hook_name;
$hooks[$hook_name] = array('file' => $hook_dir.'/'.$file, 'func' => $hook_func);
}
return $hooks;
}
/**
* Call a hook in all enabled modules.
*
* This function iterates over all enabled modules and calls a hook in each module.
*
* @param string $hook The name of the hook.
* @param mixed &$data The data which should be passed to each hook. Will be passed as a reference.
*
* @throws \SimpleSAML_Error_Exception If an invalid hook is found in a module.
*/
public static function callHooks($hook, &$data = null)
{
assert(is_string($hook));
$modules = self::getModules();
$config = \SimpleSAML_Configuration::getOptionalConfig()->getArray('module.enable', array());
sort($modules);
foreach ($modules as $module) {
if (!self::isModuleEnabledWithConf($module, $config)) {
continue;
}
if (!isset(self::$module_info[$module]['hooks'])) {
self::$module_info[$module]['hooks'] = self::getModuleHooks($module);
}
if (!isset(self::$module_info[$module]['hooks'][$hook])) {
continue;
}
require_once(self::$module_info[$module]['hooks'][$hook]['file']);
if (!is_callable(self::$module_info[$module]['hooks'][$hook]['func'])) {
throw new \SimpleSAML_Error_Exception('Invalid hook \''.$hook.'\' for module \''.$module.'\'.');
}
$fn = self::$module_info[$module]['hooks'][$hook]['func'];
$fn($data);
}
}
}

1171
lib/SimpleSAML/Session.php Executable file

File diff suppressed because it is too large Load Diff

160
lib/SimpleSAML/SessionHandler.php Executable file
View File

@@ -0,0 +1,160 @@
<?php
/**
* This file is part of SimpleSAMLphp. See the file COPYING in the
* root of the distribution for licence information.
*
* This file defines a base class for session handling.
* Instantiation of session handler objects should be done through
* the class method getSessionHandler().
*
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML;
abstract class SessionHandler
{
/**
* This static variable contains a reference to the current
* instance of the session handler. This variable will be NULL if
* we haven't instantiated a session handler yet.
*
* @var \SimpleSAML\SessionHandler
*/
protected static $sessionHandler = null;
/**
* This function retrieves the current instance of the session handler.
* The session handler will be instantiated if this is the first call
* to this function.
*
* @return \SimpleSAML\SessionHandler The current session handler.
*/
public static function getSessionHandler()
{
if (self::$sessionHandler === null) {
self::createSessionHandler();
}
return self::$sessionHandler;
}
/**
* This constructor is included in case it is needed in the
* future. Including it now allows us to write parent::__construct() in
* the subclasses of this class.
*/
protected function __construct()
{
}
/**
* Create a new session id.
*
* @return string The new session id.
*/
abstract public function newSessionId();
/**
* Retrieve the session ID saved in the session cookie, if there's one.
*
* @return string|null The session id saved in the cookie or null if no session cookie was set.
*/
abstract public function getCookieSessionId();
/**
* Retrieve the session cookie name.
*
* @return string The session cookie name.
*/
abstract public function getSessionCookieName();
/**
* Save the session.
*
* @param \SimpleSAML_Session $session The session object we should save.
*/
abstract public function saveSession(\SimpleSAML_Session $session);
/**
* Load the session.
*
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
*
* @return \SimpleSAML_Session|null The session object, or null if it doesn't exist.
*/
abstract public function loadSession($sessionId = null);
/**
* Check whether the session cookie is set.
*
* This function will only return false if is is certain that the cookie isn't set.
*
* @return bool True if it was set, false if not.
*/
abstract public function hasSessionCookie();
/**
* Set a session cookie.
*
* @param string $sessionName The name of the session.
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
* @param array|null $cookieParams Additional parameters to use for the session cookie.
*
* @throws \SimpleSAML\Error\CannotSetCookie If we can't set the cookie.
*/
abstract public function setCookie($sessionName, $sessionID, array $cookieParams = null);
/**
* Initialize the session handler.
*
* This function creates an instance of the session handler which is
* selected in the 'session.handler' configuration directive. If no
* session handler is selected, then we will fall back to the default
* PHP session handler.
*/
private static function createSessionHandler()
{
$store = \SimpleSAML\Store::getInstance();
if ($store === false) {
self::$sessionHandler = new SessionHandlerPHP();
} else {
/** @var \SimpleSAML\Store $store At this point, $store can only be an object */
self::$sessionHandler = new SessionHandlerStore($store);
}
}
/**
* Get the cookie parameters that should be used for session cookies.
*
* @return array An array with the cookie parameters.
* @link http://www.php.net/manual/en/function.session-get-cookie-params.php
*/
public function getCookieParams()
{
$config = \SimpleSAML_Configuration::getInstance();
return array(
'lifetime' => $config->getInteger('session.cookie.lifetime', 0),
'path' => $config->getString('session.cookie.path', '/'),
'domain' => $config->getString('session.cookie.domain', null),
'secure' => $config->getBoolean('session.cookie.secure', false),
'httponly' => true,
);
}
}

View File

@@ -0,0 +1,173 @@
<?php
/**
* This file is part of SimpleSAMLphp. See the file COPYING in the root of the distribution for licence information.
*
* This file defines a base class for session handlers that need to store the session id in a cookie. It takes care of
* storing and retrieving the session id.
*
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
* @abstract
*/
namespace SimpleSAML;
use SimpleSAML\Utils\HTTP;
abstract class SessionHandlerCookie extends SessionHandler
{
/**
* This variable contains the current session id.
*
* @var string|null
*/
private $session_id = null;
/**
* This variable contains the session cookie name.
*
* @var string
*/
protected $cookie_name;
/**
* This constructor initializes the session id based on what we receive in a cookie. We create a new session id and
* set a cookie with this id if we don't have a session id.
*/
protected function __construct()
{
// call the constructor in the base class in case it should become necessary in the future
parent::__construct();
$config = \SimpleSAML_Configuration::getInstance();
$this->cookie_name = $config->getString('session.cookie.name', 'SimpleSAMLSessionID');
}
/**
* Create a new session id.
*
* @return string The new session id.
*/
public function newSessionId()
{
$this->session_id = self::createSessionID();
\SimpleSAML_Session::createSession($this->session_id);
return $this->session_id;
}
/**
* Retrieve the session ID saved in the session cookie, if there's one.
*
* @return string|null The session id saved in the cookie or null if no session cookie was set.
*/
public function getCookieSessionId()
{
if ($this->session_id === null) {
if (self::hasSessionCookie()) {
// attempt to retrieve the session id from the cookie
$this->session_id = $_COOKIE[$this->cookie_name];
}
// check if we have a valid session id
if (!self::isValidSessionID($this->session_id)) {
// invalid, disregard this session
return null;
}
}
return $this->session_id;
}
/**
* Retrieve the session cookie name.
*
* @return string The session cookie name.
*/
public function getSessionCookieName()
{
return $this->cookie_name;
}
/**
* This static function creates a session id. A session id consists of 32 random hexadecimal characters.
*
* @return string A random session id.
*/
private static function createSessionID()
{
return bin2hex(openssl_random_pseudo_bytes(16));
}
/**
* This static function validates a session id. A session id is valid if it only consists of characters which are
* allowed in a session id and it is the correct length.
*
* @param string $session_id The session ID we should validate.
*
* @return boolean True if this session ID is valid, false otherwise.
*/
private static function isValidSessionID($session_id)
{
if (!is_string($session_id)) {
return false;
}
if (strlen($session_id) != 32) {
return false;
}
if (preg_match('/[^0-9a-f]/', $session_id)) {
return false;
}
return true;
}
/**
* Check whether the session cookie is set.
*
* This function will only return false if is is certain that the cookie isn't set.
*
* @return boolean True if it was set, false otherwise.
*/
public function hasSessionCookie()
{
return array_key_exists($this->cookie_name, $_COOKIE);
}
/**
* Set a session cookie.
*
* @param string $sessionName The name of the session.
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
* @param array|null $cookieParams Additional parameters to use for the session cookie.
*
* @throws \SimpleSAML\Error\CannotSetCookie If we can't set the cookie.
*/
public function setCookie($sessionName, $sessionID, array $cookieParams = null)
{
assert(is_string($sessionName));
assert(is_string($sessionID) || $sessionID === null);
if ($cookieParams !== null) {
$params = array_merge($this->getCookieParams(), $cookieParams);
} else {
$params = $this->getCookieParams();
}
HTTP::setCookie($sessionName, $sessionID, $params, true);
}
}

View File

@@ -0,0 +1,362 @@
<?php
/**
* This file is part of SimpleSAMLphp. See the file COPYING in the root of the distribution for licence information.
*
* This file defines a session handler which uses the default php session handler for storage.
*
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML;
use SimpleSAML\Error\CannotSetCookie;
use SimpleSAML\Utils\HTTP;
class SessionHandlerPHP extends SessionHandler
{
/**
* This variable contains the session cookie name.
*
* @var string
*/
protected $cookie_name;
/**
* An associative array containing the details of a session existing previously to creating or loading one with this
* session handler. The keys of the array will be:
*
* - id: the ID of the session, as returned by session_id().
* - name: the name of the session, as returned by session_name().
* - cookie_params: the parameters of the session cookie, as returned by session_get_cookie_params().
*
* @var array
*/
private $previous_session = array();
/**
* Initialize the PHP session handling. This constructor is protected because it should only be called from
* \SimpleSAML\SessionHandler::createSessionHandler(...).
*/
protected function __construct()
{
// call the parent constructor in case it should become necessary in the future
parent::__construct();
$config = \SimpleSAML_Configuration::getInstance();
$this->cookie_name = $config->getString('session.phpsession.cookiename', null);
if (session_status() === PHP_SESSION_ACTIVE) {
if (session_name() === $this->cookie_name || $this->cookie_name === null) {
Logger::warning(
'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."
);
}
/*
* We shouldn't have a session at this point, so it might be an application session. Save the details to
* retrieve it later and commit.
*/
$this->previous_session['cookie_params'] = session_get_cookie_params();
$this->previous_session['id'] = session_id();
$this->previous_session['name'] = session_name();
session_write_close();
}
if (!empty($this->cookie_name)) {
session_name($this->cookie_name);
} else {
$this->cookie_name = session_name();
}
$params = $this->getCookieParams();
if (!headers_sent()) {
session_set_cookie_params(
$params['lifetime'],
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
$savepath = $config->getString('session.phpsession.savepath', null);
if (!empty($savepath)) {
session_save_path($savepath);
}
}
/**
* This method starts a session, making sure no warnings are generated due to headers being already sent.
*/
private function sessionStart()
{
$cacheLimiter = session_cache_limiter();
if (headers_sent()) {
/*
* session_start() tries to send HTTP headers depending on the configuration, according to the
* documentation:
*
* http://php.net/manual/en/function.session-start.php
*
* If headers have been already sent, it will then trigger an error since no more headers can be sent.
* Being unable to send headers does not mean we cannot recover the session by calling session_start(),
* so we still want to call it. In this case, though, we want to avoid session_start() to send any
* headers at all so that no error is generated, so we clear the cache limiter temporarily (no headers
* sent then) and restore it after successfully starting the session.
*/
session_cache_limiter('');
}
session_cache_limiter($cacheLimiter);
@session_start();
}
/**
* Restore a previously-existing session.
*
* Use this method to restore a previous PHP session existing before SimpleSAMLphp initialized its own session.
*
* WARNING: do not use this method directly, unless you know what you are doing. Calling this method directly,
* outside of SimpleSAML_Session, could cause SimpleSAMLphp's session to be lost or mess the application's one. The
* session must always be saved properly before calling this method. If you don't understand what this is about,
* don't use this method.
*/
public function restorePrevious()
{
if (empty($this->previous_session)) {
return; // nothing to do here
}
// close our own session
session_write_close();
session_name($this->previous_session['name']);
session_set_cookie_params(
$this->previous_session['cookie_params']['lifetime'],
$this->previous_session['cookie_params']['path'],
$this->previous_session['cookie_params']['domain'],
$this->previous_session['cookie_params']['secure'],
$this->previous_session['cookie_params']['httponly']
);
session_id($this->previous_session['id']);
$this->previous_session = array();
$this->sessionStart();
/*
* At this point, we have restored a previously-existing session, so we can't continue to use our session here.
* Therefore, we need to load our session again in case we need it. We remove this handler from the parent
* class so that the handler is initialized again if we ever need to do something with the session.
*/
parent::$sessionHandler = null;
}
/**
* Create a new session id.
*
* @return string The new session id.
*/
public function newSessionId()
{
// generate new (secure) session id
$sessionId = bin2hex(openssl_random_pseudo_bytes(16));
\SimpleSAML_Session::createSession($sessionId);
return $sessionId;
}
/**
* Retrieve the session ID saved in the session cookie, if there's one.
*
* @return string|null The session id saved in the cookie or null if no session cookie was set.
*
* @throws \SimpleSAML_Error_Exception If the cookie is marked as secure but we are not using HTTPS.
*/
public function getCookieSessionId()
{
if (!self::hasSessionCookie()) {
return null; // there's no session cookie, can't return ID
}
// do not rely on session_id() as it can return the ID of a previous session. Get it from the cookie instead.
session_id($_COOKIE[$this->cookie_name]);
$session_cookie_params = session_get_cookie_params();
if ($session_cookie_params['secure'] && !HTTP::isHTTPS()) {
throw new \SimpleSAML_Error_Exception('Session start with secure cookie not allowed on http.');
}
$this->sessionStart();
return session_id();
}
/**
* Retrieve the session cookie name.
*
* @return string The session cookie name.
*/
public function getSessionCookieName()
{
return $this->cookie_name;
}
/**
* Save the current session to the PHP session array.
*
* @param \SimpleSAML_Session $session The session object we should save.
*/
public function saveSession(\SimpleSAML_Session $session)
{
$_SESSION['SimpleSAMLphp_SESSION'] = serialize($session);
}
/**
* Load the session from the PHP session array.
*
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
*
* @return \SimpleSAML_Session|null The session object, or null if it doesn't exist.
*
* @throws \SimpleSAML_Error_Exception If it wasn't possible to disable session cookies or we are trying to load a
* PHP session with a specific identifier and it doesn't match with the current session identifier.
*/
public function loadSession($sessionId = null)
{
assert(is_string($sessionId) || $sessionId === null);
if ($sessionId !== null) {
if (session_id() === '') {
// session not initiated with getCookieSessionId(), start session without setting cookie
$ret = ini_set('session.use_cookies', '0');
if ($ret === false) {
throw new \SimpleSAML_Error_Exception('Disabling PHP option session.use_cookies failed.');
}
session_id($sessionId);
$this->sessionStart();
} elseif ($sessionId !== session_id()) {
throw new \SimpleSAML_Error_Exception('Cannot load PHP session with a specific ID.');
}
} elseif (session_id() === '') {
self::getCookieSessionId();
}
if (!isset($_SESSION['SimpleSAMLphp_SESSION'])) {
return null;
}
$session = $_SESSION['SimpleSAMLphp_SESSION'];
assert(is_string($session));
$session = unserialize($session);
return ($session !== false) ? $session : null;
}
/**
* Check whether the session cookie is set.
*
* This function will only return false if is is certain that the cookie isn't set.
*
* @return boolean True if it was set, false otherwise.
*/
public function hasSessionCookie()
{
return array_key_exists($this->cookie_name, $_COOKIE);
}
/**
* Get the cookie parameters that should be used for session cookies.
*
* This function contains some adjustments from the default to provide backwards-compatibility.
*
* @return array The cookie parameters for our sessions.
* @link http://www.php.net/manual/en/function.session-get-cookie-params.php
*
* @throws \SimpleSAML_Error_Exception If both 'session.phpsession.limitedpath' and 'session.cookie.path' options
* are set at the same time in the configuration.
*/
public function getCookieParams()
{
$config = \SimpleSAML_Configuration::getInstance();
$ret = parent::getCookieParams();
if ($config->hasValue('session.phpsession.limitedpath') && $config->hasValue('session.cookie.path')) {
throw new \SimpleSAML_Error_Exception(
'You cannot set both the session.phpsession.limitedpath and session.cookie.path options.'
);
} elseif ($config->hasValue('session.phpsession.limitedpath')) {
$ret['path'] = $config->getBoolean(
'session.phpsession.limitedpath',
false
) ? $config->getBasePath() : '/';
}
$ret['httponly'] = $config->getBoolean('session.phpsession.httponly', true);
return $ret;
}
/**
* Set a session cookie.
*
* @param string $sessionName The name of the session.
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
* @param array|null $cookieParams Additional parameters to use for the session cookie.
*
* @throws \SimpleSAML\Error\CannotSetCookie If we can't set the cookie.
*/
public function setCookie($sessionName, $sessionID, array $cookieParams = null)
{
if ($cookieParams === null) {
$cookieParams = session_get_cookie_params();
}
if ($cookieParams['secure'] && !HTTP::isHTTPS()) {
throw new CannotSetCookie(
'Setting secure cookie on plain HTTP is not allowed.',
CannotSetCookie::SECURE_COOKIE
);
}
if (headers_sent()) {
throw new CannotSetCookie(
'Headers already sent.',
CannotSetCookie::HEADERS_SENT
);
}
session_set_cookie_params(
$cookieParams['lifetime'],
$cookieParams['path'],
$cookieParams['domain'],
$cookieParams['secure'],
$cookieParams['httponly']
);
if (session_id() !== '') {
// session already started, close it
session_write_close();
}
session_id($sessionID);
$this->sessionStart();
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Session storage in the data store.
*
* @package SimpleSAMLphp
*/
namespace SimpleSAML;
class SessionHandlerStore extends SessionHandlerCookie
{
/**
* The data store we save the session to.
*
* @var \SimpleSAML\Store
*/
private $store;
/**
* Initialize the session.
*
* @param \SimpleSAML\Store $store The store to use.
*/
protected function __construct(Store $store)
{
parent::__construct();
$this->store = $store;
}
/**
* Load a session from the data store.
*
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
*
* @return \SimpleSAML_Session|null The session object, or null if it doesn't exist.
*/
public function loadSession($sessionId = null)
{
assert(is_string($sessionId) || $sessionId === null);
if ($sessionId === null) {
$sessionId = $this->getCookieSessionId();
if ($sessionId === null) {
// no session cookie, nothing to load
return null;
}
}
$session = $this->store->get('session', $sessionId);
if ($session !== null) {
assert($session instanceof \SimpleSAML_Session);
return $session;
}
return null;
}
/**
* Save a session to the data store.
*
* @param \SimpleSAML_Session $session The session object we should save.
*/
public function saveSession(\SimpleSAML_Session $session)
{
$sessionId = $session->getSessionId();
$config = \SimpleSAML_Configuration::getInstance();
$sessionDuration = $config->getInteger('session.duration', 8 * 60 * 60);
$expire = time() + $sessionDuration;
$this->store->set('session', $sessionId, $session, $expire);
}
}

100
lib/SimpleSAML/Stats.php Executable file
View File

@@ -0,0 +1,100 @@
<?php
/**
* Statistics handler class.
*
* This class is responsible for taking a statistics event and logging it.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Stats
{
/**
* Whether this class is initialized.
*
* @var boolean
*/
private static $initialized = false;
/**
* The statistics output callbacks.
*
* @var array
*/
private static $outputs = null;
/**
* Create an output from a configuration object.
*
* @param SimpleSAML_Configuration $config The configuration object.
*
* @return mixed A new instance of the configured class.
*/
private static function createOutput(SimpleSAML_Configuration $config)
{
$cls = $config->getString('class');
$cls = SimpleSAML\Module::resolveClass($cls, 'Stats_Output', 'SimpleSAML_Stats_Output');
$output = new $cls($config);
return $output;
}
/**
* Initialize the outputs.
*/
private static function initOutputs()
{
$config = SimpleSAML_Configuration::getInstance();
$outputCfgs = $config->getConfigList('statistics.out', array());
self::$outputs = array();
foreach ($outputCfgs as $cfg) {
self::$outputs[] = self::createOutput($cfg);
}
}
/**
* Notify about an event.
*
* @param string $event The event.
* @param array $data Event data. Optional.
*
* @return void|boolean False if output is not enabled, void otherwise.
*/
public static function log($event, array $data = array())
{
assert(is_string($event));
assert(!isset($data['op']));
assert(!isset($data['time']));
assert(!isset($data['_id']));
if (!self::$initialized) {
self::initOutputs();
self::$initialized = true;
}
if (empty(self::$outputs)) {
// not enabled
return;
}
$data['op'] = $event;
$data['time'] = microtime(true);
// the ID generation is designed to cluster IDs related in time close together
$int_t = (int) $data['time'];
$hd = openssl_random_pseudo_bytes(16);
$data['_id'] = sprintf('%016x%s', $int_t, bin2hex($hd));
foreach (self::$outputs as $out) {
$out->emit($data);
}
}
}

29
lib/SimpleSAML/Stats/Output.php Executable file
View File

@@ -0,0 +1,29 @@
<?php
/**
* Interface for statistics outputs.
*
* @package SimpleSAMLphp
*/
abstract class SimpleSAML_Stats_Output
{
/**
* Initialize the output.
*
* @param SimpleSAML_Configuration $config The configuration for this output.
*/
public function __construct(SimpleSAML_Configuration $config)
{
// do nothing by default
}
/**
* Write a stats event.
*
* @param array $data The event.
*/
abstract public function emit(array $data);
}

106
lib/SimpleSAML/Store.php Executable file
View File

@@ -0,0 +1,106 @@
<?php
namespace SimpleSAML;
use SimpleSAML\Error\CriticalConfigurationError;
/**
* Base class for data stores.
*
* @package SimpleSAMLphp
*/
abstract class Store
{
/**
* Our singleton instance.
*
* This is false if the data store isn't enabled, and null if we haven't attempted to initialize it.
*
* @var \SimpleSAML\Store|false|null
*/
private static $instance;
/**
* Retrieve our singleton instance.
*
* @return false|\SimpleSAML\Store The data store, or false if it isn't enabled.
*
* @throws \SimpleSAML\Error\CriticalConfigurationError
*/
public static function getInstance()
{
if (self::$instance !== null) {
return self::$instance;
}
$config = \SimpleSAML_Configuration::getInstance();
$storeType = $config->getString('store.type', null);
if ($storeType === null) {
$storeType = $config->getString('session.handler', 'phpsession');
}
switch ($storeType) {
case 'phpsession':
// we cannot support advanced features with the PHP session store
self::$instance = false;
break;
case 'memcache':
self::$instance = new Store\Memcache();
break;
case 'sql':
self::$instance = new Store\SQL();
break;
case 'redis':
self::$instance = new Store\Redis();
break;
default:
// datastore from module
try {
$className = Module::resolveClass($storeType, 'Store', '\SimpleSAML\Store');
} catch (\Exception $e) {
$c = $config->toArray();
$c['store.type'] = 'phpsession';
throw new CriticalConfigurationError(
"Invalid 'store.type' configuration option. Cannot find store '$storeType'.",
null,
$c
);
}
self::$instance = new $className();
}
return self::$instance;
}
/**
* Retrieve a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
*
* @return mixed|null The value.
*/
abstract public function get($type, $key);
/**
* Save a value to the data store.
*
* @param string $type The data type.
* @param string $key The key.
* @param mixed $value The value.
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
*/
abstract public function set($type, $key, $value, $expire = null);
/**
* Delete a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
*/
abstract public function delete($type, $key);
}

View File

@@ -0,0 +1,84 @@
<?php
namespace SimpleSAML\Store;
use \SimpleSAML_Configuration as Configuration;
use \SimpleSAML\Store;
/**
* A memcache based data store.
*
* @package SimpleSAMLphp
*/
class Memcache extends Store
{
/**
* This variable contains the session name prefix.
*
* @var string
*/
private $prefix;
/**
* This function implements the constructor for this class. It loads the Memcache configuration.
*/
protected function __construct()
{
$config = Configuration::getInstance();
$this->prefix = $config->getString('memcache_store.prefix', 'simpleSAMLphp');
}
/**
* Retrieve a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
* @return mixed|null The value.
*/
public function get($type, $key)
{
assert(is_string($type));
assert(is_string($key));
return \SimpleSAML_Memcache::get($this->prefix . '.' . $type . '.' . $key);
}
/**
* Save a value to the data store.
*
* @param string $type The data type.
* @param string $key The key.
* @param mixed $value The value.
* @param int|NULL $expire The expiration time (unix timestamp), or NULL if it never expires.
*/
public function set($type, $key, $value, $expire = null)
{
assert(is_string($type));
assert(is_string($key));
assert($expire === null || (is_int($expire) && $expire > 2592000));
if ($expire === null) {
$expire = 0;
}
\SimpleSAML_Memcache::set($this->prefix . '.' . $type . '.' . $key, $value, $expire);
}
/**
* Delete a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
*/
public function delete($type, $key)
{
assert(is_string($type));
assert(is_string($key));
\SimpleSAML_Memcache::delete($this->prefix . '.' . $type . '.' . $key);
}
}

119
lib/SimpleSAML/Store/Redis.php Executable file
View File

@@ -0,0 +1,119 @@
<?php
namespace SimpleSAML\Store;
use \SimpleSAML_Configuration as Configuration;
use \SimpleSAML\Store;
/**
* A data store using Redis to keep the data.
*
* @package SimpleSAMLphp
*/
class Redis extends Store
{
public $redis;
/**
* Initialize the Redis data store.
*/
public function __construct($redis = null)
{
assert($redis === null || is_subclass_of($redis, 'Predis\\Client'));
if (!class_exists('\Predis\Client')) {
throw new \SimpleSAML\Error\CriticalConfigurationError('predis/predis is not available.');
}
if ($redis === null) {
$config = Configuration::getInstance();
$host = $config->getString('store.redis.host', 'localhost');
$port = $config->getInteger('store.redis.port', 6379);
$prefix = $config->getString('store.redis.prefix', 'SimpleSAMLphp');
$redis = new \Predis\Client(
array(
'scheme' => 'tcp',
'host' => $host,
'port' => $port,
),
array(
'prefix' => $prefix,
)
);
}
$this->redis = $redis;
}
/**
* Deconstruct the Redis data store.
*/
public function __destruct()
{
if (method_exists($this->redis, 'disconnect')) {
$this->redis->disconnect();
}
}
/**
* Retrieve a value from the data store.
*
* @param string $type The type of the data.
* @param string $key The key to retrieve.
*
* @return mixed|null The value associated with that key, or null if there's no such key.
*/
public function get($type, $key)
{
assert(is_string($type));
assert(is_string($key));
$result = $this->redis->get("{$type}.{$key}");
if ($result === false || $result === null) {
return null;
}
return unserialize($result);
}
/**
* Save a value in the data store.
*
* @param string $type The type of the data.
* @param string $key The key to insert.
* @param mixed $value The value itself.
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
*/
public function set($type, $key, $value, $expire = null)
{
assert(is_string($type));
assert(is_string($key));
assert($expire === null || (is_int($expire) && $expire > 2592000));
$serialized = serialize($value);
if ($expire === null) {
$this->redis->set("{$type}.{$key}", $serialized);
} else {
// setex expire time is in seconds (not unix timestamp)
$this->redis->setex("{$type}.{$key}", $expire - time(), $serialized);
}
}
/**
* Delete an entry from the data store.
*
* @param string $type The type of the data
* @param string $key The key to delete.
*/
public function delete($type, $key)
{
assert(is_string($type));
assert(is_string($key));
$this->redis->del("{$type}.{$key}");
}
}

389
lib/SimpleSAML/Store/SQL.php Executable file
View File

@@ -0,0 +1,389 @@
<?php
namespace SimpleSAML\Store;
use \SimpleSAML_Configuration as Configuration;
use \SimpleSAML\Logger;
use \SimpleSAML\Store;
/**
* A data store using a RDBMS to keep the data.
*
* @package SimpleSAMLphp
*/
class SQL extends Store
{
/**
* The PDO object for our database.
*
* @var \PDO
*/
public $pdo;
/**
* Our database driver.
*
* @var string
*/
public $driver;
/**
* The prefix we should use for our tables.
*
* @var string
*/
public $prefix;
/**
* Associative array of table versions.
*
* @var array
*/
private $tableVersions;
/**
* Initialize the SQL data store.
*/
public function __construct()
{
$config = Configuration::getInstance();
$dsn = $config->getString('store.sql.dsn');
$username = $config->getString('store.sql.username', null);
$password = $config->getString('store.sql.password', null);
$options = $config->getArray('store.sql.options', null);
$this->prefix = $config->getString('store.sql.prefix', 'simpleSAMLphp');
try {
$this->pdo = new \PDO($dsn, $username, $password, $options);
} catch (\PDOException $e) {
throw new \Exception("Database error: " . $e->getMessage());
}
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
if ($this->driver === 'mysql') {
$this->pdo->exec('SET time_zone = "+00:00"');
}
$this->initTableVersionTable();
$this->initKVTable();
}
/**
* Initialize the table-version table.
*/
private function initTableVersionTable()
{
$this->tableVersions = array();
try {
$fetchTableVersion = $this->pdo->query('SELECT _name, _version FROM '.$this->prefix.'_tableVersion');
} catch (\PDOException $e) {
$this->pdo->exec(
'CREATE TABLE '.$this->prefix.
'_tableVersion (_name VARCHAR(30) NOT NULL UNIQUE, _version INTEGER NOT NULL)'
);
return;
}
while (($row = $fetchTableVersion->fetch(\PDO::FETCH_ASSOC)) !== false) {
$this->tableVersions[$row['_name']] = (int) $row['_version'];
}
}
/**
* Initialize key-value table.
*/
private function initKVTable()
{
$current_version = $this->getTableVersion('kvstore');
$text_t = 'TEXT';
if ($this->driver === 'mysql') {
// TEXT data type has size constraints that can be hit at some point, so we use LONGTEXT instead
$text_t = 'LONGTEXT';
}
/**
* Queries for updates, grouped by version.
* New updates can be added as a new array in this array
*/
$table_updates = array(
array(
'CREATE TABLE '.$this->prefix.
'_kvstore (_type VARCHAR(30) NOT NULL, _key VARCHAR(50) NOT NULL, _value '.$text_t.
' NOT NULL, _expire TIMESTAMP, PRIMARY KEY (_key, _type))',
'CREATE INDEX '.$this->prefix.'_kvstore_expire ON '.$this->prefix.'_kvstore (_expire)'
),
/**
* This upgrade removes the default NOT NULL constraint on the _expire field in MySQL.
* Because SQLite does not support field alterations, the approach is to:
* Create a new table without the NOT NULL constraint
* Copy the current data to the new table
* Drop the old table
* Rename the new table correctly
* Readd the index
*/
array(
'CREATE TABLE '.$this->prefix.
'_kvstore_new (_type VARCHAR(30) NOT NULL, _key VARCHAR(50) NOT NULL, _value '.$text_t.
' NOT NULL, _expire TIMESTAMP NULL, PRIMARY KEY (_key, _type))',
'INSERT INTO '.$this->prefix.'_kvstore_new SELECT * FROM ' . $this->prefix.'_kvstore',
'DROP TABLE '.$this->prefix.'_kvstore',
'ALTER TABLE '.$this->prefix.'_kvstore_new RENAME TO ' . $this->prefix . '_kvstore',
'CREATE INDEX '.$this->prefix.'_kvstore_expire ON '.$this->prefix.'_kvstore (_expire)'
)
);
$latest_version = count($table_updates);
if ($current_version == $latest_version) {
return;
}
// Only run queries for after the current version
$updates_to_run = array_slice($table_updates, $current_version);
foreach ($updates_to_run as $version_updates) {
foreach ($version_updates as $query) {
$this->pdo->exec($query);
}
}
$this->setTableVersion('kvstore', $latest_version);
}
/**
* Get table version.
*
* @param string $name Table name.
*
* @return int The table version, or 0 if the table doesn't exist.
*/
public function getTableVersion($name)
{
assert(is_string($name));
if (!isset($this->tableVersions[$name])) {
return 0;
}
return $this->tableVersions[$name];
}
/**
* Set table version.
*
* @param string $name Table name.
* @param int $version Table version.
*/
public function setTableVersion($name, $version)
{
assert(is_string($name));
assert(is_int($version));
$this->insertOrUpdate(
$this->prefix.'_tableVersion',
array('_name'),
array('_name' => $name, '_version' => $version)
);
$this->tableVersions[$name] = $version;
}
/**
* Insert or update a key-value in the store.
*
* Since various databases implement different methods for doing this, we abstract it away here.
*
* @param string $table The table we should update.
* @param array $keys The key columns.
* @param array $data Associative array with columns.
*/
public function insertOrUpdate($table, array $keys, array $data)
{
assert(is_string($table));
$colNames = '('.implode(', ', array_keys($data)).')';
$values = 'VALUES(:'.implode(', :', array_keys($data)).')';
switch ($this->driver) {
case 'mysql':
$query = 'REPLACE INTO '.$table.' '.$colNames.' '.$values;
$query = $this->pdo->prepare($query);
$query->execute($data);
return;
case 'sqlite':
$query = 'INSERT OR REPLACE INTO '.$table.' '.$colNames.' '.$values;
$query = $this->pdo->prepare($query);
$query->execute($data);
return;
}
// default implementation, try INSERT, and UPDATE if that fails.
$insertQuery = 'INSERT INTO '.$table.' '.$colNames.' '.$values;
$insertQuery = $this->pdo->prepare($insertQuery);
try {
$insertQuery->execute($data);
return;
} catch (\PDOException $e) {
$ecode = (string) $e->getCode();
switch ($ecode) {
case '23505': // PostgreSQL
break;
default:
Logger::error('Error while saving data: '.$e->getMessage());
throw $e;
}
}
$updateCols = array();
$condCols = array();
foreach ($data as $col => $value) {
$tmp = $col.' = :'.$col;
if (in_array($col, $keys, true)) {
$condCols[] = $tmp;
} else {
$updateCols[] = $tmp;
}
}
$updateQuery = 'UPDATE '.$table.' SET '.implode(',', $updateCols).' WHERE '.implode(' AND ', $condCols);
$updateQuery = $this->pdo->prepare($updateQuery);
$updateQuery->execute($data);
}
/**
* Clean the key-value table of expired entries.
*/
private function cleanKVStore()
{
Logger::debug('store.sql: Cleaning key-value store.');
$query = 'DELETE FROM '.$this->prefix.'_kvstore WHERE _expire < :now';
$params = array('now' => gmdate('Y-m-d H:i:s'));
$query = $this->pdo->prepare($query);
$query->execute($params);
}
/**
* Retrieve a value from the data store.
*
* @param string $type The type of the data.
* @param string $key The key to retrieve.
*
* @return mixed|null The value associated with that key, or null if there's no such key.
*/
public function get($type, $key)
{
assert(is_string($type));
assert(is_string($key));
if (strlen($key) > 50) {
$key = sha1($key);
}
$query = 'SELECT _value FROM '.$this->prefix.
'_kvstore WHERE _type = :type AND _key = :key AND (_expire IS NULL OR _expire > :now)';
$params = array('type' => $type, 'key' => $key, 'now' => gmdate('Y-m-d H:i:s'));
$query = $this->pdo->prepare($query);
$query->execute($params);
$row = $query->fetch(\PDO::FETCH_ASSOC);
if ($row === false) {
return null;
}
$value = $row['_value'];
if (is_resource($value)) {
$value = stream_get_contents($value);
}
$value = urldecode($value);
$value = unserialize($value);
if ($value === false) {
return null;
}
return $value;
}
/**
* Save a value in the data store.
*
* @param string $type The type of the data.
* @param string $key The key to insert.
* @param mixed $value The value itself.
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
*/
public function set($type, $key, $value, $expire = null)
{
assert(is_string($type));
assert(is_string($key));
assert($expire === null || (is_int($expire) && $expire > 2592000));
if (rand(0, 1000) < 10) {
$this->cleanKVStore();
}
if (strlen($key) > 50) {
$key = sha1($key);
}
if ($expire !== null) {
$expire = gmdate('Y-m-d H:i:s', $expire);
}
$value = serialize($value);
$value = rawurlencode($value);
$data = array(
'_type' => $type,
'_key' => $key,
'_value' => $value,
'_expire' => $expire,
);
$this->insertOrUpdate($this->prefix.'_kvstore', array('_type', '_key'), $data);
}
/**
* Delete an entry from the data store.
*
* @param string $type The type of the data
* @param string $key The key to delete.
*/
public function delete($type, $key)
{
assert(is_string($type));
assert(is_string($key));
if (strlen($key) > 50) {
$key = sha1($key);
}
$data = array(
'_type' => $type,
'_key' => $key,
);
$query = 'DELETE FROM '.$this->prefix.'_kvstore WHERE _type=:_type AND _key=:_key';
$query = $this->pdo->prepare($query);
$query->execute($data);
}
}

707
lib/SimpleSAML/Utilities.php Executable file
View File

@@ -0,0 +1,707 @@
<?php
/**
* Misc static functions that is used several places.in example parsing and id generation.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*
* @deprecated This entire class will be removed in SimpleSAMLphp 2.0.
*/
class SimpleSAML_Utilities
{
/**
* @deprecated This property will be removed in SSP 2.0. Please use SimpleSAML\Logger::isErrorMasked() instead.
*/
public static $logMask = 0;
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfHost() instead.
*/
public static function getSelfHost()
{
return \SimpleSAML\Utils\HTTP::getSelfHost();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURLHost() instead.
*/
public static function selfURLhost()
{
return \SimpleSAML\Utils\HTTP::getSelfURLHost();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::isHTTPS() instead.
*/
public static function isHTTPS()
{
return \SimpleSAML\Utils\HTTP::isHTTPS();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURLNoQuery()
* instead.
*/
public static function selfURLNoQuery()
{
return \SimpleSAML\Utils\HTTP::getSelfURLNoQuery();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfHostWithPath()
* instead.
*/
public static function getSelfHostWithPath()
{
return \SimpleSAML\Utils\HTTP::getSelfHostWithPath();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getFirstPathElement()
* instead.
*/
public static function getFirstPathElement($trailingslash = true)
{
return \SimpleSAML\Utils\HTTP::getFirstPathElement($trailingslash);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURL() instead.
*/
public static function selfURL()
{
return \SimpleSAML\Utils\HTTP::getSelfURL();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getBaseURL() instead.
*/
public static function getBaseURL()
{
return \SimpleSAML\Utils\HTTP::getBaseURL();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::addURLParameters() instead.
*/
public static function addURLparameter($url, $parameters)
{
return \SimpleSAML\Utils\HTTP::addURLParameters($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\HTTP::checkURLAllowed() instead.
*/
public static function checkURLAllowed($url, array $trustedSites = null)
{
return \SimpleSAML\Utils\HTTP::checkURLAllowed($url, $trustedSites);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML_Auth_State::parseStateID() instead.
*/
public static function parseStateID($stateId)
{
return SimpleSAML_Auth_State::parseStateID($stateId);
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
public static function checkDateConditions($start = null, $end = null)
{
$currentTime = time();
if (!empty($start)) {
$startTime = \SAML2\Utils::xsDateTimeToTimestamp($start);
// Allow for a 10 minute difference in Time
if (($startTime < 0) || (($startTime - 600) > $currentTime)) {
return false;
}
}
if (!empty($end)) {
$endTime = \SAML2\Utils::xsDateTimeToTimestamp($end);
if (($endTime < 0) || ($endTime <= $currentTime)) {
return false;
}
}
return true;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Random::generateID() instead.
*/
public static function generateID()
{
return SimpleSAML\Utils\Random::generateID();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\Time::generateTimestamp()
* instead.
*/
public static function generateTimestamp($instant = null)
{
return SimpleSAML\Utils\Time::generateTimestamp($instant);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\Time::parseDuration() instead.
*/
public static function parseDuration($duration, $timestamp = null)
{
return SimpleSAML\Utils\Time::parseDuration($duration, $timestamp);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please raise a SimpleSAML_Error_Error exception instead.
*/
public static function fatalError($trackId = 'na', $errorCode = null, Exception $e = null)
{
throw new SimpleSAML_Error_Error($errorCode, $e);
}
/**
* @deprecated This method will be removed in version 2.0. Use SimpleSAML\Utils\Net::ipCIDRcheck() instead.
*/
public static function ipCIDRcheck($cidr, $ip = null)
{
return SimpleSAML\Utils\Net::ipCIDRcheck($cidr, $ip);
}
private static function _doRedirect($url, $parameters = array())
{
assert(is_string($url));
assert(!empty($url));
assert(is_array($parameters));
if (!empty($parameters)) {
$url = self::addURLparameter($url, $parameters);
}
/* Set the HTTP result code. This is either 303 See Other or
* 302 Found. HTTP 303 See Other is sent if the HTTP version
* is HTTP/1.1 and the request type was a POST request.
*/
if ($_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.1' &&
$_SERVER['REQUEST_METHOD'] === 'POST'
) {
$code = 303;
} else {
$code = 302;
}
if (strlen($url) > 2048) {
SimpleSAML\Logger::warning('Redirecting to a URL longer than 2048 bytes.');
}
// Set the location header
header('Location: '.$url, true, $code);
// Disable caching of this response
header('Pragma: no-cache');
header('Cache-Control: no-cache, must-revalidate');
// Show a minimal web page with a clickable link to the URL
echo '<?xml version="1.0" encoding="UTF-8"?>'."\n";
echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'.
' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'."\n";
echo '<html xmlns="http://www.w3.org/1999/xhtml">';
echo '<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Redirect</title>
</head>';
echo '<body>';
echo '<h1>Redirect</h1>';
echo '<p>';
echo 'You were redirected to: ';
echo '<a id="redirlink" href="'.
htmlspecialchars($url).'">'.htmlspecialchars($url).'</a>';
echo '<script type="text/javascript">document.getElementById("redirlink").focus();</script>';
echo '</p>';
echo '</body>';
echo '</html>';
// End script execution
exit;
}
/**
* @deprecated 1.12.0 This method will be removed from the API. Instead, use the redirectTrustedURL() or
* redirectUntrustedURL() functions accordingly.
*/
public static function redirect($url, $parameters = array(), $allowed_redirect_hosts = null)
{
assert(is_string($url));
assert(strlen($url) > 0);
assert(is_array($parameters));
if ($allowed_redirect_hosts !== null) {
$url = self::checkURLAllowed($url, $allowed_redirect_hosts);
} else {
$url = self::normalizeURL($url);
}
self::_doRedirect($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::redirectTrustedURL()
* instead.
*/
public static function redirectTrustedURL($url, $parameters = array())
{
\SimpleSAML\Utils\HTTP::redirectTrustedURL($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::redirectUntrustedURL()
* instead.
*/
public static function redirectUntrustedURL($url, $parameters = array())
{
\SimpleSAML\Utils\HTTP::redirectUntrustedURL($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Arrays::transpose() instead.
*/
public static function transposeArray($in)
{
return SimpleSAML\Utils\Arrays::transpose($in);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::isDOMNodeOfType()
* instead.
*/
public static function isDOMElementOfType(DOMNode $element, $name, $nsURI)
{
return SimpleSAML\Utils\XML::isDOMNodeOfType($element, $name, $nsURI);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::getDOMChildren() instead.
*/
public static function getDOMChildren(DOMElement $element, $localName, $namespaceURI)
{
return SimpleSAML\Utils\XML::getDOMChildren($element, $localName, $namespaceURI);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::getDOMText() instead.
*/
public static function getDOMText($element)
{
return SimpleSAML\Utils\XML::getDOMText($element);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getAcceptLanguage()
* instead.
*/
public static function getAcceptLanguage()
{
return \SimpleSAML\Utils\HTTP::getAcceptLanguage();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::isValid() instead.
*/
public static function validateXML($xml, $schema)
{
$result = \SimpleSAML\Utils\XML::isValid($xml, $schema);
return ($result === true) ? '' : $result;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::checkSAMLMessage() instead.
*/
public static function validateXMLDocument($message, $type)
{
\SimpleSAML\Utils\XML::checkSAMLMessage($message, $type);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use openssl_random_pseudo_bytes() instead.
*/
public static function generateRandomBytes($length)
{
assert(is_int($length));
return openssl_random_pseudo_bytes($length);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use bin2hex() instead.
*/
public static function stringToHex($bytes)
{
$ret = '';
for ($i = 0; $i < strlen($bytes); $i++) {
$ret .= sprintf('%02x', ord($bytes[$i]));
}
return $ret;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::resolvePath() instead.
*/
public static function resolvePath($path, $base = null)
{
return \SimpleSAML\Utils\System::resolvePath($path, $base);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::resolveURL() instead.
*/
public static function resolveURL($url, $base = null)
{
return \SimpleSAML\Utils\HTTP::resolveURL($url, $base);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::normalizeURL() instead.
*/
public static function normalizeURL($url)
{
return \SimpleSAML\Utils\HTTP::normalizeURL($url);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::parseQueryString() instead.
*/
public static function parseQueryString($query_string)
{
return \SimpleSAML\Utils\HTTP::parseQueryString($query_string);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* SimpleSAML\Utils\Attributes::normalizeAttributesArray() instead.
*/
public static function parseAttributes($attributes)
{
return SimpleSAML\Utils\Attributes::normalizeAttributesArray($attributes);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Config::getSecretSalt() instead.
*/
public static function getSecretSalt()
{
return SimpleSAML\Utils\Config::getSecretSalt();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please call error_get_last() directly.
*/
public static function getLastError()
{
if (!function_exists('error_get_last')) {
return '[Cannot get error message]';
}
$error = error_get_last();
if ($error === null) {
return '[No error message found]';
}
return $error['message'];
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Config::getCertPath() instead.
*/
public static function resolveCert($path)
{
return \SimpleSAML\Utils\Config::getCertPath($path);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::loadPublicKey() instead.
*/
public static function loadPublicKey(SimpleSAML_Configuration $metadata, $required = false, $prefix = '')
{
return SimpleSAML\Utils\Crypto::loadPublicKey($metadata, $required, $prefix);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::loadPrivateKey() instead.
*/
public static function loadPrivateKey(SimpleSAML_Configuration $metadata, $required = false, $prefix = '')
{
return SimpleSAML\Utils\Crypto::loadPrivateKey($metadata, $required, $prefix);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::formatDOMElement() instead.
*/
public static function formatDOMElement(DOMElement $root, $indentBase = '')
{
SimpleSAML\Utils\XML::formatDOMElement($root, $indentBase);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::formatXMLString() instead.
*/
public static function formatXMLString($xml, $indentBase = '')
{
return SimpleSAML\Utils\XML::formatXMLString($xml, $indentBase);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Arrays::arrayize() instead.
*/
public static function arrayize($data, $index = 0)
{
return SimpleSAML\Utils\Arrays::arrayize($data, $index);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::isAdmin() instead.
*/
public static function isAdmin()
{
return SimpleSAML\Utils\Auth::isAdmin();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::getAdminLoginURL instead();
*/
public static function getAdminLoginURL($returnTo = null)
{
return SimpleSAML\Utils\Auth::getAdminLoginURL($returnTo);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::requireAdmin() instead.
*/
public static function requireAdmin()
{
\SimpleSAML\Utils\Auth::requireAdmin();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::submitPOSTData() instead.
*/
public static function postRedirect($destination, $post)
{
\SimpleSAML\Utils\HTTP::submitPOSTData($destination, $post);
}
/**
* @deprecated This method will be removed in SSP 2.0. PLease use SimpleSAML\Utils\HTTP::getPOSTRedirectURL()
* instead.
*/
public static function createPostRedirectLink($destination, $post)
{
return \SimpleSAML\Utils\HTTP::getPOSTRedirectURL($destination, $post);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getPOSTRedirectURL()
* instead.
*/
public static function createHttpPostRedirectLink($destination, $post)
{
assert(is_string($destination));
assert(is_array($post));
$postId = SimpleSAML\Utils\Random::generateID();
$postData = array(
'post' => $post,
'url' => $destination,
);
$session = SimpleSAML_Session::getSessionFromRequest();
$session->setData('core_postdatalink', $postId, $postData);
$redirInfo = base64_encode(SimpleSAML\Utils\Crypto::aesEncrypt($session->getSessionId().':'.$postId));
$url = SimpleSAML\Module::getModuleURL('core/postredirect.php', array('RedirInfo' => $redirInfo));
$url = preg_replace("#^https:#", "http:", $url);
return $url;
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
public static function validateCA($certificate, $caFile)
{
\SimpleSAML\XML\Validator::validateCertificate($certificate, $caFile);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Time::initTimezone() instead.
*/
public static function initTimezone()
{
\SimpleSAML\Utils\Time::initTimezone();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::writeFile() instead.
*/
public static function writeFile($filename, $data, $mode = 0600)
{
\SimpleSAML\Utils\System::writeFile($filename, $data, $mode);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::getTempDir instead.
*/
public static function getTempDir()
{
return SimpleSAML\Utils\System::getTempDir();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Logger::maskErrors() instead.
*/
public static function maskErrors($mask)
{
SimpleSAML\Logger::maskErrors($mask);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Logger::popErrorMask() instead.
*/
public static function popErrorMask()
{
SimpleSAML\Logger::popErrorMask();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* SimpleSAML\Utils\Config\Metadata::getDefaultEndpoint() instead.
*/
public static function getDefaultEndpoint(array $endpoints, array $bindings = null)
{
return \SimpleSAML\Utils\Config\Metadata::getDefaultEndpoint($endpoints, $bindings);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::checkSessionCookie()
* instead.
*/
public static function checkCookie($retryURL = null)
{
\SimpleSAML\Utils\HTTP::checkSessionCookie($retryURL);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::debugSAMLMessage() instead.
*/
public static function debugMessage($message, $type)
{
\SimpleSAML\Utils\XML::debugSAMLMessage($message, $type);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::fetch() instead.
*/
public static function fetch($path, $context = array(), $getHeaders = false)
{
return \SimpleSAML\Utils\HTTP::fetch($path, $context, $getHeaders);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::aesEncrypt() instead.
*/
public static function aesEncrypt($clear)
{
return SimpleSAML\Utils\Crypto::aesEncrypt($clear);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::aesDecrypt() instead.
*/
public static function aesDecrypt($encData)
{
return SimpleSAML\Utils\Crypto::aesDecrypt($encData);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::getOS() instead.
*/
public static function isWindowsOS()
{
return SimpleSAML\Utils\System::getOS() === SimpleSAML\Utils\System::WINDOWS;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::setCookie() instead.
*/
public static function setCookie($name, $value, array $params = null, $throw = true)
{
\SimpleSAML\Utils\HTTP::setCookie($name, $value, $params, $throw);
}
}

60
lib/SimpleSAML/Utils/Arrays.php Executable file
View File

@@ -0,0 +1,60 @@
<?php
namespace SimpleSAML\Utils;
/**
* Array-related utility methods.
*
* @package SimpleSAMLphp
*/
class Arrays
{
/**
* Put a non-array variable into an array.
*
* @param mixed $data The data to place into an array.
* @param mixed $index The index or key of the array where to place the data. Defaults to 0.
*
* @return array An array with one element containing $data, with key $index, or $data itself if it's already an
* array.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function arrayize($data, $index = 0)
{
return (is_array($data)) ? $data : array($index => $data);
}
/**
* This function transposes a two-dimensional array, so that $a['k1']['k2'] becomes $a['k2']['k1'].
*
* @param array $array The two-dimensional array to transpose.
*
* @return mixed The transposed array, or false if $array is not a valid two-dimensional array.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
*/
public static function transpose($array)
{
if (!is_array($array)) {
return false;
}
$ret = array();
foreach ($array as $k1 => $a2) {
if (!is_array($a2)) {
return false;
}
foreach ($a2 as $k2 => $v) {
if (!array_key_exists($k2, $ret)) {
$ret[$k2] = array();
}
$ret[$k2][$k1] = $v;
}
}
return $ret;
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace SimpleSAML\Utils;
/**
* Attribute-related utility methods.
*
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAML
*/
class Attributes
{
/**
* Look for an attribute in a normalized attributes array, failing if it's not there.
*
* @param array $attributes The normalized array containing attributes.
* @param string $expected The name of the attribute we are looking for.
* @param bool $allow_multiple Whether to allow multiple values in the attribute or not.
*
* @return mixed The value of the attribute we are expecting. If the attribute has multiple values and
* $allow_multiple is set to true, the first value will be returned.
*
* @throws \InvalidArgumentException If $attributes is not an array or $expected is not a string.
* @throws \SimpleSAML_Error_Exception If the expected attribute was not found in the attributes array.
*/
public static function getExpectedAttribute($attributes, $expected, $allow_multiple = false)
{
if (!is_array($attributes)) {
throw new \InvalidArgumentException(
'The attributes array is not an array, it is: '.print_r($attributes, true).'.'
);
}
if (!is_string($expected)) {
throw new \InvalidArgumentException(
'The expected attribute is not a string, it is: '.print_r($expected, true).'.'
);
}
if (!array_key_exists($expected, $attributes)) {
throw new \SimpleSAML_Error_Exception("No such attribute '".$expected."' found.");
}
$attribute = $attributes[$expected];
if (!is_array($attribute)) {
throw new \InvalidArgumentException('The attributes array is not normalized, values should be arrays.');
}
if (count($attribute) === 0) {
throw new \SimpleSAML_Error_Exception("Empty attribute '".$expected."'.'");
} elseif (count($attribute) > 1) {
if ($allow_multiple === false) {
throw new \SimpleSAML_Error_Exception(
'More than one value found for the attribute, multiple values not allowed.'
);
}
}
return reset($attribute);
}
/**
* Validate and normalize an array with attributes.
*
* This function takes in an associative array with attributes, and parses and validates
* this array. On success, it will return a normalized array, where each attribute name
* is an index to an array of one or more strings. On failure an exception will be thrown.
* This exception will contain an message describing what is wrong.
*
* @param array $attributes The array containing attributes that we should validate and normalize.
*
* @return array The normalized attributes array.
* @throws \InvalidArgumentException If input is not an array, array keys are not strings or attribute values are
* not strings.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function normalizeAttributesArray($attributes)
{
if (!is_array($attributes)) {
throw new \InvalidArgumentException(
'The attributes array is not an array, it is: '.print_r($attributes, true).'".'
);
}
$newAttrs = array();
foreach ($attributes as $name => $values) {
if (!is_string($name)) {
throw new \InvalidArgumentException('Invalid attribute name: "'.print_r($name, true).'".');
}
$values = Arrays::arrayize($values);
foreach ($values as $value) {
if (!is_string($value)) {
throw new \InvalidArgumentException(
'Invalid attribute value for attribute '.$name.': "'.print_r($value, true).'".'
);
}
}
$newAttrs[$name] = $values;
}
return $newAttrs;
}
/**
* Extract an attribute's namespace, or revert to default.
*
* This function takes in a namespaced attribute name and splits it in a namespace/attribute name tuple.
* When no namespace is found in the attribute name, it will be namespaced with the default namespace.
* This default namespace can be overriden by supplying a second parameter to this function.
*
* @param string $name The namespaced attribute name.
* @param string $defaultns The default namespace that should be used when no namespace is found.
*
* @return array The attribute name, split to the namespace and the actual attribute name.
*/
public static function getAttributeNamespace($name, $defaultns)
{
$slash = strrpos($name, '/');
if ($slash !== false) {
$defaultns = substr($name, 0, $slash);
$name = substr($name, $slash + 1);
}
return array(htmlspecialchars($defaultns), htmlspecialchars($name));
}
}

76
lib/SimpleSAML/Utils/Auth.php Executable file
View File

@@ -0,0 +1,76 @@
<?php
namespace SimpleSAML\Utils;
use SimpleSAML\Module;
/**
* Auth-related utility methods.
*
* @package SimpleSAMLphp
*/
class Auth
{
/**
* Retrieve a admin login URL.
*
* @param string|NULL $returnTo The URL the user should arrive on after admin authentication. Defaults to null.
*
* @return string A URL which can be used for admin authentication.
* @throws \InvalidArgumentException If $returnTo is neither a string nor null.
*/
public static function getAdminLoginURL($returnTo = null)
{
if (!(is_string($returnTo) || is_null($returnTo))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
if ($returnTo === null) {
$returnTo = HTTP::getSelfURL();
}
return Module::getModuleURL('core/login-admin.php', array('ReturnTo' => $returnTo));
}
/**
* Check whether the current user is admin.
*
* @return boolean True if the current user is an admin user, false otherwise.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isAdmin()
{
$session = \SimpleSAML_Session::getSessionFromRequest();
return $session->isValid('admin') || $session->isValid('login-admin');
}
/**
* Require admin access to the current page.
*
* This is a helper function for limiting a page to those with administrative access. It will redirect the user to
* a login page if the current user doesn't have admin access.
*
* @return void This function will only return if the user is admin.
* @throws \SimpleSAML_Error_Exception If no "admin" authentication source was configured.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function requireAdmin()
{
if (self::isAdmin()) {
return;
}
// not authenticated as admin user, start authentication
if (\SimpleSAML_Auth_Source::getById('admin') !== null) {
$as = new \SimpleSAML\Auth\Simple('admin');
$as->login();
} else {
throw new \SimpleSAML_Error_Exception(
'Cannot find "admin" auth source, and admin privileges are required.'
);
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace SimpleSAML\Utils;
/**
* Indicates an implementation caches state internally and may be cleared.
*
* Primarily designed to allow SSP state to be cleared between unit tests.
* @package SimpleSAML\Utils
*/
interface ClearableState
{
/**
* Clear any cached internal state.
*/
public static function clearInternalState();
}

92
lib/SimpleSAML/Utils/Config.php Executable file
View File

@@ -0,0 +1,92 @@
<?php
namespace SimpleSAML\Utils;
/**
* Utility class for SimpleSAMLphp configuration management and manipulation.
*
* @package SimpleSAMLphp
*/
class Config
{
/**
* Resolves a path that may be relative to the cert-directory.
*
* @param string $path The (possibly relative) path to the file.
*
* @return string The file path.
* @throws \InvalidArgumentException If $path is not a string.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getCertPath($path)
{
if (!is_string($path)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$globalConfig = \SimpleSAML_Configuration::getInstance();
$base = $globalConfig->getPathValue('certdir', 'cert/');
return System::resolvePath($path, $base);
}
/**
* Retrieve the secret salt.
*
* This function retrieves the value which is configured as the secret salt. It will check that the value exists
* and is set to a non-default value. If it isn't, an exception will be thrown.
*
* The secret salt can be used as a component in hash functions, to make it difficult to test all possible values
* in order to retrieve the original value. It can also be used as a simple method for signing data, by hashing the
* data together with the salt.
*
* @return string The secret salt.
* @throws \InvalidArgumentException If the secret salt hasn't been configured.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getSecretSalt()
{
$secretSalt = \SimpleSAML_Configuration::getInstance()->getString('secretsalt');
if ($secretSalt === 'defaultsecretsalt') {
throw new \InvalidArgumentException('The "secretsalt" configuration option must be set to a secret value.');
}
return $secretSalt;
}
/**
* Returns the path to the config dir
*
* If the SIMPLESAMLPHP_CONFIG_DIR environment variable has been set, it takes precedence over the default
* $simplesamldir/config directory.
*
* @return string The path to the configuration directory.
*/
public static function getConfigDir()
{
$configDir = dirname(dirname(dirname(__DIR__))) . '/config';
/** @var string|false $configDirEnv */
$configDirEnv = getenv('SIMPLESAMLPHP_CONFIG_DIR');
if($configDirEnv === false) {
$configDirEnv = getenv('REDIRECT_SIMPLESAMLPHP_CONFIG_DIR');
}
if ($configDirEnv !== false) {
if (!is_dir($configDirEnv)) {
throw new \InvalidArgumentException(
sprintf(
'Config directory specified by environment variable SIMPLESAMLPHP_CONFIG_DIR is not a ' .
'directory. Given: "%s"',
$configDirEnv
)
);
}
$configDir = $configDirEnv;
}
return $configDir;
}
}

View File

@@ -0,0 +1,282 @@
<?php
namespace SimpleSAML\Utils\Config;
/**
* Class with utilities to fetch different configuration objects from metadata configuration arrays.
*
* @package SimpleSAMLphp
* @author Jaime Pérez Crespo, UNINETT AS <jaime.perez@uninett.no>
*/
class Metadata
{
/**
* The string that identities Entity Categories.
*
* @var string
*/
public static $ENTITY_CATEGORY = 'http://macedir.org/entity-category';
/**
* The string the identifies the REFEDS "Hide From Discovery" Entity Category.
*
* @var string
*/
public static $HIDE_FROM_DISCOVERY = 'http://refeds.org/category/hide-from-discovery';
/**
* Valid options for the ContactPerson element
*
* The 'attributes' option isn't defined in section 2.3.2.2 of the OASIS document, but
* it is required to allow additons to the main contact person element for trust
* frameworks.
*
* @var array The valid configuration options for a contact configuration array.
* @see "Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0", section 2.3.2.2.
*/
public static $VALID_CONTACT_OPTIONS = array(
'contactType',
'emailAddress',
'givenName',
'surName',
'telephoneNumber',
'company',
'attributes',
);
/**
* @var array The valid types of contact for a contact configuration array.
* @see "Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0", section 2.3.2.2.
*/
public static $VALID_CONTACT_TYPES = array(
'technical',
'support',
'administrative',
'billing',
'other',
);
/**
* Parse and sanitize a contact from an array.
*
* Accepts an array with the following elements:
* - contactType The type of the contact (as string). Mandatory.
* - emailAddress Email address (as string), or array of email addresses. Optional.
* - telephoneNumber Telephone number of contact (as string), or array of telephone numbers. Optional.
* - name Full name of contact, either as <GivenName> <SurName>, or as <SurName>, <GivenName>. Optional.
* - surName Surname of contact (as string). Optional.
* - givenName Given name of contact (as string). Optional.
* - company Company name of contact (as string). Optional.
*
* The following values are allowed as "contactType":
* - technical
* - support
* - administrative
* - billing
* - other
*
* If given a "name" it will try to decompose it into its given name and surname, only if neither givenName nor
* surName are present. It works as follows:
* - "surname1 surname2, given_name1 given_name2"
* givenName: "given_name1 given_name2"
* surname: "surname1 surname2"
* - "given_name surname"
* givenName: "given_name"
* surname: "surname"
*
* otherwise it will just return the name as "givenName" in the resulting array.
*
* @param array $contact The contact to parse and sanitize.
*
* @return array An array holding valid contact configuration options. If a key 'name' was part of the input array,
* it will try to decompose the name into its parts, and place the parts into givenName and surName, if those are
* missing.
* @throws \InvalidArgumentException If $contact is neither an array nor null, or the contact does not conform to
* valid configuration rules for contacts.
*/
public static function getContact($contact)
{
if (!(is_array($contact) || is_null($contact))) {
throw new \InvalidArgumentException('Invalid input parameters');
}
// check the type
if (!isset($contact['contactType']) || !in_array($contact['contactType'], self::$VALID_CONTACT_TYPES, true)) {
$types = join(', ', array_map(
function ($t) {
return '"'.$t.'"';
},
self::$VALID_CONTACT_TYPES
));
throw new \InvalidArgumentException('"contactType" is mandatory and must be one of '.$types.".");
}
// check attributes is an associative array
if (isset($contact['attributes'])) {
if (empty($contact['attributes'])
|| !is_array($contact['attributes'])
|| count(array_filter(array_keys($contact['attributes']), 'is_string')) === 0
) {
throw new \InvalidArgumentException('"attributes" must be an array and cannot be empty.');
}
}
// try to fill in givenName and surName from name
if (isset($contact['name']) && !isset($contact['givenName']) && !isset($contact['surName'])) {
// first check if it's comma separated
$names = explode(',', $contact['name'], 2);
if (count($names) === 2) {
$contact['surName'] = preg_replace('/\s+/', ' ', trim($names[0]));
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($names[1]));
} else {
// check if it's in "given name surname" format
$names = explode(' ', preg_replace('/\s+/', ' ', trim($contact['name'])));
if (count($names) === 2) {
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($names[0]));
$contact['surName'] = preg_replace('/\s+/', ' ', trim($names[1]));
} else {
// nothing works, return it as given name
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($contact['name']));
}
}
}
// check givenName
if (isset($contact['givenName']) && (
empty($contact['givenName']) || !is_string($contact['givenName'])
)
) {
throw new \InvalidArgumentException('"givenName" must be a string and cannot be empty.');
}
// check surName
if (isset($contact['surName']) && (
empty($contact['surName']) || !is_string($contact['surName'])
)
) {
throw new \InvalidArgumentException('"surName" must be a string and cannot be empty.');
}
// check company
if (isset($contact['company']) && (
empty($contact['company']) || !is_string($contact['company'])
)
) {
throw new \InvalidArgumentException('"company" must be a string and cannot be empty.');
}
// check emailAddress
if (isset($contact['emailAddress'])) {
if (empty($contact['emailAddress']) ||
!(is_string($contact['emailAddress']) || is_array($contact['emailAddress']))
) {
throw new \InvalidArgumentException('"emailAddress" must be a string or an array and cannot be empty.');
}
if (is_array($contact['emailAddress'])) {
foreach ($contact['emailAddress'] as $address) {
if (!is_string($address) || empty($address)) {
throw new \InvalidArgumentException('Email addresses must be a string and cannot be empty.');
}
}
}
}
// check telephoneNumber
if (isset($contact['telephoneNumber'])) {
if (empty($contact['telephoneNumber']) ||
!(is_string($contact['telephoneNumber']) || is_array($contact['telephoneNumber']))
) {
throw new \InvalidArgumentException(
'"telephoneNumber" must be a string or an array and cannot be empty.'
);
}
if (is_array($contact['telephoneNumber'])) {
foreach ($contact['telephoneNumber'] as $address) {
if (!is_string($address) || empty($address)) {
throw new \InvalidArgumentException('Telephone numbers must be a string and cannot be empty.');
}
}
}
}
// make sure only valid options are outputted
return array_intersect_key($contact, array_flip(self::$VALID_CONTACT_OPTIONS));
}
/**
* Find the default endpoint in an endpoint array.
*
* @param array $endpoints An array with endpoints.
* @param array $bindings An array with acceptable bindings. Can be null if any binding is allowed.
*
* @return array|NULL The default endpoint, or null if no acceptable endpoints are used.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getDefaultEndpoint(array $endpoints, array $bindings = null)
{
$firstNotFalse = null;
$firstAllowed = null;
// look through the endpoint list for acceptable endpoints
foreach ($endpoints as $ep) {
if ($bindings !== null && !in_array($ep['Binding'], $bindings, true)) {
// unsupported binding, skip it
continue;
}
if (isset($ep['isDefault'])) {
if ($ep['isDefault'] === true) {
// this is the first endpoint with isDefault set to true
return $ep;
}
// isDefault is set to false, but the endpoint is still usable as a last resort
if ($firstAllowed === null) {
// this is the first endpoint that we can use
$firstAllowed = $ep;
}
} else {
if ($firstNotFalse === null) {
// this is the first endpoint without isDefault set
$firstNotFalse = $ep;
}
}
}
if ($firstNotFalse !== null) {
// we have an endpoint without isDefault set to false
return $firstNotFalse;
}
/* $firstAllowed either contains the first endpoint we can use, or it contains null if we cannot use any of the
* endpoints. Either way we return its value.
*/
return $firstAllowed;
}
/**
* Determine if an entity should be hidden in the discovery service.
*
* This method searches for the "Hide From Discovery" REFEDS Entity Category, and tells if the entity should be
* hidden or not depending on it.
*
* @see https://refeds.org/category/hide-from-discovery
*
* @param array $metadata An associative array with the metadata representing an entity.
*
* @return boolean True if the entity should be hidden, false otherwise.
*/
public static function isHiddenFromDiscovery(array $metadata)
{
\SimpleSAML\Logger::maskErrors(E_ALL);
$hidden = in_array(self::$HIDE_FROM_DISCOVERY, $metadata['EntityAttributes'][self::$ENTITY_CATEGORY], true);
\SimpleSAML\Logger::popErrorMask();
return $hidden === true;
}
}

471
lib/SimpleSAML/Utils/Crypto.php Executable file
View File

@@ -0,0 +1,471 @@
<?php
namespace SimpleSAML\Utils;
/**
* A class for cryptography-related functions.
*
* @package SimpleSAMLphp
*/
class Crypto
{
/**
* Decrypt data using AES-256-CBC and the key provided as a parameter.
*
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
* @param string $secret The secret to use to decrypt the data.
*
* @return string The decrypted data.
* @throws \InvalidArgumentException If $ciphertext is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @see \SimpleSAML\Utils\Crypto::aesDecrypt()
*/
private static function _aesDecrypt($ciphertext, $secret)
{
if (!is_string($ciphertext)) {
throw new \InvalidArgumentException(
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
);
}
/** @var int $len */
$len = mb_strlen($ciphertext, '8bit');
if ($len < 48) {
throw new \InvalidArgumentException(
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
);
}
if (!function_exists("openssl_decrypt")) {
throw new \SimpleSAML_Error_Exception("The openssl PHP module is not loaded.");
}
// derive encryption and authentication keys from the secret
$key = openssl_digest($secret, 'sha512');
$hmac = mb_substr($ciphertext, 0, 32, '8bit');
$iv = mb_substr($ciphertext, 32, 16, '8bit');
$msg = mb_substr($ciphertext, 48, $len - 48, '8bit');
// authenticate the ciphertext
if (self::secureCompare(hash_hmac('sha256', $iv.$msg, substr($key, 64, 64), true), $hmac)) {
$plaintext = openssl_decrypt(
$msg,
'AES-256-CBC',
substr($key, 0, 64),
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
$iv
);
if ($plaintext !== false) {
return $plaintext;
}
}
throw new \SimpleSAML_Error_Exception("Failed to decrypt ciphertext.");
}
/**
* Decrypt data using AES-256-CBC and the system-wide secret salt as key.
*
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
*
* @return string The decrypted data.
* @throws \InvalidArgumentException If $ciphertext is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function aesDecrypt($ciphertext)
{
return self::_aesDecrypt($ciphertext, Config::getSecretSalt());
}
/**
* Encrypt data using AES-256-CBC and the key provided as a parameter.
*
* @param string $data The data to encrypt.
* @param string $secret The secret to use to encrypt the data.
*
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
* @throws \InvalidArgumentException If $data is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @see \SimpleSAML\Utils\Crypto::aesEncrypt()
*/
private static function _aesEncrypt($data, $secret)
{
if (!is_string($data)) {
throw new \InvalidArgumentException('Input parameter "$data" must be a string.');
}
if (!function_exists("openssl_encrypt")) {
throw new \SimpleSAML_Error_Exception('The openssl PHP module is not loaded.');
}
// derive encryption and authentication keys from the secret
$key = openssl_digest($secret, 'sha512');
// generate a random IV
$iv = openssl_random_pseudo_bytes(16);
// encrypt the message
/** @var string|false $ciphertext */
$ciphertext = openssl_encrypt(
$data,
'AES-256-CBC',
substr($key, 0, 64),
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
$iv
);
if ($ciphertext === false) {
throw new \SimpleSAML_Error_Exception("Failed to encrypt plaintext.");
}
// return the ciphertext with proper authentication
return hash_hmac('sha256', $iv.$ciphertext, substr($key, 64, 64), true).$iv.$ciphertext;
}
/**
* Encrypt data using AES-256-CBC and the system-wide secret salt as key.
*
* @param string $data The data to encrypt.
*
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
* @throws \InvalidArgumentException If $data is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function aesEncrypt($data)
{
return self::_aesEncrypt($data, Config::getSecretSalt());
}
/**
* Convert data from DER to PEM encoding.
*
* @param string $der Data encoded in DER format.
* @param string $type The type of data we are encoding, as expressed by the PEM header. Defaults to "CERTIFICATE".
* @return string The same data encoded in PEM format.
* @see RFC7648 for known types and PEM format specifics.
*/
public static function der2pem($der, $type = 'CERTIFICATE')
{
return "-----BEGIN ".$type."-----\n".
chunk_split(base64_encode($der), 64, "\n").
"-----END ".$type."-----\n";
}
/**
* Load a private key from metadata.
*
* This function loads a private key from a metadata array. It looks for the following elements:
* - 'privatekey': Name of a private key file in the cert-directory.
* - 'privatekey_pass': Password for the private key.
*
* It returns and array with the following elements:
* - 'PEM': Data for the private key, in PEM-format.
* - 'password': Password for the private key.
*
* @param \SimpleSAML_Configuration $metadata The metadata array the private key should be loaded from.
* @param bool $required Whether the private key is required. If this is true, a
* missing key will cause an exception. Defaults to false.
* @param string $prefix The prefix which should be used when reading from the metadata
* array. Defaults to ''.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
*
* @return array|NULL Extracted private key, or NULL if no private key is present.
* @throws \InvalidArgumentException If $required is not boolean or $prefix is not a string.
* @throws \SimpleSAML_Error_Exception If no private key is found in the metadata, or it was not possible to load
* it.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function loadPrivateKey(\SimpleSAML_Configuration $metadata, $required = false, $prefix = '', $full_path = false)
{
if (!is_bool($required) || !is_string($prefix) || !is_bool($full_path)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$file = $metadata->getString($prefix.'privatekey', null);
if ($file === null) {
// no private key found
if ($required) {
throw new \SimpleSAML_Error_Exception('No private key found in metadata.');
} else {
return null;
}
}
if (!$full_path) {
$file = Config::getCertPath($file);
}
$data = @file_get_contents($file);
if ($data === false) {
throw new \SimpleSAML_Error_Exception('Unable to load private key from file "'.$file.'"');
}
$ret = array(
'PEM' => $data,
);
if ($metadata->hasValue($prefix.'privatekey_pass')) {
$ret['password'] = $metadata->getString($prefix.'privatekey_pass');
}
return $ret;
}
/**
* Get public key or certificate from metadata.
*
* This function implements a function to retrieve the public key or certificate from a metadata array.
*
* It will search for the following elements in the metadata:
* - 'certData': The certificate as a base64-encoded string.
* - 'certificate': A file with a certificate or public key in PEM-format.
* - 'certFingerprint': The fingerprint of the certificate. Can be a single fingerprint, or an array of multiple
* valid fingerprints. (deprecated)
*
* This function will return an array with these elements:
* - 'PEM': The public key/certificate in PEM-encoding.
* - 'certData': The certificate data, base64 encoded, on a single line. (Only present if this is a certificate.)
* - 'certFingerprint': Array of valid certificate fingerprints. (Deprecated. Only present if this is a
* certificate.)
*
* @param \SimpleSAML_Configuration $metadata The metadata.
* @param bool $required Whether the private key is required. If this is TRUE, a missing key
* will cause an exception. Default is FALSE.
* @param string $prefix The prefix which should be used when reading from the metadata array.
* Defaults to ''.
*
* @return array|NULL Public key or certificate data, or NULL if no public key or certificate was found.
* @throws \InvalidArgumentException If $metadata is not an instance of \SimpleSAML_Configuration, $required is not
* boolean or $prefix is not a string.
* @throws \SimpleSAML_Error_Exception If no private key is found in the metadata, or it was not possible to load
* it.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Lasse Birnbaum Jensen
*/
public static function loadPublicKey(\SimpleSAML_Configuration $metadata, $required = false, $prefix = '')
{
if (!is_bool($required) || !is_string($prefix)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$keys = $metadata->getPublicKeys(null, false, $prefix);
if (!empty($keys)) {
foreach ($keys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
if ($key['signing'] !== true) {
continue;
}
$certData = $key['X509Certificate'];
$pem = "-----BEGIN CERTIFICATE-----\n".
chunk_split($certData, 64).
"-----END CERTIFICATE-----\n";
$certFingerprint = strtolower(sha1(base64_decode($certData)));
return array(
'certData' => $certData,
'PEM' => $pem,
'certFingerprint' => array($certFingerprint),
);
}
// no valid key found
} elseif ($metadata->hasValue($prefix.'certFingerprint')) {
// we only have a fingerprint available
$fps = $metadata->getArrayizeString($prefix.'certFingerprint');
// normalize fingerprint(s) - lowercase and no colons
foreach ($fps as &$fp) {
assert(is_string($fp));
$fp = strtolower(str_replace(':', '', $fp));
}
/*
* We can't build a full certificate from a fingerprint, and may as well return an array with only the
* fingerprint(s) immediately.
*/
return array('certFingerprint' => $fps);
}
// no public key/certificate available
if ($required) {
throw new \SimpleSAML_Error_Exception('No public key / certificate found in metadata.');
} else {
return null;
}
}
/**
* Convert from PEM to DER encoding.
*
* @param string $pem Data encoded in PEM format.
* @return string The same data encoded in DER format.
* @throws \InvalidArgumentException If $pem is not encoded in PEM format.
* @see RFC7648 for PEM format specifics.
*/
public static function pem2der($pem)
{
$pem = trim($pem);
$begin = "-----BEGIN ";
$end = "-----END ";
$lines = explode("\n", $pem);
$last = count($lines) - 1;
if (strpos($lines[0], $begin) !== 0) {
throw new \InvalidArgumentException("pem2der: input is not encoded in PEM format.");
}
unset($lines[0]);
if (strpos($lines[$last], $end) !== 0) {
throw new \InvalidArgumentException("pem2der: input is not encoded in PEM format.");
}
unset($lines[$last]);
return base64_decode(implode($lines));
}
/**
* This function hashes a password with a given algorithm.
*
* @param string $password The password to hash.
* @param string $algorithm The hashing algorithm, uppercase, optionally prepended with 'S' (salted). See
* hash_algos() for a complete list of hashing algorithms.
* @param string $salt An optional salt to use.
*
* @return string The hashed password.
* @throws \InvalidArgumentException If the input parameters are not strings.
* @throws \SimpleSAML_Error_Exception If the algorithm specified is not supported.
*
* @see hash_algos()
*
* @author Dyonisius Visser, TERENA <visser@terena.org>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function pwHash($password, $algorithm, $salt = null)
{
if (!is_string($algorithm) || !is_string($password)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// hash w/o salt
if (in_array(strtolower($algorithm), hash_algos(), true)) {
$alg_str = '{'.str_replace('SHA1', 'SHA', $algorithm).'}'; // LDAP compatibility
$hash = hash(strtolower($algorithm), $password, true);
return $alg_str.base64_encode($hash);
}
// hash w/ salt
if ($salt === null) { // no salt provided, generate one
// default 8 byte salt, but 4 byte for LDAP SHA1 hashes
$bytes = ($algorithm == 'SSHA1') ? 4 : 8;
$salt = openssl_random_pseudo_bytes($bytes);
}
if ($algorithm[0] == 'S' && in_array(substr(strtolower($algorithm), 1), hash_algos(), true)) {
$alg = substr(strtolower($algorithm), 1); // 'sha256' etc
$alg_str = '{'.str_replace('SSHA1', 'SSHA', $algorithm).'}'; // LDAP compatibility
$hash = hash($alg, $password.$salt, true);
return $alg_str.base64_encode($hash.$salt);
}
throw new \SimpleSAML_Error_Exception('Hashing algorithm \''.strtolower($algorithm).'\' is not supported');
}
/**
* Compare two strings securely.
*
* This method checks if two strings are equal in constant time, avoiding timing attacks. Use it every time we need
* to compare a string with a secret that shouldn't be leaked, i.e. when verifying passwords, one-time codes, etc.
*
* @param string $known A known string.
* @param string $user A user-provided string to compare with the known string.
*
* @return bool True if both strings are equal, false otherwise.
*/
public static function secureCompare($known, $user)
{
if (function_exists('hash_equals')) {
// use hash_equals() if available (PHP >= 5.6)
return hash_equals($known, $user);
}
// compare manually in constant time
$len = mb_strlen($known, '8bit'); // see mbstring.func_overload
if ($len !== mb_strlen($user, '8bit')) {
return false; // length differs
}
$diff = 0;
for ($i = 0; $i < $len; $i++) {
$diff |= ord($known[$i]) ^ ord($user[$i]);
}
// if all the bytes in $a and $b are identical, $diff should be equal to 0
return $diff === 0;
}
/**
* This function checks if a password is valid
*
* @param string $hash The password as it appears in password file, optionally prepended with algorithm.
* @param string $password The password to check in clear.
*
* @return boolean True if the hash corresponds with the given password, false otherwise.
* @throws \InvalidArgumentException If the input parameters are not strings.
* @throws \SimpleSAML_Error_Exception If the algorithm specified is not supported.
*
* @author Dyonisius Visser, TERENA <visser@terena.org>
*/
public static function pwValid($hash, $password)
{
if (!is_string($hash) || !is_string($password)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// match algorithm string (e.g. '{SSHA256}', '{MD5}')
if (preg_match('/^{(.*?)}(.*)$/', $hash, $matches)) {
// LDAP compatibility
$alg = preg_replace('/^(S?SHA)$/', '${1}1', $matches[1]);
// hash w/o salt
if (in_array(strtolower($alg), hash_algos(), true)) {
return self::secureCompare($hash, self::pwHash($password, $alg));
}
// hash w/ salt
if ($alg[0] === 'S' && in_array(substr(strtolower($alg), 1), hash_algos(), true)) {
$php_alg = substr(strtolower($alg), 1);
// get hash length of this algorithm to learn how long the salt is
$hash_length = strlen(hash($php_alg, '', true));
$salt = substr(base64_decode($matches[2]), $hash_length);
return self::secureCompare($hash, self::pwHash($password, $alg, $salt));
}
} else {
return $hash === $password;
}
throw new \SimpleSAML_Error_Exception('Hashing algorithm \''.strtolower($alg).'\' is not supported');
}
}

1222
lib/SimpleSAML/Utils/HTTP.php Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
<?php
namespace SimpleSAML\Utils;
/**
* Provides a non-static wrapper for the HTTP utility class.
*
* @package SimpleSAML\Utils
*/
class HttpAdapter
{
/**
* @see HTTP::getServerHTTPS()
*/
public function getServerHTTPS()
{
return HTTP::getServerHTTPS();
}
/**
* @see HTTP::getServerPort()
*/
public function getServerPort()
{
return HTTP::getServerPort();
}
/**
* @see HTTP::addURLParameters()
*/
public function addURLParameters($url, $parameters)
{
return HTTP::addURLParameters($url, $parameters);
}
/**
* @see HTTP::checkSessionCookie()
*/
public function checkSessionCookie($retryURL = null)
{
HTTP::checkSessionCookie($retryURL);
}
/**
* @see HTTP::checkURLAllowed()
*/
public function checkURLAllowed($url, array $trustedSites = null)
{
return HTTP::checkURLAllowed($url, $trustedSites);
}
/**
* @see HTTP::fetch()
*/
public function fetch($url, $context = array(), $getHeaders = false)
{
return HTTP::fetch($url, $context, $getHeaders);
}
/**
* @see HTTP::getAcceptLanguage()
*/
public function getAcceptLanguage()
{
return HTTP::getAcceptLanguage();
}
/**
* @see HTTP::guessBasePath()
*/
public function guessBasePath()
{
return HTTP::guessBasePath();
}
/**
* @see HTTP::getBaseURL()
*/
public function getBaseURL()
{
return HTTP::getBaseURL();
}
/**
* @see HTTP::getFirstPathElement()
*/
public function getFirstPathElement($trailingslash = true)
{
return HTTP::getFirstPathElement($trailingslash);
}
/**
* @see HTTP::getPOSTRedirectURL()
*/
public function getPOSTRedirectURL($destination, $data)
{
return HTTP::getPOSTRedirectURL($destination, $data);
}
/**
* @see HTTP::getSelfHost()
*/
public function getSelfHost()
{
return HTTP::getSelfHost();
}
/**
* @see HTTP::getSelfHostWithNonStandardPort()
*/
public function getSelfHostWithNonStandardPort()
{
return HTTP::getSelfHostWithNonStandardPort();
}
/**
* @see HTTP::getSelfHostWithPath()
*/
public function getSelfHostWithPath()
{
return HTTP::getSelfHostWithPath();
}
/**
* @see HTTP::getSelfURL()
*/
public function getSelfURL()
{
return HTTP::getSelfURL();
}
/**
* @see HTTP::getSelfURLHost()
*/
public function getSelfURLHost()
{
return HTTP::getSelfURLHost();
}
/**
* @see HTTP::getSelfURLNoQuery()
*/
public function getSelfURLNoQuery()
{
return HTTP::getSelfURLNoQuery();
}
/**
* @see HTTP::isHTTPS()
*/
public function isHTTPS()
{
return HTTP::isHTTPS();
}
/**
* @see HTTP::normalizeURL()
*/
public function normalizeURL($url)
{
return HTTP::normalizeURL($url);
}
/**
* @see HTTP::parseQueryString()
*/
public function parseQueryString($query_string)
{
return HTTP::parseQueryString($query_string);
}
/**
* @see HTTP::redirectTrustedURL()
*/
public function redirectTrustedURL($url, $parameters = array())
{
HTTP::redirectTrustedURL($url, $parameters);
}
/**
* @see HTTP::redirectUntrustedURL()
*/
public function redirectUntrustedURL($url, $parameters = array())
{
HTTP::redirectUntrustedURL($url, $parameters);
}
/**
* @see HTTP::resolveURL()
*/
public function resolveURL($url, $base = null)
{
return HTTP::resolveURL($url, $base);
}
/**
* @see HTTP::setCookie()
*/
public function setCookie($name, $value, $params = null, $throw = true)
{
HTTP::setCookie($name, $value, $params, $throw);
}
/**
* @see HTTP::submitPOSTData()
*/
public function submitPOSTData($destination, $data)
{
HTTP::submitPOSTData($destination, $data);
}
}

85
lib/SimpleSAML/Utils/Net.php Executable file
View File

@@ -0,0 +1,85 @@
<?php
namespace SimpleSAML\Utils;
/**
* Net-related utility methods.
*
* @package SimpleSAMLphp
*/
class Net
{
/**
* Check whether an IP address is part of a CIDR.
*
* @param string $cidr The network CIDR address.
* @param string $ip The IP address to check. Optional. Current remote address will be used if none specified. Do
* not rely on default parameter if running behind load balancers.
*
* @return boolean True if the IP address belongs to the specified CIDR, false otherwise.
*
* @author Andreas Åkre Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Brook Schofield, GÉANT
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function ipCIDRcheck($cidr, $ip = null)
{
if ($ip === null) {
$ip = $_SERVER['REMOTE_ADDR'];
}
if (strpos($cidr, '/') === false) {
return false;
}
list ($net, $mask) = explode('/', $cidr);
$mask = intval($mask);
$ip_ip = array();
$ip_net = array();
if (strstr($ip, ':') || strstr($net, ':')) {
// Validate IPv6 with inet_pton, convert to hex with bin2hex
// then store as a long with hexdec
$ip_pack = @inet_pton($ip);
$net_pack = @inet_pton($net);
if ($ip_pack === false || $net_pack === false) {
// not valid IPv6 address (warning silenced)
return false;
}
$ip_ip = str_split(bin2hex($ip_pack), 8);
foreach ($ip_ip as &$value) {
$value = hexdec($value);
}
$ip_net = str_split(bin2hex($net_pack), 8);
foreach ($ip_net as &$value) {
$value = hexdec($value);
}
} else {
$ip_ip[0] = ip2long($ip);
$ip_net[0] = ip2long($net);
}
for ($i = 0; $mask > 0 && $i < sizeof($ip_ip); $i++) {
if ($mask > 32) {
$iteration_mask = 32;
} else {
$iteration_mask = $mask;
}
$mask -= 32;
$ip_mask = ~((1 << (32 - $iteration_mask)) - 1);
$ip_net_mask = $ip_net[$i] & $ip_mask;
$ip_ip_mask = $ip_ip[$i] & $ip_mask;
if ($ip_ip_mask != $ip_net_mask) {
return false;
}
}
return true;
}
}

30
lib/SimpleSAML/Utils/Random.php Executable file
View File

@@ -0,0 +1,30 @@
<?php
namespace SimpleSAML\Utils;
/**
* Utility class for random data generation and manipulation.
*
* @package SimpleSAMLphp
*/
class Random
{
/**
* The fixed length of random identifiers.
*/
const ID_LENGTH = 43;
/**
* Generate a random identifier, ID_LENGTH bytes long.
*
* @return string A ID_LENGTH-bytes long string with a random, hex-encoded string.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function generateID()
{
return '_'.bin2hex(openssl_random_pseudo_bytes((int)((self::ID_LENGTH - 1)/2)));
}
}

239
lib/SimpleSAML/Utils/System.php Executable file
View File

@@ -0,0 +1,239 @@
<?php
namespace SimpleSAML\Utils;
/**
* System-related utility methods.
*
* @package SimpleSAMLphp
*/
class System
{
const WINDOWS = 1;
const LINUX = 2;
const OSX = 3;
const HPUX = 4;
const UNIX = 5;
const BSD = 6;
const IRIX = 7;
const SUNOS = 8;
/**
* This function returns the Operating System we are running on.
*
* @return mixed A predefined constant identifying the OS we are running on. False if we are unable to determine it.
*
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function getOS()
{
if (stristr(PHP_OS, 'LINUX')) {
return self::LINUX;
}
if (stristr(PHP_OS, 'DARWIN')) {
return self::OSX;
}
if (stristr(PHP_OS, 'WIN')) {
return self::WINDOWS;
}
if (stristr(PHP_OS, 'BSD')) {
return self::BSD;
}
if (stristr(PHP_OS, 'UNIX')) {
return self::UNIX;
}
if (stristr(PHP_OS, 'HP-UX')) {
return self::HPUX;
}
if (stristr(PHP_OS, 'IRIX')) {
return self::IRIX;
}
if (stristr(PHP_OS, 'SUNOS')) {
return self::SUNOS;
}
return false;
}
/**
* This function retrieves the path to a directory where temporary files can be saved.
*
* @return string Path to a temporary directory, without a trailing directory separator.
* @throws \SimpleSAML_Error_Exception If the temporary directory cannot be created or it exists and does not belong
* to the current user.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function getTempDir()
{
$globalConfig = \SimpleSAML_Configuration::getInstance();
$tempDir = rtrim(
$globalConfig->getString(
'tempdir',
sys_get_temp_dir().DIRECTORY_SEPARATOR.'simplesaml'
),
DIRECTORY_SEPARATOR
);
if (!is_dir($tempDir)) {
if (!mkdir($tempDir, 0700, true)) {
$error = error_get_last();
throw new \SimpleSAML_Error_Exception(
'Error creating temporary directory "'.$tempDir.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
} elseif (function_exists('posix_getuid')) {
// check that the owner of the temp directory is the current user
$stat = lstat($tempDir);
if ($stat['uid'] !== posix_getuid()) {
throw new \SimpleSAML_Error_Exception(
'Temporary directory "'.$tempDir.'" does not belong to the current user.'
);
}
}
return $tempDir;
}
/**
* Resolve a (possibly) relative path from the given base path.
*
* A path which starts with a '/' is assumed to be absolute, all others are assumed to be
* relative. The default base path is the root of the SimpleSAMLphp installation.
*
* @param string $path The path we should resolve.
* @param string|null $base The base path, where we should search for $path from. Default value is the root of the
* SimpleSAMLphp installation.
*
* @return string An absolute path referring to $path.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function resolvePath($path, $base = null)
{
if ($base === null) {
$config = \SimpleSAML_Configuration::getInstance();
$base = $config->getBaseDir();
}
// normalise directory separator
$base = str_replace('\\', '/', $base);
$path = str_replace('\\', '/', $path);
// remove trailing slashes
$base = rtrim($base, '/');
$path = rtrim($path, '/');
// check for absolute path
if (substr($path, 0, 1) === '/') {
// absolute path. */
$ret = '/';
} elseif (static::pathContainsDriveLetter($path)) {
$ret = '';
} else {
// path relative to base
$ret = $base;
}
$path = explode('/', $path);
foreach ($path as $d) {
if ($d === '.') {
continue;
} elseif ($d === '..') {
$ret = dirname($ret);
} else {
if ($ret && substr($ret, -1) !== '/') {
$ret .= '/';
}
$ret .= $d;
}
}
return $ret;
}
/**
* Atomically write a file.
*
* This is a helper function for writing data atomically to a file. It does this by writing the file data to a
* temporary file, then renaming it to the required file name.
*
* @param string $filename The path to the file we want to write to.
* @param string $data The data we should write to the file.
* @param int $mode The permissions to apply to the file. Defaults to 0600.
*
* @throws \InvalidArgumentException If any of the input parameters doesn't have the proper types.
* @throws \SimpleSAML_Error_Exception If the file cannot be saved, permissions cannot be changed or it is not
* possible to write to the target file.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Andjelko Horvat
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*
* @return void
*/
public static function writeFile($filename, $data, $mode = 0600)
{
if (!is_string($filename) || !is_string($data) || !is_numeric($mode)) {
throw new \InvalidArgumentException('Invalid input parameters');
}
$tmpFile = self::getTempDir().DIRECTORY_SEPARATOR.rand();
$res = @file_put_contents($tmpFile, $data);
if ($res === false) {
$error = error_get_last();
throw new \SimpleSAML_Error_Exception(
'Error saving file "'.$tmpFile.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
if (self::getOS() !== self::WINDOWS) {
if (!chmod($tmpFile, $mode)) {
unlink($tmpFile);
$error = error_get_last();
//$error = (is_array($error) ? $error['message'] : 'no error available');
throw new \SimpleSAML_Error_Exception(
'Error changing file mode of "'.$tmpFile.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
}
if (!rename($tmpFile, $filename)) {
unlink($tmpFile);
$error = error_get_last();
throw new \SimpleSAML_Error_Exception(
'Error moving "'.$tmpFile.'" to "'.$filename.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
if (function_exists('opcache_invalidate')) {
opcache_invalidate($filename);
}
}
/**
* Check if the supplied path contains a Windows-style drive letter.
*
* @param string $path
*
* @return bool
*/
private static function pathContainsDriveLetter($path)
{
$letterAsciiValue = ord(strtoupper(substr($path, 0, 1)));
return substr($path, 1, 1) === ':'
&& $letterAsciiValue >= 65 && $letterAsciiValue <= 90;
}
}

167
lib/SimpleSAML/Utils/Time.php Executable file
View File

@@ -0,0 +1,167 @@
<?php
/**
* Time-related utility methods.
*
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Utils;
use SimpleSAML\Logger;
class Time
{
/**
* Whether the timezone has been initialized or not.
*
* @var bool
*/
private static $tz_initialized = false;
/**
* This function generates a timestamp on the form used by the SAML protocols.
*
* @param int $instant The time the timestamp should represent. Defaults to current time.
*
* @return string The timestamp.
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function generateTimestamp($instant = null)
{
if ($instant === null) {
$instant = time();
}
return gmdate('Y-m-d\TH:i:s\Z', $instant);
}
/**
* Initialize the timezone.
*
* This function should be called before any calls to date().
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*
* @throws \SimpleSAML_Error_Exception If the timezone set in the configuration is invalid.
*
* @return void
*/
public static function initTimezone()
{
if (self::$tz_initialized) {
return;
}
$globalConfig = \SimpleSAML_Configuration::getInstance();
$timezone = $globalConfig->getString('timezone', null);
if ($timezone !== null) {
if (!date_default_timezone_set($timezone)) {
throw new \SimpleSAML_Error_Exception('Invalid timezone set in the "timezone" option in config.php.');
}
self::$tz_initialized = true;
return;
}
// we don't have a timezone configured
Logger::maskErrors(E_ALL);
$serverTimezone = date_default_timezone_get();
Logger::popErrorMask();
// set the timezone to the default
date_default_timezone_set($serverTimezone);
self::$tz_initialized = true;
}
/**
* Interpret a ISO8601 duration value relative to a given timestamp. Please note no fractions are allowed, neither
* durations specified in the formats PYYYYMMDDThhmmss nor P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss].
*
* @param string $duration The duration, as a string.
* @param int $timestamp The unix timestamp we should apply the duration to. Optional, default to the current
* time.
*
* @return int The new timestamp, after the duration is applied.
* @throws \InvalidArgumentException If $duration is not a valid ISO 8601 duration or if the input parameters do
* not have the right data types.
*/
public static function parseDuration($duration, $timestamp = null)
{
if (!(is_string($duration) && (is_int($timestamp) || is_null($timestamp)))) {
throw new \InvalidArgumentException('Invalid input parameters');
}
// parse the duration. We use a very strict pattern
$durationRegEx = '#^(-?)P(?:(?:(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)'.
'(?:[.,]\d+)?S)?)?)|(?:(\\d+)W))$#D';
if (!preg_match($durationRegEx, $duration, $matches)) {
throw new \InvalidArgumentException('Invalid ISO 8601 duration: '.$duration);
}
$durYears = (empty($matches[2]) ? 0 : (int) $matches[2]);
$durMonths = (empty($matches[3]) ? 0 : (int) $matches[3]);
$durDays = (empty($matches[4]) ? 0 : (int) $matches[4]);
$durHours = (empty($matches[5]) ? 0 : (int) $matches[5]);
$durMinutes = (empty($matches[6]) ? 0 : (int) $matches[6]);
$durSeconds = (empty($matches[7]) ? 0 : (int) $matches[7]);
$durWeeks = (empty($matches[8]) ? 0 : (int) $matches[8]);
if (!empty($matches[1])) {
// negative
$durYears = -$durYears;
$durMonths = -$durMonths;
$durDays = -$durDays;
$durHours = -$durHours;
$durMinutes = -$durMinutes;
$durSeconds = -$durSeconds;
$durWeeks = -$durWeeks;
}
if ($timestamp === null) {
$timestamp = time();
}
if ($durYears !== 0 || $durMonths !== 0) {
/* Special handling of months and years, since they aren't a specific interval, but
* instead depend on the current time.
*/
/* We need the year and month from the timestamp. Unfortunately, PHP doesn't have the
* gmtime function. Instead we use the gmdate function, and split the result.
*/
$yearmonth = explode(':', gmdate('Y:n', $timestamp));
$year = (int) ($yearmonth[0]);
$month = (int) ($yearmonth[1]);
// remove the year and month from the timestamp
$timestamp -= gmmktime(0, 0, 0, $month, 1, $year);
// add years and months, and normalize the numbers afterwards
$year += $durYears;
$month += $durMonths;
while ($month > 12) {
$year += 1;
$month -= 12;
}
while ($month < 1) {
$year -= 1;
$month += 12;
}
// add year and month back into timestamp
$timestamp += gmmktime(0, 0, 0, $month, 1, $year);
}
// add the other elements
$timestamp += $durWeeks * 7 * 24 * 60 * 60;
$timestamp += $durDays * 24 * 60 * 60;
$timestamp += $durHours * 60 * 60;
$timestamp += $durMinutes * 60;
$timestamp += $durSeconds;
return $timestamp;
}
}

453
lib/SimpleSAML/Utils/XML.php Executable file
View File

@@ -0,0 +1,453 @@
<?php
/**
* Utility class for XML and DOM manipulation.
*
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Utils;
use SimpleSAML\Logger;
use SimpleSAML\XML\Errors;
class XML
{
/**
* This function performs some sanity checks on XML documents, and optionally validates them against their schema
* if the 'validatexml' debugging option is enabled. A warning will be printed to the log if validation fails.
*
* @param string $message The SAML document we want to check.
* @param string $type The type of document. Can be one of:
* - 'saml20'
* - 'saml11'
* - 'saml-meta'
*
* @throws \InvalidArgumentException If $message is not a string or $type is not a string containing one of the
* values allowed.
* @throws \SimpleSAML_Error_Exception If $message contains a doctype declaration.
*
* @return void
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function checkSAMLMessage($message, $type)
{
$allowed_types = array('saml20', 'saml11', 'saml-meta');
if (!(is_string($message) && in_array($type, $allowed_types, true))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// a SAML message should not contain a doctype-declaration
if (strpos($message, '<!DOCTYPE') !== false) {
throw new \SimpleSAML_Error_Exception('XML contained a doctype declaration.');
}
// see if debugging is enabled for XML validation
$debug = \SimpleSAML_Configuration::getInstance()->getArrayize('debug', array('validatexml' => false));
$enabled = \SimpleSAML_Configuration::getInstance()->getBoolean('debug.validatexml', false);
if (!(in_array('validatexml', $debug, true) // implicitly enabled
|| (array_key_exists('validatexml', $debug) && $debug['validatexml'] === true) // explicitly enabled
// TODO: deprecate this option and remove it in 2.0
|| $enabled // old 'debug.validatexml' configuration option
)) {
// XML validation is disabled
return;
}
$result = true;
switch ($type) {
case 'saml11':
$result = self::isValid($message, 'oasis-sstc-saml-schema-protocol-1.1.xsd');
break;
case 'saml20':
$result = self::isValid($message, 'saml-schema-protocol-2.0.xsd');
break;
case 'saml-meta':
$result = self::isValid($message, 'saml-schema-metadata-2.0.xsd');
}
if ($result !== true) {
Logger::warning($result);
}
}
/**
* Helper function to log SAML messages that we send or receive.
*
* @param string|\DOMElement $message The message, as an string containing the XML or an XML element.
* @param string $type Whether this message is sent or received, encrypted or decrypted. The following
* values are supported:
* - 'in': for messages received.
* - 'out': for outgoing messages.
* - 'decrypt': for decrypted messages.
* - 'encrypt': for encrypted messages.
*
* @throws \InvalidArgumentException If $type is not a string or $message is neither a string nor a \DOMElement.
*
* @return void
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function debugSAMLMessage($message, $type)
{
if (!(is_string($type) && (is_string($message) || $message instanceof \DOMElement))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// see if debugging is enabled for SAML messages
$debug = \SimpleSAML_Configuration::getInstance()->getArrayize('debug', array('saml' => false));
if (!(in_array('saml', $debug, true) // implicitly enabled
|| (array_key_exists('saml', $debug) && $debug['saml'] === true) // explicitly enabled
// TODO: deprecate the old style and remove it in 2.0
|| (array_key_exists(0, $debug) && $debug[0] === true) // old style 'debug'
)) {
// debugging messages is disabled
return;
}
if ($message instanceof \DOMElement) {
$message = $message->ownerDocument->saveXML($message);
}
switch ($type) {
case 'in':
Logger::debug('Received message:');
break;
case 'out':
Logger::debug('Sending message:');
break;
case 'decrypt':
Logger::debug('Decrypted message:');
break;
case 'encrypt':
Logger::debug('Encrypted message:');
break;
default:
assert(false);
}
$str = self::formatXMLString($message);
foreach (explode("\n", $str) as $line) {
Logger::debug($line);
}
}
/**
* Format a DOM element.
*
* This function takes in a DOM element, and inserts whitespace to make it more readable. Note that whitespace
* added previously will be removed.
*
* @param \DOMNode $root The root element which should be formatted.
* @param string $indentBase The indentation this element should be assumed to have. Defaults to an empty
* string.
*
* @throws \InvalidArgumentException If $root is not a DOMElement or $indentBase is not a string.
*
* @return void
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function formatDOMElement(\DOMNode $root, $indentBase = '')
{
if (!is_string($indentBase)) {
throw new \InvalidArgumentException('Invalid input parameters');
}
// check what this element contains
$fullText = ''; // all text in this element
$textNodes = array(); // text nodes which should be deleted
$childNodes = array(); // other child nodes
for ($i = 0; $i < $root->childNodes->length; $i++) {
/** @var \DOMElement $child */
$child = $root->childNodes->item($i);
if ($child instanceof \DOMText) {
$textNodes[] = $child;
$fullText .= $child->wholeText;
} elseif ($child instanceof \DOMComment || $child instanceof \DOMElement) {
$childNodes[] = $child;
} else {
// unknown node type. We don't know how to format this
return;
}
}
$fullText = trim($fullText);
if (strlen($fullText) > 0) {
// we contain textelf
$hasText = true;
} else {
$hasText = false;
}
$hasChildNode = (count($childNodes) > 0);
if ($hasText && $hasChildNode) {
// element contains both text and child nodes - we don't know how to format this one
return;
}
// remove text nodes
foreach ($textNodes as $node) {
$root->removeChild($node);
}
if ($hasText) {
// only text - add a single text node to the element with the full text
$root->appendChild(new \DOMText($fullText));
return;
}
if (!$hasChildNode) {
// empty node. Nothing to do
return;
}
/* Element contains only child nodes - add indentation before each one, and
* format child elements.
*/
$childIndentation = $indentBase.' ';
foreach ($childNodes as $node) {
// add indentation before node
$root->insertBefore(new \DOMText("\n".$childIndentation), $node);
// format child elements
if ($node instanceof \DOMElement) {
self::formatDOMElement($node, $childIndentation);
}
}
// add indentation before closing tag
$root->appendChild(new \DOMText("\n".$indentBase));
}
/**
* Format an XML string.
*
* This function formats an XML string using the formatDOMElement() function.
*
* @param string $xml An XML string which should be formatted.
* @param string $indentBase Optional indentation which should be applied to all the output. Optional, defaults
* to ''.
*
* @return string The formatted string.
* @throws \InvalidArgumentException If the parameters are not strings.
* @throws \DOMException If the input does not parse correctly as an XML string.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function formatXMLString($xml, $indentBase = '')
{
if (!is_string($xml) || !is_string($indentBase)) {
throw new \InvalidArgumentException('Invalid input parameters');
}
try {
$doc = \SAML2\DOMDocumentFactory::fromString($xml);
} catch (\Exception $e) {
throw new \DOMException('Error parsing XML string.');
}
$root = $doc->firstChild;
self::formatDOMElement($root, $indentBase);
return $doc->saveXML($root);
}
/**
* This function finds direct descendants of a DOM element with the specified
* localName and namespace. They are returned in an array.
*
* This function accepts the same shortcuts for namespaces as the isDOMNodeOfType function.
*
* @param \DOMNode $element The element we should look in.
* @param string $localName The name the element should have.
* @param string $namespaceURI The namespace the element should have.
*
* @return array Array with the matching elements in the order they are found. An empty array is
* returned if no elements match.
* @throws \InvalidArgumentException If $element is not an instance of DOMElement, $localName is not a string or
* $namespaceURI is not a string.
*/
public static function getDOMChildren(\DOMNode $element, $localName, $namespaceURI)
{
if (!is_string($localName) || !is_string($namespaceURI)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$ret = array();
for ($i = 0; $i < $element->childNodes->length; $i++) {
/** @var \DOMElement $child */
$child = $element->childNodes->item($i);
// skip text nodes and comment elements
if ($child instanceof \DOMText || $child instanceof \DOMComment) {
continue;
}
if (self::isDOMNodeOfType($child, $localName, $namespaceURI) === true) {
$ret[] = $child;
}
}
return $ret;
}
/**
* This function extracts the text from DOMElements which should contain only text content.
*
* @param \DOMElement $element The element we should extract text from.
*
* @return string The text content of the element.
* @throws \SimpleSAML_Error_Exception If the element contains a non-text child node.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getDOMText(\DOMElement $element)
{
$txt = '';
for ($i = 0; $i < $element->childNodes->length; $i++) {
/** @var \DOMElement $child */
$child = $element->childNodes->item($i);
if (!($child instanceof \DOMText)) {
throw new \SimpleSAML_Error_Exception($element->localName.' contained a non-text child node.');
}
$txt .= $child->wholeText;
}
$txt = trim($txt);
return $txt;
}
/**
* This function checks if the DOMElement has the correct localName and namespaceURI.
*
* We also define the following shortcuts for namespaces:
* - '@ds': 'http://www.w3.org/2000/09/xmldsig#'
* - '@md': 'urn:oasis:names:tc:SAML:2.0:metadata'
* - '@saml1': 'urn:oasis:names:tc:SAML:1.0:assertion'
* - '@saml1md': 'urn:oasis:names:tc:SAML:profiles:v1metadata'
* - '@saml1p': 'urn:oasis:names:tc:SAML:1.0:protocol'
* - '@saml2': 'urn:oasis:names:tc:SAML:2.0:assertion'
* - '@saml2p': 'urn:oasis:names:tc:SAML:2.0:protocol'
*
* @param \DOMNode $element The element we should check.
* @param string $name The local name the element should have.
* @param string $nsURI The namespaceURI the element should have.
*
* @return boolean True if both namespace and local name matches, false otherwise.
* @throws \InvalidArgumentException If the namespace shortcut is unknown.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isDOMNodeOfType(\DOMNode $element, $name, $nsURI)
{
if (!is_string($name) || !is_string($nsURI) || strlen($nsURI) === 0) {
// most likely a comment-node
return false;
}
// check if the namespace is a shortcut, and expand it if it is
if ($nsURI[0] === '@') {
// the defined shortcuts
$shortcuts = array(
'@ds' => 'http://www.w3.org/2000/09/xmldsig#',
'@md' => 'urn:oasis:names:tc:SAML:2.0:metadata',
'@saml1' => 'urn:oasis:names:tc:SAML:1.0:assertion',
'@saml1md' => 'urn:oasis:names:tc:SAML:profiles:v1metadata',
'@saml1p' => 'urn:oasis:names:tc:SAML:1.0:protocol',
'@saml2' => 'urn:oasis:names:tc:SAML:2.0:assertion',
'@saml2p' => 'urn:oasis:names:tc:SAML:2.0:protocol',
'@shibmd' => 'urn:mace:shibboleth:metadata:1.0',
);
// check if it is a valid shortcut
if (!array_key_exists($nsURI, $shortcuts)) {
throw new \InvalidArgumentException('Unknown namespace shortcut: '.$nsURI);
}
// expand the shortcut
$nsURI = $shortcuts[$nsURI];
}
if ($element->localName !== $name) {
return false;
}
if ($element->namespaceURI !== $nsURI) {
return false;
}
return true;
}
/**
* This function attempts to validate an XML string against the specified schema. It will parse the string into a
* DOM document and validate this document against the schema.
*
* Note that this function returns values that are evaluated as a logical true, both when validation works and when
* it doesn't. Please use strict comparisons to check the values returned.
*
* @param string|\DOMDocument $xml The XML string or document which should be validated.
* @param string $schema The filename of the schema that should be used to validate the document.
*
* @return boolean|string Returns a string with errors found if validation fails. True if validation passes ok.
* @throws \InvalidArgumentException If $schema is not a string, or $xml is neither a string nor a \DOMDocument.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isValid($xml, $schema)
{
if (!(is_string($schema) && (is_string($xml) || $xml instanceof \DOMDocument))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
Errors::begin();
if ($xml instanceof \DOMDocument) {
$dom = $xml;
$res = true;
} else {
try {
$dom = \SAML2\DOMDocumentFactory::fromString($xml);
$res = true;
} catch (\Exception $e) {
$res = false;
}
}
if ($res) {
$config = \SimpleSAML_Configuration::getInstance();
/** @var string $schemaPath */
$schemaPath = $config->resolvePath('schemas');
$schemaFile = $schemaPath.'/'.$schema;
$res = $dom->schemaValidate($schemaFile);
if ($res) {
Errors::end();
return true;
}
$errorText = "Schema validation failed on XML string:\n";
} else {
$errorText = "Failed to parse XML string for schema validation:\n";
}
$errors = Errors::end();
$errorText .= Errors::formatErrors($errors);
return $errorText;
}
}

122
lib/SimpleSAML/XHTML/EMail.php Executable file
View File

@@ -0,0 +1,122 @@
<?php
/**
* A minimalistic Emailer class. Creates and sends HTML emails.
*
* @author Andreas kre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class SimpleSAML_XHTML_EMail
{
private $to = null;
private $cc = null;
private $body = null;
private $from = null;
private $replyto = null;
private $subject = null;
private $headers = array();
/**
* Constructor
*/
public function __construct($to, $subject, $from = null, $cc = null, $replyto = null)
{
$this->to = $to;
$this->cc = $cc;
$this->from = $from;
$this->replyto = $replyto;
$this->subject = $subject;
}
/*
* @param string $body
* @return void
*/
public function setBody($body)
{
$this->body = $body;
}
/*
* @param string $body
* @return void
*/
private function getHTML($body)
{
return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>SimpleSAMLphp Email report</title>
<style type="text/css">
pre, div.box {
margin: .4em 2em .4em 1em;
padding: 4px;
}
pre {
background: #eee;
border: 1px solid #aaa;
}
</style>
</head>
<body>
<div class="container" style="background: #fafafa; border: 1px solid #eee; margin: 2em; padding: .6em;">
' . $body . '
</div>
</body>
</html>';
}
/*
* @return void
*/
public function send()
{
if ($this->to === null) {
throw new Exception('EMail field [to] is required and not set.');
} elseif ($this->subject === null) {
throw new Exception('EMail field [subject] is required and not set.');
} elseif ($this->body === null) {
throw new Exception('EMail field [body] is required and not set.');
}
$random_hash = bin2hex(openssl_random_pseudo_bytes(16));
if (isset($this->from)) {
$this->headers[]= 'From: ' . $this->from;
}
if (isset($this->replyto)) {
$this->headers[]= 'Reply-To: ' . $this->replyto;
}
$this->headers[] = 'Content-Type: multipart/alternative; boundary="simplesamlphp-' . $random_hash . '"';
$message = '
--simplesamlphp-' . $random_hash . '
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit
' . strip_tags(html_entity_decode($this->body)) . '
--simplesamlphp-' . $random_hash . '
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 8bit
' . $this->getHTML($this->body) . '
--simplesamlphp-' . $random_hash . '--
';
$headers = implode("\n", $this->headers);
$mail_sent = @mail($this->to, $this->subject, $message, $headers);
SimpleSAML\Logger::debug('Email: Sending e-mail to [' . $this->to . '] : ' . ($mail_sent ? 'OK' : 'Failed'));
if (!$mail_sent) {
throw new Exception('Error when sending e-mail');
}
}
}

598
lib/SimpleSAML/XHTML/IdPDisco.php Executable file
View File

@@ -0,0 +1,598 @@
<?php
/**
* This class implements a generic IdP discovery service, for use in various IdP
* discovery service pages. This should reduce code duplication.
*
* Experimental support added for Extended IdP Metadata Discovery Protocol by Andreas 2008-08-28
* More information: https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-idp-discovery.pdf
*
* @author Jaime Pérez <jaime.perez@uninett.no>, UNINETT AS.
* @author Olav Morken, UNINETT AS.
* @author Andreas Åkre Solberg <andreas@uninett.no>, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_XHTML_IdPDisco
{
/**
* An instance of the configuration class.
*
* @var SimpleSAML_Configuration
*/
protected $config;
/**
* The identifier of this discovery service.
*
* @var string
*/
protected $instance;
/**
* An instance of the metadata handler, which will allow us to fetch metadata about IdPs.
*
* @var SimpleSAML_Metadata_MetaDataStorageHandler
*/
protected $metadata;
/**
* The users session.
*
* @var SimpleSAML_Session
*/
protected $session;
/**
* The metadata sets we find allowed entities in, in prioritized order.
*
* @var array
*/
protected $metadataSets;
/**
* The entity id of the SP which accesses this IdP discovery service.
*
* @var string
*/
protected $spEntityId;
/**
* HTTP parameter from the request, indicating whether the discovery service
* can interact with the user or not.
*
* @var boolean
*/
protected $isPassive;
/**
* The SP request to set the IdPentityID...
*
* @var string|null
*/
protected $setIdPentityID = null;
/**
* The name of the query parameter which should contain the users choice of IdP.
* This option default to 'entityID' for Shibboleth compatibility.
*
* @var string
*/
protected $returnIdParam;
/**
* The list of scoped idp's. The intersection between the metadata idpList
* and scopedIDPList (given as a $_GET IDPList[] parameter) is presented to
* the user. If the intersection is empty the metadata idpList is used.
*
* @var array
*/
protected $scopedIDPList = array();
/**
* The URL the user should be redirected to after choosing an IdP.
*
* @var string
*/
protected $returnURL;
/**
* Initializes this discovery service.
*
* The constructor does the parsing of the request. If this is an invalid request, it will throw an exception.
*
* @param array $metadataSets Array with metadata sets we find remote entities in.
* @param string $instance The name of this instance of the discovery service.
*
* @throws Exception If the request is invalid.
*/
public function __construct(array $metadataSets, $instance)
{
assert(is_string($instance));
// initialize standard classes
$this->config = SimpleSAML_Configuration::getInstance();
$this->metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
$this->session = SimpleSAML_Session::getSessionFromRequest();
$this->instance = $instance;
$this->metadataSets = $metadataSets;
$this->log('Accessing discovery service.');
// standard discovery service parameters
if (!array_key_exists('entityID', $_GET)) {
throw new Exception('Missing parameter: entityID');
} else {
$this->spEntityId = $_GET['entityID'];
}
if (!array_key_exists('returnIDParam', $_GET)) {
$this->returnIdParam = 'entityID';
} else {
$this->returnIdParam = $_GET['returnIDParam'];
}
$this->log('returnIdParam initially set to ['.$this->returnIdParam.']');
if (!array_key_exists('return', $_GET)) {
throw new Exception('Missing parameter: return');
} else {
$this->returnURL = \SimpleSAML\Utils\HTTP::checkURLAllowed($_GET['return']);
}
$this->isPassive = false;
if (array_key_exists('isPassive', $_GET)) {
if ($_GET['isPassive'] === 'true') {
$this->isPassive = true;
}
}
$this->log('isPassive initially set to ['.($this->isPassive ? 'TRUE' : 'FALSE').']');
if (array_key_exists('IdPentityID', $_GET)) {
$this->setIdPentityID = $_GET['IdPentityID'];
}
if (array_key_exists('IDPList', $_REQUEST)) {
$this->scopedIDPList = $_REQUEST['IDPList'];
}
}
/**
* Log a message.
*
* This is an helper function for logging messages. It will prefix the messages with our
* discovery service type.
*
* @param string $message The message which should be logged.
*/
protected function log($message)
{
SimpleSAML\Logger::info('idpDisco.'.$this->instance.': '.$message);
}
/**
* Retrieve cookie with the given name.
*
* This function will retrieve a cookie with the given name for the current discovery
* service type.
*
* @param string $name The name of the cookie.
*
* @return string The value of the cookie with the given name, or null if no cookie with that name exists.
*/
protected function getCookie($name)
{
$prefixedName = 'idpdisco_'.$this->instance.'_'.$name;
if (array_key_exists($prefixedName, $_COOKIE)) {
return $_COOKIE[$prefixedName];
} else {
return null;
}
}
/**
* Save cookie with the given name and value.
*
* This function will save a cookie with the given name and value for the current discovery
* service type.
*
* @param string $name The name of the cookie.
* @param string $value The value of the cookie.
*/
protected function setCookie($name, $value)
{
$prefixedName = 'idpdisco_'.$this->instance.'_'.$name;
$params = array(
// we save the cookies for 90 days
'lifetime' => (60 * 60 * 24 * 90),
// the base path for cookies. This should be the installation directory for SimpleSAMLphp
'path' => $this->config->getBasePath(),
'httponly' => false,
);
\SimpleSAML\Utils\HTTP::setCookie($prefixedName, $value, $params, false);
}
/**
* Validates the given IdP entity id.
*
* Takes a string with the IdP entity id, and returns the entity id if it is valid, or
* null if not.
*
* @param string|null $idp The entity id we want to validate. This can be null, in which case we will return null.
*
* @return string|null The entity id if it is valid, null if not.
*/
protected function validateIdP($idp)
{
if ($idp === null) {
return null;
}
if (!$this->config->getBoolean('idpdisco.validate', true)) {
return $idp;
}
foreach ($this->metadataSets as $metadataSet) {
try {
$this->metadata->getMetaData($idp, $metadataSet);
return $idp;
} catch (Exception $e) {
// continue
}
}
$this->log('Unable to validate IdP entity id ['.$idp.'].');
// the entity id wasn't valid
return null;
}
/**
* Retrieve the users choice of IdP.
*
* This function finds out which IdP the user has manually chosen, if any.
*
* @return string The entity id of the IdP the user has chosen, or null if the user has made no choice.
*/
protected function getSelectedIdP()
{
/* Parameter set from the Extended IdP Metadata Discovery Service Protocol, indicating that the user prefers
* this IdP.
*/
if (!empty($this->setIdPentityID)) {
return $this->validateIdP($this->setIdPentityID);
}
// user has clicked on a link, or selected the IdP from a drop-down list
if (array_key_exists('idpentityid', $_GET)) {
return $this->validateIdP($_GET['idpentityid']);
}
/* Search for the IdP selection from the form used by the links view. This form uses a name which equals
* idp_<entityid>, so we search for that.
*
* Unfortunately, php replaces periods in the name with underscores, and there is no reliable way to get them
* back. Therefore we do some quick and dirty parsing of the query string.
*/
$qstr = $_SERVER['QUERY_STRING'];
$matches = array();
if (preg_match('/(?:^|&)idp_([^=]+)=/', $qstr, $matches)) {
return $this->validateIdP(urldecode($matches[1]));
}
// no IdP chosen
return null;
}
/**
* Retrieve the users saved choice of IdP.
*
* @return string The entity id of the IdP the user has saved, or null if the user hasn't saved any choice.
*/
protected function getSavedIdP()
{
if (!$this->config->getBoolean('idpdisco.enableremember', false)) {
// saving of IdP choices is disabled
return null;
}
if ($this->getCookie('remember') === '1') {
$this->log('Return previously saved IdP because of remember cookie set to 1');
return $this->getPreviousIdP();
}
if ($this->isPassive) {
$this->log('Return previously saved IdP because of isPassive');
return $this->getPreviousIdP();
}
return null;
}
/**
* Retrieve the previous IdP the user used.
*
* @return string The entity id of the previous IdP the user used, or null if this is the first time.
*/
protected function getPreviousIdP()
{
return $this->validateIdP($this->getCookie('lastidp'));
}
/**
* Retrieve a recommended IdP based on the IP address of the client.
*
* @return string|null The entity ID of the IdP if one is found, or null if not.
*/
protected function getFromCIDRhint()
{
foreach ($this->metadataSets as $metadataSet) {
$idp = $this->metadata->getPreferredEntityIdFromCIDRhint($metadataSet, $_SERVER['REMOTE_ADDR']);
if (!empty($idp)) {
return $idp;
}
}
return null;
}
/**
* Try to determine which IdP the user should most likely use.
*
* This function will first look at the previous IdP the user has chosen. If the user
* hasn't chosen an IdP before, it will look at the IP address.
*
* @return string The entity id of the IdP the user should most likely use.
*/
protected function getRecommendedIdP()
{
$idp = $this->getPreviousIdP();
if ($idp !== null) {
$this->log('Preferred IdP from previous use ['.$idp.'].');
return $idp;
}
$idp = $this->getFromCIDRhint();
if (!empty($idp)) {
$this->log('Preferred IdP from CIDR hint ['.$idp.'].');
return $idp;
}
return null;
}
/**
* Save the current IdP choice to a cookie.
*
* @param string $idp The entityID of the IdP.
*/
protected function setPreviousIdP($idp)
{
assert(is_string($idp));
$this->log('Choice made ['.$idp.'] Setting cookie.');
$this->setCookie('lastidp', $idp);
}
/**
* Determine whether the choice of IdP should be saved.
*
* @return boolean True if the choice should be saved, false otherwise.
*/
protected function saveIdP()
{
if (!$this->config->getBoolean('idpdisco.enableremember', false)) {
// saving of IdP choices is disabled
return false;
}
if (array_key_exists('remember', $_GET)) {
return true;
}
return false;
}
/**
* Determine which IdP the user should go to, if any.
*
* @return string The entity id of the IdP the user should be sent to, or null if the user should choose.
*/
protected function getTargetIdP()
{
// first, check if the user has chosen an IdP
$idp = $this->getSelectedIdP();
if ($idp !== null) {
// the user selected this IdP. Save the choice in a cookie
$this->setPreviousIdP($idp);
if ($this->saveIdP()) {
$this->setCookie('remember', '1');
} else {
$this->setCookie('remember', '0');
}
return $idp;
}
$this->log('getSelectedIdP() returned null');
// check if the user has saved an choice earlier
$idp = $this->getSavedIdP();
if ($idp !== null) {
$this->log('Using saved choice ['.$idp.'].');
return $idp;
}
// the user has made no choice
return null;
}
/**
* Retrieve the list of IdPs which are stored in the metadata.
*
* @return array An array with entityid => metadata mappings.
*/
protected function getIdPList()
{
$idpList = array();
foreach ($this->metadataSets as $metadataSet) {
$newList = $this->metadata->getList($metadataSet);
/*
* Note that we merge the entities in reverse order. This ensures that it is the entity in the first
* metadata set that "wins" if two metadata sets have the same entity.
*/
$idpList = array_merge($newList, $idpList);
}
return $idpList;
}
/**
* Return the list of scoped idp
*
* @return array An array of IdP entities
*/
protected function getScopedIDPList()
{
return $this->scopedIDPList;
}
/**
* Filter the list of IdPs.
*
* This method returns the IdPs that comply with the following conditions:
* - The IdP does not have the 'hide.from.discovery' configuration option.
*
* @param array $list An associative array containing metadata for the IdPs to apply the filtering to.
*
* @return array An associative array containing metadata for the IdPs that were not filtered out.
*/
protected function filterList($list)
{
foreach ($list as $entity => $metadata) {
if (array_key_exists('hide.from.discovery', $metadata) && $metadata['hide.from.discovery'] === true) {
unset($list[$entity]);
}
}
return $list;
}
/**
* Check if an IdP is set or if the request is passive, and redirect accordingly.
*
* @return void If there is no IdP targeted and this is not a passive request.
*/
protected function start()
{
$idp = $this->getTargetIdp();
if ($idp !== null) {
$extDiscoveryStorage = $this->config->getString('idpdisco.extDiscoveryStorage', null);
if ($extDiscoveryStorage !== null) {
$this->log('Choice made ['.$idp.'] (Forwarding to external discovery storage)');
\SimpleSAML\Utils\HTTP::redirectTrustedURL($extDiscoveryStorage, array(
'entityID' => $this->spEntityId,
'IdPentityID' => $idp,
'returnIDParam' => $this->returnIdParam,
'isPassive' => 'true',
'return' => $this->returnURL
));
} else {
$this->log(
'Choice made ['.$idp.'] (Redirecting the user back. returnIDParam='.$this->returnIdParam.')'
);
\SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL, array($this->returnIdParam => $idp));
}
}
if ($this->isPassive) {
$this->log('Choice not made. (Redirecting the user back without answer)');
\SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL);
}
}
/**
* Handles a request to this discovery service.
*
* The IdP disco parameters should be set before calling this function.
*/
public function handleRequest()
{
$this->start();
// no choice made. Show discovery service page
$idpList = $this->getIdPList();
$idpList = $this->filterList($idpList);
$preferredIdP = $this->getRecommendedIdP();
$idpintersection = array_intersect(array_keys($idpList), $this->getScopedIDPList());
if (sizeof($idpintersection) > 0) {
$idpList = array_intersect_key($idpList, array_fill_keys($idpintersection, null));
}
$idpintersection = array_values($idpintersection);
if (sizeof($idpintersection) == 1) {
$this->log(
'Choice made ['.$idpintersection[0].'] (Redirecting the user back. returnIDParam='.
$this->returnIdParam.')'
);
\SimpleSAML\Utils\HTTP::redirectTrustedURL(
$this->returnURL,
array($this->returnIdParam => $idpintersection[0])
);
}
/*
* Make use of an XHTML template to present the select IdP choice to the user. Currently the supported options
* is either a drop down menu or a list view.
*/
switch ($this->config->getString('idpdisco.layout', 'links')) {
case 'dropdown':
$templateFile = 'selectidp-dropdown.php';
break;
case 'links':
$templateFile = 'selectidp-links.php';
break;
default:
throw new Exception('Invalid value for the \'idpdisco.layout\' option.');
}
$t = new SimpleSAML_XHTML_Template($this->config, $templateFile, 'disco');
$t->data['idplist'] = $idpList;
$t->data['preferredidp'] = $preferredIdP;
$t->data['return'] = $this->returnURL;
$t->data['returnIDParam'] = $this->returnIdParam;
$t->data['entityID'] = $this->spEntityId;
$t->data['urlpattern'] = htmlspecialchars(\SimpleSAML\Utils\HTTP::getSelfURLNoQuery());
$t->data['rememberenabled'] = $this->config->getBoolean('idpdisco.enableremember', false);
$t->show();
}
}

724
lib/SimpleSAML/XHTML/Template.php Executable file
View File

@@ -0,0 +1,724 @@
<?php
/**
* A minimalistic XHTML PHP based template system implemented for SimpleSAMLphp.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
use JaimePerez\TwigConfigurableI18n\Twig\Environment as Twig_Environment;
use JaimePerez\TwigConfigurableI18n\Twig\Extensions\Extension\I18n as Twig_Extensions_Extension_I18n;
class SimpleSAML_XHTML_Template
{
/**
* The data associated with this template, accessible within the template itself.
*
* @var array
*/
public $data = array();
/**
* A translator instance configured to work with this template.
*
* @var \SimpleSAML\Locale\Translate
*/
private $translator;
/**
* The localization backend
*
* @var \SimpleSAML\Locale\Localization
*/
private $localization;
/**
* The configuration to use in this template.
*
* @var SimpleSAML_Configuration
*/
private $configuration;
/**
* The file to load in this template.
*
* @var string
*/
private $template = 'default.php';
/**
* The twig environment.
*
* @var false|Twig_Environment
*/
private $twig;
/**
* The template name.
*
* @var string
*/
private $twig_template;
/**
* Current module, if any.
*/
private $module;
/**
* A template controller, if any.
*
* Used to intercept certain parts of the template handling, while keeping away unwanted/unexpected hooks. Set
* the 'theme.controller' configuration option to a class that implements the
* SimpleSAML\XHTML\TemplateControllerInterface interface to use it.
*
* @var SimpleSAML\XHTML\TemplateControllerInterface
*/
private $controller;
/**
* Whether we are using a non-default theme or not.
*
* If we are using a theme, this variable holds an array with two keys: "module" and "name", those being the name
* of the module and the name of the theme, respectively. If we are using the default theme, the variable defaults
* to false.
*
* @var bool|array
*/
private $theme;
/**
* Constructor
*
* @param SimpleSAML_Configuration $configuration Configuration object
* @param string $template Which template file to load
* @param string|null $defaultDictionary The default dictionary where tags will come from.
*/
public function __construct(\SimpleSAML_Configuration $configuration, $template, $defaultDictionary = null)
{
$this->configuration = $configuration;
$this->template = $template;
// TODO: do not remove the slash from the beginning, change the templates instead!
$this->data['baseurlpath'] = ltrim($this->configuration->getBasePath(), '/');
// parse module and template name
list($this->module) = $this->findModuleAndTemplateName($template);
// parse config to find theme and module theme is in, if any
list($this->theme['module'], $this->theme['name']) = self::findModuleAndTemplateName(
$this->configuration->getString('theme.use', 'default')
);
// initialize internationalization system
$this->translator = new SimpleSAML\Locale\Translate($configuration, $defaultDictionary);
$this->localization = new \SimpleSAML\Locale\Localization($configuration);
// check if we need to attach a theme controller
$controller = $this->configuration->getString('theme.controller', false);
if ($controller && class_exists($controller) &&
class_implements($controller, '\SimpleSAML\XHTML\TemplateControllerInterface')
) {
$this->controller = new $controller();
}
$this->twig = $this->setupTwig();
}
/**
* Normalize the name of the template to one of the possible alternatives.
*
* @param string $templateName The template name to normalize.
* @return string The filename we need to look for.
*/
private function normalizeTemplateName($templateName)
{
if (strripos($templateName, '.twig')) {
return $templateName;
}
$phppos = strripos($templateName, '.php');
if ($phppos) {
$templateName = substr($templateName, 0, $phppos);
}
$tplpos = strripos($templateName, '.tpl');
if ($tplpos) {
$templateName = substr($templateName, 0, $tplpos);
}
return $templateName.'.twig';
}
/**
* Set up the places where twig can look for templates.
*
* @return Twig_Loader_Filesystem The twig template loader or false if the template does not exist.
* @throws Twig_Error_Loader In case a failure occurs.
*/
private function setupTwigTemplatepaths()
{
$filename = $this->normalizeTemplateName($this->template);
// get namespace if any
list($namespace, $filename) = self::findModuleAndTemplateName($filename);
$this->twig_template = ($namespace !== null) ? '@'.$namespace.'/'.$filename : $filename;
$loader = new \Twig_Loader_Filesystem();
$templateDirs = $this->findThemeTemplateDirs();
if ($this->module) {
$templateDirs[] = array($this->module => $this->getModuleTemplateDir($this->module));
}
if ($this->theme['module']) {
try {
$templateDirs[] = array($this->theme['module'] => $this->getModuleTemplateDir($this->theme['module']));
} catch (\InvalidArgumentException $e) {
// either the module is not enabled or it has no "templates" directory, ignore
}
}
// default, themeless templates are checked last
$templateDirs[] = array(
\Twig_Loader_Filesystem::MAIN_NAMESPACE => $this->configuration->resolvePath('templates')
);
foreach ($templateDirs as $entry) {
$loader->addPath($entry[key($entry)], key($entry));
}
return $loader;
}
/**
* Setup twig.
*/
private function setupTwig()
{
$auto_reload = $this->configuration->getBoolean('template.auto_reload', true);
$cache = $this->configuration->getString('template.cache', false);
// set up template paths
$loader = $this->setupTwigTemplatepaths();
// abort if twig template does not exist
if (!$loader->exists($this->twig_template)) {
return false;
}
// load extra i18n domains
if ($this->module) {
$this->localization->addModuleDomain($this->module);
}
if ($this->theme['module'] !== null && $this->theme['module'] !== $this->module) {
$this->localization->addModuleDomain($this->theme['module']);
}
$options = array(
'cache' => $cache,
'auto_reload' => $auto_reload,
'translation_function' => array('\SimpleSAML\Locale\Translate', 'translateSingularNativeGettext'),
'translation_function_plural' => array('\SimpleSAML\Locale\Translate', 'translatePluralNativeGettext'),
);
// set up translation
if ($this->localization->i18nBackend === \SimpleSAML\Locale\Localization::GETTEXT_I18N_BACKEND) {
$options['translation_function'] = array('\SimpleSAML\Locale\Translate', 'translateSingularGettext');
$options['translation_function_plural'] = array(
'\SimpleSAML\Locale\Translate',
'translatePluralGettext'
);
} // TODO: add a branch for the old SimpleSAMLphp backend
$twig = new Twig_Environment($loader, $options);
$twig->addExtension(new Twig_Extensions_Extension_I18n());
// initialize some basic context
$langParam = $this->configuration->getString('language.parameter.name', 'language');
$twig->addGlobal('languageParameterName', $langParam);
$twig->addGlobal('localeBackend', $this->configuration->getString('language.i18n.backend', 'SimpleSAMLphp'));
$twig->addGlobal('currentLanguage', $this->translator->getLanguage()->getLanguage());
$twig->addGlobal('isRTL', false); // language RTL configuration
if ($this->translator->getLanguage()->isLanguageRTL()) {
$twig->addGlobal('isRTL', true);
}
$queryParams = $_GET; // add query parameters, in case we need them in the template
if (isset($queryParams[$langParam])) {
unset($queryParams[$langParam]);
}
$twig->addGlobal('queryParams', $queryParams);
$twig->addGlobal('templateId', str_replace('.twig', '', $this->normalizeTemplateName($this->template)));
$twig->addGlobal('isProduction', $this->configuration->getBoolean('production', true));
// add a filter for translations out of arrays
$twig->addFilter(
new \Twig_SimpleFilter(
'translateFromArray',
array('\SimpleSAML\Locale\Translate', 'translateFromArray'),
array('needs_context' => true)
)
);
if ($this->controller) {
$this->controller->setUpTwig($twig);
}
return $twig;
}
/**
* Add overriding templates from the configured theme.
*
* @return array An array of module => templatedir lookups.
*/
private function findThemeTemplateDirs()
{
if ($this->theme['module'] === null) { // no module involved
return array();
}
// setup directories & namespaces
$themeDir = \SimpleSAML\Module::getModuleDir($this->theme['module']).'/themes/'.$this->theme['name'];
$subdirs = scandir($themeDir);
if (empty($subdirs)) { // no subdirectories in the theme directory, nothing to do here
// this is probably wrong, log a message
\SimpleSAML\Logger::warning('Empty theme directory for theme "'.$this->theme['name'].'".');
return array();
}
$themeTemplateDirs = array();
foreach ($subdirs as $entry) {
// discard anything that's not a directory. Expression is negated to profit from lazy evaluation
if (!($entry !== '.' && $entry !== '..' && is_dir($themeDir.'/'.$entry))) {
continue;
}
// set correct name for the default namespace
$ns = ($entry === 'default') ? \Twig_Loader_Filesystem::MAIN_NAMESPACE : $entry;
$themeTemplateDirs[] = array($ns => $themeDir.'/'.$entry);
}
return $themeTemplateDirs;
}
/**
* Get the template directory of a module, if it exists.
*
* @return string The templates directory of a module.
*
* @throws InvalidArgumentException If the module is not enabled or it has no templates directory.
*/
private function getModuleTemplateDir($module)
{
if (!\SimpleSAML\Module::isModuleEnabled($module)) {
throw new InvalidArgumentException('The module \''.$module.'\' is not enabled.');
}
$moduledir = \SimpleSAML\Module::getModuleDir($module);
// check if module has a /templates dir, if so, append
$templatedir = $moduledir.'/templates';
if (!is_dir($templatedir)) {
throw new InvalidArgumentException('The module \''.$module.'\' has no templates directory.');
}
return $templatedir;
}
/**
* Add the templates from a given module.
*
* Note that the module must be installed, enabled, and contain a "templates" directory.
*
* @param string $module The module where we need to search for templates.
*
* @throws InvalidArgumentException If the module is not enabled or it has no templates directory.
*/
public function addTemplatesFromModule($module)
{
$dir = $this->getModuleTemplateDir($module);
/** @var Twig_Loader_Filesystem $loader */
$loader = $this->twig->getLoader();
$loader->addPath($dir, $module);
}
/**
* Generate an array for its use in the language bar, indexed by the ISO 639-2 codes of the languages available,
* containing their localized names and the URL that should be used in order to change to that language.
*
* @return array The array containing information of all available languages.
*/
private function generateLanguageBar()
{
$languages = $this->translator->getLanguage()->getLanguageList();
$langmap = null;
if (count($languages) > 1) {
$parameterName = $this->getTranslator()->getLanguage()->getLanguageParameterName();
$langmap = array();
foreach ($languages as $lang => $current) {
$lang = strtolower($lang);
$langname = $this->translator->getLanguage()->getLanguageLocalizedName($lang);
$url = false;
if (!$current) {
$url = htmlspecialchars(\SimpleSAML\Utils\HTTP::addURLParameters(
'',
array($parameterName => $lang)
));
}
$langmap[$lang] = array(
'name' => $langname,
'url' => $url,
);
}
}
return $langmap;
}
/**
* Set some default context
*/
private function twigDefaultContext()
{
// show language bar by default
if (!isset($this->data['hideLanguageBar'])) {
$this->data['hideLanguageBar'] = false;
}
// get languagebar
$this->data['languageBar'] = null;
if ($this->data['hideLanguageBar'] === false) {
$languageBar = $this->generateLanguageBar();
if (is_null($languageBar)) {
$this->data['hideLanguageBar'] = true;
} else {
$this->data['languageBar'] = $languageBar;
}
}
// assure that there is a <title> and <h1>
if (isset($this->data['header']) && !isset($this->data['pagetitle'])) {
$this->data['pagetitle'] = $this->data['header'];
}
if (!isset($this->data['pagetitle'])) {
$this->data['pagetitle'] = 'SimpleSAMLphp';
}
}
/**
* Show the template to the user.
*/
public function show()
{
if ($this->twig !== false) {
$this->twigDefaultContext();
if ($this->controller) {
$this->controller->display($this->data);
}
echo $this->twig->render($this->twig_template, $this->data);
} else {
$filename = $this->findTemplatePath($this->template);
require($filename);
}
}
/**
* Find module the template is in, if any
*
* @param string $template The relative path from the theme directory to the template file.
*
* @return array An array with the name of the module and template
*/
private function findModuleAndTemplateName($template)
{
$tmp = explode(':', $template, 2);
return (count($tmp) === 2) ? array($tmp[0], $tmp[1]) : array(null, $tmp[0]);
}
/**
* Find template path.
*
* This function locates the given template based on the template name. It will first search for the template in
* the current theme directory, and then the default theme.
*
* The template name may be on the form <module name>:<template path>, in which case it will search for the
* template file in the given module.
*
* @param string $template The relative path from the theme directory to the template file.
*
* @return string The absolute path to the template file.
*
* @throws Exception If the template file couldn't be found.
*/
private function findTemplatePath($template, $throw_exception = true)
{
assert(is_string($template));
list($templateModule, $templateName) = $this->findModuleAndTemplateName($template);
$templateModule = ($templateModule !== null) ? $templateModule : 'default';
// first check the current theme
if ($this->theme['module'] !== null) {
// .../module/<themeModule>/themes/<themeName>/<templateModule>/<templateName>
$filename = \SimpleSAML\Module::getModuleDir($this->theme['module']).
'/themes/'.$this->theme['name'].'/'.$templateModule.'/'.$templateName;
} elseif ($templateModule !== 'default') {
// .../module/<templateModule>/templates/<templateName>
$filename = \SimpleSAML\Module::getModuleDir($templateModule).'/templates/'.$templateName;
} else {
// .../templates/<theme>/<templateName>
$filename = $this->configuration->getPathValue('templatedir', 'templates/').$templateName;
}
if (file_exists($filename)) {
return $filename;
}
// not found in current theme
\SimpleSAML\Logger::debug(
$_SERVER['PHP_SELF'].' - Template: Could not find template file ['.$template.'] at ['.
$filename.'] - now trying the base template'
);
// try default theme
if ($templateModule !== 'default') {
// .../module/<templateModule>/templates/<templateName>
$filename = \SimpleSAML\Module::getModuleDir($templateModule).'/templates/'.$templateName;
} else {
// .../templates/<templateName>
$filename = $this->configuration->getPathValue('templatedir', 'templates/').'/'.$templateName;
}
if (file_exists($filename)) {
return $filename;
}
// not found in default template
if ($throw_exception) {
// log error and throw exception
$error = 'Template: Could not find template file ['.$template.'] at ['.$filename.']';
\SimpleSAML\Logger::critical($_SERVER['PHP_SELF'].' - '.$error);
throw new Exception($error);
} else {
// missing template expected, return NULL
return null;
}
}
/**
* Return the internal translator object used by this template.
*
* @return \SimpleSAML\Locale\Translate The translator that will be used with this template.
*/
public function getTranslator()
{
return $this->translator;
}
/**
* Get the current instance of Twig in use.
*
* @return false|Twig_Environment The Twig instance in use, or false if Twig is not used.
*/
public function getTwig()
{
return $this->twig;
}
/*
* Deprecated methods of this interface, all of them should go away.
*/
/**
* @param $name
*
* @return string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguage()
* instead.
*/
public function getAttributeTranslation($name)
{
return $this->translator->getAttributeTranslation($name);
}
/**
* @return string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguage()
* instead.
*/
public function getLanguage()
{
return $this->translator->getLanguage()->getLanguage();
}
/**
* @param $language
* @param bool $setLanguageCookie
*
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::setLanguage()
* instead.
*/
public function setLanguage($language, $setLanguageCookie = true)
{
$this->translator->getLanguage()->setLanguage($language, $setLanguageCookie);
}
/**
* @return null|string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguageCookie()
* instead.
*/
public static function getLanguageCookie()
{
return \SimpleSAML\Locale\Language::getLanguageCookie();
}
/**
* @param $language
*
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::setLanguageCookie()
* instead.
*/
public static function setLanguageCookie($language)
{
\SimpleSAML\Locale\Language::setLanguageCookie($language);
}
/**
* Wraps Language->getLanguageList
*/
private function getLanguageList()
{
return $this->translator->getLanguage()->getLanguageList();
}
/**
* @param $tag
*
* @return array
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::getTag() instead.
*/
public function getTag($tag)
{
return $this->translator->getTag($tag);
}
/**
* Temporary wrapper for \SimpleSAML\Locale\Translate::getPreferredTranslation().
*
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::getPreferredTranslation() instead.
*/
public function getTranslation($translations)
{
return $this->translator->getPreferredTranslation($translations);
}
/**
* Includes a file relative to the template base directory.
* This function can be used to include headers and footers etc.
*
*/
private function includeAtTemplateBase($file)
{
$data = $this->data;
$filename = $this->findTemplatePath($file);
include($filename);
}
/**
* Wraps Translate->includeInlineTranslation()
*
* @see \SimpleSAML\Locale\Translate::includeInlineTranslation()
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::includeInlineTranslation() instead.
*/
public function includeInlineTranslation($tag, $translation)
{
$this->translator->includeInlineTranslation($tag, $translation);
}
/**
* @param $file
* @param null $otherConfig
*
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::includeLanguageFile() instead.
*/
public function includeLanguageFile($file, $otherConfig = null)
{
$this->translator->includeLanguageFile($file, $otherConfig);
}
/**
* Wrap Language->isLanguageRTL
*/
private function isLanguageRTL()
{
return $this->translator->getLanguage()->isLanguageRTL();
}
/**
* Merge two translation arrays.
*
* @param array $def The array holding string definitions.
* @param array $lang The array holding translations for every string.
*
* @return array The recursive merge of both arrays.
* @deprecated This method will be removed in SimpleSAMLphp 2.0. Please use array_merge_recursive() instead.
*/
public static function lang_merge($def, $lang)
{
foreach ($def as $key => $value) {
if (array_key_exists($key, $lang)) {
$def[$key] = array_merge($value, $lang[$key]);
}
}
return $def;
}
/**
* Behave like Language->noop to mark a tag for translation but actually do it later.
*
* @see \SimpleSAML\Locale\Translate::noop()
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::noop() instead.
*/
public static function noop($tag)
{
return $tag;
}
/**
* Wrap Language->t to translate tag into the current language, with a fallback to english.
*
* @see \SimpleSAML\Locale\Translate::t()
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::t() instead.
*/
public function t(
$tag,
$replacements = array(),
$fallbackdefault = true,
$oldreplacements = array(),
$striptags = false
) {
return $this->translator->t($tag, $replacements, $fallbackdefault, $oldreplacements, $striptags);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace SimpleSAML\XHTML;
/**
* Interface that allows modules to run several hooks for templates.
*
* @package SimpleSAMLphp
*/
interface TemplateControllerInterface
{
/**
* Implement to modify the twig environment after its initialization (e.g. add filters or extensions).
*
* @param \Twig_Environment $twig The current twig environment.
*
* @return void
*/
public function setUpTwig(\Twig_Environment &$twig);
/**
* Implement to add, delete or modify the data passed to the template.
*
* This method will be called right before displaying the template.
*
* @param array $data The current data used by the template.
*
* @return void
*/
public function display(&$data);
}

140
lib/SimpleSAML/XML/Errors.php Executable file
View File

@@ -0,0 +1,140 @@
<?php
/**
* This class defines an interface for accessing errors from the XML library.
*
* In PHP versions which doesn't support accessing error information, this class
* will hide that, and pretend that no errors were logged.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML;
use LibXMLError;
class Errors
{
/**
* @var array This is an stack of error logs. The topmost element is the one we are currently working on.
*/
private static $errorStack = array();
/**
* @var bool This is the xml error state we had before we began logging.
*/
private static $xmlErrorState;
/**
* Append current XML errors to the current stack level.
*/
private static function addErrors()
{
$currentErrors = libxml_get_errors();
libxml_clear_errors();
$level = count(self::$errorStack) - 1;
self::$errorStack[$level] = array_merge(self::$errorStack[$level], $currentErrors);
}
/**
* Start error logging.
*
* A call to this function will begin a new error logging context. Every call must have
* a corresponding call to end().
*/
public static function begin()
{
// Check whether the error access functions are present
if (!function_exists('libxml_use_internal_errors')) {
return;
}
if (count(self::$errorStack) === 0) {
// No error logging is currently in progress. Initialize it.
self::$xmlErrorState = libxml_use_internal_errors(true);
libxml_clear_errors();
} else {
/* We have already started error logging. Append the current errors to the
* list of errors in this level.
*/
self::addErrors();
}
// Add a new level to the error stack
self::$errorStack[] = array();
}
/**
* End error logging.
*
* @return array An array with the LibXMLErrors which has occurred since begin() was called.
*/
public static function end()
{
// Check whether the error access functions are present
if (!function_exists('libxml_use_internal_errors')) {
// Pretend that no errors occurred
return array();
}
// Add any errors which may have occurred
self::addErrors();
$ret = array_pop(self::$errorStack);
if (count(self::$errorStack) === 0) {
// Disable our error logging and restore the previous state
libxml_use_internal_errors(self::$xmlErrorState);
}
return $ret;
}
/**
* Format an error as a string.
*
* This function formats the given LibXMLError object as a string.
*
* @param \LibXMLError $error The LibXMLError which should be formatted.
* @return string A string representing the given LibXMLError.
*/
public static function formatError($error)
{
assert($error instanceof LibXMLError);
return 'level=' . $error->level . ',code=' . $error->code . ',line=' . $error->line . ',col=' . $error->column .
',msg=' . trim($error->message);
}
/**
* Format a list of errors as a string.
*
* This fucntion takes an array of LibXMLError objects and creates a string with all the errors.
* Each error will be separated by a newline, and the string will end with a newline-character.
*
* @param array $errors An array of errors.
* @return string A string representing the errors. An empty string will be returned if there were no
* errors in the array.
*/
public static function formatErrors($errors)
{
assert(is_array($errors));
$ret = '';
foreach ($errors as $error) {
$ret .= self::formatError($error) . "\n";
}
return $ret;
}
}

77
lib/SimpleSAML/XML/Parser.php Executable file
View File

@@ -0,0 +1,77 @@
<?php
/**
* This file will help doing XPath queries in SAML 2 XML documents.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML;
class Parser
{
public $simplexml = null;
public function __construct($xml)
{
;
$this->simplexml = new \SimpleXMLElement($xml);
$this->simplexml->registerXPathNamespace('saml2', 'urn:oasis:names:tc:SAML:2.0:assertion');
$this->simplexml->registerXPathNamespace('saml2meta', 'urn:oasis:names:tc:SAML:2.0:metadata');
$this->simplexml->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#');
}
public static function fromSimpleXMLElement(\SimpleXMLElement $element)
{
// Traverse all existing namespaces in element
$namespaces = $element->getNamespaces();
foreach ($namespaces as $prefix => $ns) {
$element[(($prefix === '') ? 'xmlns' : 'xmlns:' . $prefix)] = $ns;
}
/* Create a new parser with the xml document where the namespace definitions
* are added.
*/
$parser = new Parser($element->asXML());
return $parser;
}
public function getValueDefault($xpath, $defvalue)
{
try {
return $this->getValue($xpath, true);
} catch (\Exception $e) {
return $defvalue;
}
}
public function getValue($xpath, $required = false)
{
$result = $this->simplexml->xpath($xpath);
if (!is_array($result) || empty($result)) {
if ($required) {
throw new \Exception('Could not get value from XML document using the following XPath expression: ' . $xpath);
} else {
return null;
}
}
return (string) $result[0];
}
public function getValueAlternatives(array $xpath, $required = false)
{
foreach ($xpath as $x) {
$seek = $this->getValue($x);
if ($seek) {
return $seek;
}
}
if ($required) {
throw new \Exception('Could not get value from XML document using multiple alternative XPath expressions.');
} else {
return null;
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* The Shibboleth 1.3 Authentication Request. Not part of SAML 1.1,
* but an extension using query paramters no XML.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML\Shib13;
class AuthnRequest
{
private $issuer = null;
private $relayState = null;
public function setRelayState($relayState)
{
$this->relayState = $relayState;
}
public function getRelayState()
{
return $this->relayState;
}
public function setIssuer($issuer)
{
$this->issuer = $issuer;
}
public function getIssuer()
{
return $this->issuer;
}
public function createRedirect($destination, $shire)
{
$metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
$idpmetadata = $metadata->getMetaDataConfig($destination, 'shib13-idp-remote');
$desturl = $idpmetadata->getDefaultEndpoint('SingleSignOnService', array('urn:mace:shibboleth:1.0:profiles:AuthnRequest'));
$desturl = $desturl['Location'];
$target = $this->getRelayState();
$url = $desturl . '?' .
'providerId=' . urlencode($this->getIssuer()) .
'&shire=' . urlencode($shire) .
(isset($target) ? '&target=' . urlencode($target) : '');
return $url;
}
}

View File

@@ -0,0 +1,480 @@
<?php
/**
* A Shibboleth 1.3 authentication response.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML\Shib13;
use DOMDocument;
use DOMNode;
use SAML2\DOMDocumentFactory;
use SAML2\Utils;
use SimpleSAML\Utils\Config;
use SimpleSAML\Utils\Random;
use SimpleSAML\Utils\Time;
use SimpleSAML\XML\Validator;
class AuthnResponse
{
/**
* @var \SimpleSAML\XML\Validator This variable contains an XML validator for this message.
*/
private $validator = null;
/**
* @var bool Whether this response was validated by some external means (e.g. SSL).
*/
private $messageValidated = false;
const SHIB_PROTOCOL_NS = 'urn:oasis:names:tc:SAML:1.0:protocol';
const SHIB_ASSERT_NS = 'urn:oasis:names:tc:SAML:1.0:assertion';
/**
* @var \DOMDocument The DOMDocument which represents this message.
*/
private $dom;
/**
* @var string|null The relaystate which is associated with this response.
*/
private $relayState = null;
/**
* Set whether this message was validated externally.
*
* @param bool $messageValidated TRUE if the message is already validated, FALSE if not.
*/
public function setMessageValidated($messageValidated)
{
assert(is_bool($messageValidated));
$this->messageValidated = $messageValidated;
}
public function setXML($xml)
{
assert(is_string($xml));
try {
$this->dom = DOMDocumentFactory::fromString(str_replace("\r", "", $xml));
} catch (\Exception $e) {
throw new \Exception('Unable to parse AuthnResponse XML.');
}
}
public function setRelayState($relayState)
{
$this->relayState = $relayState;
}
public function getRelayState()
{
return $this->relayState;
}
public function validate()
{
assert($this->dom instanceof DOMDocument);
if ($this->messageValidated) {
// This message was validated externally
return true;
}
// Validate the signature
$this->validator = new Validator($this->dom, array('ResponseID', 'AssertionID'));
// Get the issuer of the response
$issuer = $this->getIssuer();
// Get the metadata of the issuer
$metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
$md = $metadata->getMetaDataConfig($issuer, 'shib13-idp-remote');
$publicKeys = $md->getPublicKeys('signing');
if (!empty($publicKeys)) {
$certFingerprints = array();
foreach ($publicKeys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
$certFingerprints[] = sha1(base64_decode($key['X509Certificate']));
}
$this->validator->validateFingerprint($certFingerprints);
} elseif ($md->hasValue('certFingerprint')) {
$certFingerprints = $md->getArrayizeString('certFingerprint');
// Validate the fingerprint
$this->validator->validateFingerprint($certFingerprints);
} elseif ($md->hasValue('caFile')) {
// Validate against CA
$this->validator->validateCA(Config::getCertPath($md->getString('caFile')));
} else {
throw new \SimpleSAML_Error_Exception('Missing certificate in Shibboleth 1.3 IdP Remote metadata for identity provider [' . $issuer . '].');
}
return true;
}
/**
* Checks if the given node is validated by the signature on this response.
*
* @param \DOMElement $node Node to be validated.
* @return bool TRUE if the node is validated or FALSE if not.
*/
private function isNodeValidated($node)
{
if ($this->messageValidated) {
// This message was validated externally
return true;
}
if ($this->validator === null) {
return false;
}
// Convert the node to a DOM node if it is an element from SimpleXML
if ($node instanceof \SimpleXMLElement) {
$node = dom_import_simplexml($node);
}
assert($node instanceof DOMNode);
return $this->validator->isNodeValidated($node);
}
/**
* This function runs an xPath query on this authentication response.
*
* @param string $query The query which should be run.
* @param \DOMNode $node The node which this query is relative to. If this node is NULL (the default)
* then the query will be relative to the root of the response.
* @return \DOMNodeList
*/
private function doXPathQuery($query, $node = null)
{
assert(is_string($query));
assert($this->dom instanceof DOMDocument);
if ($node === null) {
$node = $this->dom->documentElement;
}
assert($node instanceof DOMNode);
$xPath = new \DOMXpath($this->dom);
$xPath->registerNamespace('shibp', self::SHIB_PROTOCOL_NS);
$xPath->registerNamespace('shib', self::SHIB_ASSERT_NS);
return $xPath->query($query, $node);
}
/**
* Retrieve the session index of this response.
*
* @return string|null The session index of this response.
*/
public function getSessionIndex()
{
assert($this->dom instanceof DOMDocument);
$query = '/shibp:Response/shib:Assertion/shib:AuthnStatement';
$nodelist = $this->doXPathQuery($query);
if ($node = $nodelist->item(0)) {
return $node->getAttribute('SessionIndex');
}
return null;
}
public function getAttributes()
{
$metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
$md = $metadata->getMetadata($this->getIssuer(), 'shib13-idp-remote');
$base64 = isset($md['base64attributes']) ? $md['base64attributes'] : false;
if (! ($this->dom instanceof \DOMDocument)) {
return array();
}
$attributes = array();
$assertions = $this->doXPathQuery('/shibp:Response/shib:Assertion');
foreach ($assertions as $assertion) {
if (!$this->isNodeValidated($assertion)) {
throw new \Exception('Shib13 AuthnResponse contained an unsigned assertion.');
}
$conditions = $this->doXPathQuery('shib:Conditions', $assertion);
if ($conditions && $conditions->length > 0) {
$condition = $conditions->item(0);
$start = $condition->getAttribute('NotBefore');
$end = $condition->getAttribute('NotOnOrAfter');
if ($start && $end) {
if (!self::checkDateConditions($start, $end)) {
error_log('Date check failed ... (from ' . $start . ' to ' . $end . ')');
continue;
}
}
}
$attribute_nodes = $this->doXPathQuery('shib:AttributeStatement/shib:Attribute/shib:AttributeValue', $assertion);
/** @var \DOMElement $attribute */
foreach ($attribute_nodes as $attribute) {
$value = $attribute->textContent;
$name = $attribute->parentNode->getAttribute('AttributeName');
if ($attribute->hasAttribute('Scope')) {
$scopePart = '@' . $attribute->getAttribute('Scope');
} else {
$scopePart = '';
}
if (!is_string($name)) {
throw new \Exception('Shib13 Attribute node without an AttributeName.');
}
if (!array_key_exists($name, $attributes)) {
$attributes[$name] = array();
}
if ($base64) {
$encodedvalues = explode('_', $value);
foreach ($encodedvalues as $v) {
$attributes[$name][] = base64_decode($v) . $scopePart;
}
} else {
$attributes[$name][] = $value . $scopePart;
}
}
}
return $attributes;
}
public function getIssuer()
{
$query = '/shibp:Response/shib:Assertion/@Issuer';
$nodelist = $this->doXPathQuery($query);
if ($attr = $nodelist->item(0)) {
return $attr->value;
} else {
throw new \Exception('Could not find Issuer field in Authentication response');
}
}
public function getNameID()
{
$nameID = array();
$query = '/shibp:Response/shib:Assertion/shib:AuthenticationStatement/shib:Subject/shib:NameIdentifier';
$nodelist = $this->doXPathQuery($query);
if ($node = $nodelist->item(0)) {
$nameID["Value"] = $node->nodeValue;
$nameID["Format"] = $node->getAttribute('Format');
}
return $nameID;
}
/**
* Build a authentication response.
*
* @param \SimpleSAML_Configuration $idp Metadata for the IdP the response is sent from.
* @param \SimpleSAML_Configuration $sp Metadata for the SP the response is sent to.
* @param string $shire The endpoint on the SP the response is sent to.
* @param array|null $attributes The attributes which should be included in the response.
* @return string The response.
*/
public function generate(\SimpleSAML_Configuration $idp, \SimpleSAML_Configuration $sp, $shire, $attributes)
{
assert(is_string($shire));
assert($attributes === null || is_array($attributes));
if ($sp->hasValue('scopedattributes')) {
$scopedAttributes = $sp->getArray('scopedattributes');
} elseif ($idp->hasValue('scopedattributes')) {
$scopedAttributes = $idp->getArray('scopedattributes');
} else {
$scopedAttributes = array();
}
$id = Random::generateID();
$issueInstant = Time::generateTimestamp();
// 30 seconds timeskew back in time to allow differing clocks
$notBefore = Time::generateTimestamp(time() - 30);
$assertionExpire = Time::generateTimestamp(time() + 60 * 5);# 5 minutes
$assertionid = Random::generateID();
$spEntityId = $sp->getString('entityid');
$audience = $sp->getString('audience', $spEntityId);
$base64 = $sp->getBoolean('base64attributes', false);
$namequalifier = $sp->getString('NameQualifier', $spEntityId);
$nameid = Random::generateID();
$subjectNode =
'<Subject>' .
'<NameIdentifier' .
' Format="urn:mace:shibboleth:1.0:nameIdentifier"' .
' NameQualifier="' . htmlspecialchars($namequalifier) . '"' .
'>' .
htmlspecialchars($nameid) .
'</NameIdentifier>' .
'<SubjectConfirmation>' .
'<ConfirmationMethod>' .
'urn:oasis:names:tc:SAML:1.0:cm:bearer' .
'</ConfirmationMethod>' .
'</SubjectConfirmation>' .
'</Subject>';
$encodedattributes = '';
if (is_array($attributes)) {
$encodedattributes .= '<AttributeStatement>';
$encodedattributes .= $subjectNode;
foreach ($attributes as $name => $value) {
$encodedattributes .= $this->enc_attribute($name, $value, $base64, $scopedAttributes);
}
$encodedattributes .= '</AttributeStatement>';
}
/*
* The SAML 1.1 response message
*/
$response = '<Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" IssueInstant="' . $issueInstant. '"
MajorVersion="1" MinorVersion="1"
Recipient="' . htmlspecialchars($shire) . '" ResponseID="' . $id . '">
<Status>
<StatusCode Value="samlp:Success" />
</Status>
<Assertion xmlns="urn:oasis:names:tc:SAML:1.0:assertion"
AssertionID="' . $assertionid . '" IssueInstant="' . $issueInstant. '"
Issuer="' . htmlspecialchars($idp->getString('entityid')) . '" MajorVersion="1" MinorVersion="1">
<Conditions NotBefore="' . $notBefore. '" NotOnOrAfter="'. $assertionExpire . '">
<AudienceRestrictionCondition>
<Audience>' . htmlspecialchars($audience) . '</Audience>
</AudienceRestrictionCondition>
</Conditions>
<AuthenticationStatement AuthenticationInstant="' . $issueInstant. '"
AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:unspecified">' .
$subjectNode . '
</AuthenticationStatement>
' . $encodedattributes . '
</Assertion>
</Response>';
return $response;
}
/**
* Format a shib13 attribute.
*
* @param string $name Name of the attribute.
* @param array $values Values of the attribute (as an array of strings).
* @param bool $base64 Whether the attriubte values should be base64-encoded.
* @param array $scopedAttributes Array of attributes names which are scoped.
* @return string The attribute encoded as an XML-string.
*/
private function enc_attribute($name, $values, $base64, $scopedAttributes)
{
assert(is_string($name));
assert(is_array($values));
assert(is_bool($base64));
assert(is_array($scopedAttributes));
if (in_array($name, $scopedAttributes, true)) {
$scoped = true;
} else {
$scoped = false;
}
$attr = '<Attribute AttributeName="' . htmlspecialchars($name) . '" AttributeNamespace="urn:mace:shibboleth:1.0:attributeNamespace:uri">';
foreach ($values as $value) {
$scopePart = '';
if ($scoped) {
$tmp = explode('@', $value, 2);
if (count($tmp) === 2) {
$value = $tmp[0];
$scopePart = ' Scope="' . htmlspecialchars($tmp[1]) . '"';
}
}
if ($base64) {
$value = base64_encode($value);
}
$attr .= '<AttributeValue' . $scopePart . '>' . htmlspecialchars($value) . '</AttributeValue>';
}
$attr .= '</Attribute>';
return $attr;
}
/**
* Check if we are currently between the given date & time conditions.
*
* Note that this function allows a 10-minute leap from the initial time as marked by $start.
*
* @param string|null $start A SAML2 timestamp marking the start of the period to check. Defaults to null, in which
* case there's no limitations in the past.
* @param string|null $end A SAML2 timestamp marking the end of the period to check. Defaults to null, in which
* case there's no limitations in the future.
*
* @return bool True if the current time belongs to the period specified by $start and $end. False otherwise.
*
* @see \SAML2\Utils::xsDateTimeToTimestamp.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
protected static function checkDateConditions($start = null, $end = null)
{
$currentTime = time();
if (!empty($start)) {
$startTime = Utils::xsDateTimeToTimestamp($start);
// allow for a 10 minute difference in time
if (($startTime < 0) || (($startTime - 600) > $currentTime)) {
return false;
}
}
if (!empty($end)) {
$endTime = Utils::xsDateTimeToTimestamp($end);
if (($endTime < 0) || ($endTime <= $currentTime)) {
return false;
}
}
return true;
}
}

313
lib/SimpleSAML/XML/Signer.php Executable file
View File

@@ -0,0 +1,313 @@
<?php
/**
* A helper class for signing XML.
*
* This is a helper class for signing XML documents.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML;
use DOMComment;
use DOMElement;
use DOMText;
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use RobRichards\XMLSecLibs\XMLSecurityKey;
use SimpleSAML\Utils\Config;
class Signer
{
/**
* @var string The name of the ID attribute.
*/
private $idAttrName = '';
/**
* @var XMLSecurityKey|bool The private key (as an XMLSecurityKey).
*/
private $privateKey = false;
/**
* @var string The certificate (as text).
*/
private $certificate = '';
/**
* @var array Extra certificates which should be included in the response.
*/
private $extraCertificates = array();
/**
* Constructor for the metadata signer.
*
* You can pass an list of options as key-value pairs in the array. This allows you to initialize
* a metadata signer in one call.
*
* The following keys are recognized:
* - privatekey The file with the private key, relative to the cert-directory.
* - privatekey_pass The passphrase for the private key.
* - certificate The file with the certificate, relative to the cert-directory.
* - privatekey_array The private key, as an array returned from SimpleSAML_Utilities::loadPrivateKey.
* - publickey_array The public key, as an array returned from SimpleSAML_Utilities::loadPublicKey.
* - id The name of the ID attribute.
*
* @param array $options Associative array with options for the constructor. Defaults to an empty array.
*/
public function __construct($options = array())
{
assert(is_array($options));
if (array_key_exists('privatekey', $options)) {
$pass = null;
if (array_key_exists('privatekey_pass', $options)) {
$pass = $options['privatekey_pass'];
}
$this->loadPrivateKey($options['privatekey'], $pass);
}
if (array_key_exists('certificate', $options)) {
$this->loadCertificate($options['certificate']);
}
if (array_key_exists('privatekey_array', $options)) {
$this->loadPrivateKeyArray($options['privatekey_array']);
}
if (array_key_exists('publickey_array', $options)) {
$this->loadPublicKeyArray($options['publickey_array']);
}
if (array_key_exists('id', $options)) {
$this->setIdAttribute($options['id']);
}
}
/**
* Set the private key from an array.
*
* This function loads the private key from an array matching what is returned
* by SimpleSAML_Utilities::loadPrivateKey(...).
*
* @param array $privatekey The private key.
*/
public function loadPrivateKeyArray($privatekey)
{
assert(is_array($privatekey));
assert(array_key_exists('PEM', $privatekey));
$this->privateKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'private'));
if (array_key_exists('password', $privatekey)) {
$this->privateKey->passphrase = $privatekey['password'];
}
$this->privateKey->loadKey($privatekey['PEM'], false);
}
/**
* Set the private key.
*
* Will throw an exception if unable to load the private key.
*
* @param string $file The file which contains the private key. The path is assumed to be relative
* to the cert-directory.
* @param string|null $pass The passphrase on the private key. Pass no value or NULL if the private
* key is unencrypted.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
* @throws \Exception
*/
public function loadPrivateKey($file, $pass = null, $full_path = false)
{
assert(is_string($file));
assert(is_string($pass) || $pass === null);
assert(is_bool($full_path));
if (!$full_path) {
$keyFile = Config::getCertPath($file);
} else {
$keyFile = $file;
}
if (!file_exists($keyFile)) {
throw new \Exception('Could not find private key file "' . $keyFile . '".');
}
$keyData = file_get_contents($keyFile);
if ($keyData === false) {
throw new \Exception('Unable to read private key file "' . $keyFile . '".');
}
$privatekey = array('PEM' => $keyData);
if ($pass !== null) {
$privatekey['password'] = $pass;
}
$this->loadPrivateKeyArray($privatekey);
}
/**
* Set the public key / certificate we should include in the signature.
*
* This function loads the public key from an array matching what is returned
* by SimpleSAML_Utilities::loadPublicKey(...).
*
* @param array $publickey The public key.
* @throws \Exception
*/
public function loadPublicKeyArray($publickey)
{
assert(is_array($publickey));
if (!array_key_exists('PEM', $publickey)) {
// We have a public key with only a fingerprint
throw new \Exception('Tried to add a certificate fingerprint in a signature.');
}
// For now, we only assume that the public key is an X509 certificate
$this->certificate = $publickey['PEM'];
}
/**
* Set the certificate we should include in the signature.
*
* If this function isn't called, no certificate will be included.
* Will throw an exception if unable to load the certificate.
*
* @param string $file The file which contains the certificate. The path is assumed to be relative to
* the cert-directory.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
* @throws \Exception
*/
public function loadCertificate($file, $full_path = false)
{
assert(is_string($file));
assert(is_bool($full_path));
if (!$full_path) {
$certFile = Config::getCertPath($file);
} else {
$certFile = $file;
}
if (!file_exists($certFile)) {
throw new \Exception('Could not find certificate file "' . $certFile . '".');
}
$cert = file_get_contents($certFile);
if ($cert === false) {
throw new \Exception('Unable to read certificate file "' . $certFile . '".');
}
$this->certificate = $cert;
}
/**
* Set the attribute name for the ID value.
*
* @param string $idAttrName The name of the attribute which contains the id.
*/
public function setIDAttribute($idAttrName)
{
assert(is_string($idAttrName));
$this->idAttrName = $idAttrName;
}
/**
* Add an extra certificate to the certificate chain in the signature.
*
* Extra certificates will be added to the certificate chain in the order they
* are added.
*
* @param string $file The file which contains the certificate, relative to the cert-directory.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
* @throws \Exception
*/
public function addCertificate($file, $full_path = false)
{
assert(is_string($file));
assert(is_bool($full_path));
if (!$full_path) {
$certFile = Config::getCertPath($file);
} else {
$certFile = $file;
}
if (!file_exists($certFile)) {
throw new \Exception('Could not find extra certificate file "' . $certFile . '".');
}
$certificate = file_get_contents($certFile);
if ($certificate === false) {
throw new \Exception('Unable to read extra certificate file "' . $certFile . '".');
}
$this->extraCertificates[] = $certificate;
}
/**
* Signs the given DOMElement and inserts the signature at the given position.
*
* The private key must be set before calling this function.
*
* @param \DOMElement $node The DOMElement we should generate a signature for.
* @param \DOMElement $insertInto The DOMElement we should insert the signature element into.
* @param \DOMElement $insertBefore The element we should insert the signature element before. Defaults to NULL,
* in which case the signature will be appended to the element spesified in
* $insertInto.
* @throws \Exception
*/
public function sign($node, $insertInto, $insertBefore = null)
{
assert($node instanceof DOMElement);
assert($insertInto instanceof DOMElement);
assert($insertBefore === null || $insertBefore instanceof DOMElement ||
$insertBefore instanceof DOMComment || $insertBefore instanceof DOMText);
if ($this->privateKey === false) {
throw new \Exception('Private key not set.');
}
$objXMLSecDSig = new XMLSecurityDSig();
$objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$options = array();
if (!empty($this->idAttrName)) {
$options['id_name'] = $this->idAttrName;
}
$objXMLSecDSig->addReferenceList(
array($node),
XMLSecurityDSig::SHA256,
array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N),
$options
);
/** @var \RobRichards\XMLSecLibs\XMLSecurityKey $this->privateKey */
$objXMLSecDSig->sign($this->privateKey);
// Add the certificate to the signature
$objXMLSecDSig->add509Cert($this->certificate, true);
// Add extra certificates
foreach ($this->extraCertificates as $certificate) {
$objXMLSecDSig->add509Cert($certificate, true);
}
$objXMLSecDSig->insertSignature($insertInto, $insertBefore);
}
}

445
lib/SimpleSAML/XML/Validator.php Executable file
View File

@@ -0,0 +1,445 @@
<?php
/**
* This class implements helper functions for XML validation.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML;
use RobRichards\XMLSecLibs\XMLSecEnc;
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use SimpleSAML\Logger;
class Validator
{
/**
* @var string This variable contains the X509 certificate the XML document
* was signed with, or NULL if it wasn't signed with an X509 certificate.
*/
private $x509Certificate;
/**
* @var array|null This variable contains the nodes which are signed.
*/
private $validNodes = null;
/**
* This function initializes the validator.
*
* This function accepts an optional parameter $publickey, which is the public key
* or certificate which should be used to validate the signature. This parameter can
* take the following values:
* - NULL/FALSE: No validation will be performed. This is the default.
* - A string: Assumed to be a PEM-encoded certificate / public key.
* - An array: Assumed to be an array returned by SimpleSAML_Utilities::loadPublicKey.
*
* @param \DOMNode $xmlNode The XML node which contains the Signature element.
* @param string|array $idAttribute The ID attribute which is used in node references. If
* this attribute is NULL (the default), then we will use whatever is the default
* ID. Can be eigther a string with one value, or an array with multiple ID
* attrbute names.
* @param array|bool $publickey The public key / certificate which should be used to validate the XML node.
* @throws \Exception
*/
public function __construct($xmlNode, $idAttribute = null, $publickey = false)
{
assert($xmlNode instanceof \DOMNode);
if ($publickey === null) {
$publickey = false;
} elseif (is_string($publickey)) {
$publickey = array(
'PEM' => $publickey,
);
} else {
assert($publickey === false || is_array($publickey));
}
// Create an XML security object
$objXMLSecDSig = new XMLSecurityDSig();
// Add the id attribute if the user passed in an id attribute
if ($idAttribute !== null) {
if (is_string($idAttribute)) {
$objXMLSecDSig->idKeys[] = $idAttribute;
} elseif (is_array($idAttribute)) {
foreach ($idAttribute as $ida) {
$objXMLSecDSig->idKeys[] = $ida;
}
}
}
// Locate the XMLDSig Signature element to be used
$signatureElement = $objXMLSecDSig->locateSignature($xmlNode);
if (!$signatureElement) {
throw new \Exception('Could not locate XML Signature element.');
}
// Canonicalize the XMLDSig SignedInfo element in the message
$objXMLSecDSig->canonicalizeSignedInfo();
// Validate referenced xml nodes
if (!$objXMLSecDSig->validateReference()) {
throw new \Exception('XMLsec: digest validation failed');
}
// Find the key used to sign the document
$objKey = $objXMLSecDSig->locateKey();
if (empty($objKey)) {
throw new \Exception('Error loading key to handle XML signature');
}
// Load the key data
if ($publickey !== false && array_key_exists('PEM', $publickey)) {
// We have PEM data for the public key / certificate
$objKey->loadKey($publickey['PEM']);
} else {
// No PEM data. Search for key in signature
if (!XMLSecEnc::staticLocateKeyInfo($objKey, $signatureElement)) {
throw new \Exception('Error finding key data for XML signature validation.');
}
if ($publickey !== false) {
/* $publickey is set, and should therefore contain one or more fingerprints.
* Check that the response contains a certificate with a matching
* fingerprint.
*/
assert(is_array($publickey['certFingerprint']));
$certificate = $objKey->getX509Certificate();
if ($certificate === null) {
// Wasn't signed with an X509 certificate
throw new \Exception('Message wasn\'t signed with an X509 certificate,' .
' and no public key was provided in the metadata.');
}
self::validateCertificateFingerprint($certificate, $publickey['certFingerprint']);
// Key OK
}
}
// Check the signature
if ($objXMLSecDSig->verify($objKey) !== 1) {
throw new \Exception("Unable to validate Signature");
}
// Extract the certificate
$this->x509Certificate = $objKey->getX509Certificate();
// Find the list of validated nodes
$this->validNodes = $objXMLSecDSig->getValidatedNodes();
}
/**
* Retrieve the X509 certificate which was used to sign the XML.
*
* This function will return the certificate as a PEM-encoded string. If the XML
* wasn't signed by an X509 certificate, NULL will be returned.
*
* @return string The certificate as a PEM-encoded string, or NULL if not signed with an X509 certificate.
*/
public function getX509Certificate()
{
return $this->x509Certificate;
}
/**
* Calculates the fingerprint of an X509 certificate.
*
* @param string $x509cert The certificate as a base64-encoded string. The string may optionally
* be framed with '-----BEGIN CERTIFICATE-----' and '-----END CERTIFICATE-----'.
* @return string The fingerprint as a 40-character lowercase hexadecimal number. NULL is returned if the
* argument isn't an X509 certificate.
*/
private static function calculateX509Fingerprint($x509cert)
{
assert(is_string($x509cert));
$lines = explode("\n", $x509cert);
$data = '';
foreach ($lines as $line) {
// Remove '\r' from end of line if present
$line = rtrim($line);
if ($line === '-----BEGIN CERTIFICATE-----') {
// Delete junk from before the certificate
$data = '';
} elseif ($line === '-----END CERTIFICATE-----') {
// Ignore data after the certificate
break;
} elseif ($line === '-----BEGIN PUBLIC KEY-----') {
// This isn't an X509 certificate
return null;
} else {
// Append the current line to the certificate data
$data .= $line;
}
}
/* $data now contains the certificate as a base64-encoded string. The fingerprint
* of the certificate is the sha1-hash of the certificate.
*/
return strtolower(sha1(base64_decode($data)));
}
/**
* Helper function for validating the fingerprint.
*
* Checks the fingerprint of a certificate against an array of valid fingerprints.
* Will throw an exception if none of the fingerprints matches.
*
* @param string $certificate The X509 certificate we should validate.
* @param array $fingerprints The valid fingerprints.
* @throws \Exception
*/
private static function validateCertificateFingerprint($certificate, $fingerprints)
{
assert(is_string($certificate));
assert(is_array($fingerprints));
$certFingerprint = self::calculateX509Fingerprint($certificate);
if ($certFingerprint === null) {
// Couldn't calculate fingerprint from X509 certificate. Should not happen.
throw new \Exception('Unable to calculate fingerprint from X509' .
' certificate. Maybe it isn\'t an X509 certificate?');
}
foreach ($fingerprints as $fp) {
assert(is_string($fp));
if ($fp === $certFingerprint) {
// The fingerprints matched
return;
}
}
// None of the fingerprints matched. Throw an exception describing the error.
throw new \Exception('Invalid fingerprint of certificate. Expected one of [' .
implode('], [', $fingerprints) . '], but got [' . $certFingerprint . ']');
}
/**
* Validate the fingerprint of the certificate which was used to sign this document.
*
* This function accepts either a string, or an array of strings as a parameter. If this
* is an array, then any string (certificate) in the array can match. If this is a string,
* then that string must match,
*
* @param string|array $fingerprints The fingerprints which should match. This can be a single string,
* or an array of fingerprints.
* @throws \Exception
*/
public function validateFingerprint($fingerprints)
{
assert(is_string($fingerprints) || is_array($fingerprints));
if ($this->x509Certificate === null) {
throw new \Exception('Key used to sign the message was not an X509 certificate.');
}
if (!is_array($fingerprints)) {
$fingerprints = array($fingerprints);
}
// Normalize the fingerprints
foreach ($fingerprints as &$fp) {
assert(is_string($fp));
// Make sure that the fingerprint is in the correct format
$fp = strtolower(str_replace(":", "", $fp));
}
self::validateCertificateFingerprint($this->x509Certificate, $fingerprints);
}
/**
* This function checks if the given XML node was signed.
*
* @param \DOMNode $node The XML node which we should verify that was signed.
*
* @return bool TRUE if this node (or a parent node) was signed. FALSE if not.
*/
public function isNodeValidated($node)
{
assert($node instanceof \DOMNode);
while ($node !== null) {
if (in_array($node, $this->validNodes, true)) {
return true;
}
$node = $node->parentNode;
}
/* Neither this node nor any of the parent nodes could be found in the list of
* signed nodes.
*/
return false;
}
/**
* Validate the certificate used to sign the XML against a CA file.
*
* This function throws an exception if unable to validate against the given CA file.
*
* @param string $caFile File with trusted certificates, in PEM-format.
* @throws \Exception
*/
public function validateCA($caFile)
{
assert(is_string($caFile));
if ($this->x509Certificate === null) {
throw new \Exception('Key used to sign the message was not an X509 certificate.');
}
self::validateCertificate($this->x509Certificate, $caFile);
}
/**
* Validate a certificate against a CA file, by using the builtin
* openssl_x509_checkpurpose function
*
* @param string $certificate The certificate, in PEM format.
* @param string $caFile File with trusted certificates, in PEM-format.
* @return boolean|string TRUE on success, or a string with error messages if it failed.
* @deprecated
*/
private static function validateCABuiltIn($certificate, $caFile)
{
assert(is_string($certificate));
assert(is_string($caFile));
// Clear openssl errors
while (openssl_error_string() !== false) {
}
$res = openssl_x509_checkpurpose($certificate, X509_PURPOSE_ANY, array($caFile));
$errors = '';
// Log errors
while (($error = openssl_error_string()) !== false) {
$errors .= ' [' . $error . ']';
}
if ($res !== true) {
return $errors;
}
return true;
}
/**
* Validate the certificate used to sign the XML against a CA file, by using the "openssl verify" command.
*
* This function uses the openssl verify command to verify a certificate, to work around limitations
* on the openssl_x509_checkpurpose function. That function will not work on certificates without a purpose
* set.
*
* @param string $certificate The certificate, in PEM format.
* @param string $caFile File with trusted certificates, in PEM-format.
* @return bool|string TRUE on success, a string with error messages on failure.
* @throws \Exception
* @deprecated
*/
private static function validateCAExec($certificate, $caFile)
{
assert(is_string($certificate));
assert(is_string($caFile));
$command = array(
'openssl', 'verify',
'-CAfile', $caFile,
'-purpose', 'any',
);
$cmdline = '';
foreach ($command as $c) {
$cmdline .= escapeshellarg($c) . ' ';
}
$cmdline .= '2>&1';
$descSpec = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
);
$process = proc_open($cmdline, $descSpec, $pipes);
if (!is_resource($process)) {
throw new \Exception('Failed to execute verification command: ' . $cmdline);
}
if (fwrite($pipes[0], $certificate) === false) {
throw new \Exception('Failed to write certificate for verification.');
}
fclose($pipes[0]);
$out = '';
while (!feof($pipes[1])) {
$line = trim(fgets($pipes[1]));
if (strlen($line) > 0) {
$out .= ' [' . $line . ']';
}
}
fclose($pipes[1]);
$status = proc_close($process);
if ($status !== 0 || $out !== ' [stdin: OK]') {
return $out;
}
return true;
}
/**
* Validate the certificate used to sign the XML against a CA file.
*
* This function throws an exception if unable to validate against the given CA file.
*
* @param string $certificate The certificate, in PEM format.
* @param string $caFile File with trusted certificates, in PEM-format.
* @throws \Exception
* @deprecated
*/
public static function validateCertificate($certificate, $caFile)
{
assert(is_string($certificate));
assert(is_string($caFile));
if (!file_exists($caFile)) {
throw new \Exception('Could not load CA file: ' . $caFile);
}
Logger::debug('Validating certificate against CA file: ' . var_export($caFile, true));
$resBuiltin = self::validateCABuiltIn($certificate, $caFile);
if ($resBuiltin !== true) {
Logger::debug('Failed to validate with internal function: ' . var_export($resBuiltin, true));
$resExternal = self::validateCAExec($certificate, $caFile);
if ($resExternal !== true) {
Logger::debug('Failed to validate with external function: ' . var_export($resExternal, true));
throw new \Exception('Could not verify certificate against CA file "'
. $caFile . '". Internal result:' . $resBuiltin .
' External result:' . $resExternal);
}
}
Logger::debug('Successfully validated certificate.');
}
}