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);
}
}