-
Thijs Kinkhorst authored
The protectindexpage test was broken and never changed the protectindexpage setting, but the logic was also broken so it always passed and never got past the protected index page guard in the controller so not a lot of code in the controller was run. Also add more tests for the metadata controllers.
a20debe7
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
<?php
declare(strict_types=1);
namespace SimpleSAML\Auth;
use Exception;
use SAML2\Exception\Protocol\NoPassiveException;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Configuration;
use SimpleSAML\Error;
use SimpleSAML\Logger;
use SimpleSAML\Module;
use SimpleSAML\Utils;
/**
* 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.
*
* @package SimpleSAMLphp
*/
class ProcessingChain
{
/**
* The list of remaining filters which should be applied to the state.
*/
public const FILTERS_INDEX = '\SimpleSAML\Auth\ProcessingChain.filters';
/**
* The stage we use for completed requests.
*/
public 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.
*/
public const AUTHPARAM = 'AuthProcId';
/**
* All authentication processing filters, in the order they should be applied.
*/
private array $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.
* @param string $mode
*/
public function __construct(array $idpMetadata, array $spMetadata, string $mode = 'idp')
{
$config = Configuration::getInstance();
$configauthproc = $config->getOptionalArray('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);
}
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(array &$target, array $src): void
{
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, [$filter]);
}
}
/**
* Parse an array of authentication processing filters.
*
* @param array $filterSrc Array with filter configuration.
* @return array Array of ProcessingFilter objects.
*/
private static function parseFilterList(array $filterSrc): array
{
$parsedFilters = [];
foreach ($filterSrc as $priority => $filter) {
if (is_string($filter)) {
$filter = ['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(array $config, int $priority): ProcessingFilter
{
if (!array_key_exists('class', $config)) {
throw new Exception('Authentication processing filter without name given.');
}
$className = Module::resolveClass(
$config['class'],
'Auth\Process',
'\SimpleSAML\Auth\ProcessingFilter'
);
$config['%priority'] = $priority;
unset($config['class']);
/** @var \SimpleSAML\Auth\ProcessingFilter */
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
* State for more information.
*
* @see State
* @see State::EXCEPTION_HANDLER_URL
* @see State::EXCEPTION_HANDLER_FUNC
*
* @param array &$state The state we are processing.
* @throws \SimpleSAML\Error\Exception
* @throws \SimpleSAML\Error\UnserializableException
*/
public function processState(array &$state): void
{
Assert::true(array_key_exists('ReturnURL', $state) || array_key_exists('ReturnCall', $state));
Assert::true(!array_key_exists('ReturnURL', $state) || !array_key_exists('ReturnCall', $state));
$state[self::FILTERS_INDEX] = $this->filters;
try {
while (count($state[self::FILTERS_INDEX]) > 0) {
$filter = array_shift($state[self::FILTERS_INDEX]);
$filter->process($state);
}
} catch (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 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(array $state): void
{
while (count($state[self::FILTERS_INDEX]) > 0) {
$filter = array_shift($state[self::FILTERS_INDEX]);
try {
$filter->process($state);
} catch (Error\Exception $e) {
State::throwException($state, $e);
} catch (Exception $e) {
$e = new Error\UnserializableException($e);
State::throwException($state, $e);
}
}
// Completed
Assert::true(array_key_exists('ReturnURL', $state) || array_key_exists('ReturnCall', $state));
Assert::true(!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 = State::saveState($state, self::COMPLETED_STAGE);
$httpUtils = new Utils\HTTP();
$httpUtils->redirectTrustedURL($state['ReturnURL'], [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.
State::deleteState($state);
$func = $state['ReturnCall'];
Assert::isCallable($func);
call_user_func($func, $state);
Assert::true(false);
}
}
/**
* Process the given state passivly.
*
* Modules with user interaction are expected to throw an \SAML\Exception\Protocol\NoPassiveException 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(array &$state): void
{
// Should not be set when calling this method
Assert::keyNotExists($state, 'ReturnURL');
// Notify filters about passive request
$state['isPassive'] = true;
$state[self::FILTERS_INDEX] = $this->filters;
while (count($state[self::FILTERS_INDEX]) > 0) {
$filter = array_shift($state[self::FILTERS_INDEX]);
try {
$filter->process($state);
} catch (NoPassiveException $e) {
// Ignore \SAML2\Exception\Protocol\NoPassiveException exceptions
}
}
}
/**
* Retrieve a state which has finished processing.
*
* @param string $id The state identifier.
* @see State::parseStateID()
* @return array|null The state referenced by the $id parameter.
*/
public static function fetchProcessedState(string $id): ?array
{
return State::loadState($id, self::COMPLETED_STAGE);
}
}